@buaa_smat/hometrans 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.en.md +0 -0
- package/README.md +208 -124
- package/agents/build-fixer.md +17 -10
- package/agents/code-reviewer.md +4 -2
- package/dist/cli/config-store.js +57 -0
- package/dist/cli/config.js +8 -9
- package/dist/cli/init.js +671 -25
- package/dist/cli/mcp-setup.js +2 -6
- package/dist/context/index.js +46 -0
- package/env-requirements.json +181 -0
- package/package.json +3 -2
- package/resource/choose_editor.png +0 -0
- package/resource/hometrans_config.png +0 -0
- package/skills/hmos-convert-pipeline/SKILL.md +19 -11
- package/skills/hmos-fix-build-errors/SKILL.md +13 -5
- package/skills/hmos-incremental-ui-align/SKILL.md +15 -1
- package/skills/hmos-incremental-ui-align/config-example.json +8 -8
- package/resource/finish_init.png +0 -0
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
|
|
10
|
+
import { execFileSync, spawnSync } 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, getConfigDir, 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;
|
|
@@ -89,6 +90,55 @@ async function installTools(toolsRoot, config) {
|
|
|
89
90
|
function resolveAutotestDir(toolPath) {
|
|
90
91
|
return path.join(toolPath, 'test-tools', 'autotest');
|
|
91
92
|
}
|
|
93
|
+
async function readFileIfExists(p) {
|
|
94
|
+
try {
|
|
95
|
+
return await fs.readFile(p, 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Template drift check; line-ending differences alone don't count. */
|
|
102
|
+
export function templatesDiffer(a, b) {
|
|
103
|
+
if (a === null || b === null)
|
|
104
|
+
return false;
|
|
105
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
106
|
+
return norm(a) !== norm(b);
|
|
107
|
+
}
|
|
108
|
+
/** e.g. 20260610-153012 — used to name config backups. */
|
|
109
|
+
function timestampSuffix() {
|
|
110
|
+
const d = new Date();
|
|
111
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
112
|
+
return (`${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` +
|
|
113
|
+
`-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The bundled template changed since the local config was created — ask
|
|
117
|
+
* whether to regenerate. Y = back up the current file with a timestamp and
|
|
118
|
+
* recreate from the new template; N (default, and the non-interactive
|
|
119
|
+
* fallback) = keep the current file and only refresh the keys in place.
|
|
120
|
+
* Ctrl-C aborts init.
|
|
121
|
+
*/
|
|
122
|
+
async function confirmRegenerateFromTemplate(label) {
|
|
123
|
+
console.log(chalk.yellow(` ! ${label}: the bundled template changed since this file was created.`));
|
|
124
|
+
try {
|
|
125
|
+
const answers = await inquirer.prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'confirm',
|
|
128
|
+
name: 'regenerate',
|
|
129
|
+
message: `Regenerate ${label} from the new template? (Y = back up current file with a timestamp, then recreate; N = keep current file, only refresh keys)`,
|
|
130
|
+
default: false,
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
return answers.regenerate;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (isPromptAbort(err))
|
|
137
|
+
abortInit();
|
|
138
|
+
console.log(chalk.yellow(' Non-interactive: keeping current file (keys refreshed only).'));
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
92
142
|
/**
|
|
93
143
|
* Initialize / refresh `agents/test-tools/autotest/config.yaml`:
|
|
94
144
|
* - If config.yaml does not exist, seed it from config.yaml.example.
|
|
@@ -97,7 +147,7 @@ function resolveAutotestDir(toolPath) {
|
|
|
97
147
|
* Returns a status string for the result summary, or null if nothing was done
|
|
98
148
|
* (e.g., the autotest folder isn't present in this package).
|
|
99
149
|
*/
|
|
100
|
-
async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
150
|
+
async function refreshAutotestConfig(autotestDir, apiKey, prevExampleContent = null) {
|
|
101
151
|
const examplePath = path.join(autotestDir, 'config.yaml.example');
|
|
102
152
|
const configPath = path.join(autotestDir, 'config.yaml');
|
|
103
153
|
const hasExample = await fs
|
|
@@ -107,6 +157,8 @@ async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
|
107
157
|
if (!hasExample)
|
|
108
158
|
return null;
|
|
109
159
|
let seeded = false;
|
|
160
|
+
let backupName = null;
|
|
161
|
+
let templateBackupName = null;
|
|
110
162
|
const hasConfig = await fs
|
|
111
163
|
.access(configPath)
|
|
112
164
|
.then(() => true)
|
|
@@ -115,7 +167,33 @@ async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
|
115
167
|
await fs.copyFile(examplePath, configPath);
|
|
116
168
|
seeded = true;
|
|
117
169
|
}
|
|
170
|
+
else {
|
|
171
|
+
// Template drift: the bundled example changed since the local config was
|
|
172
|
+
// created (prevExampleContent = local example snapshot taken before the
|
|
173
|
+
// tools copy). Y regenerates from the new template after a timestamped
|
|
174
|
+
// backup; N keeps the local file (key-only refresh below) — but saves the
|
|
175
|
+
// old template alongside, since the new one just overwrote it on disk
|
|
176
|
+
// and the local config is still based on the old one.
|
|
177
|
+
const newExample = await fs.readFile(examplePath, 'utf-8');
|
|
178
|
+
if (templatesDiffer(prevExampleContent, newExample)) {
|
|
179
|
+
if (await confirmRegenerateFromTemplate('autotest config.yaml')) {
|
|
180
|
+
backupName = `config.yaml.${timestampSuffix()}.bak`;
|
|
181
|
+
await fs.copyFile(configPath, path.join(autotestDir, backupName));
|
|
182
|
+
await fs.copyFile(examplePath, configPath);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
templateBackupName = `config.yaml.example.${timestampSuffix()}.bak`;
|
|
186
|
+
await fs.writeFile(path.join(autotestDir, templateBackupName), prevExampleContent, 'utf-8');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
118
190
|
if (!apiKey) {
|
|
191
|
+
if (backupName) {
|
|
192
|
+
return `autotest config.yaml regenerated from new template (backup: ${backupName}; api_key left as placeholder)`;
|
|
193
|
+
}
|
|
194
|
+
if (templateBackupName) {
|
|
195
|
+
return `autotest config.yaml kept (old template backed up: ${templateBackupName})`;
|
|
196
|
+
}
|
|
119
197
|
return seeded
|
|
120
198
|
? `autotest config.yaml seeded (api_key left as placeholder)`
|
|
121
199
|
: null;
|
|
@@ -126,11 +204,115 @@ async function refreshAutotestConfig(autotestDir, apiKey) {
|
|
|
126
204
|
if (updated !== original) {
|
|
127
205
|
await fs.writeFile(configPath, updated, 'utf-8');
|
|
128
206
|
}
|
|
207
|
+
if (backupName) {
|
|
208
|
+
return `autotest config.yaml regenerated from new template + api_key filled (backup: ${backupName})`;
|
|
209
|
+
}
|
|
210
|
+
if (templateBackupName) {
|
|
211
|
+
return `autotest config.yaml api_key refreshed (kept local file; old template backed up: ${templateBackupName})`;
|
|
212
|
+
}
|
|
129
213
|
return seeded
|
|
130
214
|
? `autotest config.yaml seeded + api_key filled`
|
|
131
215
|
: `autotest config.yaml api_key refreshed`;
|
|
132
216
|
}
|
|
133
|
-
|
|
217
|
+
const UI_ALIGN_SKILL = 'hmos-incremental-ui-align';
|
|
218
|
+
/**
|
|
219
|
+
* Initialize / refresh `<skillsDir>/hmos-incremental-ui-align/config.json`
|
|
220
|
+
* in an editor's installed skills dir — same semantics as the autotest
|
|
221
|
+
* config.yaml handling:
|
|
222
|
+
* - If config.json does not exist, seed it from config-example.json.
|
|
223
|
+
* - If it exists, surgically overwrite ONLY the `glm_api_key` and
|
|
224
|
+
* `hmos_sdk_dir` values (via jsonc edits), preserving every other
|
|
225
|
+
* field the user has set. `hmos_sdk_dir` = `<DEVECO_SDK_HOME>/default`.
|
|
226
|
+
*
|
|
227
|
+
* Returns a status string for the result summary, or null if nothing was
|
|
228
|
+
* done (skill not installed there, or no values to write into an existing file).
|
|
229
|
+
*/
|
|
230
|
+
export async function refreshUiAlignConfig(skillsDir, glmApiKey, hmosSdkDir, prevExampleContent = null) {
|
|
231
|
+
const skillDir = path.join(skillsDir, UI_ALIGN_SKILL);
|
|
232
|
+
const examplePath = path.join(skillDir, 'config-example.json');
|
|
233
|
+
const configPath = path.join(skillDir, 'config.json');
|
|
234
|
+
const hasExample = await fs
|
|
235
|
+
.access(examplePath)
|
|
236
|
+
.then(() => true)
|
|
237
|
+
.catch(() => false);
|
|
238
|
+
if (!hasExample)
|
|
239
|
+
return null;
|
|
240
|
+
let seeded = false;
|
|
241
|
+
let backupName = null;
|
|
242
|
+
let templateBackupName = null;
|
|
243
|
+
const hasConfig = await fs
|
|
244
|
+
.access(configPath)
|
|
245
|
+
.then(() => true)
|
|
246
|
+
.catch(() => false);
|
|
247
|
+
if (!hasConfig) {
|
|
248
|
+
await fs.copyFile(examplePath, configPath);
|
|
249
|
+
seeded = true;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Template drift: bundled config-example.json changed since the local
|
|
253
|
+
// config.json was created (prevExampleContent = snapshot taken before the
|
|
254
|
+
// skills copy). Y regenerates from the new template after a timestamped
|
|
255
|
+
// backup; N keeps the local file (key-only update below) — but saves the
|
|
256
|
+
// old template alongside, since the new one just overwrote it on disk
|
|
257
|
+
// and the local config is still based on the old one.
|
|
258
|
+
const newExample = await fs.readFile(examplePath, 'utf-8');
|
|
259
|
+
if (templatesDiffer(prevExampleContent, newExample)) {
|
|
260
|
+
if (await confirmRegenerateFromTemplate(`${UI_ALIGN_SKILL}/config.json`)) {
|
|
261
|
+
backupName = `config.json.${timestampSuffix()}.bak`;
|
|
262
|
+
await fs.copyFile(configPath, path.join(skillDir, backupName));
|
|
263
|
+
await fs.copyFile(examplePath, configPath);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
templateBackupName = `config-example.json.${timestampSuffix()}.bak`;
|
|
267
|
+
await fs.writeFile(path.join(skillDir, templateBackupName), prevExampleContent, 'utf-8');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const updates = [];
|
|
272
|
+
if (glmApiKey)
|
|
273
|
+
updates.push(['glm_api_key', glmApiKey]);
|
|
274
|
+
if (hmosSdkDir)
|
|
275
|
+
updates.push(['hmos_sdk_dir', hmosSdkDir]);
|
|
276
|
+
if (updates.length === 0) {
|
|
277
|
+
if (backupName) {
|
|
278
|
+
return `${UI_ALIGN_SKILL}/config.json regenerated from new template (backup: ${backupName}; glm_api_key / hmos_sdk_dir left empty)`;
|
|
279
|
+
}
|
|
280
|
+
if (templateBackupName) {
|
|
281
|
+
return `${UI_ALIGN_SKILL}/config.json kept (old template backed up: ${templateBackupName})`;
|
|
282
|
+
}
|
|
283
|
+
return seeded
|
|
284
|
+
? `${UI_ALIGN_SKILL}/config.json seeded (glm_api_key / hmos_sdk_dir left empty)`
|
|
285
|
+
: null;
|
|
286
|
+
}
|
|
287
|
+
let raw = await fs.readFile(configPath, 'utf-8');
|
|
288
|
+
const parseErrors = [];
|
|
289
|
+
const tree = parseTree(raw, parseErrors);
|
|
290
|
+
if (!tree || tree.type !== 'object' || parseErrors.length > 0) {
|
|
291
|
+
// Corrupt config — never regenerate over user content; just report.
|
|
292
|
+
return `${UI_ALIGN_SKILL}/config.json NOT updated (file is not valid JSON — fix it manually)`;
|
|
293
|
+
}
|
|
294
|
+
const original = raw;
|
|
295
|
+
for (const [key, value] of updates) {
|
|
296
|
+
const edits = modify(raw, [key], value, {
|
|
297
|
+
formattingOptions: { tabSize: 2, insertSpaces: true },
|
|
298
|
+
});
|
|
299
|
+
raw = applyEdits(raw, edits);
|
|
300
|
+
}
|
|
301
|
+
if (raw !== original) {
|
|
302
|
+
await fs.writeFile(configPath, raw, 'utf-8');
|
|
303
|
+
}
|
|
304
|
+
const what = updates.map(([k]) => k).join(' + ');
|
|
305
|
+
if (backupName) {
|
|
306
|
+
return `${UI_ALIGN_SKILL}/config.json regenerated from new template + ${what} filled (backup: ${backupName})`;
|
|
307
|
+
}
|
|
308
|
+
if (templateBackupName) {
|
|
309
|
+
return `${UI_ALIGN_SKILL}/config.json ${what} refreshed (kept local file; old template backed up: ${templateBackupName})`;
|
|
310
|
+
}
|
|
311
|
+
return seeded
|
|
312
|
+
? `${UI_ALIGN_SKILL}/config.json seeded + ${what} filled`
|
|
313
|
+
: `${UI_ALIGN_SKILL}/config.json ${what} refreshed`;
|
|
314
|
+
}
|
|
315
|
+
async function installSkillsTo(skillsRoot, targetDir, skipNames = new Set()) {
|
|
134
316
|
let entries;
|
|
135
317
|
try {
|
|
136
318
|
entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
@@ -142,6 +324,8 @@ async function installSkillsTo(skillsRoot, targetDir) {
|
|
|
142
324
|
for (const entry of entries) {
|
|
143
325
|
if (!entry.isDirectory())
|
|
144
326
|
continue;
|
|
327
|
+
if (skipNames.has(entry.name))
|
|
328
|
+
continue;
|
|
145
329
|
const skillSrc = path.join(skillsRoot, entry.name);
|
|
146
330
|
const hasSkillFile = await fs
|
|
147
331
|
.access(path.join(skillSrc, 'SKILL.md'))
|
|
@@ -155,7 +339,7 @@ async function installSkillsTo(skillsRoot, targetDir) {
|
|
|
155
339
|
}
|
|
156
340
|
return installed;
|
|
157
341
|
}
|
|
158
|
-
async function installAgentsTo(agentsRoot, targetDir) {
|
|
342
|
+
async function installAgentsTo(agentsRoot, targetDir, skipNames = new Set()) {
|
|
159
343
|
let entries;
|
|
160
344
|
try {
|
|
161
345
|
entries = await fs.readdir(agentsRoot, { withFileTypes: true });
|
|
@@ -166,6 +350,8 @@ async function installAgentsTo(agentsRoot, targetDir) {
|
|
|
166
350
|
await fs.mkdir(targetDir, { recursive: true });
|
|
167
351
|
const installed = [];
|
|
168
352
|
for (const entry of entries) {
|
|
353
|
+
if (skipNames.has(entry.name))
|
|
354
|
+
continue;
|
|
169
355
|
const srcPath = path.join(agentsRoot, entry.name);
|
|
170
356
|
const destPath = path.join(targetDir, entry.name);
|
|
171
357
|
if (entry.isDirectory()) {
|
|
@@ -180,7 +366,79 @@ async function installAgentsTo(agentsRoot, targetDir) {
|
|
|
180
366
|
}
|
|
181
367
|
return installed;
|
|
182
368
|
}
|
|
183
|
-
|
|
369
|
+
/** Names of bundled skill folders (those containing a SKILL.md). */
|
|
370
|
+
async function listBundledSkillNames(skillsRoot) {
|
|
371
|
+
try {
|
|
372
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
373
|
+
const names = [];
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
if (!entry.isDirectory())
|
|
376
|
+
continue;
|
|
377
|
+
const hasSkillFile = await fs
|
|
378
|
+
.access(path.join(skillsRoot, entry.name, 'SKILL.md'))
|
|
379
|
+
.then(() => true)
|
|
380
|
+
.catch(() => false);
|
|
381
|
+
if (hasSkillFile)
|
|
382
|
+
names.push(entry.name);
|
|
383
|
+
}
|
|
384
|
+
return names;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** Top-level names (files and folders) under the bundled agents dir. */
|
|
391
|
+
async function listBundledAgentNames(agentsRoot) {
|
|
392
|
+
try {
|
|
393
|
+
const entries = await fs.readdir(agentsRoot, { withFileTypes: true });
|
|
394
|
+
return entries.map((entry) => entry.name);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function pathExists(p) {
|
|
401
|
+
try {
|
|
402
|
+
await fs.access(p);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Ask whether same-name skills/agents already present in the editor's target
|
|
411
|
+
* dirs may be overwritten. Returns true (overwrite) when the user confirms or
|
|
412
|
+
* the shell is non-interactive (legacy behavior); false keeps existing files —
|
|
413
|
+
* the caller then only refreshes the API-key config files. Ctrl-C aborts init.
|
|
414
|
+
*/
|
|
415
|
+
async function confirmOverwrite(editorName, skillConflicts, agentConflicts) {
|
|
416
|
+
console.log(chalk.yellow(` ! ${editorName}: the following already exist and would be overwritten:`));
|
|
417
|
+
if (skillConflicts.length > 0) {
|
|
418
|
+
console.log(chalk.yellow(` skills: ${skillConflicts.join(', ')}`));
|
|
419
|
+
}
|
|
420
|
+
if (agentConflicts.length > 0) {
|
|
421
|
+
console.log(chalk.yellow(` agents: ${agentConflicts.join(', ')}`));
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const answers = await inquirer.prompt([
|
|
425
|
+
{
|
|
426
|
+
type: 'confirm',
|
|
427
|
+
name: 'overwrite',
|
|
428
|
+
message: `Overwrite them in ${editorName}? (N = keep existing files; new items are still installed and API-key configs refreshed)`,
|
|
429
|
+
default: true,
|
|
430
|
+
},
|
|
431
|
+
]);
|
|
432
|
+
return answers.overwrite;
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (isPromptAbort(err))
|
|
436
|
+
abortInit();
|
|
437
|
+
console.log(chalk.yellow(' Non-interactive: overwriting by default.'));
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function installForEditor(editor, skillsRoot, agentsRoot, glmApiKey, hmosSdkDir, result) {
|
|
184
442
|
const marker = expandHome(editor.markerDir);
|
|
185
443
|
if (marker && !(await dirExists(marker))) {
|
|
186
444
|
result.skipped.push(`${editor.name} (not installed)`);
|
|
@@ -188,8 +446,30 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
188
446
|
}
|
|
189
447
|
const skillsDir = expandHome(editor.skillsDir);
|
|
190
448
|
const agentsDir = expandHome(editor.agentsDir);
|
|
449
|
+
// Same-name items already in the editor's dirs → ask before overwriting.
|
|
450
|
+
const skillConflicts = [];
|
|
451
|
+
for (const name of await listBundledSkillNames(skillsRoot)) {
|
|
452
|
+
if (await pathExists(path.join(skillsDir, name)))
|
|
453
|
+
skillConflicts.push(name);
|
|
454
|
+
}
|
|
455
|
+
const agentConflicts = [];
|
|
456
|
+
for (const name of await listBundledAgentNames(agentsRoot)) {
|
|
457
|
+
if (await pathExists(path.join(agentsDir, name)))
|
|
458
|
+
agentConflicts.push(name);
|
|
459
|
+
}
|
|
460
|
+
let overwrite = true;
|
|
461
|
+
if (skillConflicts.length > 0 || agentConflicts.length > 0) {
|
|
462
|
+
overwrite = await confirmOverwrite(editor.name, skillConflicts, agentConflicts);
|
|
463
|
+
}
|
|
464
|
+
// Snapshot the installed ui-align template BEFORE the skills copy updates
|
|
465
|
+
// it — refreshUiAlignConfig uses it to detect template drift.
|
|
466
|
+
const prevUiAlignExample = await readFileIfExists(path.join(skillsDir, UI_ALIGN_SKILL, 'config-example.json'));
|
|
467
|
+
// N = incremental install: keep the conflicting items untouched, still copy
|
|
468
|
+
// everything that doesn't exist in the target dirs yet.
|
|
469
|
+
const skipSkills = overwrite ? new Set() : new Set(skillConflicts);
|
|
470
|
+
const skipAgents = overwrite ? new Set() : new Set(agentConflicts);
|
|
191
471
|
try {
|
|
192
|
-
const skills = await installSkillsTo(skillsRoot, skillsDir);
|
|
472
|
+
const skills = await installSkillsTo(skillsRoot, skillsDir, skipSkills);
|
|
193
473
|
if (skills.length > 0) {
|
|
194
474
|
result.configured.push(`${editor.name} skills (${skills.length} -> ${prettyHome(skillsDir)})`);
|
|
195
475
|
}
|
|
@@ -197,8 +477,23 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
197
477
|
catch (err) {
|
|
198
478
|
result.errors.push(`${editor.name} skills: ${err.message}`);
|
|
199
479
|
}
|
|
480
|
+
if (!overwrite) {
|
|
481
|
+
result.skipped.push(`${editor.name} existing kept: ${skipSkills.size} skills, ${skipAgents.size} agents (new items installed, API-key configs refreshed)`);
|
|
482
|
+
}
|
|
483
|
+
// Seed / refresh the incremental-ui-align per-skill config.json with the
|
|
484
|
+
// GLM key + SDK dir from ~/.hometrans/config.json (existing files: key-only
|
|
485
|
+
// update). Runs regardless of the overwrite decision above.
|
|
200
486
|
try {
|
|
201
|
-
const
|
|
487
|
+
const status = await refreshUiAlignConfig(skillsDir, glmApiKey, hmosSdkDir, prevUiAlignExample);
|
|
488
|
+
if (status) {
|
|
489
|
+
result.configured.push(`${editor.name} ${status}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
result.errors.push(`${editor.name} ${UI_ALIGN_SKILL}/config.json: ${err.message}`);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
const agents = await installAgentsTo(agentsRoot, agentsDir, skipAgents);
|
|
202
497
|
if (agents.length > 0) {
|
|
203
498
|
result.configured.push(`${editor.name} agents (${agents.length} -> ${prettyHome(agentsDir)})`);
|
|
204
499
|
}
|
|
@@ -207,12 +502,305 @@ async function installForEditor(editor, skillsRoot, agentsRoot, result) {
|
|
|
207
502
|
result.errors.push(`${editor.name} agents: ${err.message}`);
|
|
208
503
|
}
|
|
209
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Display paths as full absolute OS-native paths (e.g. on Windows:
|
|
507
|
+
* `C:\Users\<you>\.claude\skills`) — no `~` abbreviation, no separator
|
|
508
|
+
* rewriting, so output matches what the user can copy into their shell.
|
|
509
|
+
*/
|
|
210
510
|
export function prettyHome(p) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
511
|
+
return p;
|
|
512
|
+
}
|
|
513
|
+
/** Bundled at the package root, alongside skills/ and agents/. */
|
|
514
|
+
function resolveEnvRequirementsPath() {
|
|
515
|
+
return path.resolve(__dirname, '..', '..', 'env-requirements.json');
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Copy the bundled env-requirements.json into ~/.hometrans, alongside
|
|
519
|
+
* config.json, so it sits next to the user's config. Returns the destination
|
|
520
|
+
* path, or null if the package ships no env-requirements.json.
|
|
521
|
+
*/
|
|
522
|
+
async function installEnvRequirements() {
|
|
523
|
+
const src = resolveEnvRequirementsPath();
|
|
524
|
+
if (!(await fileExists(src)))
|
|
525
|
+
return null;
|
|
526
|
+
const dest = path.join(getConfigDir(), 'env-requirements.json');
|
|
527
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
528
|
+
await fs.copyFile(src, dest);
|
|
529
|
+
return dest;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Load env-requirements.json. The leading `$`-prefixed keys are documentation
|
|
533
|
+
* and ignored. Returns null (with a warning) if the file is missing or invalid
|
|
534
|
+
* so `ht init` degrades gracefully instead of crashing.
|
|
535
|
+
*/
|
|
536
|
+
export async function loadEnvRequirements() {
|
|
537
|
+
const p = resolveEnvRequirementsPath();
|
|
538
|
+
let raw;
|
|
539
|
+
try {
|
|
540
|
+
raw = await fs.readFile(p, 'utf-8');
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
console.log(chalk.yellow(` ! env-requirements.json not found at ${p} — skipping impact check.`));
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(raw);
|
|
548
|
+
if (!parsed.tools || !Array.isArray(parsed.requirements)) {
|
|
549
|
+
throw new Error('expected { tools: {...}, requirements: [...] }');
|
|
550
|
+
}
|
|
551
|
+
return parsed;
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
console.log(chalk.yellow(` ! env-requirements.json is invalid (${err.message}) — skipping impact check.`));
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Locate a command on PATH (`where` on Windows, `which` elsewhere). */
|
|
559
|
+
function whichSync(cmd) {
|
|
560
|
+
const isWin = process.platform === 'win32';
|
|
561
|
+
try {
|
|
562
|
+
const out = execFileSync(isWin ? 'where' : 'which', [cmd], {
|
|
563
|
+
encoding: 'utf-8',
|
|
564
|
+
timeout: 5000,
|
|
565
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
566
|
+
});
|
|
567
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
568
|
+
return lines[0] || null;
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Run a tool's verify command and return its first non-empty output line.
|
|
576
|
+
* Reads the configured stream first (stdout by default; stderr for `java
|
|
577
|
+
* -version`), falling back to the combined output if that stream is empty.
|
|
578
|
+
*/
|
|
579
|
+
function captureVerify(cmd, args, stream) {
|
|
580
|
+
const r = spawnSync(cmd, args, { encoding: 'utf-8', timeout: 5000 });
|
|
581
|
+
if (r.error)
|
|
582
|
+
return null;
|
|
583
|
+
const primary = stream === 'stderr' ? r.stderr : r.stdout;
|
|
584
|
+
const text = primary && primary.trim() ? primary : `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
|
|
585
|
+
const line = (text ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0];
|
|
586
|
+
return line || null;
|
|
587
|
+
}
|
|
588
|
+
async function fileExists(p) {
|
|
589
|
+
try {
|
|
590
|
+
await fs.access(p);
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/** Parse the first dotted-number sequence into [major, minor, patch]. */
|
|
598
|
+
export function parseVersion(s) {
|
|
599
|
+
const m = s.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
600
|
+
if (!m)
|
|
601
|
+
return null;
|
|
602
|
+
return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
|
|
603
|
+
}
|
|
604
|
+
function cmpVersion(a, b) {
|
|
605
|
+
for (let i = 0; i < 3; i++) {
|
|
606
|
+
const d = a[i] - b[i];
|
|
607
|
+
if (d !== 0)
|
|
608
|
+
return d < 0 ? -1 : 1;
|
|
609
|
+
}
|
|
610
|
+
return 0;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Is `version` inside the [min, max) window? Unparseable versions or an absent
|
|
614
|
+
* range are treated as satisfied (we never block on what we can't measure).
|
|
615
|
+
*/
|
|
616
|
+
export function versionInRange(version, range) {
|
|
617
|
+
if (!range || (!range.min && !range.max))
|
|
618
|
+
return true;
|
|
619
|
+
const pv = parseVersion(version);
|
|
620
|
+
if (!pv)
|
|
621
|
+
return true;
|
|
622
|
+
if (range.min) {
|
|
623
|
+
const pm = parseVersion(range.min);
|
|
624
|
+
if (pm && cmpVersion(pv, pm) < 0)
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
if (range.max) {
|
|
628
|
+
const px = parseVersion(range.max);
|
|
629
|
+
if (px && cmpVersion(pv, px) >= 0)
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
/** Human-readable form of a range, e.g. ">= 3.10", "< 2.0", "[1.0, 2.0)". */
|
|
635
|
+
function describeRange(range) {
|
|
636
|
+
if (!range)
|
|
637
|
+
return '';
|
|
638
|
+
if (range.min && range.max)
|
|
639
|
+
return `[${range.min}, ${range.max})`;
|
|
640
|
+
if (range.min)
|
|
641
|
+
return `>= ${range.min}`;
|
|
642
|
+
if (range.max)
|
|
643
|
+
return `< ${range.max}`;
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Detect every tool declared in env-requirements.json. `source: 'env-dir'`
|
|
648
|
+
* tools are a directory check; `source: 'path'` tools are located by trying
|
|
649
|
+
* each alias on PATH, then the optional DevEco fallback path. When a `verify`
|
|
650
|
+
* command is present we also capture and parse the tool's version.
|
|
651
|
+
*/
|
|
652
|
+
async function detectEnvironment(spec, env) {
|
|
653
|
+
const status = new Map();
|
|
654
|
+
const isWin = process.platform === 'win32';
|
|
655
|
+
const baseDir = (key) => key === 'DEVECO_SDK_HOME'
|
|
656
|
+
? env.DEVECO_SDK_HOME
|
|
657
|
+
: key === 'DEVECO_PATH'
|
|
658
|
+
? env.DEVECO_PATH
|
|
659
|
+
: '';
|
|
660
|
+
for (const [id, tool] of Object.entries(spec.tools)) {
|
|
661
|
+
if (tool.source === 'env-dir') {
|
|
662
|
+
const dir = tool.envVar ? baseDir(tool.envVar) : '';
|
|
663
|
+
const ok = dir ? await dirExists(dir) : false;
|
|
664
|
+
status.set(id, { found: ok, path: ok ? dir : undefined });
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
// source 'path': try each alias on PATH, then the fallback path.
|
|
668
|
+
const aliases = tool.aliases?.length ? tool.aliases : [id];
|
|
669
|
+
let resolvedPath = null;
|
|
670
|
+
let cmdToRun = null;
|
|
671
|
+
for (const alias of aliases) {
|
|
672
|
+
const p = whichSync(alias);
|
|
673
|
+
if (p) {
|
|
674
|
+
resolvedPath = p;
|
|
675
|
+
cmdToRun = alias;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
let viaFallback = false;
|
|
680
|
+
if (!resolvedPath && tool.fallback) {
|
|
681
|
+
const base = baseDir(tool.fallback.base);
|
|
682
|
+
if (base) {
|
|
683
|
+
const exe = isWin ? `${tool.fallback.exe}.exe` : tool.fallback.exe;
|
|
684
|
+
const cand = path.join(base, ...tool.fallback.segments, exe);
|
|
685
|
+
if (await fileExists(cand)) {
|
|
686
|
+
resolvedPath = cand;
|
|
687
|
+
cmdToRun = cand;
|
|
688
|
+
viaFallback = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const st = { found: !!resolvedPath, path: resolvedPath ?? undefined };
|
|
693
|
+
if (resolvedPath && cmdToRun && tool.verify) {
|
|
694
|
+
const raw = captureVerify(cmdToRun, tool.verify, tool.versionStream === 'stderr' ? 'stderr' : 'stdout');
|
|
695
|
+
if (raw) {
|
|
696
|
+
st.note = viaFallback ? `${raw} (via ${tool.fallback.base} fallback)` : raw;
|
|
697
|
+
if (tool.versionRegex) {
|
|
698
|
+
const m = raw.match(new RegExp(tool.versionRegex));
|
|
699
|
+
if (m && m[1])
|
|
700
|
+
st.version = m[1];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else if (viaFallback) {
|
|
704
|
+
st.note = `via ${tool.fallback.base} fallback`;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else if (viaFallback) {
|
|
708
|
+
st.note = `via ${tool.fallback.base} fallback`;
|
|
709
|
+
}
|
|
710
|
+
status.set(id, st);
|
|
711
|
+
}
|
|
712
|
+
return status;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* A dependency is satisfied when its tool is present AND its detected version
|
|
716
|
+
* (if known) falls in the required range (dependency range overrides the tool
|
|
717
|
+
* default). Returns the reason token shown in the impact table when not.
|
|
718
|
+
*/
|
|
719
|
+
function evaluateDependency(dep, spec, tools) {
|
|
720
|
+
const st = tools.get(dep.tool);
|
|
721
|
+
const range = dep.version ?? spec.tools[dep.tool]?.version ?? null;
|
|
722
|
+
if (!st || !st.found) {
|
|
723
|
+
return { satisfied: false, reason: dep.tool };
|
|
724
|
+
}
|
|
725
|
+
if (st.version && !versionInRange(st.version, range)) {
|
|
726
|
+
return {
|
|
727
|
+
satisfied: false,
|
|
728
|
+
reason: `${dep.tool} ${st.version} (need ${describeRange(range)})`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return { satisfied: true, reason: '' };
|
|
732
|
+
}
|
|
733
|
+
/** Print detected tools + the per-skill/agent impact table. */
|
|
734
|
+
export async function runEnvironmentCheck(env) {
|
|
735
|
+
console.log('');
|
|
736
|
+
console.log(chalk.blue(' Environment Check'));
|
|
737
|
+
const spec = await loadEnvRequirements();
|
|
738
|
+
if (!spec)
|
|
739
|
+
return;
|
|
740
|
+
const tools = await detectEnvironment(spec, env);
|
|
741
|
+
const order = Object.keys(spec.tools);
|
|
742
|
+
for (const name of order) {
|
|
743
|
+
const st = tools.get(name);
|
|
744
|
+
const tool = spec.tools[name];
|
|
745
|
+
if (st.found) {
|
|
746
|
+
let note = st.note ?? '';
|
|
747
|
+
if (st.version && tool.version && !versionInRange(st.version, tool.version)) {
|
|
748
|
+
note += ` (requires ${describeRange(tool.version)})`;
|
|
749
|
+
}
|
|
750
|
+
const noteStr = note ? chalk.gray(` (${note})`) : '';
|
|
751
|
+
console.log(` ${chalk.green('+')} ${name.padEnd(9)} ${st.path ?? ''}${noteStr}`);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
console.log(` ${chalk.yellow('!')} ${name.padEnd(9)} ${chalk.yellow('not found')} ${chalk.gray(tool.hint ?? '')}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
console.log('');
|
|
758
|
+
console.log(chalk.blue(' Impact on skills/agents:'));
|
|
759
|
+
const nameW = 28;
|
|
760
|
+
console.log(chalk.gray(` ${'Name'.padEnd(nameW)} ${'Kind'.padEnd(6)} ${'Status'.padEnd(8)} Missing`));
|
|
761
|
+
let allOk = true;
|
|
762
|
+
for (const row of spec.requirements) {
|
|
763
|
+
const unmetReq = [];
|
|
764
|
+
const unmetOpt = [];
|
|
765
|
+
for (const dep of row.dependencies) {
|
|
766
|
+
const { satisfied, reason } = evaluateDependency(dep, spec, tools);
|
|
767
|
+
if (satisfied)
|
|
768
|
+
continue;
|
|
769
|
+
if (dep.level === 'required')
|
|
770
|
+
unmetReq.push(reason);
|
|
771
|
+
else
|
|
772
|
+
unmetOpt.push(`${reason}(optional)`);
|
|
773
|
+
}
|
|
774
|
+
let plain;
|
|
775
|
+
let color;
|
|
776
|
+
let parts;
|
|
777
|
+
if (unmetReq.length > 0) {
|
|
778
|
+
plain = 'BLOCKED';
|
|
779
|
+
color = chalk.red;
|
|
780
|
+
parts = [...unmetReq, ...unmetOpt];
|
|
781
|
+
allOk = false;
|
|
782
|
+
}
|
|
783
|
+
else if (unmetOpt.length > 0) {
|
|
784
|
+
plain = 'LIMITED';
|
|
785
|
+
color = chalk.yellow;
|
|
786
|
+
parts = unmetOpt;
|
|
787
|
+
allOk = false;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
plain = 'OK';
|
|
791
|
+
color = chalk.green;
|
|
792
|
+
parts = [];
|
|
793
|
+
}
|
|
794
|
+
const noteStr = parts.length > 0 && row.note ? chalk.gray(` — ${row.note}`) : '';
|
|
795
|
+
console.log(` ${row.name.padEnd(nameW)} ${row.kind.padEnd(6)} ${color(plain.padEnd(8))} ${parts.join(', ')}${noteStr}`);
|
|
796
|
+
}
|
|
797
|
+
console.log('');
|
|
798
|
+
if (allOk) {
|
|
799
|
+
console.log(chalk.green(' All environment dependencies found — every skill/agent is fully usable.'));
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
console.log(chalk.gray(' Install the missing tools above and re-run `ht init` to clear the impact list.'));
|
|
214
803
|
}
|
|
215
|
-
return p.replace(/\\/g, '/');
|
|
216
804
|
}
|
|
217
805
|
async function detectInstalledEditors(editors) {
|
|
218
806
|
const status = new Map();
|
|
@@ -309,33 +897,76 @@ export async function initCommand(options = {}) {
|
|
|
309
897
|
const answers = await inquirer.prompt([
|
|
310
898
|
{
|
|
311
899
|
type: 'input',
|
|
312
|
-
name: '
|
|
313
|
-
message: '
|
|
314
|
-
default: config.env.
|
|
900
|
+
name: 'DEVECO_SDK_HOME',
|
|
901
|
+
message: 'DEVECO_SDK_HOME (DevEco Studio SDK dir):',
|
|
902
|
+
default: config.env.DEVECO_SDK_HOME || undefined,
|
|
315
903
|
},
|
|
316
904
|
{
|
|
317
905
|
type: 'input',
|
|
318
|
-
name: '
|
|
319
|
-
message: '
|
|
320
|
-
default: config.env.
|
|
906
|
+
name: 'TEST_API_KEY',
|
|
907
|
+
message: 'TEST_API_KEY (LLM API key used by the integration-test agent to run test cases):',
|
|
908
|
+
default: config.env.TEST_API_KEY || undefined,
|
|
321
909
|
},
|
|
322
910
|
{
|
|
323
911
|
type: 'input',
|
|
324
|
-
name: '
|
|
325
|
-
message: '
|
|
326
|
-
default: config.env.
|
|
912
|
+
name: 'GLM_API_KEY',
|
|
913
|
+
message: 'GLM_API_KEY (LLM API key for the GLM phone-agent used in UI alignment):',
|
|
914
|
+
default: config.env.GLM_API_KEY || undefined,
|
|
327
915
|
},
|
|
328
916
|
]);
|
|
329
|
-
|
|
330
|
-
config.env.
|
|
917
|
+
const sdk = deriveSdkPaths(answers.DEVECO_SDK_HOME);
|
|
918
|
+
config.env.DEVECO_SDK_HOME = sdk.DEVECO_SDK_HOME;
|
|
919
|
+
config.env.DEVECO_PATH = sdk.DEVECO_PATH;
|
|
920
|
+
config.env.OHOS_SDK_PATH = sdk.OHOS_SDK_PATH;
|
|
921
|
+
config.env.HMS_SDK_PATH = sdk.HMS_SDK_PATH;
|
|
331
922
|
config.env.TEST_API_KEY = answers.TEST_API_KEY.trim();
|
|
923
|
+
config.env.GLM_API_KEY = answers.GLM_API_KEY.trim();
|
|
332
924
|
await saveHomeTransConfig(config);
|
|
925
|
+
// Echo the derived paths and validate each exists on disk; missing ones are
|
|
926
|
+
// a strong signal of a typo'd DEVECO_SDK_HOME or a broken DevEco install.
|
|
927
|
+
if (sdk.DEVECO_SDK_HOME) {
|
|
928
|
+
const checks = [
|
|
929
|
+
['DEVECO_SDK_HOME', sdk.DEVECO_SDK_HOME],
|
|
930
|
+
['DEVECO_PATH', sdk.DEVECO_PATH],
|
|
931
|
+
['OHOS_SDK_PATH', sdk.OHOS_SDK_PATH],
|
|
932
|
+
['HMS_SDK_PATH', sdk.HMS_SDK_PATH],
|
|
933
|
+
];
|
|
934
|
+
console.log('');
|
|
935
|
+
console.log(chalk.blue(' Derived from DEVECO_SDK_HOME:'));
|
|
936
|
+
let missingCount = 0;
|
|
937
|
+
for (const [name, p] of checks) {
|
|
938
|
+
const exists = await dirExists(p);
|
|
939
|
+
if (!exists)
|
|
940
|
+
missingCount++;
|
|
941
|
+
const mark = exists ? chalk.green('+') : chalk.yellow('!');
|
|
942
|
+
const note = exists ? '' : chalk.yellow(' (not found on disk)');
|
|
943
|
+
console.log(` ${mark} ${name.padEnd(15)} : ${p}${note}`);
|
|
944
|
+
}
|
|
945
|
+
if (missingCount > 0) {
|
|
946
|
+
console.log('');
|
|
947
|
+
console.log(chalk.yellow(' ! Check DEVECO_SDK_HOME (it should be the "sdk" folder inside your DevEco Studio install)'));
|
|
948
|
+
console.log(chalk.yellow(' and re-run `ht init` after fixing it. Skills that build or'));
|
|
949
|
+
console.log(chalk.yellow(' review code will not work until these paths resolve.'));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
333
952
|
}
|
|
334
953
|
catch (err) {
|
|
335
954
|
if (isPromptAbort(err))
|
|
336
955
|
abortInit();
|
|
337
956
|
console.log(chalk.yellow(' Parameter prompts skipped (non-interactive mode).'));
|
|
338
957
|
}
|
|
958
|
+
// Detect external tools (adb / hdc / python / uv / java / gitnexus + DevEco)
|
|
959
|
+
// and report which skills/agents are impacted by anything missing.
|
|
960
|
+
try {
|
|
961
|
+
await runEnvironmentCheck(config.env);
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
console.log(chalk.yellow(` ! environment check skipped: ${err.message}`));
|
|
965
|
+
}
|
|
966
|
+
// Snapshot the currently installed autotest template BEFORE the tools copy
|
|
967
|
+
// overwrites it — refreshAutotestConfig compares it with the new template
|
|
968
|
+
// to detect drift and offer a config.yaml regeneration.
|
|
969
|
+
const prevAutotestExample = await readFileIfExists(path.join(resolveAutotestDir(config.env.TOOL_PATH || getToolsDir()), 'config.yaml.example'));
|
|
339
970
|
// Copy bundled tools/ into ~/.hometrans/tools and record env.TOOL_PATH.
|
|
340
971
|
// Must run before the autotest config step below, which seeds config.yaml
|
|
341
972
|
// into the installed tools dir (the location agents read via env.TOOL_PATH).
|
|
@@ -350,13 +981,24 @@ export async function initCommand(options = {}) {
|
|
|
350
981
|
catch (err) {
|
|
351
982
|
console.log(chalk.red(` ! tools copy: ${err.message}`));
|
|
352
983
|
}
|
|
984
|
+
// Copy env-requirements.json into ~/.hometrans, next to config.json, so the
|
|
985
|
+
// declared environment requirements live alongside the user's config.
|
|
986
|
+
try {
|
|
987
|
+
const envReqDest = await installEnvRequirements();
|
|
988
|
+
if (envReqDest) {
|
|
989
|
+
console.log(chalk.green(` + env-requirements.json copied -> ${prettyHome(envReqDest)}`));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
console.log(chalk.red(` ! env-requirements.json copy: ${err.message}`));
|
|
994
|
+
}
|
|
353
995
|
// Initialize / refresh autotest config.yaml from the example template,
|
|
354
996
|
// inside the installed tools dir (<TOOL_PATH>/test-tools/autotest).
|
|
355
997
|
try {
|
|
356
998
|
const toolPath = installedToolPath ?? config.env.TOOL_PATH;
|
|
357
999
|
if (toolPath) {
|
|
358
1000
|
const autotestDir = resolveAutotestDir(toolPath);
|
|
359
|
-
const status = await refreshAutotestConfig(autotestDir, config.env.TEST_API_KEY);
|
|
1001
|
+
const status = await refreshAutotestConfig(autotestDir, config.env.TEST_API_KEY, prevAutotestExample);
|
|
360
1002
|
if (status) {
|
|
361
1003
|
console.log(chalk.green(` + ${status}`));
|
|
362
1004
|
console.log(chalk.gray(` ${path.join(autotestDir, 'config.yaml')}`));
|
|
@@ -369,9 +1011,13 @@ export async function initCommand(options = {}) {
|
|
|
369
1011
|
console.log('');
|
|
370
1012
|
const editorsToSetup = editors.filter((e) => selectedEditors.includes(e.name));
|
|
371
1013
|
const result = { configured: [], skipped: [], errors: [] };
|
|
1014
|
+
// hmos_sdk_dir consumed by incremental-ui-align = <DEVECO_SDK_HOME>/default.
|
|
1015
|
+
const hmosSdkDir = config.env.DEVECO_SDK_HOME
|
|
1016
|
+
? path.join(config.env.DEVECO_SDK_HOME, 'default')
|
|
1017
|
+
: '';
|
|
372
1018
|
for (const editor of editorsToSetup) {
|
|
373
1019
|
console.log(chalk.blue(` Configuring ${editor.name}...`));
|
|
374
|
-
await installForEditor(editor, skillsRoot, agentsRoot, result);
|
|
1020
|
+
await installForEditor(editor, skillsRoot, agentsRoot, config.env.GLM_API_KEY, hmosSdkDir, result);
|
|
375
1021
|
}
|
|
376
1022
|
await setupMcpForAllEditors(editorsToSetup, result);
|
|
377
1023
|
console.log('');
|