@buaa_smat/hometrans 0.1.10 → 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.
@@ -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/init.js CHANGED
@@ -7,13 +7,14 @@
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;
@@ -130,6 +131,70 @@ async function refreshAutotestConfig(autotestDir, apiKey) {
130
131
  ? `autotest config.yaml seeded + api_key filled`
131
132
  : `autotest config.yaml api_key refreshed`;
132
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
+ }
133
198
  async function installSkillsTo(skillsRoot, targetDir) {
134
199
  let entries;
135
200
  try {
@@ -180,7 +245,7 @@ async function installAgentsTo(agentsRoot, targetDir) {
180
245
  }
181
246
  return installed;
182
247
  }
183
- async function installForEditor(editor, skillsRoot, agentsRoot, result) {
248
+ async function installForEditor(editor, skillsRoot, agentsRoot, glmApiKey, hmosSdkDir, result) {
184
249
  const marker = expandHome(editor.markerDir);
185
250
  if (marker && !(await dirExists(marker))) {
186
251
  result.skipped.push(`${editor.name} (not installed)`);
@@ -197,6 +262,17 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
197
262
  catch (err) {
198
263
  result.errors.push(`${editor.name} skills: ${err.message}`);
199
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
+ }
200
276
  try {
201
277
  const agents = await installAgentsTo(agentsRoot, agentsDir);
202
278
  if (agents.length > 0) {
@@ -207,12 +283,185 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
207
283
  result.errors.push(`${editor.name} agents: ${err.message}`);
208
284
  }
209
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
+ */
210
291
  export function prettyHome(p) {
211
- const home = os.homedir();
212
- if (p.startsWith(home)) {
213
- 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.'));
214
464
  }
215
- return p.replace(/\\/g, '/');
216
465
  }
217
466
  async function detectInstalledEditors(editors) {
218
467
  const status = new Map();
@@ -309,33 +558,72 @@ export async function initCommand(options = {}) {
309
558
  const answers = await inquirer.prompt([
310
559
  {
311
560
  type: 'input',
312
- name: 'OHOS_SDK_PATH',
313
- message: 'OHOS_SDK_PATH:',
314
- 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,
315
564
  },
316
565
  {
317
566
  type: 'input',
318
- name: 'HMS_SDK_PATH',
319
- message: 'HMS_SDK_PATH:',
320
- 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,
321
570
  },
322
571
  {
323
572
  type: 'input',
324
- name: 'TEST_API_KEY',
325
- message: 'TEST_API_KEY (for autotest config.yaml):',
326
- 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,
327
576
  },
328
577
  ]);
329
- config.env.OHOS_SDK_PATH = answers.OHOS_SDK_PATH.trim();
330
- 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;
331
583
  config.env.TEST_API_KEY = answers.TEST_API_KEY.trim();
584
+ config.env.GLM_API_KEY = answers.GLM_API_KEY.trim();
332
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
+ }
333
613
  }
334
614
  catch (err) {
335
615
  if (isPromptAbort(err))
336
616
  abortInit();
337
617
  console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
338
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
+ }
339
627
  // Copy bundled tools/ into ~/.hometrans/tools and record env.TOOL_PATH.
340
628
  // Must run before the autotest config step below, which seeds config.yaml
341
629
  // into the installed tools dir (the location agents read via env.TOOL_PATH).
@@ -369,9 +657,13 @@ export async function initCommand(options = {}) {
369
657
  console.log('');
370
658
  const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
371
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
+ : '';
372
664
  for (const editor of editorsToSetup) {
373
665
  console.log(chalk.blue(` Configuring ${editor.name}...`));
374
- await installForEditor(editor, skillsRoot, agentsRoot, result);
666
+ await installForEditor(editor, skillsRoot, agentsRoot, config.env.GLM_API_KEY, hmosSdkDir, result);
375
667
  }
376
668
  await setupMcpForAllEditors(editorsToSetup, result);
377
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);
@@ -13,6 +13,29 @@ import {
13
13
  import fs from "node:fs/promises";
14
14
  import path from "node:path";
15
15
  import os from "node:os";
16
+ function deriveSdkPaths(devecoSdkHome) {
17
+ const home = devecoSdkHome.trim().replace(/[\\/]+$/, "");
18
+ if (!home) {
19
+ return { DEVECO_SDK_HOME: "", DEVECO_PATH: "", OHOS_SDK_PATH: "", HMS_SDK_PATH: "" };
20
+ }
21
+ return {
22
+ DEVECO_SDK_HOME: home,
23
+ DEVECO_PATH: path.dirname(home),
24
+ OHOS_SDK_PATH: path.join(home, "default", "openharmony", "ets"),
25
+ HMS_SDK_PATH: path.join(home, "default", "hms", "ets")
26
+ };
27
+ }
28
+ function sdkHomeFromLegacyPaths(env) {
29
+ const tryStrip = (p, suffix) => {
30
+ if (!p) return "";
31
+ const norm = p.replace(/[\\/]+$/, "").split(/[\\/]/);
32
+ if (norm.length <= suffix.length) return "";
33
+ const tail = norm.slice(-suffix.length).map((s) => s.toLowerCase());
34
+ if (tail.join("/") !== suffix.join("/")) return "";
35
+ return norm.slice(0, -suffix.length).join(path.sep);
36
+ };
37
+ return tryStrip(env.OHOS_SDK_PATH, ["default", "openharmony", "ets"]) || tryStrip(env.HMS_SDK_PATH, ["default", "hms", "ets"]);
38
+ }
16
39
  function getConfigDir() {
17
40
  return path.join(os.homedir(), ".hometrans");
18
41
  }
@@ -99,9 +122,12 @@ async function fileExists(p) {
99
122
  }
100
123
  function defaultEnv() {
101
124
  return {
125
+ DEVECO_SDK_HOME: "",
126
+ DEVECO_PATH: "",
102
127
  OHOS_SDK_PATH: "",
103
128
  HMS_SDK_PATH: "",
104
129
  TEST_API_KEY: "",
130
+ GLM_API_KEY: "",
105
131
  TOOL_PATH: ""
106
132
  };
107
133
  }
@@ -145,6 +171,26 @@ async function loadHomeTransConfig() {
145
171
  delete anyParsed.sdkPaths;
146
172
  delete anyParsed.params;
147
173
  delete anyParsed.tool_path;
174
+ let envDirty = false;
175
+ if (!config.env.DEVECO_SDK_HOME) {
176
+ const migrated = sdkHomeFromLegacyPaths(config.env);
177
+ if (migrated) {
178
+ config.env.DEVECO_SDK_HOME = migrated;
179
+ envDirty = true;
180
+ }
181
+ }
182
+ if (config.env.DEVECO_SDK_HOME) {
183
+ const derived = deriveSdkPaths(config.env.DEVECO_SDK_HOME);
184
+ for (const key of ["DEVECO_PATH", "OHOS_SDK_PATH", "HMS_SDK_PATH"]) {
185
+ if (!config.env[key]) {
186
+ config.env[key] = derived[key];
187
+ envDirty = true;
188
+ }
189
+ }
190
+ }
191
+ if (envDirty) {
192
+ await saveHomeTransConfig(config);
193
+ }
148
194
  const existingNames = new Set(config.editors.map((e) => e.name));
149
195
  const missingEditors = defaultEditors().filter(
150
196
  (e) => !existingNames.has(e.name)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buaa_smat/hometrans",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "HomeTrans (Android-to-HarmonyOS) skill + agent installer. Run `ht init` to distribute conversion skills and subagents into AI editors.",
5
5
  "license": "MIT",
6
6
  "repository": {
Binary file
Binary file
@@ -17,12 +17,17 @@ Parse `$ARGUMENTS` as positional tokens:
17
17
 
18
18
  - **Arg 1** (`android-project-path`): Path to the Android source project (required)
19
19
  - **Arg 2** (`harmonyos-project-path`): Path to the target HarmonyOS project directory (required)
20
- - **Arg 3** (`assets-output-path`): Directory to store all output/report files (required)
21
- - **Arg 4** (`max-rounds-review`): Maximum number of Stage 3→3a→3b code-review-fix rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
22
- - **Arg 5** (`max-rounds-test`): Maximum number of Stage 4→4a→4b self-test rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
23
- - **Arg 6** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing. To pass this arg, provide explicit values for Args 4 and 5 first.
20
+ - **Arg 3** (`spec-file-path`): Path to the requirement spec document (required). This is the plan/spec that drives Stage 1 logic development and Stage 3 code review — both stages read it directly from this path.
21
+ - **Arg 4** (`assets-output-path`): Directory to store all output/report files (optional). When omitted, default to `<harmonyos-project-path>/.hometrans_output` create the directory if it does not exist.
22
+ - **Arg 5** (`test-case-path`): Path to the self-test case file driving the Stage 4 self-testing loop (optional). When omitted, default to `OUTPUT/test_case.md`. If the resolved file does not exist, the Stage 4 loop is skipped (see Loop Setup).
23
+ - **Arg 6** (`pre-test-case-path`): Path to the pre-test case file (optional). When omitted, default to `OUTPUT/pre_test_case.md`. Only passed to the self-tester when the resolved file exists.
24
+ - **Arg 7** (`max-rounds-review`): Maximum number of Stage 3→3a→3b code-review-fix rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
25
+ - **Arg 8** (`max-rounds-test`): Maximum number of Stage 4→4a→4b self-test rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
26
+ - **Arg 9** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing.
24
27
 
25
- If any required argument is missing, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `skip-test` is provided but is not `true` or `false`, ask the user before proceeding.
28
+ Args are positional: to pass any optional arg, provide explicit values for all optional args before it (e.g., passing `test-case-path` requires an explicit `assets-output-path`; passing `skip-test` requires explicit Args 4–8).
29
+
30
+ If any required argument is missing, ask the user before proceeding. If `spec-file-path` does not exist or is not a readable file, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `skip-test` is provided but is not `true` or `false`, ask the user before proceeding.
26
31
 
27
32
  Define shorthand variables for the instructions below:
28
33
 
@@ -30,7 +35,10 @@ Define shorthand variables for the instructions below:
30
35
  |----------|---------|
31
36
  | `ANDROID` | android-project-path |
32
37
  | `HMOS` | harmonyos-project-path |
33
- | `OUTPUT` | assets-output-path (resolved or generated) |
38
+ | `SPEC` | spec-file-path — requirement spec document, consumed directly by Stage 1 (`spec-file`) and Stage 3 (`test-case-path`) |
39
+ | `OUTPUT` | assets-output-path; defaults to `HMOS/.hometrans_output` when not provided (created on demand) |
40
+ | `TEST_CASE` | test-case-path; defaults to `OUTPUT/test_case.md` when not provided |
41
+ | `PRE_TEST_CASE` | pre-test-case-path; defaults to `OUTPUT/pre_test_case.md` when not provided |
34
42
  | `MAX_ROUNDS_REVIEW` | max-rounds-review — positive integer (default `2`). Controls Stage 3→3a→3b code-review-fix loop. |
35
43
  | `MAX_ROUNDS_TEST` | max-rounds-test — positive integer (default `2`). Controls Stage 4→4a→4b self-test loop. |
36
44
  | `SKIP_TEST` | skip-test — `true` or `false` (default `false`). When `true`, skip Stage 4 / 4a / 4b entirely (no real device available). |
@@ -147,7 +155,7 @@ Prompt format (applies to both Stage 1 and Stage 1a): ONLY the key-value lines b
147
155
  ```
148
156
  Agent(
149
157
  subagent_type="logic-context-builder",
150
- prompt="harmonyos-project-path: HMOS\nspec-file: OUTPUT/plan.md\noutput-path: OUTPUT/logic"
158
+ prompt="harmonyos-project-path: HMOS\nspec-file: SPEC\noutput-path: OUTPUT/logic"
151
159
  )
152
160
  ```
153
161
  2. Verify `OUTPUT/logic/plan.md` exists.
@@ -200,7 +208,7 @@ For each `review_round` from `1..MAX_ROUNDS_REVIEW`, set `REVIEW_ROUND_DIR = OUT
200
208
  - `harmonyos-project-path`: `HMOS`
201
209
  - `commit-id`: For Round 1, use `REVIEW_COMMIT_ID`. For Round 2+, review the project holistically (omit `commit-id` or pass `none`) since fixes have modified the codebase beyond the original commit scope.
202
210
  - `output-path`: `REVIEW_ROUND_DIR`
203
- - `test-case-path`: `OUTPUT/plan.md`
211
+ - `test-case-path`: `SPEC`
204
212
  3. If a valid `commit-id` is available, the agent will automatically call the `extract_commit_context` MCP tool to extract commit-scoped code context before reviewing. If the MCP call fails, the agent falls back to direct git-diff analysis. If no `commit-id` is available, the agent reviews the project holistically without commit-scoped extraction.
205
213
  4. The agent writes `REVIEW_ROUND_DIR/code-review-report.md` with per-scenario verdicts.
206
214
  5. **Extract defect stats**: Read `REVIEW_ROUND_DIR/code-review-report.md`, extract the verdict breakdown (PASS/PARTIAL/FAIL/UNABLE TO VERIFY counts) and overall verdict.
@@ -277,7 +285,7 @@ Treat Stage 4 → 4a → 4b as a loop that runs up to `MAX_ROUNDS_TEST` times.
277
285
 
278
286
  1. **Locate the initial HAP file**: Use the `.hap` mirrored to `OUTPUT/` by the Stage 3 review loop finalization (or from Stage 2 if the review loop did not produce one). The default location is `OUTPUT/entry-default-signed.hap`. If that file does not exist, search for `*-signed.hap` files in `OUTPUT/` and use the first match. If no `.hap` is found in `OUTPUT/`, fall back to searching `HMOS/entry/build/default/outputs/default/`.
279
287
  2. Store the resolved path as `CURRENT_HAP`.
280
- 3. **`OUTPUT/test_case.md` existence guard**: If `OUTPUT/test_case.md` does not exist, mark Stage 4 / 4a / 4b as failed with note "No test case file available" and skip the loop entirely.
288
+ 3. **`TEST_CASE` existence guard**: If `TEST_CASE` does not exist, mark Stage 4 / 4a / 4b as failed with note "No test case file available" and skip the loop entirely.
281
289
  4. Initialize loop state:
282
290
  - `round = 1`
283
291
  - `rounds_executed = 0`
@@ -297,8 +305,8 @@ For each `round` from `1..MAX_ROUNDS_TEST`, set `ROUND_DIR = OUTPUT/round-{round
297
305
  3. Input (round 1):
298
306
  - `hap-path`: `CURRENT_HAP`
299
307
  - `output-path`: `OUTPUT` (the ROOT — the agent always writes here)
300
- - `test-case-path`: `OUTPUT/test_case.md`
301
- - `pre-test-case-path`: `OUTPUT/pre_test_case.md` (only include this line when the file exists)
308
+ - `test-case-path`: `TEST_CASE`
309
+ - `pre-test-case-path`: `PRE_TEST_CASE` (only include this line when the file exists)
302
310
  - `setup`: `true`
303
311
  4. Input (round 2+):
304
312
  - `hap-path`: `CURRENT_HAP`
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: hmos-fix-build-errors
3
3
  description: Build a HarmonyOS project via CLI and automatically fix compile errors in a loop until the build succeeds. Default unsigned HAP; pass --signed to build a signed HAP (signing config must already exist in the project's build-profile.json5).
4
- argument-hint: <harmonyos-project-path> <deveco-studio-path> [--signed]
4
+ argument-hint: <harmonyos-project-path> [deveco-studio-path] [--signed]
5
5
  allowed-tools: Agent, Read, Write, Edit, Glob, Grep, Bash
6
6
  type: tool
7
7
  domain: engineering
@@ -12,8 +12,8 @@ domain: engineering
12
12
  Automatically build a HarmonyOS NEXT project from the command line, parse compile errors, fix them, and retry — repeating until the build succeeds.
13
13
 
14
14
  - **HarmonyOS Project**: `$ARGUMENTS[0]` (the HarmonyOS project root, e.g. `D:/MyHmosApp`)
15
- - **DevEco Studio Path**: `$ARGUMENTS[1]` (DevEco Studio installation root, e.g. `D:/DevEco Studio`)
16
- - **--signed** (optional): `$ARGUMENTS[2]` if set to `--signed`, the build produces a **signed HAP**. If omitted, the build produces an **unsigned HAP** (default).
15
+ - **DevEco Studio Path** (optional): `$ARGUMENTS[1]` (DevEco Studio installation root, e.g. `D:/DevEco Studio`) — when omitted, resolved automatically (see Step 0.2)
16
+ - **--signed** (optional): if any argument equals `--signed`, the build produces a **signed HAP**. If omitted, the build produces an **unsigned HAP** (default).
17
17
 
18
18
  ---
19
19
 
@@ -21,12 +21,20 @@ Automatically build a HarmonyOS NEXT project from the command line, parse compil
21
21
 
22
22
  1. **Verify project exists** — Check that `$ARGUMENTS[0]` contains a valid HarmonyOS project (look for `build-profile.json5`, `entry/src` directory, `oh-package.json5`).
23
23
 
24
- 2. **Verify DevEco installation** Check that `$ARGUMENTS[1]` contains:
24
+ 2. **Resolve `<deveco-path>`**stop at the first source that yields a valid path:
25
+ 1. `$ARGUMENTS[1]` if provided (and not `--signed`).
26
+ 2. `~/.hometrans/config.json` (on Windows `%USERPROFILE%\.hometrans\config.json`): `env.DEVECO_PATH`; else parent dir of `env.DEVECO_SDK_HOME`; else strip the trailing `/sdk/default/openharmony/ets` from `env.OHOS_SDK_PATH` (legacy).
27
+ 3. Environment variables, same precedence: `DEVECO_PATH` → parent of `DEVECO_SDK_HOME` → strip suffix from `OHOS_SDK_PATH`.
28
+ 4. **Ask the user** for the DevEco Studio install path (suggest running `ht init` to persist it).
29
+
30
+ **Verify the resolved `<deveco-path>`** contains:
25
31
  - `tools/node/node.exe`
26
32
  - `tools/hvigor/bin/hvigorw.js`
27
33
  - `tools/ohpm/bin/ohpm`
28
34
  - `sdk/` directory
29
35
 
36
+ If verification fails for a config/env-sourced path, treat that source as invalid and continue down the list; if it fails for a user-supplied path, report what is missing and ask again.
37
+
30
38
  3. **Set up `local.properties`** — Ensure the project root has `local.properties` with:
31
39
  ```properties
32
40
  hwsdk.dir=<deveco-path>/sdk
@@ -40,7 +48,7 @@ Automatically build a HarmonyOS NEXT project from the command line, parse compil
40
48
  "<deveco-path>/tools/ohpm/bin/ohpm" install
41
49
  ```
42
50
 
43
- 5. **Determine Build Mode** — Check if `$ARGUMENTS[2]` equals `--signed`:
51
+ 5. **Determine Build Mode** — Check if any argument equals `--signed` (it may be `$ARGUMENTS[1]` when the DevEco path is omitted, or `$ARGUMENTS[2]` when it is provided):
44
52
  - **If NOT `--signed`** → **Unsigned build mode**. Ensure `build-profile.json5` does NOT have `signingConfigs` or `signingConfig` references in products (remove them if present). Go to Step 1.
45
53
  - **If `--signed`** → **Signed build mode**. Proceed to Step 0.5 to validate signing config.
46
54