@buaa_smat/hometrans 0.1.7 → 0.1.9
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 +15 -2
- package/dist/cli/config-store.js +148 -0
- package/dist/cli/config.js +40 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/init.js +378 -0
- package/dist/cli/mcp-setup.js +262 -0
- package/dist/cli/mcp.js +94 -0
- package/dist/cli/uninstall.js +310 -0
- package/dist/context/index.js +792 -0
- package/package.json +7 -2
- package/resource/choose_editor.png +0 -0
- package/resource/finish_init.png +0 -0
- package/skills/hmos-convert-pipeline/SKILL.md +5 -21
- package/skills/hmos-integration-test/SKILL.md +4 -4
- package/tools/test-tools/autotest/README.md +4 -4
- package/tools/test-tools/autotest/pyproject.toml +16 -6
- package/tools/test-tools/autotest/self_test_runner.py +4 -4
- package/tools/test-tools/autotest/uv.lock +3156 -3156
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +0 -911
- package/skills/skill-quality-evaluator/SKILL.md +0 -138
- package/skills/skill-quality-evaluator/assets/SKILL_TEMPLATE.md +0 -77
- package/skills/skill-quality-evaluator/references/Best-practices-for-skill-creators.md +0 -277
- package/skills/skill-quality-evaluator/references/Evaluating-skill-output-quality.md +0 -300
- package/skills/skill-quality-evaluator/references/Optimizing-skill-descriptions.md +0 -196
- package/skills/skill-quality-evaluator/references/Specification.md +0 -272
- package/skills/skill-quality-evaluator/references/Using-scripts-in-skills.md +0 -308
- package/skills/skill-quality-evaluator/references/report-template.md +0 -163
- package/skills/skill-quality-evaluator/references/scoring-rubric.md +0 -269
package/README.md
CHANGED
|
@@ -37,6 +37,19 @@ After confirming your editor selection, HomeTrans installs all bundled **skills*
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
Install GitNexus (code knowledge graph dependency required by the migration agents):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g gitnexus
|
|
46
|
+
gitnexus setup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For more details, see the official GitNexus repository: <https://github.com/abhigyanpatwari/GitNexus>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
40
53
|
## HarmonyOS Migration Guide
|
|
41
54
|
|
|
42
55
|
鸿蒙软件迁移指导,提供完整的 Android 到 HarmonyOS 迁移流程:
|
|
@@ -116,7 +129,6 @@ After confirming your editor selection, HomeTrans installs all bundled **skills*
|
|
|
116
129
|
| `assets-output-path` | **必选** | 输出/报告文件的存放目录(需包含 `plan.md` 需求规格文件) |
|
|
117
130
|
| `max-rounds-review` | 可选 | 代码检视循环最大轮数(正整数 `>= 1`,默认 `2`) |
|
|
118
131
|
| `max-rounds-test` | 可选 | 自测循环最大轮数(正整数 `>= 1`,默认 `2`) |
|
|
119
|
-
| `variant` | 可选 | `enhanced`(默认,全功能 agent)或 `baseline`(纯 LLM baseline agent) |
|
|
120
132
|
| `skip-test` | 可选 | `true` 跳过集成测试阶段(无真机验证环境时需要设置为true,默认 `false`) |
|
|
121
133
|
|
|
122
134
|
---
|
|
@@ -168,12 +180,13 @@ After confirming your editor selection, HomeTrans installs all bundled **skills*
|
|
|
168
180
|
|
|
169
181
|
| Skill | Trigger phrases | Description | Prerequisites | Arguments | Example |
|
|
170
182
|
|-------|-----------------|-------------|---------------|-----------|--------|
|
|
171
|
-
| `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording. 4 stages: Logic Development (Context Builder) → Logic Coding → Build → Code Review/Fix/Rebuild loop → Self-Test/Fix/Rebuild loop | `assets-output-path` 下需存在 `plan.md` 需求规格文件 | `android-project-path` (required), `harmonyos-project-path` (required), `assets-output-path` (required), `max-rounds-review` (optional, default 2), `max-rounds-test` (optional, default 2), `
|
|
183
|
+
| `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording. 4 stages: Logic Development (Context Builder) → Logic Coding → Build → Code Review/Fix/Rebuild loop → Self-Test/Fix/Rebuild loop | `assets-output-path` 下需存在 `plan.md` 需求规格文件 | `android-project-path` (required), `harmonyos-project-path` (required), `assets-output-path` (required), `max-rounds-review` (optional, default 2), `max-rounds-test` (optional, default 2), `skip-test` (optional, default false) | `"/hmos-convert-pipeline android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos assets-output-path=D:/path/to/output"` |
|
|
172
184
|
| `hmos-spec-generate` | "spec generation", "generate spec", "requirement to spec", "atomic scenarios", "scenario decomposition", "req to spec" | Generates atomic-scenario requirement specs from raw `.txt` requirement batches. Reads REQ blocks, explores the Android code graph via GitNexus, writes per-REQ trace files, then synthesizes specs from the trace | 需求描述文件 (`.txt`),每段以 `REQ` 开头 | `requirement-description-file` (required), `android-project-path` (required), `spec-output-dir` (required) | `"/hmos-spec-generate requirement-description-file=D:/path/to/req.txt android-project-path=D:/path/to/android spec-output-dir=D:/path/to/specs"` |
|
|
173
185
|
| `hmos-resources-convert` | "Android resources to HarmonyOS", "migrate Android res", "convert drawables/strings/colors" | Converts Android project resources (strings, colors, dimensions, images, drawables, icons) into HarmonyOS resources, including qualifier directories and XML→JSON format conversion | Android 项目中需包含 `res/` 资源目录 | `android-project-path` (required), `harmonyos-project-path` (required), `resource_mapping_path` (required), `apk-path` (optional) | `"/hmos-resources-convert android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos resource_mapping_path=D:/path/to/resource_mapping.md"` |
|
|
174
186
|
| `hmos-incremental-ui-align` | "UI对齐", "页面对齐", "和安卓对齐", "鸿蒙页面修复", "UI增量开发", "align HarmonyOS with Android" | Automated HarmonyOS-Android UI alignment: navigates to target pages on both devices, captures view trees + screenshots, then aligns HarmonyOS code to match Android | 需连接 Android 设备进行 UI 对比;`config-path` 指向的 `config.json` 中需配置项目路径、SDK 路径、API key | `config-path` (required) | `"/hmos-incremental-ui-align config-path=D:/path/to/config.json 帮我对齐设置页面的关于页面"` |
|
|
175
187
|
| `hmos-batch-ui-align` | "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS" | Batch-converts multiple Android Activity UI snapshots (`page_NNNN_ActivityName`) to HarmonyOS ArkUI (ArkTS) pages | `ui_info_root` 下需包含 `page_NNNN_ActivityName` 格式的页面快照子目录 | `android_project_dir` (required), `harmony_project_dir` (required), `ui_info_root` (optional), `pages` (optional) | `"/hmos-batch-ui-align android_project_dir=D:/path/to/android harmony_project_dir=D:/path/to/harmonyos ui_info_root=D:/path/to/pages"` |
|
|
176
188
|
| `hmos-integration-test` | "跑自测", "运行自测", "自动测试", "设备测试", "self test", "run autotest" | Runs on-device self-test for a HarmonyOS app: parses `test_case.md`, installs the HAP, executes AutoTest, and produces a verification report — optionally entering a test-and-fix loop | 需存在 `test_case.md` 测试用例文件,设备需安装待测 HAP | `test-case-path` (required), `hap-path` (required), `output-path` (optional), `pre-test-case-path` (optional), `android-project-path` (optional), `max-rounds` (optional, default 3) | `"/hmos-integration-test test-case-path=D:/path/to/test_case.md hap-path=D:/path/to/app-signed.hap"` |
|
|
189
|
+
| `hmos-fix-build-errors` | "build HarmonyOS project", "fix compile errors", "auto build and fix", "hmos-fix-build-errors" | Builds a HarmonyOS NEXT project from the command line, parses compile errors, fixes them, and retries in a loop until the build succeeds. Default produces an unsigned HAP; `--signed` produces a signed HAP | 有效的 HarmonyOS 项目(含 `build-profile.json5`、`entry/src`、`oh-package.json5`)+ DevEco Studio 安装目录;`--signed` 时签名配置须已存在于 `build-profile.json5` | `harmonyos-project-path` (required), `deveco-studio-path` (required), `--signed` (optional) | `"/hmos-fix-build-errors D:/MyHmosApp \"D:/DevEco Studio\" --signed"` |
|
|
177
190
|
|
|
178
191
|
---
|
|
179
192
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局配置存储:~/.hometrans/config.json
|
|
3
|
+
*
|
|
4
|
+
* 当前包含 editors 配置(Claude Code / Cursor / OpenCode / Codex),后续可扩展
|
|
5
|
+
* 其它顶层字段。首次 setup 时若文件不存在则写入默认值;之后用户可手动编辑。
|
|
6
|
+
* `ht config` 只读不改。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
export function getConfigDir() {
|
|
12
|
+
return path.join(os.homedir(), '.hometrans');
|
|
13
|
+
}
|
|
14
|
+
export function getConfigPath() {
|
|
15
|
+
return path.join(getConfigDir(), 'config.json');
|
|
16
|
+
}
|
|
17
|
+
/** 工具目录:~/.hometrans/tools,`ht init` 把打包的 tools/ 拷贝到此。 */
|
|
18
|
+
export function getToolsDir() {
|
|
19
|
+
return path.join(getConfigDir(), 'tools');
|
|
20
|
+
}
|
|
21
|
+
/** 把 ~/foo 展开为 $HOME/foo;非 ~ 开头原样返回。 */
|
|
22
|
+
export function expandHome(p) {
|
|
23
|
+
if (!p)
|
|
24
|
+
return p;
|
|
25
|
+
if (p === '~')
|
|
26
|
+
return os.homedir();
|
|
27
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) {
|
|
28
|
+
return path.join(os.homedir(), p.slice(2));
|
|
29
|
+
}
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
export function defaultEditors() {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
name: 'Claude Code',
|
|
36
|
+
markerDir: '~/.claude',
|
|
37
|
+
skillsDir: '~/.claude/skills',
|
|
38
|
+
agentsDir: '~/.claude/agents',
|
|
39
|
+
mcp: {
|
|
40
|
+
format: 'jsonc-object',
|
|
41
|
+
path: '~/.claude.json',
|
|
42
|
+
keyPath: ['mcpServers', 'hometrans'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Cursor',
|
|
47
|
+
markerDir: '~/.cursor',
|
|
48
|
+
skillsDir: '~/.cursor/skills',
|
|
49
|
+
agentsDir: '~/.cursor/agents',
|
|
50
|
+
mcp: {
|
|
51
|
+
format: 'jsonc-object',
|
|
52
|
+
path: '~/.cursor/mcp.json',
|
|
53
|
+
keyPath: ['mcpServers', 'hometrans'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'OpenCode',
|
|
58
|
+
markerDir: '~/.config/opencode',
|
|
59
|
+
skillsDir: '~/.config/opencode/skills',
|
|
60
|
+
agentsDir: '~/.config/opencode/agents',
|
|
61
|
+
mcp: {
|
|
62
|
+
format: 'jsonc-command-array',
|
|
63
|
+
path: '~/.config/opencode/opencode.json',
|
|
64
|
+
keyPath: ['mcp', 'hometrans'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Codex',
|
|
69
|
+
markerDir: '~/.codex',
|
|
70
|
+
// Codex 复用 Agent Skills 公共目录,agent 定义独立。
|
|
71
|
+
skillsDir: '~/.agents/skills',
|
|
72
|
+
agentsDir: '~/.codex/agents',
|
|
73
|
+
mcp: {
|
|
74
|
+
format: 'codex-cli',
|
|
75
|
+
path: '~/.codex/config.toml',
|
|
76
|
+
section: 'mcp_servers.hometrans',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
async function fileExists(p) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(p);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 加载 editors 配置。文件不存在时写入默认值再返回。
|
|
92
|
+
* 解析失败时抛错,避免静默覆盖用户手动修改。
|
|
93
|
+
*/
|
|
94
|
+
export function defaultEnv() {
|
|
95
|
+
return {
|
|
96
|
+
OHOS_SDK_PATH: '',
|
|
97
|
+
HMS_SDK_PATH: '',
|
|
98
|
+
TEST_API_KEY: '',
|
|
99
|
+
TOOL_PATH: '',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export async function loadHomeTransConfig() {
|
|
103
|
+
const configPath = getConfigPath();
|
|
104
|
+
if (!(await fileExists(configPath))) {
|
|
105
|
+
const config = {
|
|
106
|
+
editors: defaultEditors(),
|
|
107
|
+
env: defaultEnv(),
|
|
108
|
+
};
|
|
109
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
110
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(raw);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
throw new Error(`Failed to parse ${configPath}: ${err.message}. Fix the JSON or delete the file to restore defaults.`);
|
|
120
|
+
}
|
|
121
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.editors)) {
|
|
122
|
+
throw new Error(`Invalid config.json shape at ${configPath}: top-level must be { "editors": [...] }.`);
|
|
123
|
+
}
|
|
124
|
+
const config = parsed;
|
|
125
|
+
const anyParsed = parsed;
|
|
126
|
+
// Migrate legacy fields into `env`:
|
|
127
|
+
// - `sdkPaths` / `params` held OHOS_SDK_PATH / HMS_SDK_PATH / TEST_API_KEY.
|
|
128
|
+
// - `tool_path` held the tools dir, now env.TOOL_PATH.
|
|
129
|
+
const legacyParams = (anyParsed.params ?? anyParsed.sdkPaths);
|
|
130
|
+
const legacyToolPath = typeof anyParsed.tool_path === 'string' ? anyParsed.tool_path : undefined;
|
|
131
|
+
config.env = {
|
|
132
|
+
...defaultEnv(),
|
|
133
|
+
...(legacyParams ?? {}),
|
|
134
|
+
...(config.env ?? {}),
|
|
135
|
+
};
|
|
136
|
+
if (legacyToolPath && !config.env.TOOL_PATH) {
|
|
137
|
+
config.env.TOOL_PATH = legacyToolPath;
|
|
138
|
+
}
|
|
139
|
+
delete anyParsed.sdkPaths;
|
|
140
|
+
delete anyParsed.params;
|
|
141
|
+
delete anyParsed.tool_path;
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
export async function saveHomeTransConfig(config) {
|
|
145
|
+
const configPath = getConfigPath();
|
|
146
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
147
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
148
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ht config` — 打印 editors.json 路径与内容。
|
|
3
|
+
*
|
|
4
|
+
* 该命令只读:用户若要修改 editor 配置,直接编辑这个 JSON 文件即可。
|
|
5
|
+
* 文件不存在时自动写入默认 4 个 editor。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import { getConfigPath, loadHomeTransConfig } from './config-store.js';
|
|
9
|
+
export async function configCommand() {
|
|
10
|
+
const configPath = getConfigPath();
|
|
11
|
+
const config = await loadHomeTransConfig();
|
|
12
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(' HomeTrans Config');
|
|
15
|
+
console.log(' ================');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log(` Path: ${configPath}`);
|
|
18
|
+
console.log('');
|
|
19
|
+
// User parameters
|
|
20
|
+
const maskedKey = config.env.TEST_API_KEY
|
|
21
|
+
? config.env.TEST_API_KEY.slice(0, 4) +
|
|
22
|
+
'***' +
|
|
23
|
+
config.env.TEST_API_KEY.slice(-4)
|
|
24
|
+
: '(not set)';
|
|
25
|
+
console.log(' Parameters:');
|
|
26
|
+
console.log(` OHOS_SDK_PATH : ${config.env.OHOS_SDK_PATH || '(not set)'}`);
|
|
27
|
+
console.log(` HMS_SDK_PATH : ${config.env.HMS_SDK_PATH || '(not set)'}`);
|
|
28
|
+
console.log(` TEST_API_KEY : ${maskedKey}`);
|
|
29
|
+
console.log(` TOOL_PATH : ${config.env.TOOL_PATH || '(not set)'}`);
|
|
30
|
+
console.log('');
|
|
31
|
+
// Full config content
|
|
32
|
+
console.log(' Full Content:');
|
|
33
|
+
console.log('');
|
|
34
|
+
for (const line of raw.replace(/\r?\n$/, '').split(/\r?\n/)) {
|
|
35
|
+
console.log(` ${line}`);
|
|
36
|
+
}
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(' Edit this file to modify config, then re-run `ht init`.');
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { initCommand } from './init.js';
|
|
5
|
+
import { runMcpServer } from './mcp.js';
|
|
6
|
+
import { configCommand } from './config.js';
|
|
7
|
+
import { uninstallCommand } from './uninstall.js';
|
|
8
|
+
const _require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = _require('../../package.json');
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('hometrans')
|
|
13
|
+
.description('HomeTrans installer — distribute Android-to-HarmonyOS skills and agents into AI editors')
|
|
14
|
+
.version(pkg.version);
|
|
15
|
+
program
|
|
16
|
+
.command('init')
|
|
17
|
+
.description('Initialize HomeTrans — select editors and install skills, agents, and MCP servers')
|
|
18
|
+
.option('-a, --all', 'Skip interactive selection and configure all editors')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
await initCommand(opts);
|
|
21
|
+
});
|
|
22
|
+
program
|
|
23
|
+
.command('mcp')
|
|
24
|
+
.description('Run the HomeTrans MCP server (stdio)')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
await runMcpServer();
|
|
27
|
+
});
|
|
28
|
+
program
|
|
29
|
+
.command('config')
|
|
30
|
+
.description('Show the editors.json path and content (~/.hometrans/editors.json). Edit the file directly to customize editors, then re-run `ht init`.')
|
|
31
|
+
.action(async () => {
|
|
32
|
+
await configCommand();
|
|
33
|
+
});
|
|
34
|
+
program
|
|
35
|
+
.command('uninstall')
|
|
36
|
+
.description('Remove all hometrans skills, agents, and MCP entries from configured editors')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
await uninstallCommand();
|
|
39
|
+
});
|
|
40
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
41
|
+
console.error(err instanceof Error ? err.message : err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ht init` — interactive initialization for HomeTrans.
|
|
3
|
+
*
|
|
4
|
+
* Displays the HomeTrans banner, lets the user select which editors to
|
|
5
|
+
* configure, then copies bundled skills/agents and registers MCP servers
|
|
6
|
+
* for the chosen editors.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import figlet from 'figlet';
|
|
14
|
+
import inquirer from 'inquirer';
|
|
15
|
+
import { setupMcpForAllEditors } from './mcp-setup.js';
|
|
16
|
+
import { expandHome, getConfigPath, getToolsDir, loadHomeTransConfig, saveHomeTransConfig, } from './config-store.js';
|
|
17
|
+
function ensureChalkColor() {
|
|
18
|
+
if (process.stdout.isTTY && chalk.level === 0) {
|
|
19
|
+
chalk.level = 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
export async function dirExists(dirPath) {
|
|
25
|
+
try {
|
|
26
|
+
const stat = await fs.stat(dirPath);
|
|
27
|
+
return stat.isDirectory();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function copyDirRecursive(src, dest) {
|
|
34
|
+
await fs.mkdir(dest, { recursive: true });
|
|
35
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const srcPath = path.join(src, entry.name);
|
|
38
|
+
const destPath = path.join(dest, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
await copyDirRecursive(srcPath, destPath);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await fs.copyFile(srcPath, destPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveSkillsRoot() {
|
|
48
|
+
return path.resolve(__dirname, '..', '..', 'skills');
|
|
49
|
+
}
|
|
50
|
+
function resolveAgentsRoot() {
|
|
51
|
+
return path.resolve(__dirname, '..', '..', 'agents');
|
|
52
|
+
}
|
|
53
|
+
function resolveToolsRoot() {
|
|
54
|
+
return path.resolve(__dirname, '..', '..', 'tools');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Copy the bundled `tools/` folder into ~/.hometrans/tools and record the
|
|
58
|
+
* destination in config.env.TOOL_PATH. Returns the destination path, or null if
|
|
59
|
+
* the package ships no tools/ folder.
|
|
60
|
+
*/
|
|
61
|
+
async function installTools(toolsRoot, config) {
|
|
62
|
+
if (!(await dirExists(toolsRoot)))
|
|
63
|
+
return null;
|
|
64
|
+
const toolsDest = getToolsDir();
|
|
65
|
+
await copyDirRecursive(toolsRoot, toolsDest);
|
|
66
|
+
config.env.TOOL_PATH = toolsDest;
|
|
67
|
+
await saveHomeTransConfig(config);
|
|
68
|
+
return toolsDest;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The AutoTest tools live under the installed HomeTrans tools dir
|
|
72
|
+
* (~/.hometrans/tools/test-tools/autotest) once `installTools` has run.
|
|
73
|
+
* This is the same location the self-tester agent resolves via config.env.TOOL_PATH.
|
|
74
|
+
*/
|
|
75
|
+
function resolveAutotestDir(toolPath) {
|
|
76
|
+
return path.join(toolPath, 'test-tools', 'autotest');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Initialize / refresh `agents/test-tools/autotest/config.yaml`:
|
|
80
|
+
* - If config.yaml does not exist, seed it from config.yaml.example.
|
|
81
|
+
* - Replace every `api_key: "..."` line with the supplied apiKey.
|
|
82
|
+
*
|
|
83
|
+
* Returns a status string for the result summary, or null if nothing was done
|
|
84
|
+
* (e.g., the autotest folder isn't present in this package).
|
|
85
|
+
*/
|
|
86
|
+
async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
87
|
+
const examplePath = path.join(autotestDir, 'config.yaml.example');
|
|
88
|
+
const configPath = path.join(autotestDir, 'config.yaml');
|
|
89
|
+
const hasExample = await fs
|
|
90
|
+
.access(examplePath)
|
|
91
|
+
.then(() => true)
|
|
92
|
+
.catch(() => false);
|
|
93
|
+
if (!hasExample)
|
|
94
|
+
return null;
|
|
95
|
+
let seeded = false;
|
|
96
|
+
const hasConfig = await fs
|
|
97
|
+
.access(configPath)
|
|
98
|
+
.then(() => true)
|
|
99
|
+
.catch(() => false);
|
|
100
|
+
if (!hasConfig) {
|
|
101
|
+
await fs.copyFile(examplePath, configPath);
|
|
102
|
+
seeded = true;
|
|
103
|
+
}
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
return seeded
|
|
106
|
+
? `autotest config.yaml seeded (api_key left as placeholder)`
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
const original = await fs.readFile(configPath, 'utf-8');
|
|
110
|
+
// Match `api_key: <value>` with optional quoting, preserve indent + key.
|
|
111
|
+
const updated = original.replace(/^(\s*api_key\s*:\s*)(?:"[^"]*"|'[^']*'|\S+)\s*$/gm, (_m, prefix) => `${prefix}"${apiKey}"`);
|
|
112
|
+
if (updated !== original) {
|
|
113
|
+
await fs.writeFile(configPath, updated, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
return seeded
|
|
116
|
+
? `autotest config.yaml seeded + api_key filled`
|
|
117
|
+
: `autotest config.yaml api_key refreshed`;
|
|
118
|
+
}
|
|
119
|
+
async function installSkillsTo(skillsRoot, targetDir) {
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const installed = [];
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isDirectory())
|
|
130
|
+
continue;
|
|
131
|
+
const skillSrc = path.join(skillsRoot, entry.name);
|
|
132
|
+
const hasSkillFile = await fs
|
|
133
|
+
.access(path.join(skillSrc, 'SKILL.md'))
|
|
134
|
+
.then(() => true)
|
|
135
|
+
.catch(() => false);
|
|
136
|
+
if (!hasSkillFile)
|
|
137
|
+
continue;
|
|
138
|
+
const skillDest = path.join(targetDir, entry.name);
|
|
139
|
+
await copyDirRecursive(skillSrc, skillDest);
|
|
140
|
+
installed.push(entry.name);
|
|
141
|
+
}
|
|
142
|
+
return installed;
|
|
143
|
+
}
|
|
144
|
+
async function installAgentsTo(agentsRoot, targetDir) {
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = await fs.readdir(agentsRoot, { withFileTypes: true });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
153
|
+
const installed = [];
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const srcPath = path.join(agentsRoot, entry.name);
|
|
156
|
+
const destPath = path.join(targetDir, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
await copyDirRecursive(srcPath, destPath);
|
|
159
|
+
}
|
|
160
|
+
else if (entry.isFile()) {
|
|
161
|
+
await fs.copyFile(srcPath, destPath);
|
|
162
|
+
if (entry.name.endsWith('.md')) {
|
|
163
|
+
installed.push(entry.name.slice(0, -3));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return installed;
|
|
168
|
+
}
|
|
169
|
+
async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
170
|
+
const marker = expandHome(editor.markerDir);
|
|
171
|
+
if (marker && !(await dirExists(marker))) {
|
|
172
|
+
result.skipped.push(`${editor.name} (not installed)`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const skillsDir = expandHome(editor.skillsDir);
|
|
176
|
+
const agentsDir = expandHome(editor.agentsDir);
|
|
177
|
+
try {
|
|
178
|
+
const skills = await installSkillsTo(skillsRoot, skillsDir);
|
|
179
|
+
if (skills.length > 0) {
|
|
180
|
+
result.configured.push(`${editor.name} skills (${skills.length} -> ${prettyHome(skillsDir)})`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
result.errors.push(`${editor.name} skills: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const agents = await installAgentsTo(agentsRoot, agentsDir);
|
|
188
|
+
if (agents.length > 0) {
|
|
189
|
+
result.configured.push(`${editor.name} agents (${agents.length} -> ${prettyHome(agentsDir)})`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
result.errors.push(`${editor.name} agents: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function prettyHome(p) {
|
|
197
|
+
const home = os.homedir();
|
|
198
|
+
if (p.startsWith(home)) {
|
|
199
|
+
return '~' + p.slice(home.length).replace(/\\/g, '/');
|
|
200
|
+
}
|
|
201
|
+
return p.replace(/\\/g, '/');
|
|
202
|
+
}
|
|
203
|
+
async function detectInstalledEditors(editors) {
|
|
204
|
+
const status = new Map();
|
|
205
|
+
for (const editor of editors) {
|
|
206
|
+
const marker = expandHome(editor.markerDir);
|
|
207
|
+
if (!marker) {
|
|
208
|
+
status.set(editor.name, true);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
status.set(editor.name, await dirExists(marker));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return status;
|
|
215
|
+
}
|
|
216
|
+
export async function initCommand(options = {}) {
|
|
217
|
+
ensureChalkColor();
|
|
218
|
+
const banner = figlet.textSync('HomeTrans', { font: 'Standard' });
|
|
219
|
+
console.log(chalk.cyan(`\n${banner.trimEnd()}`));
|
|
220
|
+
console.log(chalk.gray('\n Android-to-HarmonyOS skill & agent installer for AI editors\n'));
|
|
221
|
+
const skillsRoot = resolveSkillsRoot();
|
|
222
|
+
const agentsRoot = resolveAgentsRoot();
|
|
223
|
+
const toolsRoot = resolveToolsRoot();
|
|
224
|
+
const hasSkills = await dirExists(skillsRoot);
|
|
225
|
+
const hasAgents = await dirExists(agentsRoot);
|
|
226
|
+
if (!hasSkills && !hasAgents) {
|
|
227
|
+
console.error(chalk.red(` ! Neither skills/ nor agents/ found at package root.`));
|
|
228
|
+
console.error(chalk.gray(` Looked in: ${skillsRoot}`));
|
|
229
|
+
console.error(chalk.gray(` ${agentsRoot}`));
|
|
230
|
+
console.error(chalk.gray(' Reinstall hometrans or run `npm run build` from the package root.'));
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const config = await loadHomeTransConfig();
|
|
235
|
+
const { editors } = config;
|
|
236
|
+
const installedStatus = await detectInstalledEditors(editors);
|
|
237
|
+
let selectedEditors;
|
|
238
|
+
if (options.all) {
|
|
239
|
+
selectedEditors = editors.map((e) => e.name);
|
|
240
|
+
console.log(chalk.blue(' --all: selecting all editors.\n'));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const choices = editors.map((editor) => {
|
|
244
|
+
const installed = installedStatus.get(editor.name);
|
|
245
|
+
const label = installed
|
|
246
|
+
? `${editor.name} ${chalk.green('(detected)')}`
|
|
247
|
+
: `${editor.name} ${chalk.gray('(not detected)')}`;
|
|
248
|
+
return {
|
|
249
|
+
name: label,
|
|
250
|
+
value: editor.name,
|
|
251
|
+
checked: installed,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
try {
|
|
255
|
+
const answers = await inquirer.prompt([
|
|
256
|
+
{
|
|
257
|
+
type: 'checkbox',
|
|
258
|
+
name: 'selectedEditors',
|
|
259
|
+
message: 'Select editors to configure (arrow keys + space to toggle, enter to confirm):',
|
|
260
|
+
choices,
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
selectedEditors = answers.selectedEditors;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
console.log(chalk.yellow('\n Interactive selection failed. Auto-selecting all detected editors.'));
|
|
267
|
+
console.log(chalk.gray(' Tip: use --all to skip interactive selection.\n'));
|
|
268
|
+
selectedEditors = editors
|
|
269
|
+
.filter((e) => installedStatus.get(e.name))
|
|
270
|
+
.map((e) => e.name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (selectedEditors.length === 0) {
|
|
274
|
+
console.log(chalk.yellow('\n No editors selected. Nothing to do.\n'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// --- User parameters (SDK paths, test API key) ---
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log(chalk.blue(' Parameter Configuration'));
|
|
280
|
+
console.log(chalk.gray(' (press Enter to keep current value or leave empty)\n'));
|
|
281
|
+
try {
|
|
282
|
+
const answers = await inquirer.prompt([
|
|
283
|
+
{
|
|
284
|
+
type: 'input',
|
|
285
|
+
name: 'OHOS_SDK_PATH',
|
|
286
|
+
message: 'OHOS_SDK_PATH:',
|
|
287
|
+
default: config.env.OHOS_SDK_PATH || undefined,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
type: 'input',
|
|
291
|
+
name: 'HMS_SDK_PATH',
|
|
292
|
+
message: 'HMS_SDK_PATH:',
|
|
293
|
+
default: config.env.HMS_SDK_PATH || undefined,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: 'input',
|
|
297
|
+
name: 'TEST_API_KEY',
|
|
298
|
+
message: 'TEST_API_KEY (for autotest config.yaml):',
|
|
299
|
+
default: config.env.TEST_API_KEY || undefined,
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
config.env.OHOS_SDK_PATH = answers.OHOS_SDK_PATH.trim();
|
|
303
|
+
config.env.HMS_SDK_PATH = answers.HMS_SDK_PATH.trim();
|
|
304
|
+
config.env.TEST_API_KEY = answers.TEST_API_KEY.trim();
|
|
305
|
+
await saveHomeTransConfig(config);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
|
|
309
|
+
}
|
|
310
|
+
// Copy bundled tools/ into ~/.hometrans/tools and record env.TOOL_PATH.
|
|
311
|
+
// Must run before the autotest config step below, which seeds config.yaml
|
|
312
|
+
// into the installed tools dir (the location agents read via env.TOOL_PATH).
|
|
313
|
+
let installedToolPath = null;
|
|
314
|
+
try {
|
|
315
|
+
installedToolPath = await installTools(toolsRoot, config);
|
|
316
|
+
if (installedToolPath) {
|
|
317
|
+
console.log(chalk.green(` + tools copied -> ${prettyHome(installedToolPath)}`));
|
|
318
|
+
console.log(chalk.gray(` TOOL_PATH set in ${prettyHome(getConfigPath())}`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
console.log(chalk.red(` ! tools copy: ${err.message}`));
|
|
323
|
+
}
|
|
324
|
+
// Initialize / refresh autotest config.yaml from the example template,
|
|
325
|
+
// inside the installed tools dir (<TOOL_PATH>/test-tools/autotest).
|
|
326
|
+
try {
|
|
327
|
+
const toolPath = installedToolPath ?? config.env.TOOL_PATH;
|
|
328
|
+
if (toolPath) {
|
|
329
|
+
const autotestDir = resolveAutotestDir(toolPath);
|
|
330
|
+
const status = await refreshAutotestConfig(autotestDir, config.env.TEST_API_KEY);
|
|
331
|
+
if (status) {
|
|
332
|
+
console.log(chalk.green(` + ${status}`));
|
|
333
|
+
console.log(chalk.gray(` ${path.join(autotestDir, 'config.yaml')}`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.log(chalk.red(` ! autotest config.yaml: ${err.message}`));
|
|
339
|
+
}
|
|
340
|
+
console.log('');
|
|
341
|
+
const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
|
|
342
|
+
const result = { configured: [], skipped: [], errors: [] };
|
|
343
|
+
for (const editor of editorsToSetup) {
|
|
344
|
+
console.log(chalk.blue(` Configuring ${editor.name}...`));
|
|
345
|
+
await installForEditor(editor, skillsRoot, agentsRoot, result);
|
|
346
|
+
}
|
|
347
|
+
await setupMcpForAllEditors(editorsToSetup, result);
|
|
348
|
+
console.log('');
|
|
349
|
+
if (result.configured.length > 0) {
|
|
350
|
+
console.log(chalk.green(' Configured:'));
|
|
351
|
+
for (const name of result.configured)
|
|
352
|
+
console.log(chalk.green(` + ${name}`));
|
|
353
|
+
}
|
|
354
|
+
if (result.skipped.length > 0) {
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(chalk.yellow(' Skipped:'));
|
|
357
|
+
for (const name of result.skipped)
|
|
358
|
+
console.log(chalk.yellow(` - ${name}`));
|
|
359
|
+
}
|
|
360
|
+
if (result.errors.length > 0) {
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(chalk.red(' Errors:'));
|
|
363
|
+
for (const err of result.errors)
|
|
364
|
+
console.log(chalk.red(` ! ${err}`));
|
|
365
|
+
}
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log(chalk.gray(` Source skills: ${skillsRoot}`));
|
|
368
|
+
console.log(chalk.gray(` Source agents: ${agentsRoot}`));
|
|
369
|
+
console.log('');
|
|
370
|
+
if (result.configured.length === 0 && result.errors.length === 0) {
|
|
371
|
+
console.log(chalk.yellow(' No editors were configured. Make sure the selected editors are installed,'));
|
|
372
|
+
console.log(chalk.yellow(' then re-run `ht init`.'));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(chalk.green(' Done. Re-open your editor for the new skills/agents to be picked up.'));
|
|
376
|
+
}
|
|
377
|
+
console.log('');
|
|
378
|
+
}
|