@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.
- package/README.md +303 -9
- package/dist/oniro-app.js +499 -17
- 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
|
|
@@ -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 {
|
|
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
|
|
242
|
+
const result = await buildHap({
|
|
240
243
|
config,
|
|
241
244
|
projectDir: dir,
|
|
242
245
|
product: opts.product,
|
|
243
246
|
module: opts.module,
|
|
244
|
-
|
|
247
|
+
mode: opts.mode,
|
|
245
248
|
task: opts.task,
|
|
249
|
+
autoInstallDeps: opts.deps,
|
|
250
|
+
parallel: opts.parallel,
|
|
246
251
|
logger,
|
|
247
|
-
//
|
|
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
|
|
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(
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
887
|
+
var CLI_VERSION = "0.8.1";
|
|
415
888
|
function buildProgram() {
|
|
416
|
-
const program = new
|
|
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() {
|