@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.
- package/README.md +303 -9
- package/dist/oniro-app.js +488 -15
- package/dist/oniro-app.js.map +1 -1
- package/package.json +4 -3
- package/templates/NativeCpp/AppScope/app.json5 +10 -0
- package/templates/NativeCpp/AppScope/resources/base/element/string.json +8 -0
- package/templates/NativeCpp/AppScope/resources/base/media/background.png +0 -0
- package/templates/NativeCpp/AppScope/resources/base/media/foreground.png +0 -0
- package/templates/NativeCpp/AppScope/resources/base/media/layered_image.json +7 -0
- package/templates/NativeCpp/build-profile.json5 +35 -0
- package/templates/NativeCpp/entry/build-profile.json5 +48 -0
- package/templates/NativeCpp/entry/hvigorfile.ts +6 -0
- package/templates/NativeCpp/entry/obfuscation-rules.txt +23 -0
- package/templates/NativeCpp/entry/oh-package.json5 +12 -0
- package/templates/NativeCpp/entry/src/main/cpp/CMakeLists.txt +15 -0
- package/templates/NativeCpp/entry/src/main/cpp/napi_init.cpp +53 -0
- package/templates/NativeCpp/entry/src/main/cpp/types/libentry/Index.d.ts +1 -0
- package/templates/NativeCpp/entry/src/main/cpp/types/libentry/oh-package.json5 +6 -0
- package/templates/NativeCpp/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
- package/templates/NativeCpp/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
- package/templates/NativeCpp/entry/src/main/ets/pages/Index.ets +26 -0
- package/templates/NativeCpp/entry/src/main/module.json5 +50 -0
- package/templates/NativeCpp/entry/src/main/resources/base/element/color.json +8 -0
- package/templates/NativeCpp/entry/src/main/resources/base/element/float.json +8 -0
- package/templates/NativeCpp/entry/src/main/resources/base/element/string.json +16 -0
- package/templates/NativeCpp/entry/src/main/resources/base/media/background.png +0 -0
- package/templates/NativeCpp/entry/src/main/resources/base/media/foreground.png +0 -0
- package/templates/NativeCpp/entry/src/main/resources/base/media/layered_image.json +7 -0
- package/templates/NativeCpp/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/templates/NativeCpp/entry/src/main/resources/base/profile/backup_config.json +3 -0
- package/templates/NativeCpp/entry/src/main/resources/base/profile/main_pages.json +5 -0
- package/templates/NativeCpp/entry/src/main/resources/dark/element/color.json +8 -0
- package/templates/NativeCpp/entry/src/mock/Libentry.mock.ets +7 -0
- package/templates/NativeCpp/entry/src/mock/mock-config.json5 +5 -0
- package/templates/NativeCpp/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
- package/templates/NativeCpp/entry/src/ohosTest/ets/test/List.test.ets +5 -0
- package/templates/NativeCpp/entry/src/ohosTest/module.json5 +11 -0
- package/templates/NativeCpp/entry/src/test/List.test.ets +5 -0
- package/templates/NativeCpp/entry/src/test/LocalUnit.test.ets +33 -0
- package/templates/NativeCpp/hvigor/hvigor-config.json5 +23 -0
- package/templates/NativeCpp/hvigorfile.ts +6 -0
- package/templates/NativeCpp/oh-package.json5 +10 -0
- 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
|
|
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 {
|
|
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
|
|
241
|
+
const result = await buildHap({
|
|
240
242
|
config,
|
|
241
243
|
projectDir: dir,
|
|
242
244
|
product: opts.product,
|
|
243
245
|
module: opts.module,
|
|
244
|
-
|
|
246
|
+
mode: opts.mode,
|
|
245
247
|
task: opts.task,
|
|
248
|
+
autoInstallDeps: opts.deps,
|
|
249
|
+
parallel: opts.parallel,
|
|
246
250
|
logger,
|
|
247
|
-
//
|
|
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
|
|
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.
|
|
878
|
+
var CLI_VERSION = "0.8.0";
|
|
415
879
|
function buildProgram() {
|
|
416
|
-
const program = new
|
|
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() {
|