@oniroproject/oniro-app 0.6.2 → 0.8.1

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.
Files changed (43) hide show
  1. package/README.md +303 -9
  2. package/dist/oniro-app.js +499 -17
  3. package/dist/oniro-app.js.map +1 -1
  4. package/package.json +4 -3
  5. package/templates/NativeCpp/AppScope/app.json5 +10 -0
  6. package/templates/NativeCpp/AppScope/resources/base/element/string.json +8 -0
  7. package/templates/NativeCpp/AppScope/resources/base/media/background.png +0 -0
  8. package/templates/NativeCpp/AppScope/resources/base/media/foreground.png +0 -0
  9. package/templates/NativeCpp/AppScope/resources/base/media/layered_image.json +7 -0
  10. package/templates/NativeCpp/build-profile.json5 +35 -0
  11. package/templates/NativeCpp/entry/build-profile.json5 +48 -0
  12. package/templates/NativeCpp/entry/hvigorfile.ts +6 -0
  13. package/templates/NativeCpp/entry/obfuscation-rules.txt +23 -0
  14. package/templates/NativeCpp/entry/oh-package.json5 +12 -0
  15. package/templates/NativeCpp/entry/src/main/cpp/CMakeLists.txt +15 -0
  16. package/templates/NativeCpp/entry/src/main/cpp/napi_init.cpp +53 -0
  17. package/templates/NativeCpp/entry/src/main/cpp/types/libentry/Index.d.ts +1 -0
  18. package/templates/NativeCpp/entry/src/main/cpp/types/libentry/oh-package.json5 +6 -0
  19. package/templates/NativeCpp/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
  20. package/templates/NativeCpp/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
  21. package/templates/NativeCpp/entry/src/main/ets/pages/Index.ets +26 -0
  22. package/templates/NativeCpp/entry/src/main/module.json5 +50 -0
  23. package/templates/NativeCpp/entry/src/main/resources/base/element/color.json +8 -0
  24. package/templates/NativeCpp/entry/src/main/resources/base/element/float.json +8 -0
  25. package/templates/NativeCpp/entry/src/main/resources/base/element/string.json +16 -0
  26. package/templates/NativeCpp/entry/src/main/resources/base/media/background.png +0 -0
  27. package/templates/NativeCpp/entry/src/main/resources/base/media/foreground.png +0 -0
  28. package/templates/NativeCpp/entry/src/main/resources/base/media/layered_image.json +7 -0
  29. package/templates/NativeCpp/entry/src/main/resources/base/media/startIcon.png +0 -0
  30. package/templates/NativeCpp/entry/src/main/resources/base/profile/backup_config.json +3 -0
  31. package/templates/NativeCpp/entry/src/main/resources/base/profile/main_pages.json +5 -0
  32. package/templates/NativeCpp/entry/src/main/resources/dark/element/color.json +8 -0
  33. package/templates/NativeCpp/entry/src/mock/Libentry.mock.ets +7 -0
  34. package/templates/NativeCpp/entry/src/mock/mock-config.json5 +5 -0
  35. package/templates/NativeCpp/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
  36. package/templates/NativeCpp/entry/src/ohosTest/ets/test/List.test.ets +5 -0
  37. package/templates/NativeCpp/entry/src/ohosTest/module.json5 +11 -0
  38. package/templates/NativeCpp/entry/src/test/List.test.ets +5 -0
  39. package/templates/NativeCpp/entry/src/test/LocalUnit.test.ets +33 -0
  40. package/templates/NativeCpp/hvigor/hvigor-config.json5 +23 -0
  41. package/templates/NativeCpp/hvigorfile.ts +6 -0
  42. package/templates/NativeCpp/oh-package.json5 +10 -0
  43. package/templates/NativeCpp/template.json +6 -0
package/dist/oniro-app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/oniro-app.ts
4
- import { Command as Command2 } from "commander";
4
+ import { Command as Command4 } from "commander";
5
5
  import { OniroError } from "@oniroproject/core";
6
6
 
7
7
  // src/commands/sdk.ts
@@ -64,12 +64,15 @@ import * as os from "os";
64
64
  var ENV_VAR_BY_KEY = {
65
65
  sdkRootDir: "ONIRO_SDK_ROOT_DIR",
66
66
  cmdToolsPath: "ONIRO_CMD_TOOLS_PATH",
67
+ hdcPath: "ONIRO_HDC",
67
68
  emulatorDir: "ONIRO_EMULATOR_DIR",
68
69
  hapPath: "ONIRO_HAP_PATH",
69
70
  cmdToolsUrlLinux: "ONIRO_CMD_TOOLS_URL_LINUX",
70
71
  cmdToolsUrlWindows: "ONIRO_CMD_TOOLS_URL_WINDOWS",
71
72
  cmdToolsUrlMac: "ONIRO_CMD_TOOLS_URL_MAC",
72
- emulatorUrl: "ONIRO_EMULATOR_URL"
73
+ emulatorUrl: "ONIRO_EMULATOR_URL",
74
+ sdkUrlBase: "ONIRO_SDK_URL_BASE",
75
+ applicationCertPath: "ONIRO_APPLICATION_CERT_PATH"
73
76
  };
74
77
  function createEnvConfig(env = process.env) {
75
78
  return {
@@ -230,25 +233,31 @@ function registerEmulatorCommand(program) {
230
233
 
231
234
  // src/commands/build.ts
232
235
  import * as path2 from "path";
233
- import { runHvigorw } from "@oniroproject/core";
236
+ import { buildHap } from "@oniroproject/core";
234
237
  function registerBuildCommand(program) {
235
- program.command("build [project-dir]").description("Build an OpenHarmony app via hvigorw. Defaults to the current directory.").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").action(async (projectDir, opts) => {
238
+ program.command("build [project-dir]").description("Build an OpenHarmony app via hvigorw. Defaults to the current directory.").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) => {
236
239
  const { config, logger } = getRuntime();
237
240
  const dir = path2.resolve(projectDir ?? process.cwd());
238
241
  logger.info(`Building project at ${dir}...`);
239
- await runHvigorw({
242
+ const result = await buildHap({
240
243
  config,
241
244
  projectDir: dir,
242
245
  product: opts.product,
243
246
  module: opts.module,
244
- buildMode: opts.mode,
247
+ mode: opts.mode,
245
248
  task: opts.task,
249
+ autoInstallDeps: opts.deps,
250
+ parallel: opts.parallel,
246
251
  logger,
247
- // Forward output as it arrives. We don't double-print since the logger
248
- // already echoes spawn output; this hook is for streaming to other UIs.
252
+ // The logger already echoes spawn output; this hook is for streaming to other UIs.
249
253
  onOutput: () => {
250
254
  }
251
255
  });
256
+ for (const w of result.warnings) logger.warn(w);
257
+ if (opts.json) {
258
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
259
+ `);
260
+ }
252
261
  logger.info("Build complete.");
253
262
  });
254
263
  }
@@ -260,6 +269,7 @@ import {
260
269
  APL_VALUES,
261
270
  APP_FEATURE_VALUES,
262
271
  generateSigningConfigs,
272
+ prepareSigning,
263
273
  getOhosBaseSdkHome
264
274
  } from "@oniroproject/core";
265
275
  function parseAclsList(value) {
@@ -288,10 +298,24 @@ function registerSignCommand(program) {
288
298
  "--acls <list>",
289
299
  "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"
290
300
  ).argParser(parseAclsList)
291
- ).action(
301
+ ).option("--bootstrap", "No-op if signing material is already present; otherwise generate it.").option("--store-password <pwd>", "Keystore store password (default 123456 \u2014 the SDK keystore password).").option("--key-password <pwd>", "Keystore key password (default 123456 \u2014 the SDK keystore password).").action(
292
302
  (projectDir, opts) => {
293
303
  const { config, logger } = getRuntime();
294
304
  const dir = path3.resolve(projectDir ?? process.cwd());
305
+ const passwords = opts.storePassword || opts.keyPassword ? { store: opts.storePassword ?? "123456", key: opts.keyPassword ?? "123456" } : void 0;
306
+ if (opts.bootstrap) {
307
+ const result = prepareSigning({
308
+ config,
309
+ projectDir: dir,
310
+ apl: opts.apl,
311
+ appFeature: opts.appFeature,
312
+ acls: opts.acls,
313
+ passwords,
314
+ logger
315
+ });
316
+ logger.info(result.source === "present" ? "Signing material already present." : "Signing configs generated.");
317
+ return;
318
+ }
295
319
  const sdkHome = getOhosBaseSdkHome(config);
296
320
  logger.info(`Generating signing configs in ${dir} using SDK at ${sdkHome}...`);
297
321
  generateSigningConfigs({
@@ -300,6 +324,7 @@ function registerSignCommand(program) {
300
324
  apl: opts.apl,
301
325
  appFeature: opts.appFeature,
302
326
  acls: opts.acls,
327
+ passwords,
303
328
  logger
304
329
  });
305
330
  logger.info("Signing configs generated.");
@@ -309,9 +334,9 @@ function registerSignCommand(program) {
309
334
 
310
335
  // src/commands/app.ts
311
336
  import * as path4 from "path";
312
- import { installApp, launchApp } from "@oniroproject/core";
337
+ import { installApp, launchApp, uninstallApp, forceStop, applyChanges, getBundleName } from "@oniroproject/core";
313
338
  function registerAppCommand(program) {
314
- const app = program.command("app").description("Install and launch built apps on a connected device/emulator.");
339
+ const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
315
340
  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).").action(async (projectDir, opts) => {
316
341
  const { config, logger } = getRuntime();
317
342
  const dir = path4.resolve(projectDir ?? process.cwd());
@@ -319,13 +344,57 @@ function registerAppCommand(program) {
319
344
  await installApp({ config, projectDir: dir, hapPath: opts.hap, logger });
320
345
  logger.info("App installed.");
321
346
  });
322
- app.command("launch [project-dir]").description("Launch the app on the connected device/emulator via hdc.").option("--module <module>", "Module folder name", "entry").action(async (projectDir, opts) => {
347
+ app.command("launch [project-dir]").description(
348
+ "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."
349
+ ).option("--bundle <bundle>", "Launch an already-installed app by bundle name (no project needed).").option("--module <module>", "Module folder name (project mode).", "entry").option("--ability <name>", "Explicit ability to launch (defaults to the module mainElement in project mode, or the installed bundle main ability in --bundle mode).").option("--device <serial>", "Target device serial.").action(async (projectDir, opts) => {
323
350
  const { config, logger } = getRuntime();
324
- const dir = path4.resolve(projectDir ?? process.cwd());
325
- logger.info(`Launching app from ${dir}...`);
326
- await launchApp({ config, projectDir: dir, moduleName: opts.module, logger });
351
+ if (opts.bundle) {
352
+ logger.info(`Launching installed bundle ${opts.bundle}...`);
353
+ await launchApp({ config, bundleName: opts.bundle, abilityName: opts.ability, deviceSerial: opts.device, logger });
354
+ } else {
355
+ const dir = path4.resolve(projectDir ?? process.cwd());
356
+ logger.info(`Launching app from ${dir}...`);
357
+ await launchApp({ config, projectDir: dir, moduleName: opts.module, abilityName: opts.ability, deviceSerial: opts.device, logger });
358
+ }
327
359
  logger.info("App launched.");
328
360
  });
361
+ app.command("apply [project-dir]").description(
362
+ "Install a HAP and verify the running process took the change. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
363
+ ).option("--bundle <bundle>", "Bundle name to apply changes to. Defaults to the bundleName in the project AppScope/app.json5.").option("--module <module>", "Module to resolve the HAP from (multi-module projects).").option("--hap <path>", "Explicit .hap path (relative to project-dir or absolute).").option("--installed-hap <path>", "Local path to the currently-installed HAP, to enable the asset-cache reboot.").option("--system", "Treat as a persistent/system bundle (refuses uninstall on sign-info mismatch).").option("--allow-uninstall", "Permit uninstalling a system bundle on sign-info mismatch (dangerous).").option("--device <serial>", "Target device serial.").option("--json", "Emit the result as JSON.").action(async (projectDir, opts) => {
364
+ const { config, logger } = getRuntime();
365
+ const dir = path4.resolve(projectDir ?? process.cwd());
366
+ const bundle = opts.bundle ?? getBundleName(dir);
367
+ const result = await applyChanges({
368
+ config,
369
+ bundle,
370
+ hapPath: opts.hap,
371
+ projectDir: dir,
372
+ module: opts.module,
373
+ installedHapPath: opts.installedHap,
374
+ isSystemBundle: opts.system,
375
+ allowUninstall: opts.allowUninstall,
376
+ deviceSerial: opts.device,
377
+ logger
378
+ });
379
+ if (opts.json) {
380
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
381
+ `);
382
+ } else {
383
+ logger.info(
384
+ `apply: method=${result.method} pid ${result.preInstallPid ?? "-"} -> ${result.postInstallPid ?? "-"}${result.cacheCleared ? " (cache cleared)" : ""}`
385
+ );
386
+ }
387
+ });
388
+ app.command("uninstall <bundle>").description("Uninstall an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
389
+ const { config, logger } = getRuntime();
390
+ await uninstallApp({ config, bundle, deviceSerial: opts.device, logger });
391
+ logger.info(`Uninstalled ${bundle}.`);
392
+ });
393
+ app.command("stop <bundle>").description("Force-stop an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
394
+ const { config, logger } = getRuntime();
395
+ await forceStop({ config, bundle, deviceSerial: opts.device, logger });
396
+ logger.info(`Stopped ${bundle}.`);
397
+ });
329
398
  }
330
399
 
331
400
  // src/commands/create.ts
@@ -410,10 +479,414 @@ function registerTemplatesCommand(program) {
410
479
  });
411
480
  }
412
481
 
482
+ // src/commands/reboot.ts
483
+ import { Option as Option2 } from "commander";
484
+ import { reboot } from "@oniroproject/core";
485
+ function registerRebootCommand(program) {
486
+ program.command("reboot").description("Reboot the connected device/emulator.").addOption(
487
+ new Option2("--mode <mode>", "Reboot target.").choices(["system", "bootloader", "recovery"]).default("system")
488
+ ).option("--wait-bundle <bundle>", "After a system reboot, wait until this bundle is running again.").option("--timeout <ms>", "Max time to wait for the bundle, ms.", "180000").option("--device <serial>", "Target device serial.").action(
489
+ async (opts) => {
490
+ const { config, logger } = getRuntime();
491
+ logger.info(`Rebooting device (${opts.mode})...`);
492
+ await reboot({
493
+ config,
494
+ mode: opts.mode,
495
+ waitForBundle: opts.waitBundle,
496
+ timeoutMs: Number(opts.timeout),
497
+ deviceSerial: opts.device,
498
+ logger
499
+ });
500
+ logger.info("Reboot complete.");
501
+ }
502
+ );
503
+ }
504
+
505
+ // src/commands/wait.ts
506
+ import { waitForLog, waitForBoot, waitForBundle } from "@oniroproject/core";
507
+ function registerWaitCommand(program) {
508
+ 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) => {
509
+ const { config, logger } = getRuntime();
510
+ const timeoutMs = Number(opts.timeout);
511
+ if (opts.log) {
512
+ const entry = await waitForLog({
513
+ config,
514
+ pattern: new RegExp(opts.log),
515
+ timeoutMs,
516
+ bundle: opts.bundle,
517
+ domain: opts.domain,
518
+ deviceSerial: opts.device,
519
+ logger
520
+ });
521
+ if (opts.json) process.stdout.write(`${JSON.stringify(entry, null, 2)}
522
+ `);
523
+ else logger.info(`matched: ${entry.tag}: ${entry.message}`);
524
+ return;
525
+ }
526
+ if (opts.boot) {
527
+ await waitForBoot({ config, untilPidOf: opts.pidOf, timeoutMs, deviceSerial: opts.device, logger });
528
+ logger.info("Device booted.");
529
+ return;
530
+ }
531
+ if (opts.bundle) {
532
+ await waitForBundle({ config, bundle: opts.bundle, timeoutMs, deviceSerial: opts.device, logger });
533
+ logger.info(`Bundle ${opts.bundle} is running.`);
534
+ return;
535
+ }
536
+ throw new Error("Specify one of --log <pattern>, --boot, or --bundle <bundle>.");
537
+ });
538
+ }
539
+
540
+ // src/commands/watch.ts
541
+ import { watchLog } from "@oniroproject/core";
542
+ function registerWatchCommand(program) {
543
+ program.command("watch").description("Collect hilog lines matching a pattern for a fixed duration.").option("--log <pattern>", "Regex tested against each line (tag: message).").option("--for <ms>", "Duration to watch, ms.", "10000").option("--bundle <bundle>", "Filter to this bundle.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--no-dedup", "Do not collapse consecutive duplicate lines.").option("--device <serial>", "Target device serial.").option("--json", "Emit the entries as JSON.").action(async (opts) => {
544
+ const { config, logger } = getRuntime();
545
+ if (!opts.log) throw new Error("watch requires --log <pattern>.");
546
+ const entries = await watchLog({
547
+ config,
548
+ pattern: new RegExp(opts.log),
549
+ durationMs: Number(opts.for),
550
+ bundle: opts.bundle,
551
+ domain: opts.domain,
552
+ dedup: opts.dedup,
553
+ deviceSerial: opts.device,
554
+ logger
555
+ });
556
+ if (opts.json) {
557
+ process.stdout.write(`${JSON.stringify(entries, null, 2)}
558
+ `);
559
+ } else {
560
+ for (const e of entries) process.stdout.write(`${e.time} ${e.level} ${e.tag}: ${e.message}
561
+ `);
562
+ }
563
+ });
564
+ }
565
+
566
+ // src/commands/screenshot.ts
567
+ import * as fs3 from "fs";
568
+ import * as path7 from "path";
569
+ import { takeScreenshot, captureBurst } from "@oniroproject/core";
570
+
571
+ // src/lib/screenshotImage.ts
572
+ import sharp from "sharp";
573
+ function buildGridSvg(width, height) {
574
+ const lineColor = "rgba(255,80,80,0.55)";
575
+ const labelBg = "rgba(0,0,0,0.7)";
576
+ const labelFg = "#fff";
577
+ const fontSize = Math.max(10, Math.round(Math.min(width, height) / 60));
578
+ const padding = Math.round(fontSize * 0.4);
579
+ const labels = [];
580
+ const lines = [];
581
+ for (let i = 0; i <= 10; i++) {
582
+ const f = i / 10;
583
+ const x = Math.round(f * (width - 1));
584
+ const y = Math.round(f * (height - 1));
585
+ if (i > 0 && i < 10) {
586
+ lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="${height}" stroke="${lineColor}" stroke-width="1"/>`);
587
+ lines.push(`<line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${lineColor}" stroke-width="1"/>`);
588
+ }
589
+ const tag = f.toFixed(1);
590
+ const tx = i === 0 ? padding : i === 10 ? width - padding : x;
591
+ const anchorX = i === 0 ? "start" : i === 10 ? "end" : "middle";
592
+ labels.push(
593
+ `<text x="${tx}" y="${fontSize + padding}" font-family="monospace" font-size="${fontSize}" font-weight="bold" fill="${labelFg}" stroke="${labelBg}" stroke-width="3" paint-order="stroke" text-anchor="${anchorX}">${tag}</text>`
594
+ );
595
+ const ty = i === 0 ? fontSize + padding : i === 10 ? height - padding : y + fontSize / 3;
596
+ labels.push(
597
+ `<text x="${padding}" y="${ty}" font-family="monospace" font-size="${fontSize}" font-weight="bold" fill="${labelFg}" stroke="${labelBg}" stroke-width="3" paint-order="stroke" text-anchor="start">${tag}</text>`
598
+ );
599
+ }
600
+ labels.push(
601
+ `<text x="${width / 2}" y="${fontSize * 2.4 + padding}" font-family="monospace" font-size="${fontSize}" font-weight="bold" fill="${labelFg}" stroke="${labelBg}" stroke-width="3" paint-order="stroke" text-anchor="middle">X \u2192</text>`
602
+ );
603
+ labels.push(
604
+ `<text x="${padding}" y="${fontSize * 2.6 + padding}" font-family="monospace" font-size="${fontSize}" font-weight="bold" fill="${labelFg}" stroke="${labelBg}" stroke-width="3" paint-order="stroke" text-anchor="start">Y \u2193</text>`
605
+ );
606
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${lines.join("")}${labels.join("")}</svg>`;
607
+ }
608
+ async function renderWithGrid(jpeg, opts) {
609
+ const meta = await sharp(jpeg).metadata();
610
+ const originalWidth = meta.width ?? 0;
611
+ const originalHeight = meta.height ?? 0;
612
+ const longest = Math.max(originalWidth, originalHeight) || 1;
613
+ const scale = Math.min(1, opts.maxDim / longest);
614
+ const width = Math.max(1, Math.round(originalWidth * scale));
615
+ const height = Math.max(1, Math.round(originalHeight * scale));
616
+ const resized = await sharp(jpeg).resize(width, height).jpeg({ quality: 80 }).toBuffer();
617
+ if (!opts.grid) {
618
+ return { buf: resized, width, height, originalWidth, originalHeight };
619
+ }
620
+ const svg = buildGridSvg(width, height);
621
+ const buf = await sharp(resized).composite([{ input: Buffer.from(svg) }]).jpeg({ quality: 80 }).toBuffer();
622
+ return { buf, width, height, originalWidth, originalHeight };
623
+ }
624
+ function buildTileLabelSvg(width, height, idx, diff) {
625
+ const fontSize = Math.max(12, Math.round(Math.min(width, height) / 16));
626
+ const pad = Math.round(fontSize * 0.4);
627
+ let label = `#${idx}`;
628
+ if (typeof diff === "number") label += ` \u0394${diff.toFixed(2)}`;
629
+ const boxW = Math.round(label.length * fontSize * 0.62) + pad * 2;
630
+ const boxH = fontSize + pad * 2;
631
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="${boxW}" height="${boxH}" fill="rgba(0,0,0,0.6)"/><text x="${pad}" y="${fontSize + pad - 1}" font-family="monospace" font-size="${fontSize}" font-weight="bold" fill="#fff" stroke="rgba(0,0,0,0.85)" stroke-width="3" paint-order="stroke" text-anchor="start">${label}</text></svg>`;
632
+ }
633
+ async function buildContactSheet(frames, opts) {
634
+ if (frames.length === 0) throw new Error("buildContactSheet: no frames");
635
+ const count = frames.length;
636
+ const cols = Math.max(1, opts.cols ?? Math.ceil(Math.sqrt(count)));
637
+ const rows = Math.ceil(count / cols);
638
+ const gap = opts.gap ?? 8;
639
+ const meta0 = await sharp(frames[0]).metadata();
640
+ const fw = meta0.width ?? 1;
641
+ const fh = meta0.height ?? 1;
642
+ const longest = Math.max(fw, fh) || 1;
643
+ const tileScale = Math.min(1, opts.maxDim / cols / longest);
644
+ const tileWidth = Math.max(1, Math.round(fw * tileScale));
645
+ const tileHeight = Math.max(1, Math.round(fh * tileScale));
646
+ const tiles = await Promise.all(
647
+ frames.map((frame, i) => {
648
+ const label = Buffer.from(buildTileLabelSvg(tileWidth, tileHeight, i, opts.diffs?.[i]));
649
+ return sharp(frame).resize(tileWidth, tileHeight, { fit: "fill" }).composite([{ input: label, top: 0, left: 0 }]).png().toBuffer();
650
+ })
651
+ );
652
+ const width = cols * tileWidth + (cols + 1) * gap;
653
+ const height = rows * tileHeight + (rows + 1) * gap;
654
+ const placements = tiles.map((input, i) => {
655
+ const r = Math.floor(i / cols);
656
+ const c = i % cols;
657
+ return { input, left: gap + c * (tileWidth + gap), top: gap + r * (tileHeight + gap) };
658
+ });
659
+ const buf = await sharp({
660
+ create: { width, height, channels: 3, background: { r: 20, g: 20, b: 20 } }
661
+ }).composite(placements).jpeg({ quality: 80 }).toBuffer();
662
+ return { buf, width, height, cols, rows, tileWidth, tileHeight, count };
663
+ }
664
+ async function frameDiffs(frames, opts = {}) {
665
+ if (frames.length === 0) return [];
666
+ const sample = opts.sample ?? 32;
667
+ const thumbs = await Promise.all(
668
+ frames.map((f) => sharp(f).greyscale().resize(sample, sample, { fit: "fill" }).raw().toBuffer())
669
+ );
670
+ const out = [0];
671
+ for (let i = 1; i < thumbs.length; i++) {
672
+ const a = thumbs[i - 1];
673
+ const b = thumbs[i];
674
+ if (!a || !b) {
675
+ out.push(0);
676
+ continue;
677
+ }
678
+ const n = Math.min(a.length, b.length) || 1;
679
+ let s = 0;
680
+ for (let k = 0; k < n; k++) s += Math.abs((a[k] ?? 0) - (b[k] ?? 0));
681
+ out.push(+(s / n / 255).toFixed(4));
682
+ }
683
+ return out;
684
+ }
685
+
686
+ // src/commands/screenshot.ts
687
+ var DEFAULT_CONTACT_SHEET_FRAMES = 8;
688
+ function clampMaxDim(raw) {
689
+ const n = Number(raw);
690
+ if (!Number.isFinite(n)) return 1024;
691
+ return Math.max(256, Math.min(4096, Math.round(n)));
692
+ }
693
+ function registerScreenshotCommand(program) {
694
+ program.command("screenshot").description(
695
+ "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."
696
+ ).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) => {
697
+ const { config, logger } = getRuntime();
698
+ const maxDim = clampMaxDim(opts.maxDim);
699
+ const intervalMs = Number(opts.interval) || 50;
700
+ if (opts.contactSheet) {
701
+ const count = opts.burst ? Math.max(1, Number(opts.burst)) : DEFAULT_CONTACT_SHEET_FRAMES;
702
+ const frames = await captureBurst({ config, count, intervalMs, deviceSerial: opts.device, logger });
703
+ const diffs = await frameDiffs(frames);
704
+ const sheet = await buildContactSheet(frames, { maxDim, diffs });
705
+ fs3.writeFileSync(opts.output, sheet.buf);
706
+ if (opts.burst) {
707
+ const ext = path7.extname(opts.output) || ".jpeg";
708
+ const base = opts.output.slice(0, opts.output.length - ext.length);
709
+ frames.forEach((frame, i) => fs3.writeFileSync(`${base}-${i}${ext}`, frame));
710
+ }
711
+ const perFrame = frames.map((_, i) => ({ idx: i, t_ms: i * intervalMs, diffVsPrev: diffs[i] ?? 0 }));
712
+ if (opts.json) {
713
+ process.stdout.write(
714
+ `${JSON.stringify(
715
+ {
716
+ sheet: opts.output,
717
+ frameCount: frames.length,
718
+ cols: sheet.cols,
719
+ rows: sheet.rows,
720
+ intervalMs,
721
+ note: "t_ms is nominal (idx*interval); diffVsPrev is 0..1 mean greyscale diff vs the previous frame.",
722
+ frames: perFrame
723
+ },
724
+ null,
725
+ 2
726
+ )}
727
+ `
728
+ );
729
+ } else {
730
+ process.stdout.write(`${JSON.stringify(perFrame)}
731
+ `);
732
+ }
733
+ let peak = 0;
734
+ for (let i = 1; i < diffs.length; i++) if ((diffs[i] ?? 0) > (diffs[peak] ?? 0)) peak = i;
735
+ logger.info(
736
+ `Wrote contact sheet ${opts.output} (${sheet.cols}x${sheet.rows}, ${frames.length} frames). Biggest change at frame ${peak} (\u0394${(diffs[peak] ?? 0).toFixed(2)}).`
737
+ );
738
+ return;
739
+ }
740
+ if (opts.burst) {
741
+ const frames = await captureBurst({
742
+ config,
743
+ count: Number(opts.burst),
744
+ intervalMs,
745
+ deviceSerial: opts.device,
746
+ logger
747
+ });
748
+ const ext = path7.extname(opts.output) || ".jpeg";
749
+ const base = opts.output.slice(0, opts.output.length - ext.length);
750
+ frames.forEach((frame, i) => fs3.writeFileSync(`${base}-${i}${ext}`, frame));
751
+ logger.info(`Wrote ${frames.length} frames to ${base}-<0..${frames.length - 1}>${ext}.`);
752
+ return;
753
+ }
754
+ const shot = await takeScreenshot({ config, deviceSerial: opts.device, logger });
755
+ const maxDimProvided = command.getOptionValueSource("maxDim") === "cli";
756
+ if (opts.grid || maxDimProvided) {
757
+ const rendered = await renderWithGrid(shot.pixels, { maxDim, grid: !!opts.grid });
758
+ fs3.writeFileSync(opts.output, rendered.buf);
759
+ logger.info(
760
+ `Wrote ${opts.output} (${rendered.width}x${rendered.height} from ${rendered.originalWidth}x${rendered.originalHeight}${opts.grid ? ", 10x10 grid" : ""}).`
761
+ );
762
+ return;
763
+ }
764
+ fs3.writeFileSync(opts.output, shot.pixels);
765
+ logger.info(`Wrote ${opts.output} (${shot.width}x${shot.height}).`);
766
+ });
767
+ }
768
+
769
+ // src/commands/dump.ts
770
+ import { dumpLayout } from "@oniroproject/core";
771
+ function registerDumpCommand(program) {
772
+ 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) => {
773
+ const { config, logger } = getRuntime();
774
+ const what = target ?? "layout";
775
+ if (what !== "layout") throw new Error(`Unknown dump target "${what}". Supported: layout.`);
776
+ const result = await dumpLayout({
777
+ config,
778
+ bundleName: opts.bundle,
779
+ prune: !opts.raw,
780
+ deviceSerial: opts.device,
781
+ logger
782
+ });
783
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
784
+ `);
785
+ });
786
+ }
787
+
788
+ // src/commands/devices.ts
789
+ import { listDevices } from "@oniroproject/core";
790
+ function registerDevicesCommand(program) {
791
+ program.command("devices").description("List connected hdc devices.").option("--json", "Emit machine-readable JSON.").action(async (opts) => {
792
+ const { config } = getRuntime();
793
+ const devices = await listDevices(config);
794
+ if (opts.json) {
795
+ process.stdout.write(`${JSON.stringify(devices, null, 2)}
796
+ `);
797
+ return;
798
+ }
799
+ if (devices.length === 0) {
800
+ process.stdout.write("(no devices)\n");
801
+ return;
802
+ }
803
+ for (const d of devices) {
804
+ process.stdout.write(`${d.serial} ${d.connection ?? ""} ${d.status}
805
+ `);
806
+ }
807
+ });
808
+ }
809
+
810
+ // src/commands/file.ts
811
+ import { sendFile, recvFile } from "@oniroproject/core";
812
+ function registerFileCommand(program) {
813
+ const file = program.command("file").description("Transfer files to/from the device (hdc file send/recv).");
814
+ file.command("send <local> <remote>").description("Push a local file to the device.").option("--device <serial>", "Target device serial.").action(async (local, remote, opts) => {
815
+ const { config, logger } = getRuntime();
816
+ await sendFile({ config, local, remote, deviceSerial: opts.device, logger });
817
+ logger.info(`Sent ${local} -> ${remote}.`);
818
+ });
819
+ file.command("recv <remote> <local>").description("Pull a file from the device.").option("--device <serial>", "Target device serial.").action(async (remote, local, opts) => {
820
+ const { config, logger } = getRuntime();
821
+ await recvFile({ config, remote, local, deviceSerial: opts.device, logger });
822
+ logger.info(`Received ${remote} -> ${local}.`);
823
+ });
824
+ }
825
+
826
+ // src/commands/lint.ts
827
+ import * as path8 from "path";
828
+ import { runCodelinter } from "@oniroproject/core";
829
+ function registerLintCommand(program) {
830
+ 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) => {
831
+ const { config, logger } = getRuntime();
832
+ const dir = path8.resolve(projectDir ?? process.cwd());
833
+ const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
834
+ if (opts.json) {
835
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
836
+ `);
837
+ } else {
838
+ for (const f of result.findings) {
839
+ process.stdout.write(`${f.file}:${f.line} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
840
+ `);
841
+ }
842
+ if (result.findings.length === 0) process.stdout.write("No findings.\n");
843
+ }
844
+ if (result.code !== 0) process.exitCode = result.code;
845
+ });
846
+ }
847
+
848
+ // src/commands/input.ts
849
+ import { Option as Option3 } from "commander";
850
+ import { sendInput, sendGesture } from "@oniroproject/core";
851
+ var INPUT_TYPES = ["click", "doubleClick", "longClick", "swipe", "drag", "fling", "keyEvent", "inputText"];
852
+ var num = (v) => v === void 0 ? void 0 : Number(v);
853
+ function registerInputCommand(program) {
854
+ 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) => {
855
+ const { config, logger } = getRuntime();
856
+ await sendInput({
857
+ config,
858
+ type: opts.type,
859
+ pxX: num(opts.x),
860
+ pxY: num(opts.y),
861
+ pxX2: num(opts.x2),
862
+ pxY2: num(opts.y2),
863
+ speed: num(opts.speed),
864
+ key: opts.key,
865
+ text: opts.text,
866
+ deviceSerial: opts.device,
867
+ logger
868
+ });
869
+ logger.info(`Sent ${opts.type}.`);
870
+ });
871
+ 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) => {
872
+ const { config, logger } = getRuntime();
873
+ const waypoints = JSON.parse(opts.waypoints);
874
+ await sendGesture({
875
+ config,
876
+ waypoints,
877
+ holdStartMs: num(opts.holdStart),
878
+ holdEndMs: num(opts.holdEnd),
879
+ deviceSerial: opts.device,
880
+ logger
881
+ });
882
+ logger.info("Gesture sent.");
883
+ });
884
+ }
885
+
413
886
  // src/bin/oniro-app.ts
414
- var CLI_VERSION = "0.6.2";
887
+ var CLI_VERSION = "0.8.1";
415
888
  function buildProgram() {
416
- const program = new Command2().name("oniro-app").description(
889
+ const program = new Command4().name("oniro-app").description(
417
890
  "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."
418
891
  ).version(CLI_VERSION).showHelpAfterError();
419
892
  registerSdkCommand(program);
@@ -424,6 +897,15 @@ function buildProgram() {
424
897
  registerAppCommand(program);
425
898
  registerCreateCommand(program);
426
899
  registerTemplatesCommand(program);
900
+ registerRebootCommand(program);
901
+ registerWaitCommand(program);
902
+ registerWatchCommand(program);
903
+ registerScreenshotCommand(program);
904
+ registerDumpCommand(program);
905
+ registerDevicesCommand(program);
906
+ registerFileCommand(program);
907
+ registerLintCommand(program);
908
+ registerInputCommand(program);
427
909
  return program;
428
910
  }
429
911
  async function main() {