@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.
Files changed (36) hide show
  1. package/README.md +108 -99
  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 +431 -248
  10. package/dist/cli/uninstall.js +103 -8
  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 +8 -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
  36. 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
- * 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.
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 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()}`));
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: '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,
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.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();
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(' Test configuration skipped (non-interactive mode).'));
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
- * 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,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. Test Configuration) points the user at it.
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(' Parameter Configuration'));
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
- console.log('');
978
- console.log(chalk.blue(' Derived from DEVECO_SDK_HOME:'));
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 [name, p] of checks) {
981
- const exists = await dirExists(p);
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
- // 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)
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 promptTestConfig.
1019
- let installedToolPath = null;
1216
+ // config from ~/.hometrans/config.json at run time — see promptModelConfig.
1020
1217
  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
- }
1218
+ await installTools(toolsRoot, config);
1026
1219
  }
1027
1220
  catch (err) {
1028
1221
  console.log(chalk.red(` ! tools copy: ${err.message}`));
1029
1222
  }
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.)
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.env);
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.blue(` Configuring ${editor.name}...`));
1241
+ console.log(chalk.cyan(` Configuring ${editor.name}...`));
1059
1242
  await installForEditor(editor, skillsRoot, agentsRoot, result);
1060
1243
  }
1061
1244
  await setupMcpForAllEditors(editorsToSetup, result);