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