@oniroproject/oniro-app 0.12.0 → 0.14.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/oniro-app.js CHANGED
@@ -175,7 +175,9 @@ function registerCmdToolsCommand(program) {
175
175
  await installCmdTools({ config, progress, logger, localZipPath: opts.fromZip, tmpDir: opts.tmpDir });
176
176
  logger.info("Command-line tools installed.");
177
177
  });
178
- cmdtools.command("status").description("Report whether the command-line tools are installed and their version.").option("--json", "Emit machine-readable JSON.").action((opts) => {
178
+ cmdtools.command("status").description(
179
+ "Report whether the command-line tools are installed, their version and flavour (HarmonyOS or OpenHarmony), and the hdc binary every device command uses."
180
+ ).option("--json", "Emit machine-readable JSON.").action((opts) => {
179
181
  const { config } = getRuntime();
180
182
  const status = getCmdToolsStatus(config);
181
183
  if (opts.json) {
@@ -184,6 +186,12 @@ function registerCmdToolsCommand(program) {
184
186
  return;
185
187
  }
186
188
  process.stdout.write(`${status.status}
189
+ `);
190
+ process.stdout.write(`path: ${status.path}
191
+ `);
192
+ if (status.flavour) process.stdout.write(`flavour: ${status.flavour}
193
+ `);
194
+ process.stdout.write(`hdc: ${status.hdc.path}${status.hdc.exists ? "" : " (missing)"}
187
195
  `);
188
196
  if (!status.installed) process.exit(1);
189
197
  });
@@ -197,6 +205,7 @@ function registerCmdToolsCommand(program) {
197
205
  // src/commands/emulator.ts
198
206
  import {
199
207
  attemptHdcConnection,
208
+ getEmulatorStatus,
200
209
  installEmulator,
201
210
  isEmulatorInstalled,
202
211
  removeEmulator,
@@ -230,6 +239,24 @@ function registerEmulatorCommand(program) {
230
239
  });
231
240
  logger.info("Emulator start command complete.");
232
241
  });
242
+ emu.command("status").description(
243
+ "Report whether the Oniro emulator is installed, running, and connected to hdc, without starting it. Exits 1 when it is not installed."
244
+ ).option("--address <addr>", "hdc address the emulator listens on", "127.0.0.1:55555").option("--json", "Emit machine-readable JSON.").action(async (opts) => {
245
+ const { config, logger } = getRuntime();
246
+ const status = await getEmulatorStatus(config, { address: opts.address, logger });
247
+ if (opts.json) {
248
+ process.stdout.write(`${JSON.stringify(status, null, 2)}
249
+ `);
250
+ } else {
251
+ process.stdout.write(
252
+ `installed: ${status.installed ? "yes" : "no"} (${status.dir})
253
+ running: ${status.running ? "yes" : "no"}
254
+ connected: ${status.connected ? "yes" : "no"} (${status.address})
255
+ `
256
+ );
257
+ }
258
+ if (!status.installed) process.exitCode = 1;
259
+ });
233
260
  emu.command("stop").description("Kill running emulator processes.").action(async () => {
234
261
  const { logger } = getRuntime();
235
262
  await stopEmulator(logger);
@@ -253,15 +280,15 @@ function registerEmulatorCommand(program) {
253
280
 
254
281
  // src/commands/build.ts
255
282
  import * as path2 from "path";
256
- import { buildHap } from "@oniroproject/core";
283
+ import { SdkNotInstalledError, buildHap, downloadAndInstallSdk as downloadAndInstallSdk2 } from "@oniroproject/core";
257
284
  function registerBuildCommand(program) {
258
285
  program.command("build [project-dir]").description(
259
- 'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio).'
260
- ).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--json", "emit the build result (discovered HAPs, warnings) as JSON").action(async (projectDir, opts) => {
261
- const { config, logger } = getRuntime();
286
+ 'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio). An OpenHarmony project fails early, naming the release to install, when the SDK for its compileSdkVersion is missing.'
287
+ ).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--install-sdk", "install the OpenHarmony SDK the project compiles against when it is missing, then build").option("--json", "emit the build result (discovered HAPs, warnings) as JSON").action(async (projectDir, opts) => {
288
+ const { config, logger, progress } = getRuntime();
262
289
  const dir = path2.resolve(projectDir ?? process.cwd());
263
290
  logger.info(`Building project at ${dir}...`);
264
- const result = await buildHap({
291
+ const build = () => buildHap({
265
292
  config,
266
293
  projectDir: dir,
267
294
  product: opts.product,
@@ -275,6 +302,15 @@ function registerBuildCommand(program) {
275
302
  onOutput: () => {
276
303
  }
277
304
  });
305
+ let result;
306
+ try {
307
+ result = await build();
308
+ } catch (err) {
309
+ if (!(err instanceof SdkNotInstalledError && err.version && opts.installSdk)) throw err;
310
+ logger.info(`Installing OpenHarmony SDK ${err.version} (api ${err.api}), which the project compiles against...`);
311
+ await downloadAndInstallSdk2({ config, version: err.version, api: err.api, progress, logger });
312
+ result = await build();
313
+ }
278
314
  for (const w of result.warnings) logger.warn(w);
279
315
  if (opts.json) {
280
316
  process.stdout.write(`${JSON.stringify(result, null, 2)}
@@ -326,6 +362,11 @@ function registerSignCommand(program) {
326
362
  "--acls <list>",
327
363
  "Comma-separated permission names to write into the profile's acls.allowed-acls. Required for apps that request permissions above their apl (e.g. a system_basic app requesting ohos.permission.CAPTURE_SCREEN). Example: --acls ohos.permission.REBOOT,ohos.permission.INJECT_INPUT_EVENT"
328
364
  ).argParser(parseAclsList)
365
+ ).addOption(
366
+ new Option(
367
+ "--app-privilege-capabilities <list>",
368
+ `Comma-separated privileges to write into the profile's app-privilege-capabilities. An app that declares a privileged extension ability (e.g. an AccessibilityExtensionAbility) needs AllowAppUsePrivilegeExtension, else install fails with 9568344 "not allow use privilege extension". Others include AllowAppMultiProcess and AllowAppDesktopIconHide.`
369
+ ).argParser(parseAclsList)
329
370
  ).option(
330
371
  "--harmonyos",
331
372
  "Sign a HarmonyOS project through AppGallery Connect. Needs `oniro-app auth login`, and a connected device unless the team already has devices registered."
@@ -359,6 +400,7 @@ function registerSignCommand(program) {
359
400
  apl: opts.apl,
360
401
  appFeature: opts.appFeature,
361
402
  acls: opts.acls,
403
+ appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
362
404
  passwords,
363
405
  logger
364
406
  });
@@ -373,6 +415,7 @@ function registerSignCommand(program) {
373
415
  apl: opts.apl,
374
416
  appFeature: opts.appFeature,
375
417
  acls: opts.acls,
418
+ appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
376
419
  passwords,
377
420
  logger
378
421
  });
@@ -448,12 +491,16 @@ import * as path4 from "path";
448
491
  import { installApp, launchApp, uninstallApp, forceStop, applyChanges, getBundleName } from "@oniroproject/core";
449
492
  function registerAppCommand(program) {
450
493
  const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
451
- app.command("install [project-dir]").description("Install the signed .hap on the connected device/emulator via hdc.").option("--hap <path>", "Override the .hap path (relative to project-dir or absolute).").option("--device <serial>", "Target device serial.").action(async (projectDir, opts) => {
494
+ app.command("install [project-dir-or-hap]").description(
495
+ "Install a built .hap on the connected device/emulator via hdc. Takes a project dir (default: the current one), whose newest signed HAP is installed, else its unsigned one, or a .hap file. Fails when the bundle manager rejects the HAP, and says how to sign when a target refuses an unsigned one."
496
+ ).option("--hap <path>", "Override the .hap path (relative to project-dir or absolute).").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
452
497
  const { config, logger } = getRuntime();
453
- const dir = path4.resolve(projectDir ?? process.cwd());
454
- logger.info(`Installing app from ${dir}...`);
455
- await installApp({ config, projectDir: dir, hapPath: opts.hap, deviceSerial: opts.device, logger });
456
- logger.info("App installed.");
498
+ const isHap = target !== void 0 && /\.hap$/i.test(target);
499
+ const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
500
+ const hapPath = isHap ? path4.resolve(target) : opts.hap;
501
+ logger.info(`Installing ${hapPath ?? `app from ${dir}`}...`);
502
+ const result = await installApp({ config, projectDir: dir, hapPath, deviceSerial: opts.device, logger });
503
+ logger.info(`Installed ${result.hapPath}.`);
457
504
  });
458
505
  app.command("launch [project-dir]").description(
459
506
  "Launch an app on the connected device/emulator. Project mode (default): resolves bundle + main ability from the project. Bundle mode (--bundle): launch an already-installed app by bundle name, no project needed."
@@ -614,11 +661,40 @@ function registerRebootCommand(program) {
614
661
  }
615
662
 
616
663
  // src/commands/wait.ts
617
- import { waitForLog, waitForBoot, waitForBundle } from "@oniroproject/core";
664
+ import { waitForLog, waitForBoot, waitForBundle, waitForTarget, describeLayoutNode } from "@oniroproject/core";
665
+
666
+ // src/lib/target.ts
667
+ function addTargetOptions(cmd, verb, opts = {}) {
668
+ cmd.option(
669
+ "--target <text>",
670
+ `${verb} the element whose text, description, or placeholder matches (case-insensitive; exact matches win, else substring). Covered elements are skipped.`
671
+ ).option("--target-id <id>", "Match the element by its id or key.").option("--target-type <type>", "Restrict the match to a component type (e.g. Button, TextInput).");
672
+ if (opts.index !== false) {
673
+ cmd.option("--target-index <n>", "Pick the n-th (0-based) of several matching elements.");
674
+ }
675
+ return cmd;
676
+ }
677
+ function selectorFrom(opts) {
678
+ if (opts.target === void 0 && opts.targetId === void 0 && opts.targetType === void 0) return void 0;
679
+ return { text: opts.target, id: opts.targetId, type: opts.targetType };
680
+ }
681
+ function targetIndexFrom(opts) {
682
+ if (opts.targetIndex === void 0) return void 0;
683
+ const n = Number(opts.targetIndex);
684
+ if (!Number.isInteger(n) || n < 0) throw new Error(`--target-index must be a non-negative integer; got ${opts.targetIndex}.`);
685
+ return n;
686
+ }
687
+
688
+ // src/commands/wait.ts
618
689
  function registerWaitCommand(program) {
619
- program.command("wait").description("Wait for a device condition: a log line (--log), boot (--boot), or a bundle to start (--bundle).").option("--log <pattern>", "Resolve when a hilog line (tag: message) matches this regex.").option("--boot", "Resolve when the device is reachable (optionally also --pid-of).").option("--bundle <bundle>", "With --log: filter to this bundle. Alone: wait until this bundle is running.").option("--pid-of <name>", "With --boot: also require this process to have a pid.").option("--domain <domain>", "With --log: hilog domain filter (e.g. 0xD003900).").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log: emit the matched entry as JSON.").action(async (opts) => {
690
+ const wait = program.command("wait").description(
691
+ "Wait for a device condition: a log line (--log), boot (--boot), a bundle to start (--bundle), or an on-screen element (--target / --target-id / --target-type)."
692
+ ).option("--log <pattern>", "Resolve when a hilog line (tag: message) matches this regex.").option("--boot", "Resolve when the device is reachable (optionally also --pid-of).").option("--bundle <bundle>", "With --log: filter to this bundle. Alone: wait until this bundle is running.").option("--pid-of <name>", "With --boot: also require this process to have a pid.").option("--domain <domain>", "With --log: hilog domain filter (e.g. 0xD003900).");
693
+ addTargetOptions(wait, "Resolve when", { index: false });
694
+ wait.option("--gone", "With --target*: resolve when no uncovered element matches any more.").option("--interval <ms>", "With --target*: poll interval, ms (each poll is a layout dump).", "500").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log or --target*: emit the matched entry/element as JSON.").action(async (opts) => {
620
695
  const { config, logger } = getRuntime();
621
696
  const timeoutMs = Number(opts.timeout);
697
+ const selector = selectorFrom(opts);
622
698
  if (opts.log) {
623
699
  const entry = await waitForLog({
624
700
  config,
@@ -639,12 +715,31 @@ function registerWaitCommand(program) {
639
715
  logger.info("Device booted.");
640
716
  return;
641
717
  }
718
+ if (selector) {
719
+ const node = await waitForTarget({
720
+ config,
721
+ selector,
722
+ gone: opts.gone,
723
+ timeoutMs,
724
+ pollMs: Number(opts.interval) || 500,
725
+ deviceSerial: opts.device,
726
+ logger
727
+ });
728
+ if (!node) {
729
+ logger.info("Element is gone.");
730
+ return;
731
+ }
732
+ if (opts.json) process.stdout.write(`${JSON.stringify(node, null, 2)}
733
+ `);
734
+ else logger.info(`found: ${describeLayoutNode(node)}`);
735
+ return;
736
+ }
642
737
  if (opts.bundle) {
643
738
  await waitForBundle({ config, bundle: opts.bundle, timeoutMs, deviceSerial: opts.device, logger });
644
739
  logger.info(`Bundle ${opts.bundle} is running.`);
645
740
  return;
646
741
  }
647
- throw new Error("Specify one of --log <pattern>, --boot, or --bundle <bundle>.");
742
+ throw new Error("Specify one of --log <pattern>, --boot, --bundle <bundle>, or --target <text> (or --target-id / --target-type).");
648
743
  });
649
744
  }
650
745
 
@@ -677,7 +772,7 @@ function registerWatchCommand(program) {
677
772
  // src/commands/screenshot.ts
678
773
  import * as fs3 from "fs";
679
774
  import * as path7 from "path";
680
- import { takeScreenshot, captureBurst } from "@oniroproject/core";
775
+ import { takeScreenshot, captureBurst, locateTarget, describeLayoutNode as describeLayoutNode2 } from "@oniroproject/core";
681
776
 
682
777
  // src/lib/screenshotImage.ts
683
778
  import sharp from "sharp";
@@ -732,6 +827,32 @@ async function renderWithGrid(jpeg, opts) {
732
827
  const buf = await sharp(resized).composite([{ input: Buffer.from(svg) }]).jpeg({ quality: 80 }).toBuffer();
733
828
  return { buf, width, height, originalWidth, originalHeight };
734
829
  }
830
+ function parseCropRect(spec) {
831
+ const parts = spec.split(",").map((s) => Number(s.trim()));
832
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) {
833
+ throw new Error(`--crop expects four 0-1 fractions "x1,y1,x2,y2"; got "${spec}".`);
834
+ }
835
+ const [x1, y1, x2, y2] = parts;
836
+ if (!(x2 > x1 && y2 > y1)) throw new Error(`--crop needs x1 < x2 and y1 < y2; got "${spec}".`);
837
+ return [x1, y1, x2, y2];
838
+ }
839
+ function padCropRect(r, pad) {
840
+ return [Math.max(0, r[0] - pad), Math.max(0, r[1] - pad), Math.min(1, r[2] + pad), Math.min(1, r[3] + pad)];
841
+ }
842
+ async function cropImage(jpeg, rect, opts) {
843
+ const meta = await sharp(jpeg).metadata();
844
+ const w = meta.width ?? 1;
845
+ const h = meta.height ?? 1;
846
+ const left = Math.min(w - 1, Math.floor(rect[0] * w));
847
+ const top = Math.min(h - 1, Math.floor(rect[1] * h));
848
+ const cropWidth = Math.max(1, Math.min(w, Math.ceil(rect[2] * w)) - left);
849
+ const cropHeight = Math.max(1, Math.min(h, Math.ceil(rect[3] * h)) - top);
850
+ const scale = Math.min(1, opts.maxDim / Math.max(cropWidth, cropHeight));
851
+ const width = Math.max(1, Math.round(cropWidth * scale));
852
+ const height = Math.max(1, Math.round(cropHeight * scale));
853
+ const buf = await sharp(jpeg).extract({ left, top, width: cropWidth, height: cropHeight }).resize(width, height).jpeg({ quality: 85 }).toBuffer();
854
+ return { buf, width, height, left, top, cropWidth, cropHeight };
855
+ }
735
856
  function buildTileLabelSvg(width, height, idx, diff) {
736
857
  const fontSize = Math.max(12, Math.round(Math.min(width, height) / 16));
737
858
  const pad = Math.round(fontSize * 0.4);
@@ -796,18 +917,53 @@ async function frameDiffs(frames, opts = {}) {
796
917
 
797
918
  // src/commands/screenshot.ts
798
919
  var DEFAULT_CONTACT_SHEET_FRAMES = 8;
920
+ var TARGET_CROP_PAD = 0.02;
799
921
  function clampMaxDim(raw) {
800
922
  const n = Number(raw);
801
923
  if (!Number.isFinite(n)) return 1024;
802
924
  return Math.max(256, Math.min(4096, Math.round(n)));
803
925
  }
804
926
  function registerScreenshotCommand(program) {
805
- program.command("screenshot").description(
806
- "Capture the screen. Default: full-resolution raw JPEG. --grid: downscaled JPEG with a 10x10 grid (axes 0.0-1.0) for picking tap coordinates. --contact-sheet: capture a burst and tile it into one image with per-frame change diffs."
807
- ).option("-o, --output <file>", "Output file path.", "screenshot.jpeg").option("--grid", "Downscale (to --max-dim) and overlay a 10x10 coordinate grid.").option("--max-dim <px>", "Longest-side cap for --grid / --contact-sheet (256-4096).", "1024").option("--contact-sheet", "Capture a burst and composite it into one tiled contact sheet.").option("--burst <count>", "Capture N frames; writes <base>-<i><ext> (or feeds --contact-sheet).").option("--interval <ms>", "Delay between burst frames, ms.", "50").option("--json", "Emit the full result object as JSON on stdout (with --contact-sheet).").option("--device <serial>", "Target device serial.").action(async (opts, command) => {
927
+ const screenshot = program.command("screenshot").description(
928
+ "Capture the screen. Default: full-resolution raw JPEG. --grid: downscaled JPEG with a 10x10 grid (axes 0.0-1.0) for picking tap coordinates. --crop / --target: a full-resolution crop of one region or element, for checking detail cheaply. --contact-sheet: capture a burst and tile it into one image with per-frame change diffs."
929
+ ).option("-o, --output <file>", "Output file path.", "screenshot.jpeg").option("--grid", "Downscale (to --max-dim) and overlay a 10x10 coordinate grid.").option("--max-dim <px>", "Longest-side cap for --grid / --crop / --target / --contact-sheet (256-4096).", "1024").option("--crop <x1,y1,x2,y2>", "Crop to this region, given as 0-1 fractions of the screen.");
930
+ addTargetOptions(screenshot, "Crop to");
931
+ screenshot.option("--contact-sheet", "Capture a burst and composite it into one tiled contact sheet.").option("--burst <count>", "Capture N frames; writes <base>-<i><ext> (or feeds --contact-sheet).").option("--interval <ms>", "Delay between burst frames, ms.", "50").option("--json", "Emit the full result object as JSON on stdout (with --contact-sheet).").option("--device <serial>", "Target device serial.").action(async (opts, command) => {
808
932
  const { config, logger } = getRuntime();
809
933
  const maxDim = clampMaxDim(opts.maxDim);
810
934
  const intervalMs = Number(opts.interval) || 50;
935
+ const selector = selectorFrom(opts);
936
+ if (opts.crop !== void 0 || selector) {
937
+ if (opts.crop !== void 0 && selector) throw new Error("Use either --crop or --target*, not both.");
938
+ if (opts.grid || opts.contactSheet || opts.burst) {
939
+ throw new Error("--crop / --target cannot be combined with --grid, --contact-sheet, or --burst.");
940
+ }
941
+ let rect;
942
+ let label;
943
+ if (selector) {
944
+ const located = await locateTarget({
945
+ config,
946
+ selector,
947
+ index: targetIndexFrom(opts),
948
+ hideKeyboard: false,
949
+ deviceSerial: opts.device,
950
+ logger
951
+ });
952
+ if (!located.node.b) throw new Error(`${describeLayoutNode2(located.node)} has no bounds to crop to.`);
953
+ rect = padCropRect(located.node.b, TARGET_CROP_PAD);
954
+ label = describeLayoutNode2(located.node);
955
+ } else {
956
+ rect = parseCropRect(opts.crop);
957
+ label = rect.map((n) => n.toFixed(3)).join(",");
958
+ }
959
+ const shot2 = await takeScreenshot({ config, deviceSerial: opts.device, logger });
960
+ const cropped = await cropImage(shot2.pixels, rect, { maxDim });
961
+ fs3.writeFileSync(opts.output, cropped.buf);
962
+ logger.info(
963
+ `Wrote ${opts.output} (${cropped.width}x${cropped.height} from a ${cropped.cropWidth}x${cropped.cropHeight} crop at ${cropped.left},${cropped.top}: ${label}).`
964
+ );
965
+ return;
966
+ }
811
967
  if (opts.contactSheet) {
812
968
  const count = opts.burst ? Math.max(1, Number(opts.burst)) : DEFAULT_CONTACT_SHEET_FRAMES;
813
969
  const frames = await captureBurst({ config, count, intervalMs, deviceSerial: opts.device, logger });
@@ -879,31 +1035,125 @@ function registerScreenshotCommand(program) {
879
1035
 
880
1036
  // src/commands/dump.ts
881
1037
  import { dumpLayout } from "@oniroproject/core";
1038
+
1039
+ // src/lib/layoutText.ts
1040
+ import { flattenLayout } from "@oniroproject/core";
1041
+ var MAX_TEXT = 80;
1042
+ function quote(s) {
1043
+ return JSON.stringify(s.length > MAX_TEXT ? `${s.slice(0, MAX_TEXT - 1)}\u2026` : s);
1044
+ }
1045
+ function flags(n) {
1046
+ const out = [];
1047
+ if (n.click) out.push("click");
1048
+ if (n.longClick) out.push("longClick");
1049
+ if (n.scroll) out.push("scroll");
1050
+ if (n.checked) out.push("checked");
1051
+ else if (n.check) out.push("checkable");
1052
+ if (n.sel) out.push("selected");
1053
+ if (n.focus) out.push("focus");
1054
+ if (n.disabled) out.push("disabled");
1055
+ if (n.covered) out.push("covered");
1056
+ return out;
1057
+ }
1058
+ function formatLayoutLine(n) {
1059
+ const parts = [n.type ?? "node"];
1060
+ if (n.text !== void 0) parts.push(quote(n.text));
1061
+ if (n.desc !== void 0) parts.push(`desc=${quote(n.desc)}`);
1062
+ if (n.hint !== void 0) parts.push(`hint=${quote(n.hint)}`);
1063
+ if (n.id !== void 0) parts.push(`#${n.id}`);
1064
+ if (n.key !== void 0) parts.push(`key=${n.key}`);
1065
+ if (n.c) parts.push(`c=${n.c[0].toFixed(3)},${n.c[1].toFixed(3)}`);
1066
+ parts.push(...flags(n));
1067
+ return parts.join(" ");
1068
+ }
1069
+ function isListed(n) {
1070
+ const labelled = n.text !== void 0 || n.desc !== void 0 || n.hint !== void 0;
1071
+ return labelled || !!(n.click || n.longClick || n.scroll || n.check || n.focus);
1072
+ }
1073
+ function formatFlatLayout(result) {
1074
+ const lines = [];
1075
+ const d = result.display;
1076
+ lines.push(
1077
+ `# display ${d ? `${d.width}x${d.height}` : "unknown"}; c = element center as 0-1 fractions (input --fx/--fy), or act by label with input --target "<text>"`
1078
+ );
1079
+ const keyboardBundle = result.keyboard?.shown ? result.keyboard.bundle : void 0;
1080
+ let window;
1081
+ for (const n of flattenLayout(result.tree)) {
1082
+ if (n.window !== window) {
1083
+ window = n.window;
1084
+ if (window !== void 0 && window === keyboardBundle) {
1085
+ lines.push(`[keyboard ${window}] up \u2014 dismiss with input --type hideKeyboard`);
1086
+ } else if (window !== void 0) {
1087
+ lines.push(`[window ${window}]`);
1088
+ }
1089
+ }
1090
+ if (window !== void 0 && window === keyboardBundle) continue;
1091
+ if (n.bundle !== void 0 && n.text === void 0 && !n.click) continue;
1092
+ if (isListed(n)) lines.push(formatLayoutLine(n));
1093
+ }
1094
+ return `${lines.join("\n")}
1095
+ `;
1096
+ }
1097
+
1098
+ // src/commands/dump.ts
882
1099
  function registerDumpCommand(program) {
883
- program.command("dump [target]").description("Dump device state as JSON. Currently supports `dump layout` (the default target).").option("--bundle <bundle>", "Filter the layout to a single window/bundle.").option("--raw", "Return the unpruned layout tree.").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
1100
+ program.command("dump [target]").description(
1101
+ "Dump device state. Currently supports `dump layout` (the default target): JSON by default, or one line per element with --flat."
1102
+ ).option("--bundle <bundle>", "Filter the layout to a single window/bundle.").option("--raw", "Return the unpruned layout tree.").option("--flat", "One line per element (type, label, center as 0-1 fractions, flags) instead of a JSON tree.").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
884
1103
  const { config, logger } = getRuntime();
885
1104
  const what = target ?? "layout";
886
1105
  if (what !== "layout") throw new Error(`Unknown dump target "${what}". Supported: layout.`);
1106
+ if (opts.raw && opts.flat) throw new Error("--raw and --flat cannot be combined.");
887
1107
  const result = await dumpLayout({
888
1108
  config,
889
1109
  bundleName: opts.bundle,
890
1110
  prune: !opts.raw,
1111
+ detectKeyboard: !opts.raw,
891
1112
  deviceSerial: opts.device,
892
1113
  logger
893
1114
  });
894
- process.stdout.write(`${JSON.stringify(result, null, 2)}
1115
+ process.stdout.write(opts.flat ? formatFlatLayout(result) : `${JSON.stringify(result, null, 2)}
895
1116
  `);
896
1117
  });
897
1118
  }
898
1119
 
899
1120
  // src/commands/devices.ts
900
- import { listDevices } from "@oniroproject/core";
1121
+ import { getDeviceInfo, listDevices } from "@oniroproject/core";
1122
+ function formatLongDevice(d, info) {
1123
+ const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
1124
+ if (!info) return head;
1125
+ const os2 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
1126
+ const fields = [
1127
+ ["os", os2 || void 0],
1128
+ ["api", info.sdkApiVersion],
1129
+ ["abi", info.abi],
1130
+ ["type", info.deviceType],
1131
+ ["model", info.model],
1132
+ ["fullname", info.osFullName]
1133
+ ];
1134
+ const rest = fields.filter(([, v]) => v).map(([k, v]) => `${k}=${/\s/.test(v) ? JSON.stringify(v) : v}`);
1135
+ return [head, ...rest].join(" ");
1136
+ }
901
1137
  function registerDevicesCommand(program) {
902
- program.command("devices").description("List connected hdc devices.").option("--json", "Emit machine-readable JSON.").action(async (opts) => {
903
- const { config } = getRuntime();
1138
+ program.command("devices").description("List connected hdc devices.").option(
1139
+ "--long",
1140
+ "Also ask each connected device for its OS family (HarmonyOS / OpenHarmony), version, API level, CPU ABI, device type and model."
1141
+ ).option("--json", "Emit machine-readable JSON.").action(async (opts) => {
1142
+ const { config, logger } = getRuntime();
904
1143
  const devices = await listDevices(config);
1144
+ const infos = /* @__PURE__ */ new Map();
1145
+ if (opts.long) {
1146
+ for (const d of devices.filter((x) => x.status === "Connected")) {
1147
+ try {
1148
+ infos.set(d.serial, await getDeviceInfo({ config, deviceSerial: d.serial, logger }));
1149
+ } catch (err) {
1150
+ logger.warn(`Could not query ${d.serial}: ${err.message}`);
1151
+ }
1152
+ }
1153
+ }
905
1154
  if (opts.json) {
906
- process.stdout.write(`${JSON.stringify(devices, null, 2)}
1155
+ const out = opts.long ? devices.map((d) => ({ ...d, info: infos.get(d.serial) ?? null })) : devices;
1156
+ process.stdout.write(`${JSON.stringify(out, null, 2)}
907
1157
  `);
908
1158
  return;
909
1159
  }
@@ -912,7 +1162,7 @@ function registerDevicesCommand(program) {
912
1162
  return;
913
1163
  }
914
1164
  for (const d of devices) {
915
- process.stdout.write(`${d.serial} ${d.connection ?? ""} ${d.status}
1165
+ process.stdout.write(`${opts.long ? formatLongDevice(d, infos.get(d.serial)) : `${d.serial} ${d.connection ?? ""} ${d.status}`}
916
1166
  `);
917
1167
  }
918
1168
  });
@@ -922,10 +1172,14 @@ function registerDevicesCommand(program) {
922
1172
  import { sendFile, recvFile } from "@oniroproject/core";
923
1173
  function registerFileCommand(program) {
924
1174
  const file = program.command("file").description("Transfer files to/from the device (hdc file send/recv).");
925
- file.command("send <local> <remote>").description("Push a local file to the device.").option("--device <serial>", "Target device serial.").action(async (local, remote, opts) => {
1175
+ file.command("send <local> <remote>").description(
1176
+ "Push a local file to the device. With --bundle, <remote> may be relative to that app's sandbox (files/x is its filesDir, haps/entry/files/x the entry module's), and the file is chowned to the app so it can read it. Prints the device path."
1177
+ ).option("--bundle <bundle>", "Push into this app's sandbox and give the file to the app's user.").option("--device <serial>", "Target device serial.").action(async (local, remote, opts) => {
926
1178
  const { config, logger } = getRuntime();
927
- await sendFile({ config, local, remote, deviceSerial: opts.device, logger });
928
- logger.info(`Sent ${local} -> ${remote}.`);
1179
+ const written = await sendFile({ config, local, remote, bundle: opts.bundle, deviceSerial: opts.device, logger });
1180
+ logger.info(`Sent ${local} -> ${written}${opts.bundle ? ` (owned by ${opts.bundle})` : ""}.`);
1181
+ process.stdout.write(`${written}
1182
+ `);
929
1183
  });
930
1184
  file.command("recv <remote> <local>").description("Pull a file from the device.").option("--device <serial>", "Target device serial.").action(async (remote, local, opts) => {
931
1185
  const { config, logger } = getRuntime();
@@ -936,37 +1190,108 @@ function registerFileCommand(program) {
936
1190
 
937
1191
  // src/commands/lint.ts
938
1192
  import * as path8 from "path";
939
- import { runCodelinter } from "@oniroproject/core";
1193
+ import {
1194
+ detectCmdToolsFlavour,
1195
+ detectRuntimeOs as detectRuntimeOs2,
1196
+ getCmdToolsPath,
1197
+ runCodelinter
1198
+ } from "@oniroproject/core";
1199
+ function lintTotals(result) {
1200
+ if (result.summary) return result.summary;
1201
+ const count = (s) => result.findings.filter((f) => f.severity === s).length;
1202
+ return {
1203
+ defects: result.findings.length,
1204
+ errors: count("error"),
1205
+ warnings: count("warning"),
1206
+ suggestions: count("suggestion")
1207
+ };
1208
+ }
1209
+ function lintExitCode(result) {
1210
+ if (lintTotals(result).errors > 0) return 1;
1211
+ const noReport = result.summary === null && result.findings.length === 0;
1212
+ return noReport && result.code !== 0 ? result.code : 0;
1213
+ }
940
1214
  function registerLintCommand(program) {
941
- program.command("lint [project-dir]").description("Run the OpenHarmony codelinter on a project. Defaults to the current directory.").option("--files <globs...>", "Specific files/globs to lint (default: the whole project).").option("--json", "Emit findings as JSON.").action(async (projectDir, opts) => {
1215
+ program.command("lint [project-dir]").description(
1216
+ "Run the OpenHarmony codelinter on a project. Defaults to the current directory. Exits 1 on error-severity findings or when codelinter fails to run; warnings alone exit 0."
1217
+ ).option("--files <globs...>", "Specific files/globs to lint (default: the whole project).").option("--json", "Emit the report (findings, summary, notices, incomplete) as JSON.").action(async (projectDir, opts) => {
942
1218
  const { config, logger } = getRuntime();
943
1219
  const dir = path8.resolve(projectDir ?? process.cwd());
944
1220
  const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
1221
+ const totals = lintTotals(result);
945
1222
  if (opts.json) {
946
1223
  process.stdout.write(`${JSON.stringify(result, null, 2)}
947
1224
  `);
948
1225
  } else {
949
1226
  for (const f of result.findings) {
950
- process.stdout.write(`${f.file}:${f.line} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
1227
+ const at = `${f.file}:${f.line}${f.column ? `:${f.column}` : ""}`;
1228
+ process.stdout.write(`${at} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
951
1229
  `);
952
1230
  }
953
- if (result.findings.length === 0) process.stdout.write("No findings.\n");
1231
+ process.stdout.write(
1232
+ totals.defects === 0 ? "No defects found.\n" : `${totals.defects} defects: ${totals.errors} errors, ${totals.warnings} warnings, ${totals.suggestions} suggestions.
1233
+ `
1234
+ );
1235
+ }
1236
+ if (result.summary === null && result.findings.length === 0 && result.code !== 0) {
1237
+ logger.error(`codelinter exited ${result.code} without a report:
1238
+ ${result.raw.trim().split("\n").slice(-15).join("\n")}`);
1239
+ }
1240
+ if (result.incomplete) {
1241
+ for (const n of result.notices) logger.warn(`codelinter: ${n}`);
1242
+ const cmdToolsPath = getCmdToolsPath(config);
1243
+ const flavour = detectCmdToolsFlavour(cmdToolsPath);
1244
+ const projectOs = detectRuntimeOs2(dir);
1245
+ if (flavour !== projectOs) {
1246
+ logger.warn(
1247
+ `The command-line tools at ${cmdToolsPath} are the ${flavour} flavour, but the project targets ${projectOs} (runtimeOS in build-profile.json5), so codelinter skipped part of its checks: treat the counts as a lower bound. For a complete report, point ONIRO_CMD_TOOLS_PATH at ${projectOs} command-line tools. The build's ArkTS compiler checks are not affected.`
1248
+ );
1249
+ }
954
1250
  }
955
- if (result.code !== 0) process.exitCode = result.code;
1251
+ process.exitCode = lintExitCode(result);
956
1252
  });
957
1253
  }
958
1254
 
959
1255
  // src/commands/input.ts
960
1256
  import { Option as Option3 } from "commander";
961
- import { sendInput, sendGesture } from "@oniroproject/core";
962
- var INPUT_TYPES = ["click", "doubleClick", "longClick", "swipe", "drag", "fling", "keyEvent", "inputText"];
1257
+ import {
1258
+ performInput,
1259
+ sendGesture,
1260
+ getDisplaySize,
1261
+ describeLayoutNode as describeLayoutNode3
1262
+ } from "@oniroproject/core";
1263
+ var INPUT_TYPES = [
1264
+ "click",
1265
+ "doubleClick",
1266
+ "longClick",
1267
+ "swipe",
1268
+ "drag",
1269
+ "fling",
1270
+ "keyEvent",
1271
+ "inputText",
1272
+ "clearText",
1273
+ "hideKeyboard"
1274
+ ];
963
1275
  var num = (v) => v === void 0 ? void 0 : Number(v);
964
1276
  function registerInputCommand(program) {
965
- program.command("input").description("Inject a UI input event (pixel coordinates) via uitest uiInput.").addOption(new Option3("--type <type>", "Input type.").choices(INPUT_TYPES).makeOptionMandatory()).option("--x <px>", "X pixel (click/swipe start).").option("--y <px>", "Y pixel (click/swipe start).").option("--x2 <px>", "End X pixel (swipe/drag/fling).").option("--y2 <px>", "End Y pixel (swipe/drag/fling).").option("--speed <px-per-s>", "Velocity for swipe/drag/fling.").option("--key <key>", "Key id or symbolic name (Back/Home/Power) for keyEvent.").option("--text <text>", "Text for inputText.").option("--device <serial>", "Target device serial.").action(async (opts) => {
1277
+ const input = program.command("input").description(
1278
+ "Inject a UI input event via uitest uiInput. Position it with --target (an on-screen element), --fx/--fy (0-1 fractions of the display, as in `dump layout` and the screenshot grid), or --x/--y (pixels)."
1279
+ ).addOption(new Option3("--type <type>", "Input type.").choices(INPUT_TYPES).makeOptionMandatory());
1280
+ addTargetOptions(input, "Act on");
1281
+ input.option("--fx <fraction>", "Start X as a 0-1 fraction of the display width.").option("--fy <fraction>", "Start Y as a 0-1 fraction of the display height.").option("--fx2 <fraction>", "End X fraction (swipe/drag/fling).").option("--fy2 <fraction>", "End Y fraction (swipe/drag/fling).").option("--x <px>", "X pixel (click/swipe start).").option("--y <px>", "Y pixel (click/swipe start).").option("--x2 <px>", "End X pixel (swipe/drag/fling).").option("--y2 <px>", "End Y pixel (swipe/drag/fling).").option("--speed <px-per-s>", "Velocity for swipe/drag/fling.").option(
1282
+ "--key <key>",
1283
+ "Key for keyEvent: Back/Home/Power, a name (Delete, Enter, Tab, Escape, MoveHome, MoveEnd, \u2026), a letter, a KeyCode id, or a combo like Ctrl+A."
1284
+ ).option("--text <text>", "Text for inputText.").option("--clear", "inputText: clear the field (Ctrl+A, Delete) before typing.").option("--hide-keyboard", "Dismiss the soft keyboard afterwards, if it is up.").option("--device <serial>", "Target device serial.").action(async (opts) => {
966
1285
  const { config, logger } = getRuntime();
967
- await sendInput({
1286
+ const result = await performInput({
968
1287
  config,
969
1288
  type: opts.type,
1289
+ target: selectorFrom(opts),
1290
+ targetIndex: targetIndexFrom(opts),
1291
+ fx: num(opts.fx),
1292
+ fy: num(opts.fy),
1293
+ fx2: num(opts.fx2),
1294
+ fy2: num(opts.fy2),
970
1295
  pxX: num(opts.x),
971
1296
  pxY: num(opts.y),
972
1297
  pxX2: num(opts.x2),
@@ -974,14 +1299,31 @@ function registerInputCommand(program) {
974
1299
  speed: num(opts.speed),
975
1300
  key: opts.key,
976
1301
  text: opts.text,
1302
+ clear: opts.clear,
1303
+ hideKeyboard: opts.hideKeyboard,
977
1304
  deviceSerial: opts.device,
978
1305
  logger
979
1306
  });
980
- logger.info(`Sent ${opts.type}.`);
1307
+ if (opts.type === "hideKeyboard") {
1308
+ logger.info(result.keyboardHidden ? "Dismissed the keyboard." : "The keyboard was not shown.");
1309
+ return;
1310
+ }
1311
+ const at = result.x !== void 0 ? ` at ${result.x},${result.y}` : "";
1312
+ const on = result.target ? ` on ${describeLayoutNode3(result.target)}` : "";
1313
+ const kb = result.keyboardHidden ? " (dismissed the keyboard)" : "";
1314
+ logger.info(`Sent ${opts.type}${at}${on}${kb}.`);
981
1315
  });
982
- program.command("gesture").description('Inject a multi-waypoint gesture. Waypoints are pixel JSON: [{"x":..,"y":..,"t":..}].').requiredOption("--waypoints <json>", "JSON array of {x,y,t} pixel waypoints (t = ms from start).").option("--hold-start <ms>", "Leading press-hold before moving (best-effort: uses `uitest drag`).").option("--hold-end <ms>", "Trailing hold before lifting (no uitest equivalent; ignored).").option("--device <serial>", "Target device serial.").action(async (opts) => {
1316
+ program.command("gesture").description('Inject a multi-waypoint gesture. Waypoints are JSON: [{"x":..,"y":..,"t":..}] in pixels (or fractions with --fractions).').requiredOption("--waypoints <json>", "JSON array of {x,y,t} waypoints (t = ms from start).").option("--fractions", "Waypoint x/y are 0-1 fractions of the display instead of pixels.").option("--hold-start <ms>", "Leading press-hold before moving (best-effort: uses `uitest drag`).").option("--hold-end <ms>", "Trailing hold before lifting (no uitest equivalent; ignored).").option("--device <serial>", "Target device serial.").action(async (opts) => {
983
1317
  const { config, logger } = getRuntime();
984
- const waypoints = JSON.parse(opts.waypoints);
1318
+ let waypoints = JSON.parse(opts.waypoints);
1319
+ if (opts.fractions) {
1320
+ const bad = waypoints.find((w) => !(w.x >= 0 && w.x <= 1 && w.y >= 0 && w.y <= 1));
1321
+ if (bad) throw new Error(`With --fractions, waypoint x and y must be 0-1; got ${bad.x},${bad.y}.`);
1322
+ const display = await getDisplaySize(config, { deviceSerial: opts.device, logger });
1323
+ if (!display) throw new Error("Could not read the display size to convert fractions to pixels.");
1324
+ const px = (f, size) => Math.min(size - 1, Math.round(f * size));
1325
+ waypoints = waypoints.map((w) => ({ ...w, x: px(w.x, display.width), y: px(w.y, display.height) }));
1326
+ }
985
1327
  await sendGesture({
986
1328
  config,
987
1329
  waypoints,
@@ -994,12 +1336,70 @@ function registerInputCommand(program) {
994
1336
  });
995
1337
  }
996
1338
 
1339
+ // src/commands/logs.ts
1340
+ import * as path9 from "path";
1341
+ import { captureLog } from "@oniroproject/core";
1342
+ function registerLogsCommand(program) {
1343
+ program.command("logs").description(
1344
+ "Capture hilog to a file for a whole session: the device's small log buffer wraps within seconds under chatty components, so a dump at the end loses lines. Runs until --for elapses or the process is interrupted; start it in the background, e.g. `oniro-app logs --clear -o hilog_full.txt &`. Every line is flushed as it arrives and the stream reconnects after a reboot."
1345
+ ).requiredOption("-o, --output <file>", "File to append the lines to.").option("--for <ms>", "Stop after this many ms (default: run until interrupted).").option("--grep <regex>", "Keep only lines matching this regex (default: all).").option("--clear", "Clear the device log buffer first (hilog -r), so the file covers only this session.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--device <serial>", "Target device serial.").action(async (opts) => {
1346
+ const { config, logger } = getRuntime();
1347
+ const file = path9.resolve(opts.output);
1348
+ const abort = new AbortController();
1349
+ const stop = () => abort.abort();
1350
+ process.once("SIGINT", stop);
1351
+ process.once("SIGTERM", stop);
1352
+ logger.info(`Capturing hilog to ${file}${opts.for ? ` for ${opts.for} ms` : " until interrupted"}...`);
1353
+ const result = await captureLog({
1354
+ config,
1355
+ file,
1356
+ durationMs: opts.for !== void 0 ? Number(opts.for) : void 0,
1357
+ pattern: opts.grep ? new RegExp(opts.grep) : void 0,
1358
+ clear: opts.clear,
1359
+ domain: opts.domain,
1360
+ deviceSerial: opts.device,
1361
+ abortSignal: abort.signal,
1362
+ logger
1363
+ });
1364
+ logger.info(`Wrote ${result.lines} lines to ${result.file} (${result.reconnects} reconnects).`);
1365
+ process.stdout.write(`${result.file}
1366
+ `);
1367
+ });
1368
+ }
1369
+
1370
+ // src/commands/hdc.ts
1371
+ import { getHdcPath, hdcTargetArgsFor, spawnCompat } from "@oniroproject/core";
1372
+ function hdcPassthroughArgv(args, device) {
1373
+ return args[0] === "-t" ? [...args] : [...hdcTargetArgsFor(args, device), ...args];
1374
+ }
1375
+ function registerHdcCommand(program) {
1376
+ program.command("hdc").description(
1377
+ "Run the hdc binary oniro-app itself uses (see `cmdtools status`), addressed at --device or ONIRO_DEVICE_SERIAL, with stdio passed through. Exits with hdc's own code, which is 0 even for a `[Fail]` line. Example: oniro-app hdc shell param get const.ohos.apiversion"
1378
+ ).argument("[args...]", "hdc arguments, passed through verbatim").option("--device <serial>", "Target device serial.").option("--path", "Print the hdc binary path and exit.").passThroughOptions().allowUnknownOption().action(async (args, opts) => {
1379
+ const { config } = getRuntime();
1380
+ const hdc = getHdcPath(config);
1381
+ if (opts.path) {
1382
+ process.stdout.write(`${hdc}
1383
+ `);
1384
+ return;
1385
+ }
1386
+ await new Promise((resolve8, reject) => {
1387
+ const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
1388
+ child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
1389
+ child.on("close", (code) => {
1390
+ process.exitCode = code ?? 1;
1391
+ resolve8();
1392
+ });
1393
+ });
1394
+ });
1395
+ }
1396
+
997
1397
  // src/bin/oniro-app.ts
998
- var CLI_VERSION = "0.12.0";
1398
+ var CLI_VERSION = "0.14.0";
999
1399
  function buildProgram() {
1000
1400
  const program = new Command4().name("oniro-app").description(
1001
1401
  "Cross-platform CLI for Oniro/OpenHarmony app development.\n\nDesigned for non-interactive use (CI, scripts, agents): every command\ntakes explicit flags, results go to stdout, progress/logs go to stderr,\nand a non-zero exit code indicates failure."
1002
- ).version(CLI_VERSION).showHelpAfterError();
1402
+ ).version(CLI_VERSION).showHelpAfterError().enablePositionalOptions();
1003
1403
  registerSdkCommand(program);
1004
1404
  registerCmdToolsCommand(program);
1005
1405
  registerEmulatorCommand(program);
@@ -1018,6 +1418,8 @@ function buildProgram() {
1018
1418
  registerFileCommand(program);
1019
1419
  registerLintCommand(program);
1020
1420
  registerInputCommand(program);
1421
+ registerLogsCommand(program);
1422
+ registerHdcCommand(program);
1021
1423
  return program;
1022
1424
  }
1023
1425
  async function main() {