@buaa_smat/hometrans 0.1.9 → 0.1.11

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