@hmharness/domain-harmony 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,439 @@
1
+ /**
2
+ * @hmharness/domain-harmony
3
+ * HarmonyOS domain tools. Device + toolchain probes are zero-risk;
4
+ * build/install/launch/logs cover the code-to-device lifecycle and landed
5
+ * in Phase 1. Device-mutating tools (install/launch/uninstall) carry
6
+ * needsApproval - the kernel loop gates them behind user confirmation.
7
+ */
8
+ import { execFile } from 'node:child_process';
9
+ import { accessSync } from 'node:fs';
10
+ import { access, readdir, readFile, stat } from 'node:fs/promises';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { promisify } from 'node:util';
13
+ import { harmonyProjectCreate } from "./project.js";
14
+ import { harmonyCjpmBuild, harmonyCjpmTest, findCjpm } from "./cangjie.js";
15
+ import { harmonyLint } from "./lint.js";
16
+ import { emulatorTools } from "./emulator.js";
17
+ import { harmonySchemaCheck } from "./schema.js";
18
+ import { harmonyBuildDoctor } from "./builddoctor.js";
19
+ import { harmonyProjectProfile } from "./profile.js";
20
+ import { harmonySign } from "./signing.js";
21
+ import { harmonyDeviceTest } from "./ondevice.js";
22
+ import { harmonyApiLookup } from "./apikg.js";
23
+ import { harmonyUiRegression } from "./uiregress.js";
24
+ const exec = promisify(execFile);
25
+ function devecoHome() {
26
+ return process.env.HM_DEVECO_HOME ?? 'C:\\DevEco-Studio';
27
+ }
28
+ async function run(cmd, args, timeoutMs = 20_000, cwd, extraEnv) {
29
+ try {
30
+ const { stdout, stderr } = await exec(cmd, args, {
31
+ timeout: timeoutMs,
32
+ windowsHide: true,
33
+ maxBuffer: 16 * 1024 * 1024,
34
+ cwd,
35
+ ...(extraEnv ? { env: { ...process.env, ...extraEnv } } : {}),
36
+ });
37
+ return { ok: true, out: (stdout || stderr || '(no output)').trim() };
38
+ }
39
+ catch (err) {
40
+ const e = err;
41
+ return { ok: false, out: [e.stdout, e.stderr, e.message].filter(Boolean).join('\n').slice(0, 4000) };
42
+ }
43
+ }
44
+ async function findHdc() {
45
+ // PATH first, then the DevEco SDK layout
46
+ const p = await run('hdc', ['--version'], 8000);
47
+ if (p.ok)
48
+ return 'hdc';
49
+ const candidate = join(devecoHome(), 'sdk', 'default', 'openharmony', 'toolchains', 'hdc.exe');
50
+ try {
51
+ await access(candidate);
52
+ return candidate;
53
+ }
54
+ catch {
55
+ return '';
56
+ }
57
+ }
58
+ /** hdc command prefix honoring an optional target, e.g. ["-t", "127.0.0.1:5555"]. */
59
+ function hdcArgs(target) {
60
+ const t = target?.trim();
61
+ return t ? ['-t', t] : [];
62
+ }
63
+ /* ------------------------------------------------------------------ */
64
+ /* Zero-risk probes (Phase 0) */
65
+ /* ------------------------------------------------------------------ */
66
+ export const harmonyDevices = {
67
+ name: 'harmony_devices',
68
+ description: 'List connected HarmonyOS devices/emulators (targets) via hdc. Returns the raw target list: index, state and connect string. Use before any device operation to learn the target id.',
69
+ parameters: { type: 'object', properties: {}, required: [] },
70
+ async execute() {
71
+ const hdc = await findHdc();
72
+ if (!hdc) {
73
+ return {
74
+ output: 'hdc not found. Install DevEco Studio (SDK component) or add its toolchains dir to PATH, or set HM_DEVECO_HOME.',
75
+ isError: true,
76
+ };
77
+ }
78
+ const r = await run(hdc, ['list', 'targets']);
79
+ const lines = r.out.split('\n').map((l) => l.trim()).filter((l) => l && l !== '[Empty]');
80
+ if (lines.length === 0)
81
+ return { output: 'No devices connected. Start an emulator or plug in a device with USB debugging.' };
82
+ return { output: lines.map((l, i) => `${i}: ${l}`).join('\n') };
83
+ },
84
+ };
85
+ export const harmonyToolchainCheck = {
86
+ name: 'harmony_toolchain_check',
87
+ description: 'Check the local HarmonyOS development toolchain: hdc (devices), hvigorw (build) and ohpm (packages), with resolved paths and versions where available.',
88
+ parameters: { type: 'object', properties: {}, required: [] },
89
+ async execute() {
90
+ const lines = [];
91
+ const hdc = await findHdc();
92
+ if (hdc) {
93
+ const v = await run(hdc, ['--version'], 8000);
94
+ lines.push(`hdc: OK (${hdc}) ${v.out.split('\n')[0] ?? ''}`.trim());
95
+ }
96
+ else
97
+ lines.push('hdc: MISSING');
98
+ const hvigorw = join(devecoHome(), 'tools', 'hvigor', 'bin', hvigorwName());
99
+ try {
100
+ await access(hvigorw);
101
+ lines.push(`hvigorw: OK (${hvigorw})`);
102
+ }
103
+ catch {
104
+ lines.push(`hvigorw: MISSING (looked at ${hvigorw})`);
105
+ }
106
+ const ohpm = join(devecoHome(), 'tools', 'ohpm', 'bin', ohpmName());
107
+ try {
108
+ await access(ohpm);
109
+ lines.push(`ohpm: OK (${ohpm})`);
110
+ }
111
+ catch {
112
+ lines.push(`ohpm: MISSING (looked at ${ohpm})`);
113
+ }
114
+ const cjpm = await findCjpm();
115
+ if (cjpm) {
116
+ const v = await run(cjpm, ['--version'], 8000);
117
+ lines.push(`cjpm: OK (${cjpm}) ${v.out.split('\n')[0] ?? ''}`.trim());
118
+ }
119
+ else {
120
+ lines.push('cjpm: MISSING (set HM_CJPM or add cangjie bin to PATH)');
121
+ }
122
+ return { output: lines.join('\n') };
123
+ },
124
+ };
125
+ /* ------------------------------------------------------------------ */
126
+ /* Build / install / launch / logs (Phase 1) */
127
+ /* ------------------------------------------------------------------ */
128
+ function hvigorwName() {
129
+ return process.platform === 'win32' ? 'hvigorw.bat' : 'hvigorw.sh';
130
+ }
131
+ function ohpmName() {
132
+ return process.platform === 'win32' ? 'ohpm.bat' : 'ohpm';
133
+ }
134
+ /** Windows .bat scripts must go through cmd; posix scripts run directly. */
135
+ async function runScript(scriptPath, args, timeoutMs, cwd, extraEnv) {
136
+ if (process.platform === 'win32') {
137
+ return run('cmd', ['/c', scriptPath, ...args], timeoutMs, cwd, buildEnv(extraEnv));
138
+ }
139
+ return run('sh', [scriptPath, ...args], timeoutMs, cwd, buildEnv(extraEnv));
140
+ }
141
+ /** hvigor/ohpm ship with a bundled node - make sure they can find it. */
142
+ function buildEnv(extra) {
143
+ const env = { ...extra };
144
+ const nodeDir = join(devecoHome(), 'tools', 'node');
145
+ try {
146
+ accessSyncOrIgnore(nodeDir);
147
+ if (!process.env.NODE_HOME)
148
+ env.NODE_HOME = nodeDir;
149
+ if (!process.env.NODE_PATH)
150
+ env.NODE_PATH = join(nodeDir, 'node_modules', 'npm');
151
+ }
152
+ catch {
153
+ /* DevEco node layout differs - fall back to inherited env */
154
+ }
155
+ // Standalone hvigorw runs need the SDK root; DevEco sets this globally,
156
+ // shells without it get "Invalid value of DEVECO_SDK_HOME".
157
+ if (!process.env.DEVECO_SDK_HOME) {
158
+ const sdkRoot = join(devecoHome(), 'sdk');
159
+ try {
160
+ accessSyncOrIgnore(sdkRoot);
161
+ env.DEVECO_SDK_HOME = sdkRoot;
162
+ }
163
+ catch {
164
+ /* no sdk dir - let hvigor report it */
165
+ }
166
+ }
167
+ return Object.keys(env).length > 0 ? env : undefined;
168
+ }
169
+ function accessSyncOrIgnore(p) {
170
+ accessSync(p);
171
+ }
172
+ /** Walk up from `start` looking for the hvigor project marker. */
173
+ async function findProjectRoot(start) {
174
+ let dir = resolve(start);
175
+ for (;;) {
176
+ if (await exists(join(dir, 'build-profile.json5')))
177
+ return dir;
178
+ const parent = dirname(dir);
179
+ if (parent === dir)
180
+ return '';
181
+ dir = parent;
182
+ }
183
+ }
184
+ async function exists(p) {
185
+ try {
186
+ await stat(p);
187
+ return true;
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
193
+ /** JSON5-lite parse: tolerant of line/block comments and trailing commas. */
194
+ function parseJson5(text) {
195
+ const cleaned = text
196
+ .replace(/\/\*[\s\S]*?\*\//g, '')
197
+ .replace(/(^|[^:"'\\])\/\/.*$/gm, '$1')
198
+ .replace(/,(\s*[}\]])/g, '$1');
199
+ return JSON.parse(cleaned);
200
+ }
201
+ async function readJson5(p) {
202
+ try {
203
+ return parseJson5(await readFile(p, 'utf8'));
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ async function bundleNameOf(projectRoot) {
210
+ const app = await readJson5(join(projectRoot, 'AppScope', 'app.json5'));
211
+ const b = app?.app?.bundleName;
212
+ if (b)
213
+ return b;
214
+ // entry/src/main/module.json5 fallback (module name - a weak substitute,
215
+ // real bundleName lives in AppScope)
216
+ const mod = await readJson5(join(projectRoot, 'entry', 'src', 'main', 'module.json5'));
217
+ return mod?.module?.name ?? '';
218
+ }
219
+ /** Newest .hap under the project's build outputs (module dirs: entry/build/...). */
220
+ async function newestHap(projectRoot) {
221
+ let best = '';
222
+ let bestMtime = 0;
223
+ const stack = [projectRoot];
224
+ for (let depth = 0; stack.length > 0 && depth < 1000; depth++) {
225
+ const dir = stack.pop();
226
+ let entries;
227
+ try {
228
+ entries = await readdir(dir, { withFileTypes: true });
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ for (const e of entries) {
234
+ const p = join(dir, e.name);
235
+ if (e.isDirectory()) {
236
+ if (['node_modules', 'oh_modules', '.hvigor', '.preview', 'src'].includes(e.name))
237
+ continue;
238
+ stack.push(p);
239
+ }
240
+ else if (e.name.endsWith('.hap')) {
241
+ const m = (await stat(p)).mtimeMs;
242
+ if (m > bestMtime) {
243
+ bestMtime = m;
244
+ best = p;
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return best;
250
+ }
251
+ export const harmonyBuild = {
252
+ name: 'harmony_build',
253
+ description: 'Build a HarmonyOS project with hvigor (assembleHap). Accepts an optional project path (defaults to walking up from cwd for build-profile.json5). Long-running: allow several minutes. Reports the build result tail and the produced .hap path.',
254
+ parameters: {
255
+ type: 'object',
256
+ properties: {
257
+ project: { type: 'string', description: 'project directory containing build-profile.json5 (default: auto-detect from cwd)' },
258
+ clean: { type: 'boolean', description: 'run a clean build (slower)' },
259
+ },
260
+ required: [],
261
+ },
262
+ async execute(args, ctx) {
263
+ const hvigorw = join(devecoHome(), 'tools', 'hvigor', 'bin', hvigorwName());
264
+ if (!(await exists(hvigorw))) {
265
+ return { output: `hvigorw not found at ${hvigorw}. Set HM_DEVECO_HOME or install DevEco Studio.`, isError: true };
266
+ }
267
+ const start = typeof args.project === 'string' && args.project ? resolve(ctx.cwd, args.project) : ctx.cwd;
268
+ const root = await findProjectRoot(start);
269
+ if (!root) {
270
+ return { output: `No HarmonyOS project found at or above ${start} (looking for build-profile.json5).`, isError: true };
271
+ }
272
+ if (args.clean === true)
273
+ await runScript(hvigorw, ['clean'], 300_000, root);
274
+ const r = await runScript(hvigorw, ['--mode', 'module', '-p', 'product=default', 'assembleHap', '--no-daemon'], 900_000, root);
275
+ const hap = await newestHap(root);
276
+ const tail = r.out.length > 4000 ? '...\n' + r.out.slice(-4000) : r.out;
277
+ const okLine = r.ok && /BUILD SUCCESSFUL/i.test(r.out);
278
+ const summary = [
279
+ `project: ${root}`,
280
+ `result: ${okLine ? 'BUILD SUCCESSFUL' : r.ok ? 'finished (check log)' : 'FAILED'}`,
281
+ hap ? `hap: ${hap}` : 'hap: none found under build/',
282
+ '',
283
+ tail,
284
+ ].join('\n');
285
+ return { output: summary, isError: !okLine };
286
+ },
287
+ };
288
+ export const harmonyInstall = {
289
+ name: 'harmony_install',
290
+ description: 'Install a .hap onto a connected HarmonyOS device via hdc (mutates the device - requires approval). Without a path, installs the newest .hap in the current project build output. Tries "hdc install" then falls back to "hdc app install".',
291
+ parameters: {
292
+ type: 'object',
293
+ properties: {
294
+ hap: { type: 'string', description: 'path to the .hap file (default: newest under the project build output)' },
295
+ target: { type: 'string', description: 'device target id from harmony_devices (default: the only/first device)' },
296
+ replace: { type: 'boolean', description: 'replace an existing installation (default true)' },
297
+ },
298
+ required: [],
299
+ },
300
+ needsApproval: () => true,
301
+ async execute(args, ctx) {
302
+ const hdc = await findHdc();
303
+ if (!hdc)
304
+ return { output: 'hdc not found.', isError: true };
305
+ let hap = typeof args.hap === 'string' && args.hap ? resolve(ctx.cwd, args.hap) : '';
306
+ if (!hap) {
307
+ const root = await findProjectRoot(ctx.cwd);
308
+ if (!root)
309
+ return { output: 'No .hap path given and no project found around cwd.', isError: true };
310
+ hap = await newestHap(root);
311
+ if (!hap)
312
+ return { output: `No .hap found under ${join(root, 'build')}. Build first with harmony_build.`, isError: true };
313
+ }
314
+ if (!(await exists(hap)))
315
+ return { output: `No such file: ${hap}`, isError: true };
316
+ const pre = hdcArgs(typeof args.target === 'string' ? args.target : undefined);
317
+ const replace = args.replace !== false;
318
+ let r = await run(hdc, [...pre, 'install', ...(replace ? ['-r'] : []), hap], 120_000);
319
+ if (!r.ok || /unknown command|invalid/i.test(r.out)) {
320
+ r = await run(hdc, [...pre, 'app', 'install', ...(replace ? ['-r'] : []), hap], 120_000);
321
+ }
322
+ return { output: `install ${hap}\n${r.out}`, isError: !r.ok };
323
+ },
324
+ };
325
+ export const harmonyLaunch = {
326
+ name: 'harmony_launch',
327
+ description: 'Launch an app on a connected HarmonyOS device via "hdc shell aa start" (mutates device state - requires approval). bundle defaults to the current project AppScope/app.json5 bundleName; ability defaults to EntryAbility.',
328
+ parameters: {
329
+ type: 'object',
330
+ properties: {
331
+ bundle: { type: 'string', description: 'bundle name, e.g. com.example.myapp (default: from the project)' },
332
+ ability: { type: 'string', description: 'ability name (default: EntryAbility)' },
333
+ target: { type: 'string', description: 'device target id from harmony_devices' },
334
+ },
335
+ required: [],
336
+ },
337
+ needsApproval: () => true,
338
+ async execute(args, ctx) {
339
+ const hdc = await findHdc();
340
+ if (!hdc)
341
+ return { output: 'hdc not found.', isError: true };
342
+ let bundle = typeof args.bundle === 'string' && args.bundle ? args.bundle : '';
343
+ if (!bundle) {
344
+ const root = await findProjectRoot(ctx.cwd);
345
+ if (!root)
346
+ return { output: 'No bundle given and no project found around cwd to read AppScope/app.json5.', isError: true };
347
+ bundle = await bundleNameOf(root);
348
+ if (!bundle)
349
+ return { output: `Could not read bundleName from the project at ${root}.`, isError: true };
350
+ }
351
+ const ability = typeof args.ability === 'string' && args.ability ? args.ability : 'EntryAbility';
352
+ const pre = hdcArgs(typeof args.target === 'string' ? args.target : undefined);
353
+ const r = await run(hdc, [...pre, 'shell', 'aa', 'start', '-a', ability, '-b', bundle], 30_000);
354
+ return { output: `aa start -a ${ability} -b ${bundle}\n${r.out}`, isError: !r.ok };
355
+ },
356
+ };
357
+ export const harmonyLogs = {
358
+ name: 'harmony_logs',
359
+ description: 'Fetch recent device logs via "hdc shell hilog -x" (dump-and-exit). Returns the last N lines, optionally filtered by a substring. Use for diagnosing crashes, ability failures, or app behavior after harmony_launch.',
360
+ parameters: {
361
+ type: 'object',
362
+ properties: {
363
+ lines: { type: 'number', description: 'how many trailing lines to return (default 200, max 2000)' },
364
+ grep: { type: 'string', description: 'only include lines containing this substring' },
365
+ target: { type: 'string', description: 'device target id from harmony_devices' },
366
+ },
367
+ required: [],
368
+ },
369
+ async execute(args) {
370
+ const hdc = await findHdc();
371
+ if (!hdc)
372
+ return { output: 'hdc not found.', isError: true };
373
+ const lines = Math.min(Math.max(Number(args.lines ?? 200), 1), 2000);
374
+ const pre = hdcArgs(typeof args.target === 'string' ? args.target : undefined);
375
+ const r = await run(hdc, [...pre, 'shell', 'hilog', '-x'], 60_000);
376
+ if (!r.ok)
377
+ return { output: r.out, isError: true };
378
+ let all = r.out.split('\n');
379
+ if (typeof args.grep === 'string' && args.grep)
380
+ all = all.filter((l) => l.includes(args.grep));
381
+ return { output: all.slice(-lines).join('\n') || '(no matching log lines)' };
382
+ },
383
+ };
384
+ export const harmonyUninstall = {
385
+ name: 'harmony_uninstall',
386
+ description: 'Uninstall a bundle from a connected device via hdc (destructive - requires approval).',
387
+ parameters: {
388
+ type: 'object',
389
+ properties: {
390
+ bundle: { type: 'string', description: 'bundle name, e.g. com.example.myapp' },
391
+ target: { type: 'string', description: 'device target id from harmony_devices' },
392
+ },
393
+ required: ['bundle'],
394
+ },
395
+ needsApproval: () => true,
396
+ async execute(args) {
397
+ const hdc = await findHdc();
398
+ if (!hdc)
399
+ return { output: 'hdc not found.', isError: true };
400
+ const pre = hdcArgs(typeof args.target === 'string' ? args.target : undefined);
401
+ let r = await run(hdc, [...pre, 'uninstall', String(args.bundle)], 60_000);
402
+ if (!r.ok || /unknown command|invalid/i.test(r.out)) {
403
+ r = await run(hdc, [...pre, 'app', 'uninstall', String(args.bundle)], 60_000);
404
+ }
405
+ return { output: r.out, isError: !r.ok };
406
+ },
407
+ };
408
+ export const harmonyTools = [
409
+ harmonyDevices,
410
+ harmonyToolchainCheck,
411
+ harmonyBuild,
412
+ harmonyInstall,
413
+ harmonyLaunch,
414
+ harmonyLogs,
415
+ harmonyUninstall,
416
+ harmonyProjectCreate,
417
+ harmonyCjpmBuild,
418
+ harmonyCjpmTest,
419
+ harmonyLint,
420
+ harmonySchemaCheck,
421
+ harmonyBuildDoctor,
422
+ harmonyProjectProfile,
423
+ harmonySign,
424
+ harmonyDeviceTest,
425
+ harmonyApiLookup,
426
+ harmonyUiRegression,
427
+ ...emulatorTools,
428
+ ];
429
+ export { harmonyProjectCreate, scaffoldProject, solidPng, sdkVersion } from "./project.js";
430
+ export { harmonyCjpmBuild, harmonyCjpmTest, findCjpm } from "./cangjie.js";
431
+ export { harmonySchemaCheck, checkProjectSchemas, parseJson5 as parseJson5Strict, validateModuleJson5, validateBuildProfile } from "./schema.js";
432
+ export { parseSdkVersion, compareSdk, capabilitiesFor, CAPABILITY_MATRIX } from "./apimatrix.js";
433
+ export { harmonyBuildDoctor, diagnoseBuildFailure, firstErrorBlock } from "./builddoctor.js";
434
+ export { harmonyProjectProfile, profileProject } from "./profile.js";
435
+ export { harmonySign, resolveSigningIdentity, ensureDebugProfile, signHap, hapsignToolPaths } from "./signing.js";
436
+ export { harmonyDeviceTest, runDeviceTest } from "./ondevice.js";
437
+ export { harmonyApiLookup, buildApiIndex, loadApiIndex, lookupSymbol, parseDeclaration, sdkApiDir } from "./apikg.js";
438
+ export { harmonyUiRegression, runUiRegression, captureDeviceScreen } from "./uiregress.js";
439
+ export { harmonyImageDownloadCheck } from "./emulator.js";
package/dist/lint.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Tool } from '@hmharness/kernel';
2
+ export declare const harmonyLint: Tool;
package/dist/lint.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @hmharness/domain-harmony - lint (codelinter wrapper, probe-based)
3
+ * The official codelinter CLI wrapped as a tool. Probe-first: if the CLI
4
+ * isn't installed locally the tool explains exactly what to install rather
5
+ * than failing opaquely. Flag surface kept to the minimum verified subset.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import { access } from 'node:fs/promises';
9
+ import { isAbsolute, join, resolve } from 'node:path';
10
+ import { promisify } from 'node:util';
11
+ const exec = promisify(execFile);
12
+ function devecoHome() {
13
+ return process.env.HM_DEVECO_HOME ?? 'C:\\DevEco-Studio';
14
+ }
15
+ /** Returns a launcher: either the bare command or [node, <run/index.js>]. */
16
+ async function findCodelinter() {
17
+ if (process.env.HM_CODELINTER)
18
+ return { cmd: process.env.HM_CODELINTER, args: [] };
19
+ try {
20
+ await exec('codelinter', ['--version'], { timeout: 8000, windowsHide: true });
21
+ return { cmd: 'codelinter', args: [] };
22
+ }
23
+ catch {
24
+ /* not on PATH */
25
+ }
26
+ // DevEco ships codelinter as a plugin with a node CLI entry
27
+ const entry = join(devecoHome(), 'plugins', 'codelinter', 'run', 'index.js');
28
+ try {
29
+ await access(entry);
30
+ return { cmd: process.execPath, args: [entry] };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export const harmonyLint = {
37
+ name: 'harmony_lint',
38
+ description: 'Lint a HarmonyOS/ArkTS project with the official codelinter CLI (probe-based). If codelinter is not installed, returns install guidance instead of failing. Install options: DevEco Studio "Command Line Tools" component, then set HM_CODELINTER or add it to PATH.',
39
+ parameters: {
40
+ type: 'object',
41
+ properties: { project: { type: 'string', description: 'project directory to lint (default: cwd)' },
42
+ },
43
+ required: [],
44
+ },
45
+ async execute(args, ctx) {
46
+ const bin = await findCodelinter();
47
+ if (!bin) {
48
+ return {
49
+ output: 'codelinter not found. Install DevEco Studio (its codelinter plugin) or set HM_CODELINTER. Probed PATH and ' + join(devecoHome(), 'plugins', 'codelinter', 'run', 'index.js') + '.',
50
+ isError: true,
51
+ };
52
+ }
53
+ const projArg = typeof args.project === 'string' && args.project ? args.project : ctx.cwd;
54
+ const dir = isAbsolute(projArg) ? projArg : resolve(ctx.cwd, projArg);
55
+ try {
56
+ const { stdout, stderr } = await exec(bin.cmd, [...bin.args, dir], { timeout: 300_000, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
57
+ const out = (stdout || stderr || '(no findings)').trim();
58
+ return { output: out.length > 20_000 ? out.slice(0, 20_000) + '\n...[truncated]' : out };
59
+ }
60
+ catch (err) {
61
+ const e = err;
62
+ return { output: [e.stdout, e.stderr, e.message].filter(Boolean).join('\n').slice(0, 8000), isError: true };
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,23 @@
1
+ import type { Tool } from '@hmharness/kernel';
2
+ export interface DeviceTestStep {
3
+ step: string;
4
+ pass: boolean;
5
+ detail: string;
6
+ }
7
+ export interface DeviceTestOptions {
8
+ hdc: string;
9
+ target?: string;
10
+ hap: string;
11
+ bundle: string;
12
+ ability: string;
13
+ /** log marker the app must print on startup (EntryAbility onCreate) */
14
+ expectLog: string;
15
+ waitMs?: number;
16
+ /** Injectable command runner (tests stub this; production uses execFile). */
17
+ runImpl?: (args: string[], timeout: number) => Promise<{
18
+ ok: boolean;
19
+ out: string;
20
+ }>;
21
+ }
22
+ export declare function runDeviceTest(o: DeviceTestOptions): Promise<DeviceTestStep[]>;
23
+ export declare const harmonyDeviceTest: Tool;