@buaa_smat/hometrans 0.1.15 → 0.1.16

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.
Files changed (35) hide show
  1. package/README.md +54 -89
  2. package/agents/build-fixer.md +15 -14
  3. package/agents/code-reviewer.md +3 -3
  4. package/agents/logic-coder.md +1 -1
  5. package/agents/logic-context-builder.md +1 -1
  6. package/agents/self-tester.md +16 -13
  7. package/dist/cli/config-store.js +127 -29
  8. package/dist/cli/config.js +9 -2
  9. package/dist/cli/init.js +367 -248
  10. package/dist/cli/uninstall.js +12 -7
  11. package/dist/context/index.js +19 -2
  12. package/env-requirements.json +19 -23
  13. package/package.json +1 -1
  14. package/resource/choose_editor.png +0 -0
  15. package/resource/set_environment.png +0 -0
  16. package/resource/set_multimodel.png +0 -0
  17. package/resource/set_sdk.png +0 -0
  18. package/skills/hmos-batch-ui-align/SKILL.md +6 -6
  19. package/skills/hmos-convert-pipeline/SKILL.md +7 -7
  20. package/skills/hmos-fix-build-errors/SKILL.md +4 -3
  21. package/skills/hmos-incremental-ui-align/README.md +9 -12
  22. package/skills/hmos-incremental-ui-align/SKILL.md +9 -9
  23. package/skills/hmos-incremental-ui-align/scripts/__pycache__/app_feature_verify.cpython-314.pyc +0 -0
  24. package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.py +128 -29
  25. package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +37 -37
  26. package/skills/hmos-incremental-ui-align/scripts/page_capture.py +7 -2
  27. package/skills/hmos-integration-test/README.md +3 -3
  28. package/skills/hmos-integration-test/SKILL.md +7 -7
  29. package/skills/hmos-spec-generate/SKILL.md +45 -52
  30. package/tools/test-tools/autotest/README.md +10 -11
  31. package/tools/test-tools/autotest/self_test_runner.py +40 -12
  32. package/resource/common_config.png +0 -0
  33. package/resource/integration_test_config.png +0 -0
  34. package/resource/set_env.png +0 -0
  35. package/resource/ui_align_config.png +0 -0
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
- /**
132
- * Render the current/default autotest block as a table so the user sees every
133
- * field, its current/default value, and a short description. Section label shows
134
- * only on the first row of each group for readability.
135
- */
136
- function printAutotestTable(at) {
137
- const fmt = (v) => (v === null ? 'null' : String(v));
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()}`));
197
- console.log('');
198
- try {
199
- const answers = await inquirer.prompt([
200
- {
201
- type: 'input',
202
- name: 'api_key',
203
- message: 'autotest api_key (LLM key the self-test agent uses to run test cases):',
204
- default: um.api_key && um.api_key !== 'YOUR_API_KEY_HERE' ? um.api_key : undefined,
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,
213
- },
214
- ]);
215
- if (answers.api_key.trim())
216
- um.api_key = answers.api_key.trim();
217
- if (answers.name.trim())
218
- um.name = answers.name.trim();
219
- if (answers.base_url.trim())
220
- um.base_url = answers.base_url.trim();
221
- if (answers.provider.trim())
222
- um.provider = answers.provider.trim();
223
- 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
- }
229
- catch (err) {
230
- if (isPromptAbort(err))
231
- abortInit();
232
- console.log(chalk.yellow(' Test configuration skipped (non-interactive mode).'));
233
- }
234
- }
235
91
  /**
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.
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.
240
103
  */
241
- async function promptUiAlignConfig(config) {
104
+ async function promptModelConfig(config) {
105
+ const um = config.autotest.unified_model;
242
106
  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).'));
107
+ console.log(chalk.blue(' MultiModel LLM Configuration'));
245
108
  try {
246
109
  const answers = await inquirer.prompt([
247
110
  {
248
111
  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,
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,
252
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 },
253
118
  ]);
254
- config.env.GLM_API_KEY = answers.GLM_API_KEY.trim();
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);
255
130
  await saveHomeTransConfig(config);
256
131
  }
257
132
  catch (err) {
258
133
  if (isPromptAbort(err))
259
134
  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
- * Strip the `color:` line from a markdown agent's YAML frontmatter. DevEco Code's
292
- * config schema rejects named colors (only hex `#RRGGBB` or a theme token is
293
- * allowed) and aborts startup, so for that editor we drop the field entirely
294
- * agents work fine without it. Only the leading frontmatter block is touched.
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 stripAgentMarkdownColor(content) {
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 stripped = block.replace(/^color:[ \t]*\S.*\r?\n/m, '');
302
- return stripped === block ? content : content.replace(block, stripped);
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
- /** Copy one agent file, stripping the frontmatter color from `.md` when requested. */
305
- async function copyAgentFile(src, dest, stripColor) {
306
- if (stripColor && src.endsWith('.md')) {
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, stripAgentMarkdownColor(raw), 'utf-8');
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, stripping colors from `.md` files. */
315
- async function copyAgentDirRecursive(src, dest, stripColor) {
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, stripColor);
229
+ await copyAgentDirRecursive(srcPath, destPath, mapColorToHex);
323
230
  }
324
231
  else {
325
- await copyAgentFile(srcPath, destPath, stripColor);
232
+ await copyAgentFile(srcPath, destPath, mapColorToHex);
326
233
  }
327
234
  }
328
235
  }
329
- async function installAgentsTo(agentsRoot, targetDir, skipNames = new Set(), stripColor = false) {
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, stripColor);
252
+ await copyAgentDirRecursive(srcPath, destPath, mapColorToHex);
346
253
  }
347
254
  else if (entry.isFile()) {
348
- await copyAgentFile(srcPath, destPath, stripColor);
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.stripAgentColors ?? false);
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.blue(' Environment Check'));
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('+')} ${name.padEnd(9)} ${st.path ?? ''}${noteStr}`);
631
+ console.log(` ${chalk.green('+')} ${label} ${st.path ?? ''}${noteStr}`);
724
632
  }
725
633
  else {
726
- console.log(` ${chalk.yellow('!')} ${name.padEnd(9)} ${chalk.yellow('not found')} ${chalk.gray(tool.hint ?? '')}`);
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.blue(' Impact on skills/agents:'));
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
- console.log('');
770
- if (allOk) {
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 + GLM key). PATH entries for node/java/hdc are not included
793
- * here — those are documented as manual PATH setup.
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
- * TEST_API_KEY is intentionally absent: the self-test api_key now lives in the
796
- * `autotest` block of config.json (configured by the Test Configuration step)
797
- * and is read from there by self_test_runner.py never from the system env.
798
- * GLM_API_KEY stays, because the UI-alignment phone-agent reads it from the OS
799
- * environment at run time.
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
- 'GLM_API_KEY',
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). Non-interactive shells skip without changing anything.
820
- * Re-throws ExitPromptError so the caller can abort init on Ctrl-C.
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(env) {
823
- const vars = MACHINE_ENV_KEYS.map((k) => [k, (env[k] ?? '').trim()]).filter(([, value]) => value.length > 0);
824
- if (vars.length === 0)
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
- console.log('');
835
- console.log(chalk.yellow(' Recommended: skills and MCP tools read these from the OS environment at runtime.'));
836
- console.log(chalk.gray(' If you choose No, they are NOT set as system variables — skills/tools that rely on\n' +
837
- ' them (self-test, UI alignment, commit-context analysis) may fail until you set them\n' +
838
- ' yourself or re-run `ht init`.'));
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,199 @@ 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
+ }
866
983
  export async function initCommand(options = {}) {
867
984
  ensureChalkColor();
868
985
  const banner = figlet.textSync('HomeTrans', { font: 'Standard' });
@@ -884,7 +1001,7 @@ export async function initCommand(options = {}) {
884
1001
  // Generate an initial ~/.hometrans/config.json up front (first-use scenario):
885
1002
  // loadHomeTransConfig writes a complete default file (editors + env + autotest)
886
1003
  // when none exists, and saveHomeTransConfig flushes it again to be certain it is
887
- // on disk before any later step (e.g. Test Configuration) points the user at it.
1004
+ // on disk before any later step (e.g. Model Configuration) points the user at it.
888
1005
  const configPath = getConfigPath();
889
1006
  const configExisted = await pathExists(configPath);
890
1007
  const config = await loadHomeTransConfig();
@@ -948,8 +1065,7 @@ export async function initCommand(options = {}) {
948
1065
  }
949
1066
  // --- User parameters (SDK paths) ---
950
1067
  console.log('');
951
- console.log(chalk.blue(' Parameter Configuration'));
952
- console.log(chalk.gray(' (press Enter to keep current value or leave empty)\n'));
1068
+ console.log(chalk.blue(' Harmony SDK Configuration'));
953
1069
  try {
954
1070
  const answers = await inquirer.prompt([
955
1071
  {
@@ -974,16 +1090,12 @@ export async function initCommand(options = {}) {
974
1090
  ['OHOS_SDK_PATH', sdk.OHOS_SDK_PATH],
975
1091
  ['HMS_SDK_PATH', sdk.HMS_SDK_PATH],
976
1092
  ];
977
- console.log('');
978
- console.log(chalk.blue(' Derived from DEVECO_SDK_HOME:'));
1093
+ // Validate the derived paths on disk silently — the listing itself is not
1094
+ // printed; only a warning surfaces when one or more paths fail to resolve.
979
1095
  let missingCount = 0;
980
- for (const [name, p] of checks) {
981
- const exists = await dirExists(p);
982
- if (!exists)
1096
+ for (const [, p] of checks) {
1097
+ if (!(await dirExists(p)))
983
1098
  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
1099
  }
988
1100
  if (missingCount > 0) {
989
1101
  console.log('');
@@ -998,15 +1110,37 @@ export async function initCommand(options = {}) {
998
1110
  abortInit();
999
1111
  console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
1000
1112
  }
1001
- // UI Migration Configuration — configure the GLM key for the UI-alignment
1002
- // phone-agent. Stays in config.env (read from the OS env at run time).
1003
- await promptUiAlignConfig(config);
1004
- // Test Configuration — configure the autotest block (self-test api_key etc.)
1005
- // in config.json. Replaces the old TEST_API_KEY env prompt; never written to
1006
- // the system environment.
1007
- await promptTestConfig(config);
1008
- // Detect external tools (adb / hdc / python / uv / java / gitnexus + DevEco)
1113
+ // Model Configuration — configure the single multimodal LLM shared by UI
1114
+ // alignment and self-test. Stored in autotest.unified_model (config.json) and
1115
+ // exported as HOMETRANS_MODEL_* env vars by maybePersistMachineEnv below.
1116
+ await promptModelConfig(config);
1117
+ // Dependency Installation auto-provisionable deps: the global npm CLIs the
1118
+ // spec-generation skill needs (homegraph + arkanalyzer) and uv (the Python
1119
+ // toolchain every script runs under). The section header is printed once here;
1120
+ // each sub-install prints its own gray sub-line. Runs before the Environment
1121
+ // Check so the check reflects anything freshly installed.
1122
+ console.log('');
1123
+ console.log(chalk.cyan(' Dependency Installation'));
1124
+ try {
1125
+ await installNodeDependencies();
1126
+ }
1127
+ catch (err) {
1128
+ if (isPromptAbort(err))
1129
+ abortInit();
1130
+ console.log(chalk.yellow(` ! homegraph / arkanalyzer install skipped: ${err.message}`));
1131
+ }
1132
+ try {
1133
+ await installUv();
1134
+ }
1135
+ catch (err) {
1136
+ if (isPromptAbort(err))
1137
+ abortInit();
1138
+ console.log(chalk.yellow(` ! uv install skipped: ${err.message}`));
1139
+ }
1140
+ // Detect external tools (adb / hdc / uv / java / homegraph + DevEco)
1009
1141
  // and report which skills/agents are impacted by anything missing.
1142
+ // (Python is no longer detected separately — every script runs via `uv run`,
1143
+ // and uv provides the interpreter on demand.)
1010
1144
  try {
1011
1145
  await runEnvironmentCheck(config.env);
1012
1146
  }
@@ -1015,36 +1149,21 @@ export async function initCommand(options = {}) {
1015
1149
  }
1016
1150
  // Copy bundled tools/ into ~/.hometrans/tools and record env.HOMETRANS_TOOL_PATH.
1017
1151
  // self_test_runner.py (under the installed tools dir) reads the autotest
1018
- // config from ~/.hometrans/config.json at run time — see promptTestConfig.
1019
- let installedToolPath = null;
1152
+ // config from ~/.hometrans/config.json at run time — see promptModelConfig.
1020
1153
  try {
1021
- installedToolPath = await installTools(toolsRoot, config);
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
- }
1154
+ await installTools(toolsRoot, config);
1026
1155
  }
1027
1156
  catch (err) {
1028
1157
  console.log(chalk.red(` ! tools copy: ${err.message}`));
1029
1158
  }
1030
- // Copy env-requirements.json into ~/.hometrans, next to config.json, so the
1031
- // declared environment requirements live alongside the user's config.
1032
- try {
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.)
1159
+ // (The autotest.unified_model block in config.json was already configured by
1160
+ // the Model Configuration step above; self_test_runner.py reads it at run time
1161
+ // and the UI-alignment phone-agent reads the exported HOMETRANS_MODEL_* vars.)
1043
1162
  // Offer to persist the resolved config values as real OS environment
1044
1163
  // variables (Windows: setx; macOS/Linux: shell rc block). Shows what will be
1045
1164
  // written and asks Y/N first; runs after HOMETRANS_TOOL_PATH is finalized above.
1046
1165
  try {
1047
- await maybePersistMachineEnv(config.env);
1166
+ await maybePersistMachineEnv(config);
1048
1167
  }
1049
1168
  catch (err) {
1050
1169
  if (isPromptAbort(err))
@@ -1055,7 +1174,7 @@ export async function initCommand(options = {}) {
1055
1174
  const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
1056
1175
  const result = { configured: [], skipped: [], errors: [] };
1057
1176
  for (const editor of editorsToSetup) {
1058
- console.log(chalk.blue(` Configuring ${editor.name}...`));
1177
+ console.log(chalk.cyan(` Configuring ${editor.name}...`));
1059
1178
  await installForEditor(editor, skillsRoot, agentsRoot, result);
1060
1179
  }
1061
1180
  await setupMcpForAllEditors(editorsToSetup, result);