@buaa_smat/hometrans 0.1.15 → 0.1.17
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 +108 -99
- package/agents/build-fixer.md +15 -14
- package/agents/code-reviewer.md +3 -3
- package/agents/logic-coder.md +1 -1
- package/agents/logic-context-builder.md +1 -1
- package/agents/self-tester.md +16 -13
- package/dist/cli/config-store.js +127 -29
- package/dist/cli/config.js +9 -2
- package/dist/cli/init.js +431 -248
- package/dist/cli/uninstall.js +103 -8
- package/dist/context/index.js +19 -2
- package/env-requirements.json +19 -23
- package/package.json +1 -1
- package/resource/choose_editor.png +0 -0
- package/resource/set_environment.png +0 -0
- package/resource/set_multimodel.png +0 -0
- package/resource/set_sdk.png +0 -0
- package/skills/hmos-batch-ui-align/SKILL.md +8 -6
- package/skills/hmos-convert-pipeline/SKILL.md +7 -7
- package/skills/hmos-fix-build-errors/SKILL.md +4 -3
- package/skills/hmos-incremental-ui-align/README.md +9 -12
- package/skills/hmos-incremental-ui-align/SKILL.md +9 -9
- package/skills/hmos-incremental-ui-align/scripts/__pycache__/app_feature_verify.cpython-314.pyc +0 -0
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.py +128 -29
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +37 -37
- package/skills/hmos-incremental-ui-align/scripts/page_capture.py +7 -2
- package/skills/hmos-integration-test/README.md +3 -3
- package/skills/hmos-integration-test/SKILL.md +7 -7
- package/skills/hmos-spec-generate/SKILL.md +45 -52
- package/tools/test-tools/autotest/README.md +10 -11
- package/tools/test-tools/autotest/self_test_runner.py +40 -12
- package/resource/common_config.png +0 -0
- package/resource/integration_test_config.png +0 -0
- package/resource/set_env.png +0 -0
- package/resource/ui_align_config.png +0 -0
- package/skills/hmos-resources-convert/tools/apktool.bat +0 -85
package/dist/cli/init.js
CHANGED
|
@@ -13,7 +13,7 @@ import chalk from 'chalk';
|
|
|
13
13
|
import figlet from 'figlet';
|
|
14
14
|
import inquirer from 'inquirer';
|
|
15
15
|
import { setupMcpForAllEditors } from './mcp-setup.js';
|
|
16
|
-
import { deriveSdkPaths, expandHome, getConfigDir, getConfigPath, getToolsDir, loadHomeTransConfig, saveHomeTransConfig, } from './config-store.js';
|
|
16
|
+
import { deriveSdkPaths, expandHome, getConfigDir, getConfigPath, getToolsDir, isPlaceholderApiKey, loadHomeTransConfig, saveHomeTransConfig, syncModelEnvIntoConfig, } from './config-store.js';
|
|
17
17
|
import { persistEnvVars } from './env-vars.js';
|
|
18
18
|
function ensureChalkColor() {
|
|
19
19
|
if (process.stdout.isTTY && chalk.level === 0) {
|
|
@@ -88,176 +88,51 @@ function maskKey(key) {
|
|
|
88
88
|
return key || '';
|
|
89
89
|
return key.length <= 8 ? '***' : `${key.slice(0, 4)}***${key.slice(-4)}`;
|
|
90
90
|
}
|
|
91
|
-
/** Inline notes for each autotest field, shown in the table's Description column. */
|
|
92
|
-
const AUTOTEST_FIELD_NOTES = {
|
|
93
|
-
name: '多模态(视觉)模型的名字',
|
|
94
|
-
base_url: '模型API服务的基础地址',
|
|
95
|
-
api_key: '用于模型API请求认证的令牌',
|
|
96
|
-
provider: '模型API服务的提供方标识,如openai',
|
|
97
|
-
temperature: '采样温度',
|
|
98
|
-
top_p: '核采样阈值',
|
|
99
|
-
frequency_penalty: '重复惩罚',
|
|
100
|
-
device_sn: '设备序列号;null 表示自动选用当前连接的设备',
|
|
101
|
-
ip: '设备连接 IP(hdc)',
|
|
102
|
-
port: '设备连接端口(hdc)',
|
|
103
|
-
device_log_level: '设备日志级别:debug/info/...',
|
|
104
|
-
max_steps: '单条用例最大执行步数',
|
|
105
|
-
verbose: '是否输出详细日志',
|
|
106
|
-
lang: '输出语言:zh/en',
|
|
107
|
-
enable_preprocess: '执行前是否先用 unified_model 把用例拆成步骤序列(多一次 LLM 调用,默认关)',
|
|
108
|
-
debug_mode: '调试会话:null 关闭 / memory 内存 / file 存盘并跑完后起调试服务(:5000)',
|
|
109
|
-
};
|
|
110
|
-
/** Visual width of a string in a monospace terminal (CJK / fullwidth glyphs = 2). */
|
|
111
|
-
function displayWidth(s) {
|
|
112
|
-
let w = 0;
|
|
113
|
-
for (const ch of s) {
|
|
114
|
-
const cp = ch.codePointAt(0) ?? 0;
|
|
115
|
-
const wide = (cp >= 0x1100 && cp <= 0x115f) ||
|
|
116
|
-
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
117
|
-
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
118
|
-
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
119
|
-
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
120
|
-
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
121
|
-
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
122
|
-
(cp >= 0x20000 && cp <= 0x3fffd);
|
|
123
|
-
w += wide ? 2 : 1;
|
|
124
|
-
}
|
|
125
|
-
return w;
|
|
126
|
-
}
|
|
127
|
-
/** padEnd by visual width so columns with CJK content still line up. */
|
|
128
|
-
function padEndDisplay(s, width) {
|
|
129
|
-
return s + ' '.repeat(Math.max(0, width - displayWidth(s)));
|
|
130
|
-
}
|
|
131
91
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
92
|
+
* Model Configuration step — configures the single multimodal (vision) LLM
|
|
93
|
+
* shared by BOTH the on-device self-test agent AND the UI-alignment phone-agent.
|
|
94
|
+
* Stored once in the `autotest.unified_model` block of ~/.hometrans/config.json
|
|
95
|
+
* and additionally exported as the HOMETRANS_MODEL_* machine environment
|
|
96
|
+
* variables (see maybePersistMachineEnv), so each runtime reads the same config:
|
|
97
|
+
* - self_test_runner.py reads autotest.unified_model from config.json;
|
|
98
|
+
* - app_feature_verify.py reads the HOMETRANS_MODEL_* env vars (falling back
|
|
99
|
+
* to config.json).
|
|
100
|
+
* Prompts for the four essentials (api_key / name / base_url / provider);
|
|
101
|
+
* all other parameters (device / agent, temperature, …) keep their defaults —
|
|
102
|
+
* edit config.json to tune them.
|
|
135
103
|
*/
|
|
136
|
-
function
|
|
137
|
-
const
|
|
138
|
-
const um = at.unified_model;
|
|
139
|
-
const data = [
|
|
140
|
-
['unified_model', 'name', um.name],
|
|
141
|
-
['unified_model', 'base_url', um.base_url],
|
|
142
|
-
['unified_model', 'provider', um.provider],
|
|
143
|
-
['unified_model', 'api_key', maskKey(um.api_key)],
|
|
144
|
-
['unified_model', 'temperature', fmt(um.temperature)],
|
|
145
|
-
['unified_model', 'top_p', fmt(um.top_p)],
|
|
146
|
-
['unified_model', 'frequency_penalty', fmt(um.frequency_penalty)],
|
|
147
|
-
['device', 'device_sn', fmt(at.device.device_sn)],
|
|
148
|
-
['device', 'ip', at.device.ip],
|
|
149
|
-
['device', 'port', fmt(at.device.port)],
|
|
150
|
-
['device', 'device_log_level', at.device.device_log_level],
|
|
151
|
-
['agent', 'max_steps', fmt(at.agent.max_steps)],
|
|
152
|
-
['agent', 'verbose', fmt(at.agent.verbose)],
|
|
153
|
-
['agent', 'lang', at.agent.lang],
|
|
154
|
-
['agent', 'enable_preprocess', fmt(at.agent.enable_preprocess)],
|
|
155
|
-
['agent', 'debug_mode', fmt(at.agent.debug_mode)],
|
|
156
|
-
];
|
|
157
|
-
const header = ['Section', 'Field', 'Current/Default', 'Description'];
|
|
158
|
-
// Section label only on the first row of each group.
|
|
159
|
-
const rows = [];
|
|
160
|
-
let prevSection = '';
|
|
161
|
-
for (const [s, f, v] of data) {
|
|
162
|
-
rows.push([s === prevSection ? '' : s, f, v, AUTOTEST_FIELD_NOTES[f] ?? '']);
|
|
163
|
-
prevSection = s;
|
|
164
|
-
}
|
|
165
|
-
const cols = header.map((h) => displayWidth(h));
|
|
166
|
-
for (const r of rows) {
|
|
167
|
-
for (let i = 0; i < cols.length; i++) {
|
|
168
|
-
cols[i] = Math.max(cols[i], displayWidth(r[i]));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const border = (l, m, r) => ` ${l}` + cols.map((w) => '─'.repeat(w + 2)).join(m) + r;
|
|
172
|
-
const row = (cells) => ' │ ' + cells.map((c, i) => padEndDisplay(c, cols[i])).join(' │ ') + ' │';
|
|
173
|
-
console.log(chalk.gray(border('┌', '┬', '┐')));
|
|
174
|
-
console.log(chalk.gray(row(header)));
|
|
175
|
-
console.log(chalk.gray(border('├', '┼', '┤')));
|
|
176
|
-
for (const r of rows)
|
|
177
|
-
console.log(chalk.gray(row(r)));
|
|
178
|
-
console.log(chalk.gray(border('└', '┴', '┘')));
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Test Configuration step — configures the `autotest` block in
|
|
182
|
-
* ~/.hometrans/config.json (used by the on-device self-test agent). Shows the
|
|
183
|
-
* default content, then prompts for the model essentials. The api_key (formerly
|
|
184
|
-
* TEST_API_KEY) is stored here and NOT written to the system environment;
|
|
185
|
-
* self_test_runner.py reads it from config.json at run time.
|
|
186
|
-
*
|
|
187
|
-
* device / agent stay at their defaults (shown above) — edit config.json to tune.
|
|
188
|
-
*/
|
|
189
|
-
async function promptTestConfig(config) {
|
|
190
|
-
const at = config.autotest;
|
|
191
|
-
const um = at.unified_model;
|
|
192
|
-
console.log('');
|
|
193
|
-
console.log(chalk.blue(' Test Configuration'));
|
|
194
|
-
printAutotestTable(at);
|
|
195
|
-
console.log(chalk.gray(' This step configures only the parameters prompted below; configure all other'));
|
|
196
|
-
console.log(chalk.gray(` parameters in config.json: ${getConfigPath()}`));
|
|
104
|
+
async function promptModelConfig(config) {
|
|
105
|
+
const um = config.autotest.unified_model;
|
|
197
106
|
console.log('');
|
|
107
|
+
console.log(chalk.blue(' MultiModel LLM Configuration'));
|
|
198
108
|
try {
|
|
199
109
|
const answers = await inquirer.prompt([
|
|
200
110
|
{
|
|
201
111
|
type: 'input',
|
|
202
|
-
name: '
|
|
203
|
-
message: '
|
|
204
|
-
default: um.api_key
|
|
205
|
-
},
|
|
206
|
-
{ type: 'input', name: 'name', message: 'model name:', default: um.name },
|
|
207
|
-
{ type: 'input', name: 'base_url', message: 'base_url:', default: um.base_url },
|
|
208
|
-
{
|
|
209
|
-
type: 'input',
|
|
210
|
-
name: 'provider',
|
|
211
|
-
message: 'provider (openai / azure / ...):',
|
|
212
|
-
default: um.provider,
|
|
112
|
+
name: 'model_api_key',
|
|
113
|
+
message: 'model_api_key (multimodal LLM key for ui alignment + integration test):',
|
|
114
|
+
default: isPlaceholderApiKey(um.api_key) ? undefined : um.api_key,
|
|
213
115
|
},
|
|
116
|
+
{ type: 'input', name: 'model_name', message: 'model_name:', default: um.name },
|
|
117
|
+
{ type: 'input', name: 'model_base_url', message: 'model_base_url:', default: um.base_url },
|
|
214
118
|
]);
|
|
215
|
-
if (answers.
|
|
216
|
-
um.api_key = answers.
|
|
217
|
-
if (answers.
|
|
218
|
-
um.name = answers.
|
|
219
|
-
if (answers.
|
|
220
|
-
um.base_url = answers.
|
|
221
|
-
|
|
222
|
-
|
|
119
|
+
if (answers.model_api_key.trim())
|
|
120
|
+
um.api_key = answers.model_api_key.trim();
|
|
121
|
+
if (answers.model_name.trim())
|
|
122
|
+
um.name = answers.model_name.trim();
|
|
123
|
+
if (answers.model_base_url.trim())
|
|
124
|
+
um.base_url = answers.model_base_url.trim();
|
|
125
|
+
// provider is no longer prompted — default to openai when unset.
|
|
126
|
+
if (!um.provider || !um.provider.trim())
|
|
127
|
+
um.provider = 'openai';
|
|
128
|
+
// Mirror into config.env.HOMETRANS_MODEL_* so config.json stays consistent.
|
|
129
|
+
syncModelEnvIntoConfig(config);
|
|
223
130
|
await saveHomeTransConfig(config);
|
|
224
|
-
const note = um.api_key && um.api_key !== 'YOUR_API_KEY_HERE'
|
|
225
|
-
? `api_key ${maskKey(um.api_key)}`
|
|
226
|
-
: 'api_key left as placeholder — self-test will not run until it is set';
|
|
227
|
-
console.log(chalk.green(` + autotest config saved (${note})`));
|
|
228
131
|
}
|
|
229
132
|
catch (err) {
|
|
230
133
|
if (isPromptAbort(err))
|
|
231
134
|
abortInit();
|
|
232
|
-
console.log(chalk.yellow('
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* UI Migration Configuration step — configures the GLM key used by the
|
|
237
|
-
* incremental UI-alignment skill's phone-agent. GLM_API_KEY is read from the OS
|
|
238
|
-
* environment at run time (app_feature_verify.py), so it stays in config.env and
|
|
239
|
-
* is persisted as a machine env var by maybePersistMachineEnv.
|
|
240
|
-
*/
|
|
241
|
-
async function promptUiAlignConfig(config) {
|
|
242
|
-
console.log('');
|
|
243
|
-
console.log(chalk.blue(' UI Migration Configuration'));
|
|
244
|
-
console.log(chalk.gray(' GLM_API_KEY: Zhipu GLM key for the UI-alignment phone-agent (read from the OS env at run time).'));
|
|
245
|
-
try {
|
|
246
|
-
const answers = await inquirer.prompt([
|
|
247
|
-
{
|
|
248
|
-
type: 'input',
|
|
249
|
-
name: 'GLM_API_KEY',
|
|
250
|
-
message: 'GLM_API_KEY (LLM API key for the GLM phone-agent used in UI alignment):',
|
|
251
|
-
default: config.env.GLM_API_KEY || undefined,
|
|
252
|
-
},
|
|
253
|
-
]);
|
|
254
|
-
config.env.GLM_API_KEY = answers.GLM_API_KEY.trim();
|
|
255
|
-
await saveHomeTransConfig(config);
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
if (isPromptAbort(err))
|
|
259
|
-
abortInit();
|
|
260
|
-
console.log(chalk.yellow(' UI migration configuration skipped (non-interactive mode).'));
|
|
135
|
+
console.log(chalk.yellow(' Model configuration skipped (non-interactive mode).'));
|
|
261
136
|
}
|
|
262
137
|
}
|
|
263
138
|
async function installSkillsTo(skillsRoot, targetDir, skipNames = new Set()) {
|
|
@@ -288,45 +163,77 @@ async function installSkillsTo(skillsRoot, targetDir, skipNames = new Set()) {
|
|
|
288
163
|
return installed;
|
|
289
164
|
}
|
|
290
165
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
166
|
+
* Named ANSI/agent colors → `#RRGGBB`. DevEco Code and opencode reject the named
|
|
167
|
+
* form (`cyan`, `blue`, …) in agent frontmatter `color:` and only accept hex, so
|
|
168
|
+
* for those editors we rewrite the named value to its hex equivalent instead of
|
|
169
|
+
* dropping it — the agent keeps its color.
|
|
170
|
+
*/
|
|
171
|
+
const AGENT_COLOR_HEX = {
|
|
172
|
+
black: '#000000',
|
|
173
|
+
red: '#ff0000',
|
|
174
|
+
green: '#008000',
|
|
175
|
+
yellow: '#ffff00',
|
|
176
|
+
blue: '#0000ff',
|
|
177
|
+
magenta: '#ff00ff',
|
|
178
|
+
purple: '#800080',
|
|
179
|
+
cyan: '#00ffff',
|
|
180
|
+
white: '#ffffff',
|
|
181
|
+
gray: '#808080',
|
|
182
|
+
grey: '#808080',
|
|
183
|
+
orange: '#ffa500',
|
|
184
|
+
pink: '#ffc0cb',
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Map a named `color:` in a markdown agent's YAML frontmatter to its `#RRGGBB`
|
|
188
|
+
* hex form. Only the leading frontmatter block is touched; an already-hex or
|
|
189
|
+
* unknown color is left untouched, as is a file without frontmatter.
|
|
295
190
|
*/
|
|
296
|
-
export function
|
|
191
|
+
export function mapAgentMarkdownColorToHex(content) {
|
|
297
192
|
const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
298
193
|
if (!fm)
|
|
299
194
|
return content;
|
|
300
195
|
const block = fm[0];
|
|
301
|
-
const
|
|
302
|
-
|
|
196
|
+
const mapped = block.replace(/^(color:[ \t]*)(\S+)([ \t]*\r?\n)/m, (whole, prefix, value, suffix) => {
|
|
197
|
+
const hex = AGENT_COLOR_HEX[value.trim().toLowerCase()];
|
|
198
|
+
return hex ? `${prefix}"${hex}"${suffix}` : whole;
|
|
199
|
+
});
|
|
200
|
+
return mapped === block ? content : content.replace(block, mapped);
|
|
303
201
|
}
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
|
|
202
|
+
/**
|
|
203
|
+
* opencode-style editors (DevEco Code, OpenCode) only accept hex `color:` in
|
|
204
|
+
* agent frontmatter — they're exactly the ones using the OpenCode `{type,
|
|
205
|
+
* command:[]}` MCP format. Identify them by that format so no per-editor config
|
|
206
|
+
* flag is needed.
|
|
207
|
+
*/
|
|
208
|
+
function needsHexAgentColor(editor) {
|
|
209
|
+
return editor.mcp?.format === 'jsonc-command-array';
|
|
210
|
+
}
|
|
211
|
+
/** Copy one agent file, mapping the frontmatter color to hex for `.md` when requested. */
|
|
212
|
+
async function copyAgentFile(src, dest, mapColorToHex) {
|
|
213
|
+
if (mapColorToHex && src.endsWith('.md')) {
|
|
307
214
|
const raw = await fs.readFile(src, 'utf-8');
|
|
308
|
-
await fs.writeFile(dest,
|
|
215
|
+
await fs.writeFile(dest, mapAgentMarkdownColorToHex(raw), 'utf-8');
|
|
309
216
|
}
|
|
310
217
|
else {
|
|
311
218
|
await fs.copyFile(src, dest);
|
|
312
219
|
}
|
|
313
220
|
}
|
|
314
|
-
/** Recursively copy an agent subdirectory,
|
|
315
|
-
async function copyAgentDirRecursive(src, dest,
|
|
221
|
+
/** Recursively copy an agent subdirectory, mapping colors to hex in `.md` files. */
|
|
222
|
+
async function copyAgentDirRecursive(src, dest, mapColorToHex) {
|
|
316
223
|
await fs.mkdir(dest, { recursive: true });
|
|
317
224
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
318
225
|
for (const entry of entries) {
|
|
319
226
|
const srcPath = path.join(src, entry.name);
|
|
320
227
|
const destPath = path.join(dest, entry.name);
|
|
321
228
|
if (entry.isDirectory()) {
|
|
322
|
-
await copyAgentDirRecursive(srcPath, destPath,
|
|
229
|
+
await copyAgentDirRecursive(srcPath, destPath, mapColorToHex);
|
|
323
230
|
}
|
|
324
231
|
else {
|
|
325
|
-
await copyAgentFile(srcPath, destPath,
|
|
232
|
+
await copyAgentFile(srcPath, destPath, mapColorToHex);
|
|
326
233
|
}
|
|
327
234
|
}
|
|
328
235
|
}
|
|
329
|
-
async function installAgentsTo(agentsRoot, targetDir, skipNames = new Set(),
|
|
236
|
+
async function installAgentsTo(agentsRoot, targetDir, skipNames = new Set(), mapColorToHex = false) {
|
|
330
237
|
let entries;
|
|
331
238
|
try {
|
|
332
239
|
entries = await fs.readdir(agentsRoot, { withFileTypes: true });
|
|
@@ -342,10 +249,10 @@ async function installAgentsTo(agentsRoot, targetDir, skipNames = new Set(), str
|
|
|
342
249
|
const srcPath = path.join(agentsRoot, entry.name);
|
|
343
250
|
const destPath = path.join(targetDir, entry.name);
|
|
344
251
|
if (entry.isDirectory()) {
|
|
345
|
-
await copyAgentDirRecursive(srcPath, destPath,
|
|
252
|
+
await copyAgentDirRecursive(srcPath, destPath, mapColorToHex);
|
|
346
253
|
}
|
|
347
254
|
else if (entry.isFile()) {
|
|
348
|
-
await copyAgentFile(srcPath, destPath,
|
|
255
|
+
await copyAgentFile(srcPath, destPath, mapColorToHex);
|
|
349
256
|
if (entry.name.endsWith('.md')) {
|
|
350
257
|
installed.push(entry.name.slice(0, -3));
|
|
351
258
|
}
|
|
@@ -465,7 +372,7 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
465
372
|
result.skipped.push(`${editor.name} existing kept: ${skipSkills.size} skills, ${skipAgents.size} agents (new items installed)`);
|
|
466
373
|
}
|
|
467
374
|
try {
|
|
468
|
-
const agents = await installAgentsTo(agentsRoot, agentsDir, skipAgents, editor
|
|
375
|
+
const agents = await installAgentsTo(agentsRoot, agentsDir, skipAgents, needsHexAgentColor(editor));
|
|
469
376
|
if (agents.length > 0) {
|
|
470
377
|
result.configured.push(`${editor.name} agents (${agents.length} -> ${prettyHome(agentsDir)})`);
|
|
471
378
|
}
|
|
@@ -705,7 +612,7 @@ function evaluateDependency(dep, spec, tools) {
|
|
|
705
612
|
/** Print detected tools + the per-skill/agent impact table. */
|
|
706
613
|
export async function runEnvironmentCheck(env) {
|
|
707
614
|
console.log('');
|
|
708
|
-
console.log(chalk.
|
|
615
|
+
console.log(chalk.cyan(' Environment Check'));
|
|
709
616
|
const spec = await loadEnvRequirements();
|
|
710
617
|
if (!spec)
|
|
711
618
|
return;
|
|
@@ -714,20 +621,21 @@ export async function runEnvironmentCheck(env) {
|
|
|
714
621
|
for (const name of order) {
|
|
715
622
|
const st = tools.get(name);
|
|
716
623
|
const tool = spec.tools[name];
|
|
624
|
+
const label = (tool.label ?? name).padEnd(12);
|
|
717
625
|
if (st.found) {
|
|
718
626
|
let note = st.note ?? '';
|
|
719
627
|
if (st.version && tool.version && !versionInRange(st.version, tool.version)) {
|
|
720
628
|
note += ` (requires ${describeRange(tool.version)})`;
|
|
721
629
|
}
|
|
722
630
|
const noteStr = note ? chalk.gray(` (${note})`) : '';
|
|
723
|
-
console.log(` ${chalk.green('+')} ${
|
|
631
|
+
console.log(` ${chalk.green('+')} ${label} ${st.path ?? ''}${noteStr}`);
|
|
724
632
|
}
|
|
725
633
|
else {
|
|
726
|
-
console.log(` ${chalk.yellow('!')} ${
|
|
634
|
+
console.log(` ${chalk.yellow('!')} ${label} ${chalk.yellow('not found')} ${chalk.gray(tool.hint ?? '')}`);
|
|
727
635
|
}
|
|
728
636
|
}
|
|
729
637
|
console.log('');
|
|
730
|
-
console.log(chalk.
|
|
638
|
+
console.log(chalk.cyan(' Impact on skills/agents:'));
|
|
731
639
|
const nameW = 28;
|
|
732
640
|
console.log(chalk.gray(` ${'Name'.padEnd(nameW)} ${'Kind'.padEnd(6)} ${'Status'.padEnd(8)} Missing`));
|
|
733
641
|
let allOk = true;
|
|
@@ -766,11 +674,8 @@ export async function runEnvironmentCheck(env) {
|
|
|
766
674
|
const noteStr = parts.length > 0 && row.note ? chalk.gray(` — ${row.note}`) : '';
|
|
767
675
|
console.log(` ${row.name.padEnd(nameW)} ${row.kind.padEnd(6)} ${color(plain.padEnd(8))} ${parts.join(', ')}${noteStr}`);
|
|
768
676
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
console.log(chalk.green(' All environment dependencies found — every skill/agent is fully usable.'));
|
|
772
|
-
}
|
|
773
|
-
else {
|
|
677
|
+
if (!allOk) {
|
|
678
|
+
console.log('');
|
|
774
679
|
console.log(chalk.gray(' Install the missing tools above and re-run `ht init` to clear the impact list.'));
|
|
775
680
|
}
|
|
776
681
|
}
|
|
@@ -789,14 +694,14 @@ async function detectInstalledEditors(editors) {
|
|
|
789
694
|
}
|
|
790
695
|
/**
|
|
791
696
|
* Config env keys that are useful as real OS environment variables (the SDK
|
|
792
|
-
* paths + tool dir
|
|
793
|
-
*
|
|
697
|
+
* paths + tool dir). PATH entries for node/java/hdc are not included here —
|
|
698
|
+
* those are documented as manual PATH setup.
|
|
794
699
|
*
|
|
795
|
-
*
|
|
796
|
-
* `autotest` block
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
700
|
+
* The multimodal model config (HOMETRANS_MODEL_*) is included too: its canonical
|
|
701
|
+
* store is the `autotest.unified_model` block, but those four fields are mirrored
|
|
702
|
+
* into config.env by syncModelEnvIntoConfig so this one list is the single source
|
|
703
|
+
* for both config.json's `env` block and the machine env vars. Shared by UI
|
|
704
|
+
* alignment and self-test.
|
|
800
705
|
*/
|
|
801
706
|
export const MACHINE_ENV_KEYS = [
|
|
802
707
|
'DEVECO_SDK_HOME',
|
|
@@ -804,7 +709,9 @@ export const MACHINE_ENV_KEYS = [
|
|
|
804
709
|
'OHOS_SDK_PATH',
|
|
805
710
|
'HMS_SDK_PATH',
|
|
806
711
|
'HOMETRANS_TOOL_PATH',
|
|
807
|
-
'
|
|
712
|
+
'HOMETRANS_MODEL_API_KEY',
|
|
713
|
+
'HOMETRANS_MODEL_NAME',
|
|
714
|
+
'HOMETRANS_MODEL_BASE_URL',
|
|
808
715
|
];
|
|
809
716
|
/** Mask API-key values for display; show paths in full. */
|
|
810
717
|
function maskEnvValue(name, value) {
|
|
@@ -816,26 +723,43 @@ function maskEnvValue(name, value) {
|
|
|
816
723
|
/**
|
|
817
724
|
* Show the resolved config values that would become machine environment
|
|
818
725
|
* variables, then ask Y/N before writing them (Windows: setx; macOS/Linux:
|
|
819
|
-
* shell rc block).
|
|
820
|
-
*
|
|
726
|
+
* shell rc block). Variables already present in the OS environment with the
|
|
727
|
+
* exact same value are skipped — they are neither shown nor re-written, and if
|
|
728
|
+
* nothing needs updating the prompt is skipped entirely. Non-interactive shells
|
|
729
|
+
* skip without changing anything. Re-throws ExitPromptError so the caller can
|
|
730
|
+
* abort init on Ctrl-C.
|
|
821
731
|
*/
|
|
822
|
-
async function maybePersistMachineEnv(
|
|
823
|
-
|
|
824
|
-
|
|
732
|
+
async function maybePersistMachineEnv(config) {
|
|
733
|
+
// Refresh the mirrored HOMETRANS_MODEL_* in config.env from autotest.unified_model
|
|
734
|
+
// (the Model Configuration step above just edited the latter), so config.env is
|
|
735
|
+
// the single source for both config.json and the machine env vars below.
|
|
736
|
+
syncModelEnvIntoConfig(config);
|
|
737
|
+
const env = config.env;
|
|
738
|
+
const allVars = MACHINE_ENV_KEYS.map((k) => [k, (env[k] ?? '').trim()]).filter(([, value]) => value.length > 0);
|
|
739
|
+
if (allVars.length === 0)
|
|
740
|
+
return;
|
|
741
|
+
// Skip variables already set in the OS environment with the same value — no
|
|
742
|
+
// need to re-write them or re-ask. process.env reflects the environment this
|
|
743
|
+
// process launched with, so a prior `ht init` (seen from a fresh terminal)
|
|
744
|
+
// already shows up here.
|
|
745
|
+
const sameValue = ([name, value]) => (process.env[name] ?? '').trim() === value;
|
|
746
|
+
const alreadySet = allVars.filter(sameValue);
|
|
747
|
+
const vars = allVars.filter((v) => !sameValue(v));
|
|
748
|
+
if (vars.length === 0) {
|
|
749
|
+
console.log('');
|
|
750
|
+
console.log(chalk.green(' + Machine environment variables already set with the current values — nothing to add.'));
|
|
825
751
|
return;
|
|
752
|
+
}
|
|
826
753
|
console.log('');
|
|
827
|
-
console.log(chalk.blue(' Add these to your machine environment variables?'));
|
|
828
|
-
console.log(chalk.gray(process.platform === 'win32'
|
|
829
|
-
? ' (Windows: persisted via setx to your user environment)'
|
|
830
|
-
: ' (macOS/Linux: written as an export block to your shell profile)'));
|
|
754
|
+
console.log(chalk.blue(' Add these to your machine environment variables? Skills and MCP tools read these from the OS environment at runtime.'));
|
|
831
755
|
for (const [name, value] of vars) {
|
|
832
756
|
console.log(` ${chalk.cyan(name)} = ${maskEnvValue(name, value)}`);
|
|
833
757
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
758
|
+
if (alreadySet.length > 0) {
|
|
759
|
+
console.log(chalk.gray(` (${alreadySet.length} already set with the same value — skipped: ${alreadySet
|
|
760
|
+
.map(([name]) => name)
|
|
761
|
+
.join(', ')})`));
|
|
762
|
+
}
|
|
839
763
|
let confirmed;
|
|
840
764
|
try {
|
|
841
765
|
const answers = await inquirer.prompt([
|
|
@@ -863,6 +787,254 @@ async function maybePersistMachineEnv(env) {
|
|
|
863
787
|
const note = await persistEnvVars(vars);
|
|
864
788
|
console.log(chalk.green(` + Environment variables ${note}`));
|
|
865
789
|
}
|
|
790
|
+
/**
|
|
791
|
+
* Run `homegraph --version`. Returns the trimmed first output line on success
|
|
792
|
+
* (an empty string still counts as success — exit 0), or null when the command
|
|
793
|
+
* is missing or exits non-zero. On Windows the global npm bin is a `.cmd` shim,
|
|
794
|
+
* so the command is run through the shell there.
|
|
795
|
+
*/
|
|
796
|
+
function homegraphVersion() {
|
|
797
|
+
try {
|
|
798
|
+
const out = execFileSync('homegraph', ['--version'], {
|
|
799
|
+
encoding: 'utf-8',
|
|
800
|
+
timeout: 15000,
|
|
801
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
802
|
+
shell: process.platform === 'win32',
|
|
803
|
+
});
|
|
804
|
+
return (out ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? '';
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Run `uv --version`. Returns the trimmed first output line on success (exit 0),
|
|
812
|
+
* or null when uv is missing / exits non-zero. Probes `uv` on PATH first, then
|
|
813
|
+
* the standalone installer's default location (`~/.local/bin/uv[.exe]`) — that
|
|
814
|
+
* binary exists right after a fresh install, before the current process PATH is
|
|
815
|
+
* refreshed, so verification works within the same `ht init` run.
|
|
816
|
+
*/
|
|
817
|
+
function uvVersion() {
|
|
818
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
819
|
+
const isWin = process.platform === 'win32';
|
|
820
|
+
const candidates = ['uv'];
|
|
821
|
+
if (home)
|
|
822
|
+
candidates.push(path.join(home, '.local', 'bin', isWin ? 'uv.exe' : 'uv'));
|
|
823
|
+
for (const cmd of candidates) {
|
|
824
|
+
try {
|
|
825
|
+
const out = execFileSync(cmd, ['--version'], {
|
|
826
|
+
encoding: 'utf-8',
|
|
827
|
+
timeout: 15000,
|
|
828
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
829
|
+
});
|
|
830
|
+
return (out ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? '';
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
// try next candidate
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Node-CLI part of the Dependency Installation step — installs the global Node
|
|
840
|
+
* CLIs the spec-generation skill needs (step 2「生成需求规格」): `homegraph` plus
|
|
841
|
+
* `arkanalyzer` (arkanalyzer parses ArkTS `.ets` and must be installed globally
|
|
842
|
+
* alongside homegraph). Verifies the result with `homegraph --version`.
|
|
843
|
+
* Idempotent: if homegraph already reports a version, the install is skipped.
|
|
844
|
+
* Never throws — failures are reported and `ht init` continues. The
|
|
845
|
+
* "Dependency Installation" section header is printed once by the caller.
|
|
846
|
+
*/
|
|
847
|
+
async function installNodeDependencies() {
|
|
848
|
+
console.log(chalk.gray(' homegraph + arkanalyzer'));
|
|
849
|
+
// Idempotent: a working `homegraph --version` means the deps are already there.
|
|
850
|
+
const existing = homegraphVersion();
|
|
851
|
+
if (existing !== null) {
|
|
852
|
+
console.log(chalk.green(` + homegraph already installed (${existing || 'version unknown'}) — skipping.`));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (!whichSync('npm')) {
|
|
856
|
+
console.log(chalk.yellow(' ! npm not found on PATH — cannot install homegraph / arkanalyzer.'));
|
|
857
|
+
console.log(chalk.gray(' Install Node.js (bundles npm), then run: npm install -g homegraph arkanalyzer'));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
let proceed = true;
|
|
861
|
+
try {
|
|
862
|
+
const answers = await inquirer.prompt([
|
|
863
|
+
{
|
|
864
|
+
type: 'confirm',
|
|
865
|
+
name: 'install',
|
|
866
|
+
message: 'Install homegraph + arkanalyzer globally now (npm install -g homegraph arkanalyzer)?',
|
|
867
|
+
default: true,
|
|
868
|
+
},
|
|
869
|
+
]);
|
|
870
|
+
proceed = answers.install;
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
if (isPromptAbort(err))
|
|
874
|
+
abortInit();
|
|
875
|
+
console.log(chalk.yellow(' Non-interactive: proceeding with install.'));
|
|
876
|
+
}
|
|
877
|
+
if (!proceed) {
|
|
878
|
+
console.log(chalk.yellow(' Skipped — install later with:'));
|
|
879
|
+
console.log(chalk.gray(' npm install -g homegraph arkanalyzer'));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
console.log(chalk.gray(' Running: npm install -g homegraph arkanalyzer ...'));
|
|
883
|
+
try {
|
|
884
|
+
// Use the bare command name (not a resolved path) so the Windows shell can
|
|
885
|
+
// resolve the `npm.cmd` shim; shell:true is required on Windows for that.
|
|
886
|
+
execFileSync('npm', ['install', '-g', 'homegraph', 'arkanalyzer'], {
|
|
887
|
+
stdio: 'inherit',
|
|
888
|
+
timeout: 5 * 60 * 1000,
|
|
889
|
+
shell: process.platform === 'win32',
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
console.log(chalk.red(` ! npm install failed: ${err.message}`));
|
|
894
|
+
console.log(chalk.gray(' Re-run manually: npm install -g homegraph arkanalyzer'));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
// Verify install: `homegraph --version` must return normally (exit 0).
|
|
898
|
+
const version = homegraphVersion();
|
|
899
|
+
if (version !== null) {
|
|
900
|
+
console.log(chalk.green(` + homegraph installed & verified (${version || 'version unknown'}).`));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
console.log(chalk.yellow(' ! Installed, but `homegraph --version` did not return successfully.'));
|
|
904
|
+
console.log(chalk.gray(' Open a new terminal (PATH refresh), then verify: homegraph --version'));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* uv part of the Dependency Installation step — uv is the only Python toolchain
|
|
909
|
+
* HomeTrans needs: every Python script runs via `uv run`, and uv provides the
|
|
910
|
+
* Python interpreter on demand (no standalone Python install required). Uses the
|
|
911
|
+
* official standalone installer (https://docs.astral.sh/uv/getting-started/installation/):
|
|
912
|
+
* - Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
913
|
+
* - macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh (wget fallback)
|
|
914
|
+
* Idempotent: a working `uv --version` skips the install. Never throws —
|
|
915
|
+
* failures are reported and `ht init` continues. The "Dependency Installation"
|
|
916
|
+
* section header is printed once by the caller.
|
|
917
|
+
*/
|
|
918
|
+
async function installUv() {
|
|
919
|
+
console.log(chalk.gray(' uv'));
|
|
920
|
+
// Idempotent: a working `uv --version` means uv is already there.
|
|
921
|
+
const existing = uvVersion();
|
|
922
|
+
if (existing !== null) {
|
|
923
|
+
console.log(chalk.green(` + uv already installed (${existing || 'version unknown'}) — skipping.`));
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const isWin = process.platform === 'win32';
|
|
927
|
+
const installCmdText = isWin
|
|
928
|
+
? 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'
|
|
929
|
+
: 'curl -LsSf https://astral.sh/uv/install.sh | sh';
|
|
930
|
+
const docs = 'https://docs.astral.sh/uv/getting-started/installation/';
|
|
931
|
+
let proceed = true;
|
|
932
|
+
try {
|
|
933
|
+
const answers = await inquirer.prompt([
|
|
934
|
+
{
|
|
935
|
+
type: 'confirm',
|
|
936
|
+
name: 'install',
|
|
937
|
+
message: `uv not found. Install it now via the official installer?\n ${installCmdText}`,
|
|
938
|
+
default: true,
|
|
939
|
+
},
|
|
940
|
+
]);
|
|
941
|
+
proceed = answers.install;
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
if (isPromptAbort(err))
|
|
945
|
+
abortInit();
|
|
946
|
+
console.log(chalk.yellow(' Non-interactive: proceeding with install.'));
|
|
947
|
+
}
|
|
948
|
+
if (!proceed) {
|
|
949
|
+
console.log(chalk.yellow(' Skipped — install later with:'));
|
|
950
|
+
console.log(chalk.gray(` ${installCmdText}`));
|
|
951
|
+
console.log(chalk.gray(` Docs: ${docs}`));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
console.log(chalk.gray(` Running: ${installCmdText} ...`));
|
|
955
|
+
try {
|
|
956
|
+
if (isWin) {
|
|
957
|
+
execFileSync('powershell', ['-ExecutionPolicy', 'ByPass', '-c', 'irm https://astral.sh/uv/install.ps1 | iex'], { stdio: 'inherit', timeout: 5 * 60 * 1000 });
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// `curl | sh` needs a shell to pipe; prefer curl, fall back to wget so the
|
|
961
|
+
// installer also works on minimal systems that ship only one of them.
|
|
962
|
+
const script = 'if command -v curl >/dev/null 2>&1; then curl -LsSf https://astral.sh/uv/install.sh | sh; ' +
|
|
963
|
+
'else wget -qO- https://astral.sh/uv/install.sh | sh; fi';
|
|
964
|
+
execFileSync('sh', ['-c', script], { stdio: 'inherit', timeout: 5 * 60 * 1000 });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
console.log(chalk.red(` ! uv install failed: ${err.message}`));
|
|
969
|
+
console.log(chalk.gray(` Re-run manually: ${installCmdText}`));
|
|
970
|
+
console.log(chalk.gray(` Docs: ${docs}`));
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
// Verify install: `uv --version` must return (PATH or standalone-install path).
|
|
974
|
+
const version = uvVersion();
|
|
975
|
+
if (version !== null) {
|
|
976
|
+
console.log(chalk.green(` + uv installed & verified (${version || 'version unknown'}).`));
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
console.log(chalk.yellow(' ! Installed, but `uv --version` did not return successfully.'));
|
|
980
|
+
console.log(chalk.gray(' Open a new terminal (PATH refresh), then verify: uv --version'));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Editor name (as in config.json / defaultEditors) → homegraph `install --target`
|
|
985
|
+
* slug. homegraph ships its own MCP-into-editor installer; after the homegraph CLI
|
|
986
|
+
* is installed globally we invoke it so the homegraph MCP server is registered into
|
|
987
|
+
* the same editors HomeTrans is configuring. Scope intentionally matches init's
|
|
988
|
+
* supported editors — editors without a mapping (a user-added custom editor, or one
|
|
989
|
+
* homegraph doesn't support) are simply skipped.
|
|
990
|
+
*/
|
|
991
|
+
const HOMEGRAPH_TARGET_BY_EDITOR = {
|
|
992
|
+
'Claude Code': 'claude',
|
|
993
|
+
Cursor: 'cursor',
|
|
994
|
+
Codex: 'codex',
|
|
995
|
+
OpenCode: 'opencode',
|
|
996
|
+
'DevEco Code': 'deveco',
|
|
997
|
+
CodeBuddy: 'codebuddy',
|
|
998
|
+
};
|
|
999
|
+
/**
|
|
1000
|
+
* Register the homegraph MCP server into the selected editors via the homegraph
|
|
1001
|
+
* CLI (`homegraph install --target=<...> --location=global --yes`). Runs after
|
|
1002
|
+
* installNodeDependencies() so the homegraph binary is present. A single invocation
|
|
1003
|
+
* with a comma-joined target list configures every selected editor at once. Never
|
|
1004
|
+
* throws — failures are reported and `ht init` continues. On Windows the global npm
|
|
1005
|
+
* bin is a `.cmd` shim, so the command runs through the shell there (same as
|
|
1006
|
+
* homegraphVersion()).
|
|
1007
|
+
*/
|
|
1008
|
+
async function installHomegraphToEditors(selectedEditors) {
|
|
1009
|
+
const targets = selectedEditors
|
|
1010
|
+
.map((name) => HOMEGRAPH_TARGET_BY_EDITOR[name])
|
|
1011
|
+
.filter((t) => Boolean(t));
|
|
1012
|
+
if (targets.length === 0)
|
|
1013
|
+
return;
|
|
1014
|
+
const targetArg = `--target=${targets.join(',')}`;
|
|
1015
|
+
console.log(chalk.gray(' homegraph MCP -> editors'));
|
|
1016
|
+
// Needs the homegraph CLI on PATH. A fresh `npm install -g` may not be visible
|
|
1017
|
+
// until a PATH refresh, but homegraphVersion() probes through the shell so the
|
|
1018
|
+
// shim is usually resolvable within the same `ht init` run.
|
|
1019
|
+
if (homegraphVersion() === null) {
|
|
1020
|
+
console.log(chalk.yellow(' ! homegraph not available — skipping MCP registration into editors.'));
|
|
1021
|
+
console.log(chalk.gray(` After homegraph is installed, run: homegraph install ${targetArg} --location=global --yes`));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
console.log(chalk.gray(` Running: homegraph install ${targetArg} --location=global --yes ...`));
|
|
1025
|
+
try {
|
|
1026
|
+
execFileSync('homegraph', ['install', targetArg, '--location=global', '--yes'], {
|
|
1027
|
+
stdio: 'inherit',
|
|
1028
|
+
timeout: 5 * 60 * 1000,
|
|
1029
|
+
shell: process.platform === 'win32',
|
|
1030
|
+
});
|
|
1031
|
+
console.log(chalk.green(` + homegraph MCP registered into: ${targets.join(', ')}`));
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
console.log(chalk.red(` ! homegraph install failed: ${err.message}`));
|
|
1035
|
+
console.log(chalk.gray(` Re-run manually: homegraph install ${targetArg} --location=global --yes`));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
866
1038
|
export async function initCommand(options = {}) {
|
|
867
1039
|
ensureChalkColor();
|
|
868
1040
|
const banner = figlet.textSync('HomeTrans', { font: 'Standard' });
|
|
@@ -884,7 +1056,7 @@ export async function initCommand(options = {}) {
|
|
|
884
1056
|
// Generate an initial ~/.hometrans/config.json up front (first-use scenario):
|
|
885
1057
|
// loadHomeTransConfig writes a complete default file (editors + env + autotest)
|
|
886
1058
|
// when none exists, and saveHomeTransConfig flushes it again to be certain it is
|
|
887
|
-
// on disk before any later step (e.g.
|
|
1059
|
+
// on disk before any later step (e.g. Model Configuration) points the user at it.
|
|
888
1060
|
const configPath = getConfigPath();
|
|
889
1061
|
const configExisted = await pathExists(configPath);
|
|
890
1062
|
const config = await loadHomeTransConfig();
|
|
@@ -948,8 +1120,7 @@ export async function initCommand(options = {}) {
|
|
|
948
1120
|
}
|
|
949
1121
|
// --- User parameters (SDK paths) ---
|
|
950
1122
|
console.log('');
|
|
951
|
-
console.log(chalk.blue('
|
|
952
|
-
console.log(chalk.gray(' (press Enter to keep current value or leave empty)\n'));
|
|
1123
|
+
console.log(chalk.blue(' Harmony SDK Configuration'));
|
|
953
1124
|
try {
|
|
954
1125
|
const answers = await inquirer.prompt([
|
|
955
1126
|
{
|
|
@@ -974,16 +1145,12 @@ export async function initCommand(options = {}) {
|
|
|
974
1145
|
['OHOS_SDK_PATH', sdk.OHOS_SDK_PATH],
|
|
975
1146
|
['HMS_SDK_PATH', sdk.HMS_SDK_PATH],
|
|
976
1147
|
];
|
|
977
|
-
|
|
978
|
-
|
|
1148
|
+
// Validate the derived paths on disk silently — the listing itself is not
|
|
1149
|
+
// printed; only a warning surfaces when one or more paths fail to resolve.
|
|
979
1150
|
let missingCount = 0;
|
|
980
|
-
for (const [
|
|
981
|
-
|
|
982
|
-
if (!exists)
|
|
1151
|
+
for (const [, p] of checks) {
|
|
1152
|
+
if (!(await dirExists(p)))
|
|
983
1153
|
missingCount++;
|
|
984
|
-
const mark = exists ? chalk.green('+') : chalk.yellow('!');
|
|
985
|
-
const note = exists ? '' : chalk.yellow(' (not found on disk)');
|
|
986
|
-
console.log(` ${mark} ${name.padEnd(15)} : ${p}${note}`);
|
|
987
1154
|
}
|
|
988
1155
|
if (missingCount > 0) {
|
|
989
1156
|
console.log('');
|
|
@@ -998,15 +1165,46 @@ export async function initCommand(options = {}) {
|
|
|
998
1165
|
abortInit();
|
|
999
1166
|
console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
|
|
1000
1167
|
}
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
//
|
|
1006
|
-
// the
|
|
1007
|
-
|
|
1008
|
-
//
|
|
1168
|
+
// Model Configuration — configure the single multimodal LLM shared by UI
|
|
1169
|
+
// alignment and self-test. Stored in autotest.unified_model (config.json) and
|
|
1170
|
+
// exported as HOMETRANS_MODEL_* env vars by maybePersistMachineEnv below.
|
|
1171
|
+
await promptModelConfig(config);
|
|
1172
|
+
// Dependency Installation — auto-provisionable deps: the global npm CLIs the
|
|
1173
|
+
// spec-generation skill needs (homegraph + arkanalyzer) and uv (the Python
|
|
1174
|
+
// toolchain every script runs under). The section header is printed once here;
|
|
1175
|
+
// each sub-install prints its own gray sub-line. Runs before the Environment
|
|
1176
|
+
// Check so the check reflects anything freshly installed.
|
|
1177
|
+
console.log('');
|
|
1178
|
+
console.log(chalk.cyan(' Dependency Installation'));
|
|
1179
|
+
try {
|
|
1180
|
+
await installNodeDependencies();
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
if (isPromptAbort(err))
|
|
1184
|
+
abortInit();
|
|
1185
|
+
console.log(chalk.yellow(` ! homegraph / arkanalyzer install skipped: ${err.message}`));
|
|
1186
|
+
}
|
|
1187
|
+
// With homegraph installed, register its MCP server into the editors the user
|
|
1188
|
+
// selected, using homegraph's own installer. Scoped to the selected editors so
|
|
1189
|
+
// it stays consistent with what HomeTrans configures below.
|
|
1190
|
+
try {
|
|
1191
|
+
await installHomegraphToEditors(selectedEditors);
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
console.log(chalk.yellow(` ! homegraph editor registration skipped: ${err.message}`));
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
await installUv();
|
|
1198
|
+
}
|
|
1199
|
+
catch (err) {
|
|
1200
|
+
if (isPromptAbort(err))
|
|
1201
|
+
abortInit();
|
|
1202
|
+
console.log(chalk.yellow(` ! uv install skipped: ${err.message}`));
|
|
1203
|
+
}
|
|
1204
|
+
// Detect external tools (adb / hdc / uv / java / homegraph + DevEco)
|
|
1009
1205
|
// and report which skills/agents are impacted by anything missing.
|
|
1206
|
+
// (Python is no longer detected separately — every script runs via `uv run`,
|
|
1207
|
+
// and uv provides the interpreter on demand.)
|
|
1010
1208
|
try {
|
|
1011
1209
|
await runEnvironmentCheck(config.env);
|
|
1012
1210
|
}
|
|
@@ -1015,36 +1213,21 @@ export async function initCommand(options = {}) {
|
|
|
1015
1213
|
}
|
|
1016
1214
|
// Copy bundled tools/ into ~/.hometrans/tools and record env.HOMETRANS_TOOL_PATH.
|
|
1017
1215
|
// self_test_runner.py (under the installed tools dir) reads the autotest
|
|
1018
|
-
// config from ~/.hometrans/config.json at run time — see
|
|
1019
|
-
let installedToolPath = null;
|
|
1216
|
+
// config from ~/.hometrans/config.json at run time — see promptModelConfig.
|
|
1020
1217
|
try {
|
|
1021
|
-
|
|
1022
|
-
if (installedToolPath) {
|
|
1023
|
-
console.log(chalk.green(` + tools copied -> ${prettyHome(installedToolPath)}`));
|
|
1024
|
-
console.log(chalk.gray(` HOMETRANS_TOOL_PATH set in ${prettyHome(getConfigPath())}`));
|
|
1025
|
-
}
|
|
1218
|
+
await installTools(toolsRoot, config);
|
|
1026
1219
|
}
|
|
1027
1220
|
catch (err) {
|
|
1028
1221
|
console.log(chalk.red(` ! tools copy: ${err.message}`));
|
|
1029
1222
|
}
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
const envReqDest = await installEnvRequirements();
|
|
1034
|
-
if (envReqDest) {
|
|
1035
|
-
console.log(chalk.green(` + env-requirements.json copied -> ${prettyHome(envReqDest)}`));
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
catch (err) {
|
|
1039
|
-
console.log(chalk.red(` ! env-requirements.json copy: ${err.message}`));
|
|
1040
|
-
}
|
|
1041
|
-
// (The autotest block in config.json was already configured by the Test
|
|
1042
|
-
// Configuration step above; self_test_runner.py reads it at run time.)
|
|
1223
|
+
// (The autotest.unified_model block in config.json was already configured by
|
|
1224
|
+
// the Model Configuration step above; self_test_runner.py reads it at run time
|
|
1225
|
+
// and the UI-alignment phone-agent reads the exported HOMETRANS_MODEL_* vars.)
|
|
1043
1226
|
// Offer to persist the resolved config values as real OS environment
|
|
1044
1227
|
// variables (Windows: setx; macOS/Linux: shell rc block). Shows what will be
|
|
1045
1228
|
// written and asks Y/N first; runs after HOMETRANS_TOOL_PATH is finalized above.
|
|
1046
1229
|
try {
|
|
1047
|
-
await maybePersistMachineEnv(config
|
|
1230
|
+
await maybePersistMachineEnv(config);
|
|
1048
1231
|
}
|
|
1049
1232
|
catch (err) {
|
|
1050
1233
|
if (isPromptAbort(err))
|
|
@@ -1055,7 +1238,7 @@ export async function initCommand(options = {}) {
|
|
|
1055
1238
|
const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
|
|
1056
1239
|
const result = { configured: [], skipped: [], errors: [] };
|
|
1057
1240
|
for (const editor of editorsToSetup) {
|
|
1058
|
-
console.log(chalk.
|
|
1241
|
+
console.log(chalk.cyan(` Configuring ${editor.name}...`));
|
|
1059
1242
|
await installForEditor(editor, skillsRoot, agentsRoot, result);
|
|
1060
1243
|
}
|
|
1061
1244
|
await setupMcpForAllEditors(editorsToSetup, result);
|