@oniroproject/oniro-app 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oniro-app.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/oniro-app.ts
4
- import { Command as Command4 } from "commander";
5
- import { OniroError as OniroError3 } from "@oniroproject/core";
4
+ import { Command as Command11, CommanderError } from "commander";
5
+ import { OniroError as OniroError6 } from "@oniroproject/core";
6
6
 
7
7
  // src/commands/sdk.ts
8
8
  import * as fs from "fs";
@@ -12,6 +12,7 @@ import {
12
12
  OHOS_DAILY_BUILDS_URL,
13
13
  downloadAndInstallSdk,
14
14
  getOsFolder,
15
+ getHarmonyOsSdkListing,
15
16
  getSdkRootDir,
16
17
  getSupportedSdksForUi,
17
18
  installSdkFromArchive,
@@ -47,16 +48,16 @@ function createCliProgress() {
47
48
  let lastEmitted = -1;
48
49
  let lastMessage = "";
49
50
  return {
50
- report({ message, increment }) {
51
+ report({ message: message2, increment }) {
51
52
  if (typeof increment === "number") total = Math.min(100, total + increment);
52
53
  const pct = total > 0 ? Math.round(total) : null;
53
- const msg = message ?? lastMessage;
54
+ const msg = message2 ?? lastMessage;
54
55
  if (pct === lastEmitted && msg === lastMessage) return;
55
56
  const prefix = pct === null ? "[progress]" : `[progress ${pct}%]`;
56
57
  process.stderr.write(`${prefix} ${msg}
57
58
  `);
58
59
  lastEmitted = pct ?? lastEmitted;
59
- if (message) lastMessage = message;
60
+ if (message2) lastMessage = message2;
60
61
  }
61
62
  };
62
63
  }
@@ -78,7 +79,8 @@ var ENV_VAR_BY_KEY = {
78
79
  applicationCertPath: "ONIRO_APPLICATION_CERT_PATH",
79
80
  harmonyosSdkPath: "ONIRO_HARMONYOS_SDK_PATH",
80
81
  harmonyosAuthDir: "ONIRO_HARMONYOS_AUTH_DIR",
81
- harmonyosSigningDir: "ONIRO_HARMONYOS_SIGNING_DIR"
82
+ harmonyosSigningDir: "ONIRO_HARMONYOS_SIGNING_DIR",
83
+ cacheDir: "ONIRO_CACHE_DIR"
82
84
  };
83
85
  function createEnvConfig(env = process.env) {
84
86
  return {
@@ -106,9 +108,23 @@ function getRuntime() {
106
108
  }
107
109
 
108
110
  // src/commands/sdk.ts
111
+ var HARMONYOS_SOURCE = {
112
+ cmdtools: "from the command-line tools",
113
+ "deveco-studio": "from DevEco Studio",
114
+ configured: "from ONIRO_HARMONYOS_SDK_PATH"
115
+ };
116
+ function formatHarmonyOsSdkNote(listing) {
117
+ const note = "HarmonyOS SDKs are not managed by `sdk`: they ship inside the HarmonyOS command-line tools (`oniro-app cmdtools`) or DevEco Studio.";
118
+ if (!listing) return `${note}
119
+ No HarmonyOS SDK found; \`oniro-app cmdtools install\` installs one.`;
120
+ const name = listing.displayName ?? `HarmonyOS ${listing.version ?? ""}`.trim();
121
+ const from = listing.source ? `, ${HARMONYOS_SOURCE[listing.source]}` : "";
122
+ return `${note}
123
+ * ${name.padEnd(17)} api ${listing.api ?? "?"} ${listing.path}${from}`;
124
+ }
109
125
  var FULL_SDK_TIP = `Tip: the full SDK (with system APIs) is not on the release mirror. Download an ohos-sdk-full archive from the OpenHarmony daily builds, ${OHOS_DAILY_BUILDS_URL}, and install it with \`oniro-app sdk install --from-archive <archive>\`.`;
110
126
  function registerSdkCommand(program) {
111
- const sdk = program.command("sdk").description("Manage OpenHarmony SDK installs.");
127
+ const sdk = program.command("sdk").description("Manage OpenHarmony SDK installs. HarmonyOS SDKs come with the command-line tools (`cmdtools`).");
112
128
  sdk.command("install [version]").description(
113
129
  "Download and install an OpenHarmony SDK (skips if already installed), or install one from a local archive with --from-archive."
114
130
  ).option(
@@ -166,16 +182,22 @@ ${FULL_SDK_TIP}`).action(async (version, opts) => {
166
182
  });
167
183
  logger.info("SDK installed.");
168
184
  });
169
- sdk.command("list").description("List known SDK versions and whether they are installed on this machine.").option("--json", "Emit machine-readable JSON instead of a table.").action((opts) => {
185
+ sdk.command("list").description(
186
+ "List known OpenHarmony SDK versions and whether they are installed on this machine, then the HarmonyOS SDK, which comes with the command-line tools (`cmdtools`) or DevEco Studio, if one is found."
187
+ ).option("--json", "Emit machine-readable JSON instead of a table.").action((opts) => {
170
188
  const { config } = getRuntime();
171
189
  const sdks = getSupportedSdksForUi(config);
190
+ const harmonyOs = getHarmonyOsSdkListing(config);
172
191
  if (opts.json) {
173
- process.stdout.write(`${JSON.stringify(sdks, null, 2)}
192
+ const rows2 = [...sdks.map((s) => ({ ...s, flavour: "OpenHarmony" })), ...harmonyOs ? [harmonyOs] : []];
193
+ process.stdout.write(`${JSON.stringify(rows2, null, 2)}
174
194
  `);
175
195
  return;
176
196
  }
177
197
  const rows = sdks.map((s) => `${s.installed ? "*" : " "} ${s.version.padEnd(8)} api ${s.api}`);
178
198
  process.stdout.write(`${rows.join("\n")}
199
+
200
+ ${formatHarmonyOsSdkNote(harmonyOs)}
179
201
  `);
180
202
  });
181
203
  sdk.command("remove <api>").description("Remove an installed SDK by API level (e.g. 18, 20).").action((api) => {
@@ -190,24 +212,40 @@ ${FULL_SDK_TIP}`).action(async (version, opts) => {
190
212
  }
191
213
 
192
214
  // src/commands/cmdtools.ts
193
- import { getCmdToolsStatus, installCmdTools, isCmdToolsInstalled, removeCmdTools } from "@oniroproject/core";
215
+ import {
216
+ describeCmdTools,
217
+ getCmdToolsStatus,
218
+ installCmdTools,
219
+ removeCmdTools
220
+ } from "@oniroproject/core";
221
+ function formatCmdToolsSdk(sdk) {
222
+ const name = [sdk.displayName, sdk.apiVersion ? `API ${sdk.apiVersion}` : void 0].filter(Boolean).join(", ");
223
+ return `${name || "SDK"}${sdk.build ? ` (${sdk.build})` : ""} at ${sdk.path}`;
224
+ }
194
225
  function registerCmdToolsCommand(program) {
195
- const cmdtools = program.command("cmdtools").description("Manage OpenHarmony command-line tools.");
196
- cmdtools.command("install").description("Install the OpenHarmony command-line tools (hvigorw, ohpm, hdc). Skips if already installed.").option("--from-zip <path>", "Install from a local ZIP instead of downloading.").option("--force", "Reinstall even if the tools are already present.").option(
226
+ const cmdtools = program.command("cmdtools").description(
227
+ "Manage the command-line tools (hvigorw, ohpm, codelinter, hdc). Their HarmonyOS edition also supplies the HarmonyOS SDK, which `sdk` does not manage."
228
+ );
229
+ cmdtools.command("install").description(
230
+ "Install the command-line tools (hvigorw, ohpm, codelinter, hdc and, in the HarmonyOS edition, the HarmonyOS SDK). Skips if already installed."
231
+ ).option("--from-zip <path>", "Install from a local ZIP instead of downloading.").option("--force", "Reinstall even if the tools are already present.").option(
197
232
  "--tmp-dir <path>",
198
233
  "Directory for download/extract temporaries (default: ONIRO_TMP_DIR, else .oniro-tmp next to the install target). Use this when that filesystem is short on space."
199
234
  ).action(async (opts) => {
200
235
  const { config, logger, progress } = getRuntime();
201
- if (isCmdToolsInstalled(config) && !opts.force) {
202
- logger.info("Command-line tools already installed; pass --force to reinstall.");
236
+ const current = getCmdToolsStatus(config);
237
+ if (current.installed && !opts.force) {
238
+ logger.info(
239
+ `Command-line tools ${describeCmdTools(current)} already installed at ${current.path}; pass --force to replace them.`
240
+ );
203
241
  return;
204
242
  }
205
- logger.info("Installing OpenHarmony command-line tools...");
206
- await installCmdTools({ config, progress, logger, localZipPath: opts.fromZip, tmpDir: opts.tmpDir });
207
- logger.info("Command-line tools installed.");
243
+ logger.info("Installing command-line tools...");
244
+ const result = await installCmdTools({ config, progress, logger, localZipPath: opts.fromZip, tmpDir: opts.tmpDir });
245
+ logger.info(`Command-line tools ${describeCmdTools(result.installed)} installed at ${result.path}.`);
208
246
  });
209
247
  cmdtools.command("status").description(
210
- "Report whether the command-line tools are installed, their version and flavour (HarmonyOS or OpenHarmony), and the hdc binary every device command uses."
248
+ "Report whether the command-line tools are installed, their version, flavour (HarmonyOS or OpenHarmony), the SDK they bundle (name, API level), their tools' versions (hvigor, ohpm, ...), and the hdc binary every device command uses."
211
249
  ).option("--json", "Emit machine-readable JSON.").action((opts) => {
212
250
  const { config } = getRuntime();
213
251
  const status = getCmdToolsStatus(config);
@@ -216,11 +254,15 @@ function registerCmdToolsCommand(program) {
216
254
  `);
217
255
  return;
218
256
  }
219
- process.stdout.write(`${status.status}
257
+ process.stdout.write(`${status.version ? `Installed ${describeCmdTools(status)}` : status.status}
220
258
  `);
221
259
  process.stdout.write(`path: ${status.path}
222
260
  `);
223
261
  if (status.flavour) process.stdout.write(`flavour: ${status.flavour}
262
+ `);
263
+ if (status.sdk) process.stdout.write(`sdk: ${formatCmdToolsSdk(status.sdk)}
264
+ `);
265
+ if (status.tools?.hvigor) process.stdout.write(`hvigor: ${status.tools.hvigor}
224
266
  `);
225
267
  process.stdout.write(`hdc: ${status.hdc.path}${status.hdc.exists ? "" : " (missing)"}
226
268
  `);
@@ -234,15 +276,56 @@ function registerCmdToolsCommand(program) {
234
276
  }
235
277
 
236
278
  // src/commands/emulator.ts
279
+ import { InvalidArgumentError } from "commander";
237
280
  import {
281
+ MAX_POOL_INSTANCE,
282
+ POOL_DEFAULT_MEMORY,
283
+ POOL_DEFAULT_RESOLUTION,
284
+ POOL_DEFAULT_SMP,
285
+ DEFAULT_BOOT_TIMEOUT_SECONDS,
238
286
  attemptHdcConnection,
239
287
  getEmulatorStatus,
288
+ getPoolInstanceStatus,
240
289
  installEmulator,
241
290
  isEmulatorInstalled,
242
291
  removeEmulator,
243
292
  startEmulator,
244
- stopEmulator
293
+ startPoolInstance,
294
+ stopEmulator,
295
+ stopPoolInstance
245
296
  } from "@oniroproject/core";
297
+ function parseInstance(v) {
298
+ const n = Number(v);
299
+ if (!/^\d+$/.test(v) || n > MAX_POOL_INSTANCE) {
300
+ throw new InvalidArgumentError(`expected an integer from 0 to ${MAX_POOL_INSTANCE}.`);
301
+ }
302
+ return n;
303
+ }
304
+ function parseSeconds(v) {
305
+ if (!/^\d+$/.test(v)) throw new InvalidArgumentError("expected a whole number of seconds.");
306
+ return Number(v);
307
+ }
308
+ function parseSmp(v) {
309
+ if (!/^\d+$/.test(v) || Number(v) < 1) throw new InvalidArgumentError("expected a positive integer.");
310
+ return Number(v);
311
+ }
312
+ var ACCELS = ["auto", "kvm", "hvf", "whpx", "tcg"];
313
+ function parseAccel(v) {
314
+ if (!ACCELS.includes(v)) throw new InvalidArgumentError(`expected one of ${ACCELS.join(", ")}.`);
315
+ return v;
316
+ }
317
+ function formatInstanceRow(i) {
318
+ const fields = [String(i.instance), i.state, `hdc=${i.address}`];
319
+ if (i.vncPort) fields.push(`vnc=127.0.0.1:${i.vncPort}`);
320
+ if (i.instance > 0 && i.serialPort) fields.push(`serial=127.0.0.1:${i.serialPort}`);
321
+ if (i.pid) fields.push(`pid=${i.pid}`);
322
+ if (i.smp) fields.push(`cpus=${i.smp}`);
323
+ if (i.memory) fields.push(`mem=${i.memory}`);
324
+ if (i.bootSeconds !== void 0) fields.push(`boot=${i.bootSeconds}s`);
325
+ return fields.join(" ");
326
+ }
327
+ var POOL_ONLY_OPTIONS = ["memory", "smp", "resolution", "window", "fresh", "bootTimeout", "qemu", "accel", "json"];
328
+ var flagName = (key) => `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
246
329
  function registerEmulatorCommand(program) {
247
330
  const emu = program.command("emulator").description("Manage the Oniro emulator.");
248
331
  emu.command("install").description("Download and install the Oniro emulator (QEMU-based). Skips if already installed.").option("--force", "Reinstall even if the emulator is already present.").option(
@@ -258,24 +341,71 @@ function registerEmulatorCommand(program) {
258
341
  await installEmulator({ config, progress, logger, tmpDir: opts.tmpDir });
259
342
  logger.info("Emulator installed.");
260
343
  });
261
- emu.command("start").description("Start the Oniro emulator via the bundled launcher (run.sh / run.bat). Detaches so the CLI can exit while the emulator keeps running.").option("--headless", "Launch headless (VNC + telnet serial, no local window). Required in CI.").option("--log <path>", "Redirect launcher stdout/stderr to this file. Defaults to discarding output.").option("--wait-for-hdc <seconds>", "Wait up to N seconds for hdc to connect. 0 = do not wait.", (v) => parseInt(v, 10), 0).option("--connect <host:port>", "Override the launcher's hdc port-forward bind address. Pass 0.0.0.0:55555 on hosts where QEMU refuses to bind 127.0.0.1 (some CI runners).").action(async (opts) => {
344
+ emu.command("start").description(
345
+ "Start the Oniro emulator and detach, so the CLI can exit while it keeps running. Without --instance (or with --instance 0) it runs the bundled launcher (run.sh / run.bat). --instance <n> (1..99) starts pool instance n instead: its own QEMU on copy-on-write overlays of the images, hdc on 127.0.0.1:55555+n, headless VNC on 127.0.0.1:5900+n, serial console on telnet 127.0.0.1:4444+n."
346
+ ).option("--instance <n>", "Pool instance number (1..99). 0 or unset: the default emulator via run.sh.", parseInstance).option("--headless", "Launch headless (VNC + telnet serial, no local window). Required in CI. Pool instances are headless unless --window.").option("--log <path>", "Redirect launcher stdout/stderr to this file. Defaults to discarding output (pool instances: <instance dir>/qemu.log).").option("--wait-for-hdc <seconds>", "Default emulator: wait up to N seconds for hdc to connect (restarts the hdc server). 0 = do not wait.", (v) => parseInt(v, 10), 0).option("--wait-ready <seconds>", "Wait up to N seconds until the guest reports bootevent.boot.completed=true. Connects with hdc tconn only, never restarting the hdc server.", parseSeconds).option("--connect <host:port>", "Default emulator: override the launcher's hdc port-forward bind address. Pass 0.0.0.0:55555 on hosts where QEMU refuses to bind 127.0.0.1 (some CI runners).").option("--memory <size>", `Pool: guest RAM in QEMU syntax (default ${POOL_DEFAULT_MEMORY}).`).option("--smp <n>", `Pool: virtual CPUs (default ${POOL_DEFAULT_SMP}).`, parseSmp).option("--resolution <WxH>", `Pool: display resolution (default ${POOL_DEFAULT_RESOLUTION}).`).option("--window", "Pool: show a local SDL window instead of VNC.").option("--fresh", "Pool: discard the instance's overlays first (factory reset).").option("--boot-timeout <seconds>", `Pool: seconds after launch past which a boot that has not completed is reported stuck (default ${DEFAULT_BOOT_TIMEOUT_SECONDS}).`, parseSeconds).option("--qemu <path>", "Pool: qemu-system-x86_64 binary (default QEMU_BIN, else PATH); qemu-img is looked up next to it.").option("--accel <kind>", `Pool: accelerator, one of ${ACCELS.join(", ")} (default auto: KVM on Linux, as run.sh).`, parseAccel).option("--json", "Pool: print the instance's status as JSON when done.").action(async (opts) => {
262
347
  const { config, logger } = getRuntime();
263
- await startEmulator({
348
+ if (opts.instance === void 0 || opts.instance === 0) {
349
+ const poolOnly = POOL_ONLY_OPTIONS.filter((k) => opts[k] !== void 0);
350
+ if (poolOnly.length > 0) {
351
+ throw new InvalidArgumentError(
352
+ `${poolOnly.map(flagName).join(", ")} ${poolOnly.length > 1 ? "apply" : "applies"} only to pool instances (--instance 1..${MAX_POOL_INSTANCE}).`
353
+ );
354
+ }
355
+ await startEmulator({
356
+ config,
357
+ logger,
358
+ headless: opts.headless === true,
359
+ logFile: opts.log,
360
+ waitForHdcSeconds: opts.waitForHdc,
361
+ waitReadySeconds: opts.waitReady,
362
+ connect: opts.connect
363
+ });
364
+ logger.info("Emulator start command complete.");
365
+ return;
366
+ }
367
+ if (opts.connect || opts.waitForHdc > 0) {
368
+ throw new InvalidArgumentError(
369
+ "--connect and --wait-for-hdc apply to the default emulator only; a pool instance listens on 127.0.0.1:55555+n, wait with --wait-ready."
370
+ );
371
+ }
372
+ const status = await startPoolInstance({
264
373
  config,
265
374
  logger,
266
- headless: opts.headless === true,
267
- logFile: opts.log,
268
- waitForHdcSeconds: opts.waitForHdc,
269
- connect: opts.connect
375
+ instance: opts.instance,
376
+ memory: opts.memory,
377
+ smp: opts.smp,
378
+ resolution: opts.resolution,
379
+ headless: opts.window !== true,
380
+ fresh: opts.fresh === true,
381
+ waitReadySeconds: opts.waitReady,
382
+ bootTimeoutSeconds: opts.bootTimeout,
383
+ qemuBin: opts.qemu,
384
+ accel: opts.accel,
385
+ logFile: opts.log
270
386
  });
271
- logger.info("Emulator start command complete.");
387
+ if (opts.json) process.stdout.write(`${JSON.stringify(status, null, 2)}
388
+ `);
389
+ else logger.info(`Instance ${status.instance}: ${status.state} (${status.address}).`);
272
390
  });
273
391
  emu.command("status").description(
274
- "Report whether the Oniro emulator is installed, running, and connected to hdc, without starting it. Exits 1 when it is not installed."
275
- ).option("--address <addr>", "hdc address the emulator listens on", "127.0.0.1:55555").option("--json", "Emit machine-readable JSON.").action(async (opts) => {
392
+ "Report whether the Oniro emulator is installed, running, and connected to hdc, without starting it, and list every instance (the default emulator as 0, then each pool instance) with its health: stopped, booting, ready, hdc-lost or stuck. Exits 1 when the emulator is not installed."
393
+ ).option("--address <addr>", "hdc address the default emulator listens on", "127.0.0.1:55555").option("--instance <n>", "Report only this instance (0 = the default emulator).", parseInstance).option("--json", "Emit machine-readable JSON: `instances` lists every instance; with --instance, that instance alone.").action(async (opts) => {
276
394
  const { config, logger } = getRuntime();
395
+ const print = (one) => {
396
+ process.stdout.write(opts.json ? `${JSON.stringify(one, null, 2)}
397
+ ` : `${formatInstanceRow(one)}
398
+ `);
399
+ };
400
+ if (opts.instance !== void 0 && opts.instance > 0) {
401
+ print(await getPoolInstanceStatus(config, opts.instance, { logger }));
402
+ if (!isEmulatorInstalled(config)) process.exitCode = 1;
403
+ return;
404
+ }
277
405
  const status = await getEmulatorStatus(config, { address: opts.address, logger });
278
- if (opts.json) {
406
+ if (opts.instance === 0) {
407
+ print(status.instances[0]);
408
+ } else if (opts.json) {
279
409
  process.stdout.write(`${JSON.stringify(status, null, 2)}
280
410
  `);
281
411
  } else {
@@ -285,13 +415,26 @@ running: ${status.running ? "yes" : "no"}
285
415
  connected: ${status.connected ? "yes" : "no"} (${status.address})
286
416
  `
287
417
  );
418
+ if (status.instances.length > 1) {
419
+ process.stdout.write(`instances:
420
+ ${status.instances.map((i) => ` ${formatInstanceRow(i)}`).join("\n")}
421
+ `);
422
+ }
288
423
  }
289
424
  if (!status.installed) process.exitCode = 1;
290
425
  });
291
- emu.command("stop").description("Kill running emulator processes.").action(async () => {
292
- const { logger } = getRuntime();
293
- await stopEmulator(logger);
294
- logger.info("Emulator stopped.");
426
+ emu.command("stop").description(
427
+ "Without --instance: kill running emulator processes (every qemu-system-x86_64, pool instances included). With --instance <n>: stop only that pool instance's QEMU and drop its target from hdc."
428
+ ).option("--instance <n>", "Pool instance to stop (1..99).", parseInstance).option("--discard", "With --instance: also delete the instance's overlays, log and state.").action(async (opts) => {
429
+ const { config, logger } = getRuntime();
430
+ if (opts.instance === void 0 || opts.instance === 0) {
431
+ if (opts.discard) throw new InvalidArgumentError(`--discard needs --instance <n> (1..${MAX_POOL_INSTANCE}).`);
432
+ await stopEmulator(logger);
433
+ logger.info("Emulator stopped.");
434
+ return;
435
+ }
436
+ const stopped = await stopPoolInstance({ config, instance: opts.instance, discard: opts.discard === true, logger });
437
+ if (!stopped) logger.info(`Instance ${opts.instance} was not running.`);
295
438
  });
296
439
  emu.command("connect").description("Attempt to connect hdc to the emulator.").option("--address <addr>", "hdc address to connect to", "127.0.0.1:55555").action(async (opts) => {
297
440
  const { config, logger } = getRuntime();
@@ -311,45 +454,80 @@ connected: ${status.connected ? "yes" : "no"} (${status.address})
311
454
 
312
455
  // src/commands/build.ts
313
456
  import * as path2 from "path";
314
- import { SdkNotInstalledError, buildHap, downloadAndInstallSdk as downloadAndInstallSdk2 } from "@oniroproject/core";
457
+ import {
458
+ OniroError,
459
+ buildWithDiagnostics,
460
+ downloadAndInstallSdk as downloadAndInstallSdk2
461
+ } from "@oniroproject/core";
462
+ async function runBuild(projectDir, opts) {
463
+ const { config, logger, progress } = getRuntime();
464
+ const dir = path2.resolve(projectDir ?? process.cwd());
465
+ logger.info(`Building project at ${dir}...`);
466
+ const build2 = () => buildWithDiagnostics({
467
+ config,
468
+ projectDir: dir,
469
+ product: opts.product,
470
+ module: opts.module,
471
+ mode: opts.mode,
472
+ task: opts.task,
473
+ autoInstallDeps: opts.deps,
474
+ parallel: opts.parallel,
475
+ logger,
476
+ // The logger already echoes spawn output; this hook is for streaming to other UIs.
477
+ onOutput: () => {
478
+ }
479
+ });
480
+ let report = await build2();
481
+ if (!report.ok && report.missingSdk?.version && opts.installSdk) {
482
+ const { version, api } = report.missingSdk;
483
+ logger.info(`Installing OpenHarmony SDK ${version} (api ${api}), which the project compiles against...`);
484
+ await downloadAndInstallSdk2({ config, version, api, progress, logger });
485
+ report = await build2();
486
+ }
487
+ return report;
488
+ }
315
489
  function registerBuildCommand(program) {
316
490
  program.command("build [project-dir]").description(
317
- 'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio). An OpenHarmony project fails early, naming the release to install, when the SDK for its compileSdkVersion is missing.'
318
- ).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--install-sdk", "install the OpenHarmony SDK the project compiles against when it is missing, then build").option("--json", "emit the build result (discovered HAPs, warnings) as JSON").action(async (projectDir, opts) => {
319
- const { config, logger, progress } = getRuntime();
320
- const dir = path2.resolve(projectDir ?? process.cwd());
321
- logger.info(`Building project at ${dir}...`);
322
- const build = () => buildHap({
323
- config,
324
- projectDir: dir,
325
- product: opts.product,
326
- module: opts.module,
327
- mode: opts.mode,
328
- task: opts.task,
329
- autoInstallDeps: opts.deps,
330
- parallel: opts.parallel,
331
- logger,
332
- // The logger already echoes spawn output; this hook is for streaming to other UIs.
333
- onOutput: () => {
334
- }
335
- });
336
- let result;
491
+ 'Build an OpenHarmony or HarmonyOS app via hvigorw. Defaults to the current directory. Projects declaring runtimeOS "HarmonyOS" build against the HarmonyOS SDK (ONIRO_HARMONYOS_SDK_PATH or an installed DevEco Studio). An OpenHarmony project fails early, naming the release to install, when the SDK for its compileSdkVersion is missing. With --json, one result object is printed whether the build passes or fails; a failed build exits 1.'
492
+ ).option("--product <product>", "hvigor product name", "default").option("--module <module>", "restrict the build to a specific module").option("--mode <mode>", "hvigor build mode (e.g. release, debug)").option("--task <task>", "hvigor task to run", "assembleHap").option("--no-deps", "skip the automatic `ohpm install --all` when oh_modules/ is missing").option("--no-parallel", "build modules serially (default: parallel)").option("--install-sdk", "install the OpenHarmony SDK the project compiles against when it is missing, then build").option(
493
+ "--json",
494
+ "print the result as JSON: ok, diagnostics (file, line, column, code, rule, message), summary, failedTask, discoveredHaps, warnings"
495
+ ).action(async (projectDir, opts) => {
496
+ const { logger } = getRuntime();
497
+ let report;
337
498
  try {
338
- result = await build();
499
+ report = await runBuild(projectDir, opts);
339
500
  } catch (err) {
340
- if (!(err instanceof SdkNotInstalledError && err.version && opts.installSdk)) throw err;
341
- logger.info(`Installing OpenHarmony SDK ${err.version} (api ${err.api}), which the project compiles against...`);
342
- await downloadAndInstallSdk2({ config, version: err.version, api: err.api, progress, logger });
343
- result = await build();
501
+ if (opts.json) {
502
+ const error = err instanceof Error ? err.message : String(err);
503
+ process.stdout.write(`${JSON.stringify({ ok: false, exitCode: 1, error, diagnostics: [] }, null, 2)}
504
+ `);
505
+ }
506
+ throw err;
344
507
  }
345
- for (const w of result.warnings) logger.warn(w);
508
+ for (const w of report.warnings) logger.warn(w);
346
509
  if (opts.json) {
347
- process.stdout.write(`${JSON.stringify(result, null, 2)}
510
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
348
511
  `);
349
512
  }
513
+ if (!report.ok) {
514
+ const listed = formatDiagnostics(report.diagnostics);
515
+ if (listed) logger.error(`Build failed${report.failedTask ? ` in ${report.failedTask}` : ""}:
516
+ ${listed}`);
517
+ throw new OniroError(report.error ?? "Build failed.");
518
+ }
350
519
  logger.info("Build complete.");
351
520
  });
352
521
  }
522
+ function formatDiagnostics(diagnostics, max = 20) {
523
+ const sorted = [...diagnostics].sort((a, b) => a.severity === b.severity ? 0 : a.severity === "error" ? -1 : 1);
524
+ const lines = sorted.slice(0, max).map((d) => {
525
+ const at = d.file ? `${d.file}${d.line ? `:${d.line}${d.column ? `:${d.column}` : ""}` : ""} ` : "";
526
+ return ` ${at}${d.severity} ${d.source}${d.code ? ` ${d.code}` : ""}: ${d.message}${d.rule ? ` (${d.rule})` : ""}`;
527
+ });
528
+ if (sorted.length > max) lines.push(` \u2026 and ${sorted.length - max} more`);
529
+ return lines.join("\n");
530
+ }
353
531
 
354
532
  // src/commands/sign.ts
355
533
  import { Option } from "commander";
@@ -357,14 +535,26 @@ import * as path3 from "path";
357
535
  import {
358
536
  APL_VALUES,
359
537
  APP_FEATURE_VALUES,
360
- OniroError,
538
+ OniroError as OniroError2,
361
539
  createHarmonyOsSession,
362
540
  detectRuntimeOs,
363
541
  generateSigningConfigs,
364
542
  harmonyOsAutoSign,
543
+ harmonyOsSignHap,
544
+ signOpenHarmonyHap,
365
545
  prepareSigning,
366
- getOhosBaseSdkHome
546
+ getOhosBaseSdkHome,
547
+ describeProfileValidity,
548
+ describeSigningMaterial
367
549
  } from "@oniroproject/core";
550
+ function profileSummary(result) {
551
+ const validity = describeProfileValidity({ type: result.profileType, notAfter: result.notAfter });
552
+ return [`profile names ${result.deviceIds.length} device(s)`, validity].filter(Boolean).join(", ");
553
+ }
554
+ function materialSuffix(summary) {
555
+ const text = describeSigningMaterial(summary);
556
+ return text ? ` (${text})` : "";
557
+ }
368
558
  function parseAclsList(value) {
369
559
  return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
370
560
  }
@@ -376,7 +566,9 @@ function registerSignCommand(program) {
376
566
  "cert (issuer=pki_internal) and its bundled validity window \u2014 dev/local builds only.",
377
567
  "Overwrites the `signingConfigs` block in build-profile.json5 (other keys preserved).",
378
568
  "HarmonyOS projects need --harmonyos instead, which has AppGallery Connect issue the",
379
- "certificate and profile for a signed-in Huawei account (`oniro-app auth login`)."
569
+ "certificate and profile for a signed-in Huawei account (`oniro-app auth login`).",
570
+ "With --hap <file>, signs a prebuilt HAP instead of a project (with --harmonyos, for HarmonyOS).",
571
+ "Ends by naming the profile type and expiry; `oniro-app app verify <hap>` checks what signed a built HAP."
380
572
  ].join(" ")
381
573
  ).addOption(
382
574
  new Option(
@@ -401,10 +593,64 @@ function registerSignCommand(program) {
401
593
  ).option(
402
594
  "--harmonyos",
403
595
  "Sign a HarmonyOS project through AppGallery Connect. Needs `oniro-app auth login`, and a device registered to the team: a connected device is registered if it reports a UDID (`hdc shell bm get -u`)."
596
+ ).option(
597
+ "--hap <file>",
598
+ "Sign this prebuilt .hap (unsigned, or signed with other material) instead of a project, and write a signed copy (see --out). The bundle name is read from the HAP. For OpenHarmony, --apl, --app-feature, --acls and --app-privilege-capabilities apply as for a project; with --harmonyos, the ACL permissions are read from the HAP too."
599
+ ).option(
600
+ "--out <file>",
601
+ "With --hap: where to write the signed HAP. Default: next to the input, `<name>-unsigned.hap` \u2192 `<name>-signed.hap`, `<name>.hap` \u2192 `<name>-signed.hap`, `<name>-signed.hap` \u2192 `<name>-resigned.hap`. Name the input itself to re-sign it in place."
404
602
  ).option("--team-id <id>", "With --harmonyos: the AGC team to sign under. Default: the account's own (`oniro-app auth team list`).").option("--product <name>", "With --harmonyos: the product to sign. Default `default`.").option("--force", "With --harmonyos: regenerate the certificate and profile even when they are still valid.").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(
405
603
  async (projectDir, opts) => {
406
604
  const { config, logger } = getRuntime();
407
605
  const dir = path3.resolve(projectDir ?? process.cwd());
606
+ const passwords = opts.storePassword || opts.keyPassword ? { store: opts.storePassword ?? "123456", key: opts.keyPassword ?? "123456" } : void 0;
607
+ if (opts.out && !opts.hap) throw new OniroError2("--out names where `--hap` writes the signed HAP; pass --hap too.");
608
+ if (opts.hap) {
609
+ if (projectDir) throw new OniroError2("Pass either a project directory or --hap, not both.");
610
+ if (opts.product || opts.bootstrap) {
611
+ throw new OniroError2(`--${opts.product ? "product" : "bootstrap"} applies to a project, not to --hap.`);
612
+ }
613
+ const ignored = opts.harmonyos ? [
614
+ opts.apl !== "normal" && "--apl",
615
+ opts.appFeature && "--app-feature",
616
+ opts.acls && "--acls (the ACLs are read from the HAP)",
617
+ opts.appPrivilegeCapabilities && "--app-privilege-capabilities",
618
+ (opts.storePassword || opts.keyPassword) && "--store-password/--key-password"
619
+ ] : [opts.teamId && "--team-id", opts.force && "--force"];
620
+ const unsupported = ignored.filter((flag) => typeof flag === "string");
621
+ if (unsupported.length > 0) {
622
+ throw new OniroError2(
623
+ `${unsupported.join(", ")} ${unsupported.length > 1 ? "do" : "does"} not apply to ${opts.harmonyos ? "--harmonyos --hap" : "--hap without --harmonyos"}.`
624
+ );
625
+ }
626
+ if (!opts.harmonyos) {
627
+ const result2 = await signOpenHarmonyHap({
628
+ hapPath: opts.hap,
629
+ outPath: opts.out,
630
+ sdkHome: getOhosBaseSdkHome(config),
631
+ apl: opts.apl,
632
+ appFeature: opts.appFeature,
633
+ acls: opts.acls,
634
+ appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
635
+ passwords,
636
+ applicationCertPath: config.get("applicationCertPath", "") || void 0,
637
+ logger
638
+ });
639
+ logger.info(`Signed ${result2.bundleName} into ${result2.outPath}${materialSuffix(result2.material)}.`);
640
+ return;
641
+ }
642
+ const result = await harmonyOsSignHap({
643
+ config,
644
+ session: createHarmonyOsSession({ config, logger }),
645
+ hapPath: opts.hap,
646
+ outPath: opts.out,
647
+ teamId: opts.teamId,
648
+ force: opts.force,
649
+ logger
650
+ });
651
+ logger.info(`Signed ${result.bundleName} into ${result.outPath} (${profileSummary(result)}).`);
652
+ return;
653
+ }
408
654
  if (opts.harmonyos) {
409
655
  const result = await harmonyOsAutoSign({
410
656
  config,
@@ -415,15 +661,14 @@ function registerSignCommand(program) {
415
661
  force: opts.force,
416
662
  logger
417
663
  });
418
- logger.info(`Signing configs ready for ${result.bundleName} (profile names ${result.deviceIds.length} device(s)).`);
664
+ logger.info(`Signing configs ready for ${result.bundleName} (${profileSummary(result)}).`);
419
665
  return;
420
666
  }
421
667
  if (detectRuntimeOs(dir, opts.product) === "HarmonyOS") {
422
- throw new OniroError(
668
+ throw new OniroError2(
423
669
  `${dir} targets HarmonyOS, which needs a certificate from AppGallery Connect: run \`oniro-app auth login\` once, then \`oniro-app sign --harmonyos\`.`
424
670
  );
425
671
  }
426
- const passwords = opts.storePassword || opts.keyPassword ? { store: opts.storePassword ?? "123456", key: opts.keyPassword ?? "123456" } : void 0;
427
672
  if (opts.bootstrap) {
428
673
  const result = prepareSigning({
429
674
  config,
@@ -440,7 +685,7 @@ function registerSignCommand(program) {
440
685
  }
441
686
  const sdkHome = getOhosBaseSdkHome(config);
442
687
  logger.info(`Generating signing configs in ${dir} using SDK at ${sdkHome}...`);
443
- generateSigningConfigs({
688
+ const generated = generateSigningConfigs({
444
689
  projectDir: dir,
445
690
  sdkHome,
446
691
  apl: opts.apl,
@@ -450,32 +695,96 @@ function registerSignCommand(program) {
450
695
  passwords,
451
696
  logger
452
697
  });
453
- logger.info("Signing configs generated.");
698
+ logger.info(`Signing configs generated${materialSuffix(generated)}.`);
454
699
  }
455
700
  );
456
701
  }
457
702
 
458
703
  // src/commands/auth.ts
704
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
705
+ import * as readline from "readline";
459
706
  import { HUAWEI_SITES, createHarmonyOsSession as createHarmonyOsSession2 } from "@oniroproject/core";
460
707
  function session() {
461
708
  const { config, logger } = getRuntime();
462
- return createHarmonyOsSession2({
463
- config,
464
- logger,
465
- onLoginUrl: (url) => process.stderr.write(`Opening the Huawei sign-in page. If no browser opens, visit:
709
+ return createHarmonyOsSession2({ config, logger });
710
+ }
711
+ function parsePort(value) {
712
+ const port = Number(value);
713
+ if (!/^\d+$/.test(value) || port < 1 || port > 65535) {
714
+ throw new InvalidArgumentError2("expected a port number from 1 to 65535.");
715
+ }
716
+ return port;
717
+ }
718
+ function browserInstructions(url) {
719
+ return `Opening the Huawei sign-in page. If no browser opens, visit:
466
720
  ${url}
467
- `)
468
- });
721
+ `;
722
+ }
723
+ function manualInstructions(url, callbackUrl, port) {
724
+ return [
725
+ "Open this address in a browser, on this machine or any other, and sign in:",
726
+ ` ${url}`,
727
+ `The browser is then sent to ${callbackUrl}?... If that page does not load (the browser is not on`,
728
+ "this machine), copy the whole address from its address bar, paste it here and press Enter.",
729
+ `(Or forward the port first, from the machine with the browser: ssh -L ${port}:127.0.0.1:${port} <this-host>.)`,
730
+ ""
731
+ ].join("\n");
469
732
  }
470
733
  function registerAuthCommand(program) {
471
734
  const auth = program.command("auth").description(
472
735
  "Sign in to a Huawei developer account, which HarmonyOS signing (`oniro-app sign --harmonyos`) needs. OpenHarmony signing needs no account. The token is stored encrypted under ONIRO_HARMONYOS_AUTH_DIR (default ~/.oniro/harmonyos)."
473
736
  );
474
- auth.command("login").description("Sign in through the browser, which redirects back to a loopback port on this machine.").option("--timeout <seconds>", "How long to wait for the browser sign-in. Default 600.", (v) => Number(v)).action(async (opts) => {
475
- const user = await session().login(opts.timeout ? { timeoutMs: Math.max(1, opts.timeout) * 1e3 } : void 0);
476
- process.stdout.write(`${user.userName || user.userId}
737
+ auth.command("login").description(
738
+ "Sign in through the browser, which redirects back to a loopback port on this machine. With the browser on another machine, either pass --manual and paste the address the browser ends up on, or fix the port with --port and forward it with `ssh -L <port>:127.0.0.1:<port>`."
739
+ ).option("--timeout <seconds>", "How long to wait for the browser sign-in. Default 600.", (v) => Number(v)).option(
740
+ "--port <n>",
741
+ "Loopback port the sign-in page redirects to, instead of a random one, so an ssh tunnel can be set up beforehand: `ssh -L <n>:127.0.0.1:<n> <this-host>` from the machine with the browser (same port both ends).",
742
+ parsePort
743
+ ).option(
744
+ "--no-browser",
745
+ "Do not open a browser. Print the sign-in URL, then finish the sign-in from the callback address (`http://127.0.0.1:<port>/callback?...`, or just its query) pasted on stdin, or from the loopback port itself, whichever comes first."
746
+ ).option("--manual", "Same as --no-browser.").option("--print-url", "Also write the sign-in URL to stdout, alone on its line, before waiting.").option(
747
+ "--json",
748
+ 'Write JSON lines to stdout: {"loginUrl","port","callbackUrl"} before waiting, then the account ({"userId","userName","site","tokenPath"}) once signed in.'
749
+ ).action(
750
+ async (opts) => {
751
+ const { config, logger } = getRuntime();
752
+ const manual = opts.manual || !opts.browser;
753
+ const s = createHarmonyOsSession2({
754
+ config,
755
+ logger,
756
+ onLoginUrl: (url, { port }) => {
757
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
758
+ if (opts.json) process.stdout.write(`${JSON.stringify({ loginUrl: url, port, callbackUrl })}
477
759
  `);
478
- });
760
+ else if (opts.printUrl) process.stdout.write(`${url}
761
+ `);
762
+ process.stderr.write(manual ? manualInstructions(url, callbackUrl, port) : browserInstructions(url));
763
+ }
764
+ });
765
+ const stdin = manual ? readline.createInterface({ input: process.stdin, terminal: false }) : void 0;
766
+ let user;
767
+ try {
768
+ user = await s.login({
769
+ timeoutMs: opts.timeout ? Math.max(1, opts.timeout) * 1e3 : void 0,
770
+ port: opts.port,
771
+ openBrowser: !manual,
772
+ pastedCallbacks: stdin
773
+ });
774
+ } finally {
775
+ stdin?.close();
776
+ if (stdin) process.stdin.destroy();
777
+ }
778
+ if (opts.json) {
779
+ const status = { userId: user.userId, userName: user.userName, site: user.siteCode, tokenPath: s.tokenPath };
780
+ process.stdout.write(`${JSON.stringify(status)}
781
+ `);
782
+ return;
783
+ }
784
+ process.stdout.write(`${user.userName || user.userId}
785
+ `);
786
+ }
787
+ );
479
788
  auth.command("logout").description("Sign out and delete the stored token.").action(async () => {
480
789
  const signedOut = await session().logout();
481
790
  getRuntime().logger.info(signedOut ? "Signed out." : "Not signed in; nothing to do.");
@@ -518,6 +827,7 @@ function registerAuthCommand(program) {
518
827
  }
519
828
 
520
829
  // src/commands/app.ts
830
+ import * as os2 from "os";
521
831
  import * as path4 from "path";
522
832
  import {
523
833
  installApp,
@@ -528,134 +838,414 @@ import {
528
838
  existingHapOrUnsigned,
529
839
  getBundleName,
530
840
  readHapBundleName,
531
- OniroError as OniroError2
841
+ smokeCheck,
842
+ latestCrash,
843
+ wakeAndUnlock,
844
+ DEFAULT_SMOKE_DELAY_MS,
845
+ OniroError as OniroError3
532
846
  } from "@oniroproject/core";
533
- function registerAppCommand(program) {
534
- const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
535
- app.command("install [project-dir-or-hap]").description(
536
- "Install a built .hap on the connected device/emulator via hdc. Takes a project dir (default: the current one), whose newest signed HAP is installed, else its unsigned one, or a .hap file. Fails when the bundle manager rejects the HAP, and says how to sign when a target refuses an unsigned one."
537
- ).option("--hap <path>", "Override the .hap path (relative to project-dir or absolute).").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
538
- const { config, logger } = getRuntime();
539
- const isHap = target !== void 0 && /\.hap$/i.test(target);
540
- const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
541
- const hapPath = isHap ? path4.resolve(target) : opts.hap;
542
- logger.info(`Installing ${hapPath ?? `app from ${dir}`}...`);
543
- const result = await installApp({ config, projectDir: dir, hapPath, deviceSerial: opts.device, logger });
544
- logger.info(`Installed ${result.hapPath}.`);
545
- });
546
- app.command("launch [project-dir]").description(
547
- "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."
548
- ).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) => {
549
- const { config, logger } = getRuntime();
550
- if (opts.bundle) {
551
- logger.info(`Launching installed bundle ${opts.bundle}...`);
552
- await launchApp({ config, bundleName: opts.bundle, abilityName: opts.ability, deviceSerial: opts.device, logger });
553
- } else {
554
- const dir = path4.resolve(projectDir ?? process.cwd());
555
- logger.info(`Launching app from ${dir}...`);
556
- await launchApp({ config, projectDir: dir, moduleName: opts.module, abilityName: opts.ability, deviceSerial: opts.device, logger });
557
- }
558
- logger.info("App launched.");
559
- });
560
- app.command("apply [project-dir-or-hap]").description(
561
- "Install a HAP (a project dir, default the current one, or a .hap file) and verify the running process took the change. Refuses a HAP of another bundle. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
562
- ).option(
563
- "--bundle <bundle>",
564
- "Bundle name to apply changes to. Defaults to the bundleName in the .hap file, else in the project AppScope/app.json5."
565
- ).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 (target, opts) => {
566
- const { config, logger } = getRuntime();
567
- const isHap = target !== void 0 && /\.hap$/i.test(target);
568
- if (isHap && opts.hap) throw new OniroError2("Pass the HAP either as the argument or with --hap, not both.");
569
- const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
570
- const hapPath = isHap ? path4.resolve(target) : opts.hap;
571
- let bundle = opts.bundle;
572
- if (!bundle && isHap) {
573
- const hap = existingHapOrUnsigned(hapPath);
574
- if (!hap) throw new OniroError2(`HAP not found at ${hapPath}.`);
575
- bundle = await readHapBundleName(hap.path).catch(() => null) ?? void 0;
576
- if (!bundle) throw new OniroError2(`Could not read the bundle name from ${hap.path}. Pass --bundle.`);
577
- }
578
- bundle ??= getBundleName(dir);
579
- const result = await applyChanges({
580
- config,
581
- bundle,
582
- hapPath,
583
- projectDir: dir,
584
- module: opts.module,
585
- installedHapPath: opts.installedHap,
586
- isSystemBundle: opts.system,
587
- allowUninstall: opts.allowUninstall,
588
- deviceSerial: opts.device,
589
- logger
590
- });
591
- if (opts.json) {
592
- process.stdout.write(`${JSON.stringify(result, null, 2)}
593
- `);
594
- } else {
595
- logger.info(
596
- `apply: method=${result.method} pid ${result.preInstallPid ?? "-"} -> ${result.postInstallPid ?? "-"}${result.cacheCleared ? " (cache cleared)" : ""}`
597
- );
847
+
848
+ // src/lib/screenshotImage.ts
849
+ import sharp from "sharp";
850
+ import { compareImages } from "@oniroproject/core";
851
+ function buildGridSvg(width, height) {
852
+ const lineColor = "rgba(255,80,80,0.55)";
853
+ const labelBg = "rgba(0,0,0,0.7)";
854
+ const labelFg = "#fff";
855
+ const fontSize = Math.max(10, Math.round(Math.min(width, height) / 60));
856
+ const padding = Math.round(fontSize * 0.4);
857
+ const labels = [];
858
+ const lines = [];
859
+ for (let i = 0; i <= 10; i++) {
860
+ const f = i / 10;
861
+ const x = Math.round(f * (width - 1));
862
+ const y = Math.round(f * (height - 1));
863
+ if (i > 0 && i < 10) {
864
+ lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="${height}" stroke="${lineColor}" stroke-width="1"/>`);
865
+ lines.push(`<line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${lineColor}" stroke-width="1"/>`);
598
866
  }
599
- });
600
- app.command("uninstall <bundle>").description("Uninstall an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
601
- const { config, logger } = getRuntime();
602
- await uninstallApp({ config, bundle, deviceSerial: opts.device, logger });
603
- logger.info(`Uninstalled ${bundle}.`);
604
- });
605
- app.command("stop <bundle>").description("Force-stop an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
606
- const { config, logger } = getRuntime();
607
- await forceStop({ config, bundle, deviceSerial: opts.device, logger });
608
- logger.info(`Stopped ${bundle}.`);
609
- });
867
+ const tag = f.toFixed(1);
868
+ const tx = i === 0 ? padding : i === 10 ? width - padding : x;
869
+ const anchorX = i === 0 ? "start" : i === 10 ? "end" : "middle";
870
+ labels.push(
871
+ `<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>`
872
+ );
873
+ const ty = i === 0 ? fontSize + padding : i === 10 ? height - padding : y + fontSize / 3;
874
+ labels.push(
875
+ `<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>`
876
+ );
877
+ }
878
+ labels.push(
879
+ `<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>`
880
+ );
881
+ labels.push(
882
+ `<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>`
883
+ );
884
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${lines.join("")}${labels.join("")}</svg>`;
610
885
  }
611
-
612
- // src/commands/create.ts
613
- import * as path6 from "path";
614
- import {
615
- ALL_SDKS as ALL_SDKS2,
616
- createScaffold,
617
- isValidBundleName,
618
- isValidProjectName
619
- } from "@oniroproject/core";
620
-
621
- // src/lib/templateRoot.ts
622
- import * as fs2 from "fs";
623
- import * as path5 from "path";
624
- import { fileURLToPath } from "url";
625
- function getBundledTemplateRoot() {
626
- const here = path5.dirname(fileURLToPath(import.meta.url));
627
- const candidates = [
628
- path5.resolve(here, "../templates"),
629
- // dist/oniro-app.js → ../templates
630
- path5.resolve(here, "../../templates")
631
- // src/bin/oniro-app.ts → ../../templates
632
- ];
633
- for (const c of candidates) {
634
- if (fs2.existsSync(c)) return c;
886
+ async function renderWithGrid(jpeg2, opts) {
887
+ const meta = await sharp(jpeg2).metadata();
888
+ const originalWidth = meta.width ?? 0;
889
+ const originalHeight = meta.height ?? 0;
890
+ const longest = Math.max(originalWidth, originalHeight) || 1;
891
+ const scale = Math.min(1, opts.maxDim / longest);
892
+ const width = Math.max(1, Math.round(originalWidth * scale));
893
+ const height = Math.max(1, Math.round(originalHeight * scale));
894
+ const resized = await sharp(jpeg2).resize(width, height).jpeg({ quality: 80 }).toBuffer();
895
+ if (!opts.grid) {
896
+ return { buf: resized, width, height, originalWidth, originalHeight };
635
897
  }
636
- return candidates[0];
898
+ const svg = buildGridSvg(width, height);
899
+ const buf = await sharp(resized).composite([{ input: Buffer.from(svg) }]).jpeg({ quality: 80 }).toBuffer();
900
+ return { buf, width, height, originalWidth, originalHeight };
637
901
  }
638
-
639
- // src/commands/create.ts
640
- function registerCreateCommand(program) {
641
- program.command("create").description("Scaffold a new Oniro/OpenHarmony or HarmonyOS app from a template. All flags are required (non-interactive).").requiredOption("--name <name>", "Project folder name (letters, digits, ._- only)").requiredOption("--bundle <bundleName>", "Bundle name in reverse-DNS form, e.g. com.example.myapp").requiredOption("--location <dir>", "Parent directory where the new project folder will be created").requiredOption("--sdk <api>", "Target SDK API level (e.g. 18, 20)").option("--template <id>", "Template id", "EmptyAbility").option("--module <name>", "Module folder name (defaults to the template default, usually `entry`)").option("--overwrite", "Replace the destination if it already exists").action(async (opts) => {
642
- const { config, logger } = getRuntime();
643
- if (!isValidProjectName(opts.name)) {
644
- throw new Error(`Invalid --name '${opts.name}'. Use letters/numbers/._- and no slashes.`);
645
- }
646
- if (!isValidBundleName(opts.bundle)) {
647
- throw new Error(`Invalid --bundle '${opts.bundle}'. Example: com.example.myapp`);
648
- }
649
- const sdkApi = Number(opts.sdk);
650
- if (!Number.isFinite(sdkApi)) {
651
- throw new Error(`--sdk must be a numeric api level. Known: ${ALL_SDKS2.map((s) => s.api).join(", ")}`);
652
- }
653
- const templateRoot = getBundledTemplateRoot();
654
- const result = await createScaffold({
655
- config,
656
- templateId: opts.template,
657
- projectName: opts.name,
658
- bundleName: opts.bundle,
902
+ function parseCropRect(spec) {
903
+ const parts = spec.split(",").map((s) => Number(s.trim()));
904
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) {
905
+ throw new Error(`--crop expects four 0-1 fractions "x1,y1,x2,y2"; got "${spec}".`);
906
+ }
907
+ const [x1, y1, x2, y2] = parts;
908
+ if (!(x2 > x1 && y2 > y1)) throw new Error(`--crop needs x1 < x2 and y1 < y2; got "${spec}".`);
909
+ return [x1, y1, x2, y2];
910
+ }
911
+ function padCropRect(r, pad) {
912
+ return [Math.max(0, r[0] - pad), Math.max(0, r[1] - pad), Math.min(1, r[2] + pad), Math.min(1, r[3] + pad)];
913
+ }
914
+ async function cropImage(jpeg2, rect, opts) {
915
+ const meta = await sharp(jpeg2).metadata();
916
+ const w = meta.width ?? 1;
917
+ const h = meta.height ?? 1;
918
+ const left = Math.min(w - 1, Math.floor(rect[0] * w));
919
+ const top = Math.min(h - 1, Math.floor(rect[1] * h));
920
+ const cropWidth = Math.max(1, Math.min(w, Math.ceil(rect[2] * w)) - left);
921
+ const cropHeight = Math.max(1, Math.min(h, Math.ceil(rect[3] * h)) - top);
922
+ const scale = Math.min(1, opts.maxDim / Math.max(cropWidth, cropHeight));
923
+ const width = Math.max(1, Math.round(cropWidth * scale));
924
+ const height = Math.max(1, Math.round(cropHeight * scale));
925
+ const buf = await sharp(jpeg2).extract({ left, top, width: cropWidth, height: cropHeight }).resize(width, height).jpeg({ quality: 85 }).toBuffer();
926
+ return { buf, width, height, left, top, cropWidth, cropHeight };
927
+ }
928
+ function buildTileLabelSvg(width, height, idx, diff) {
929
+ const fontSize = Math.max(12, Math.round(Math.min(width, height) / 16));
930
+ const pad = Math.round(fontSize * 0.4);
931
+ let label = `#${idx}`;
932
+ if (typeof diff === "number") label += ` \u0394${diff.toFixed(2)}`;
933
+ const boxW = Math.round(label.length * fontSize * 0.62) + pad * 2;
934
+ const boxH = fontSize + pad * 2;
935
+ 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>`;
936
+ }
937
+ async function buildContactSheet(frames, opts) {
938
+ if (frames.length === 0) throw new Error("buildContactSheet: no frames");
939
+ const count = frames.length;
940
+ const cols = Math.max(1, opts.cols ?? Math.ceil(Math.sqrt(count)));
941
+ const rows = Math.ceil(count / cols);
942
+ const gap = opts.gap ?? 8;
943
+ const meta0 = await sharp(frames[0]).metadata();
944
+ const fw = meta0.width ?? 1;
945
+ const fh = meta0.height ?? 1;
946
+ const longest = Math.max(fw, fh) || 1;
947
+ const tileScale = Math.min(1, opts.maxDim / cols / longest);
948
+ const tileWidth = Math.max(1, Math.round(fw * tileScale));
949
+ const tileHeight = Math.max(1, Math.round(fh * tileScale));
950
+ const tiles = await Promise.all(
951
+ frames.map((frame, i) => {
952
+ const label = Buffer.from(buildTileLabelSvg(tileWidth, tileHeight, i, opts.diffs?.[i]));
953
+ return sharp(frame).resize(tileWidth, tileHeight, { fit: "fill" }).composite([{ input: label, top: 0, left: 0 }]).png().toBuffer();
954
+ })
955
+ );
956
+ const width = cols * tileWidth + (cols + 1) * gap;
957
+ const height = rows * tileHeight + (rows + 1) * gap;
958
+ const placements = tiles.map((input2, i) => {
959
+ const r = Math.floor(i / cols);
960
+ const c = i % cols;
961
+ return { input: input2, left: gap + c * (tileWidth + gap), top: gap + r * (tileHeight + gap) };
962
+ });
963
+ const buf = await sharp({
964
+ create: { width, height, channels: 3, background: { r: 20, g: 20, b: 20 } }
965
+ }).composite(placements).jpeg({ quality: 80 }).toBuffer();
966
+ return { buf, width, height, cols, rows, tileWidth, tileHeight, count };
967
+ }
968
+ async function frameDiffs(frames, opts = {}) {
969
+ if (frames.length === 0) return [];
970
+ const sample = opts.sample ?? 32;
971
+ const thumbs = await Promise.all(
972
+ frames.map((f) => sharp(f).greyscale().resize(sample, sample, { fit: "fill" }).raw().toBuffer())
973
+ );
974
+ const out = [0];
975
+ for (let i = 1; i < thumbs.length; i++) {
976
+ const a = thumbs[i - 1];
977
+ const b = thumbs[i];
978
+ if (!a || !b) {
979
+ out.push(0);
980
+ continue;
981
+ }
982
+ const n = Math.min(a.length, b.length) || 1;
983
+ let s = 0;
984
+ for (let k = 0; k < n; k++) s += Math.abs((a[k] ?? 0) - (b[k] ?? 0));
985
+ out.push(+(s / n / 255).toFixed(4));
986
+ }
987
+ return out;
988
+ }
989
+ async function decodeGreyscale(jpeg2, maxDim = 512) {
990
+ const { data, info } = await sharp(jpeg2).greyscale().resize(maxDim, maxDim, { fit: "inside", withoutEnlargement: true }).raw().toBuffer({ resolveWithObject: true });
991
+ const n = info.width * info.height;
992
+ const grey = new Uint8Array(n);
993
+ for (let i = 0; i < n; i++) grey[i] = data[i * info.channels];
994
+ return { width: info.width, height: info.height, data: grey };
995
+ }
996
+ async function diffScreenshots(baseline, current) {
997
+ const [a, b] = await Promise.all([decodeGreyscale(baseline), decodeGreyscale(current)]);
998
+ const [, meanDiff] = await frameDiffs([baseline, current]);
999
+ return { ...compareImages(a, b), meanDiff: meanDiff ?? 0 };
1000
+ }
1001
+
1002
+ // src/commands/appVerify.ts
1003
+ import { formatHapVerification, verifyHap, verifyHapExitCode } from "@oniroproject/core";
1004
+ function registerAppVerifyCommand(app) {
1005
+ app.command("verify <hap>").description(
1006
+ "Verify a HAP's signature with the SDK's hap-sign-tool (`verify-app`), then report what signed it: the certificate chain and its root, whether it is OpenHarmony development material or a HarmonyOS certificate from AppGallery Connect (root Huawei CBG Root CA G2), and the provisioning profile: type, bundle name, apl, app-feature, devices, validity and days left. Exits 0 when valid, 1 when unsigned, invalid, expired or mismatched, 2 when it could not verify (no java or hap-sign-tool.jar). Needs java."
1007
+ ).option("--json", "Emit the result as JSON.").action(async (hap, opts) => {
1008
+ const { config, logger } = getRuntime();
1009
+ const result = await verifyHap({ hapPath: hap, config, logger });
1010
+ process.stdout.write(opts.json ? `${JSON.stringify(result, null, 2)}
1011
+ ` : formatHapVerification(result));
1012
+ process.exitCode = verifyHapExitCode(result);
1013
+ });
1014
+ }
1015
+
1016
+ // src/commands/app.ts
1017
+ function addSmokeOptions(cmd) {
1018
+ return cmd.option(
1019
+ "--smoke",
1020
+ "After launch, wait for the process, take a screenshot after --smoke-delay, and fail (exit 1) when the process died or the screen is blank."
1021
+ ).option("--smoke-delay <ms>", "With --smoke: wait between the process appearing and the screenshot.", String(DEFAULT_SMOKE_DELAY_MS)).option("--smoke-screenshot <file>", "With --smoke: where to write the screenshot (default: a file in the system temp dir).");
1022
+ }
1023
+ function smokeDelayFrom(raw) {
1024
+ const n = Number(raw);
1025
+ if (!Number.isFinite(n) || n < 0) throw new OniroError3(`--smoke-delay expects a number of milliseconds; got "${raw}".`);
1026
+ return Math.round(n);
1027
+ }
1028
+ async function wakeForSmoke(config, logger, device) {
1029
+ const screen = await wakeAndUnlock({ config, deviceSerial: device, logger }).catch((err) => {
1030
+ logger.warn(`Could not wake the screen: ${err.message}`);
1031
+ return null;
1032
+ });
1033
+ if (screen?.locked) logger.warn("The lock screen asks for a credential; the smoke screenshot will show it.");
1034
+ }
1035
+ var CRASH_CLOCK_SLACK_MS = 2e3;
1036
+ var CRASH_WAIT_MS = 15e3;
1037
+ function runSmoke(config, logger, bundle, since, opts) {
1038
+ const screenshotPath = opts.smokeScreenshot ? path4.resolve(opts.smokeScreenshot) : path4.join(os2.tmpdir(), `oniro-smoke-${bundle}-${since.getTime()}.jpeg`);
1039
+ return smokeCheck({
1040
+ config,
1041
+ bundle,
1042
+ since,
1043
+ delayMs: smokeDelayFrom(opts.smokeDelay),
1044
+ screenshotPath,
1045
+ decodeGreyscale: (jpeg2) => decodeGreyscale(jpeg2),
1046
+ crashProbe: (probe) => latestCrash({
1047
+ config: probe.config,
1048
+ bundle: probe.bundle,
1049
+ // Faultlog times are the device's clock, which need not agree with the host's:
1050
+ // ask for "as long ago as the launch" on the device's clock, plus a little slack.
1051
+ since: (Date.now() - probe.since.getTime() + CRASH_CLOCK_SLACK_MS) / 1e3,
1052
+ // hiview writes the faultlog seconds after a death; a live app is looked up once.
1053
+ waitMs: probe.alive ? 0 : CRASH_WAIT_MS,
1054
+ deviceSerial: probe.deviceSerial,
1055
+ logger: probe.logger
1056
+ }),
1057
+ deviceSerial: opts.device,
1058
+ logger
1059
+ });
1060
+ }
1061
+ function formatSmoke(s) {
1062
+ const parts = [
1063
+ s.pid === null ? "no process" : `pid ${s.pid} ${s.alive ? "alive" : "died"}`,
1064
+ s.blankScreen === null ? "blank check skipped" : `${s.blankScreen ? "blank" : "not blank"} (score ${s.blankScore})`
1065
+ ];
1066
+ if (s.crash) {
1067
+ const where = s.crash.file ? ` at ${s.crash.file}:${s.crash.line ?? "?"}` : "";
1068
+ parts.push(`${s.crash.kind ?? "crash"}${s.crash.reason ? ` (${s.crash.reason})` : ""}${where}`);
1069
+ }
1070
+ if (s.screenshot) parts.push(`screenshot ${s.screenshot}`);
1071
+ return `smoke: ${s.passed ? "passed" : `FAILED (${s.failures.join("; ")})`}: ${parts.join(", ")}`;
1072
+ }
1073
+ async function launchWithSmoke(rt2, p) {
1074
+ const { config, logger } = rt2;
1075
+ const smokeDelay = p.smokeDelay ?? DEFAULT_SMOKE_DELAY_MS;
1076
+ if (p.smoke) {
1077
+ smokeDelayFrom(smokeDelay);
1078
+ await wakeForSmoke(config, logger, p.device);
1079
+ }
1080
+ const since = /* @__PURE__ */ new Date();
1081
+ let bundle;
1082
+ if (p.bundle) {
1083
+ bundle = p.bundle;
1084
+ logger.info(`Launching installed bundle ${p.bundle}...`);
1085
+ await launchApp({ config, bundleName: p.bundle, abilityName: p.ability, deviceSerial: p.device, logger });
1086
+ } else {
1087
+ const dir = path4.resolve(p.projectDir ?? process.cwd());
1088
+ bundle = getBundleName(dir);
1089
+ logger.info(`Launching app from ${dir}...`);
1090
+ await launchApp({
1091
+ config,
1092
+ projectDir: dir,
1093
+ moduleName: p.module ?? "entry",
1094
+ abilityName: p.ability,
1095
+ deviceSerial: p.device,
1096
+ logger
1097
+ });
1098
+ }
1099
+ logger.info("App launched.");
1100
+ if (!p.smoke) return { bundle };
1101
+ const smoke = await runSmoke(config, logger, bundle, since, { ...p, smokeDelay });
1102
+ return { bundle, smoke };
1103
+ }
1104
+ async function applyWithSmoke(rt2, p) {
1105
+ const { config, logger } = rt2;
1106
+ const smokeDelay = p.smokeDelay ?? DEFAULT_SMOKE_DELAY_MS;
1107
+ if (p.smoke) smokeDelayFrom(smokeDelay);
1108
+ const target = p.target;
1109
+ const isHap = target !== void 0 && /\.hap$/i.test(target);
1110
+ if (isHap && p.hap) throw new OniroError3("Pass the HAP either as the argument or with --hap, not both.");
1111
+ const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
1112
+ const hapPath = isHap ? path4.resolve(target) : p.hap;
1113
+ let bundle = p.bundle;
1114
+ if (!bundle && isHap) {
1115
+ const hap = existingHapOrUnsigned(hapPath);
1116
+ if (!hap) throw new OniroError3(`HAP not found at ${hapPath}.`);
1117
+ bundle = await readHapBundleName(hap.path).catch(() => null) ?? void 0;
1118
+ if (!bundle) throw new OniroError3(`Could not read the bundle name from ${hap.path}. Pass --bundle.`);
1119
+ }
1120
+ bundle ??= getBundleName(dir);
1121
+ const result = await applyChanges({
1122
+ config,
1123
+ bundle,
1124
+ hapPath,
1125
+ projectDir: dir,
1126
+ module: p.module,
1127
+ installedHapPath: p.installedHap,
1128
+ isSystemBundle: p.system,
1129
+ allowUninstall: p.allowUninstall,
1130
+ deviceSerial: p.device,
1131
+ logger
1132
+ });
1133
+ if (!p.smoke) return result;
1134
+ await wakeForSmoke(config, logger, p.device);
1135
+ const since = /* @__PURE__ */ new Date();
1136
+ logger.info(`Launching ${bundle} for the smoke check...`);
1137
+ await launchApp({ config, bundleName: bundle, deviceSerial: p.device, logger });
1138
+ const smoke = await runSmoke(config, logger, bundle, since, { ...p, smokeDelay });
1139
+ return { ...result, smoke };
1140
+ }
1141
+ function registerAppCommand(program) {
1142
+ const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
1143
+ app.command("install [project-dir-or-hap]").description(
1144
+ "Install a built .hap on the connected device/emulator via hdc. Takes a project dir (default: the current one), whose newest signed HAP is installed, else its unsigned one, or a .hap file. Fails when the bundle manager rejects the HAP, and says how to sign when a target refuses an unsigned one."
1145
+ ).option("--hap <path>", "Override the .hap path (relative to project-dir or absolute).").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
1146
+ const { config, logger } = getRuntime();
1147
+ const isHap = target !== void 0 && /\.hap$/i.test(target);
1148
+ const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
1149
+ const hapPath = isHap ? path4.resolve(target) : opts.hap;
1150
+ logger.info(`Installing ${hapPath ?? `app from ${dir}`}...`);
1151
+ const result = await installApp({ config, projectDir: dir, hapPath, deviceSerial: opts.device, logger });
1152
+ logger.info(`Installed ${result.hapPath}.`);
1153
+ });
1154
+ const launch2 = app.command("launch [project-dir]").description(
1155
+ "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."
1156
+ ).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.");
1157
+ addSmokeOptions(launch2).option("--json", "With --smoke: emit `{ bundle, smoke }` as JSON.").action(async (projectDir, opts) => {
1158
+ const { config, logger } = getRuntime();
1159
+ const { bundle, smoke } = await launchWithSmoke({ config, logger }, { ...opts, projectDir });
1160
+ if (!smoke) return;
1161
+ if (opts.json) process.stdout.write(`${JSON.stringify({ bundle, smoke }, null, 2)}
1162
+ `);
1163
+ else process.stdout.write(`${formatSmoke(smoke)}
1164
+ `);
1165
+ if (!smoke.passed) process.exitCode = 1;
1166
+ });
1167
+ const apply2 = app.command("apply [project-dir-or-hap]").description(
1168
+ "Install a HAP (a project dir, default the current one, or a .hap file) and verify the running process took the change. Refuses a HAP of another bundle. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
1169
+ ).option(
1170
+ "--bundle <bundle>",
1171
+ "Bundle name to apply changes to. Defaults to the bundleName in the .hap file, else in the project AppScope/app.json5."
1172
+ ).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 (with --smoke, plus a `smoke` object).");
1173
+ addSmokeOptions(apply2).action(async (target, opts) => {
1174
+ const { config, logger } = getRuntime();
1175
+ const report = await applyWithSmoke({ config, logger }, { ...opts, target });
1176
+ const { smoke, ...result } = report;
1177
+ if (opts.json) {
1178
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
1179
+ `);
1180
+ } else {
1181
+ logger.info(
1182
+ `apply: method=${result.method} pid ${result.preInstallPid ?? "-"} -> ${result.postInstallPid ?? "-"}${result.cacheCleared ? " (cache cleared)" : ""}`
1183
+ );
1184
+ if (smoke) process.stdout.write(`${formatSmoke(smoke)}
1185
+ `);
1186
+ }
1187
+ if (smoke && !smoke.passed) process.exitCode = 1;
1188
+ });
1189
+ app.command("uninstall <bundle>").description("Uninstall an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
1190
+ const { config, logger } = getRuntime();
1191
+ await uninstallApp({ config, bundle, deviceSerial: opts.device, logger });
1192
+ logger.info(`Uninstalled ${bundle}.`);
1193
+ });
1194
+ app.command("stop <bundle>").description("Force-stop an app by bundle name.").option("--device <serial>", "Target device serial.").action(async (bundle, opts) => {
1195
+ const { config, logger } = getRuntime();
1196
+ await forceStop({ config, bundle, deviceSerial: opts.device, logger });
1197
+ logger.info(`Stopped ${bundle}.`);
1198
+ });
1199
+ registerAppVerifyCommand(app);
1200
+ }
1201
+
1202
+ // src/commands/create.ts
1203
+ import * as path6 from "path";
1204
+ import {
1205
+ ALL_SDKS as ALL_SDKS2,
1206
+ createScaffold,
1207
+ isValidBundleName,
1208
+ isValidProjectName
1209
+ } from "@oniroproject/core";
1210
+
1211
+ // src/lib/templateRoot.ts
1212
+ import * as fs2 from "fs";
1213
+ import * as path5 from "path";
1214
+ import { fileURLToPath } from "url";
1215
+ function getBundledTemplateRoot() {
1216
+ const here = path5.dirname(fileURLToPath(import.meta.url));
1217
+ const candidates = [
1218
+ path5.resolve(here, "../templates"),
1219
+ // dist/oniro-app.js → ../templates
1220
+ path5.resolve(here, "../../templates")
1221
+ // src/bin/oniro-app.ts → ../../templates
1222
+ ];
1223
+ for (const c of candidates) {
1224
+ if (fs2.existsSync(c)) return c;
1225
+ }
1226
+ return candidates[0];
1227
+ }
1228
+
1229
+ // src/commands/create.ts
1230
+ function registerCreateCommand(program) {
1231
+ program.command("create").description("Scaffold a new Oniro/OpenHarmony or HarmonyOS app from a template. All flags are required (non-interactive).").requiredOption("--name <name>", "Project folder name (letters, digits, ._- only)").requiredOption("--bundle <bundleName>", "Bundle name in reverse-DNS form, e.g. com.example.myapp").requiredOption("--location <dir>", "Parent directory where the new project folder will be created").requiredOption("--sdk <api>", "Target SDK API level (e.g. 18, 20)").option("--template <id>", "Template id", "EmptyAbility").option("--module <name>", "Module folder name (defaults to the template default, usually `entry`)").option("--overwrite", "Replace the destination if it already exists").action(async (opts) => {
1232
+ const { config, logger } = getRuntime();
1233
+ if (!isValidProjectName(opts.name)) {
1234
+ throw new Error(`Invalid --name '${opts.name}'. Use letters/numbers/._- and no slashes.`);
1235
+ }
1236
+ if (!isValidBundleName(opts.bundle)) {
1237
+ throw new Error(`Invalid --bundle '${opts.bundle}'. Example: com.example.myapp`);
1238
+ }
1239
+ const sdkApi = Number(opts.sdk);
1240
+ if (!Number.isFinite(sdkApi)) {
1241
+ throw new Error(`--sdk must be a numeric api level. Known: ${ALL_SDKS2.map((s) => s.api).join(", ")}`);
1242
+ }
1243
+ const templateRoot = getBundledTemplateRoot();
1244
+ const result = await createScaffold({
1245
+ config,
1246
+ templateId: opts.template,
1247
+ projectName: opts.name,
1248
+ bundleName: opts.bundle,
659
1249
  location: path6.resolve(opts.location),
660
1250
  sdkApi,
661
1251
  moduleName: opts.module,
@@ -675,17 +1265,17 @@ function registerTemplatesCommand(program) {
675
1265
  const templates = program.command("templates").description("Inspect the templates bundled with this CLI.");
676
1266
  templates.command("list").description("List available project templates.").option("--json", "Emit machine-readable JSON.").action((opts) => {
677
1267
  const root = getBundledTemplateRoot();
678
- const list = listTemplates(root);
1268
+ const list2 = listTemplates(root);
679
1269
  if (opts.json) {
680
- process.stdout.write(`${JSON.stringify(list, null, 2)}
1270
+ process.stdout.write(`${JSON.stringify(list2, null, 2)}
681
1271
  `);
682
1272
  return;
683
1273
  }
684
- if (list.length === 0) {
1274
+ if (list2.length === 0) {
685
1275
  process.stdout.write("No templates available.\n");
686
1276
  return;
687
1277
  }
688
- const rows = list.map((t) => `${t.id.padEnd(20)} ${t.label.padEnd(24)} ${t.description}`);
1278
+ const rows = list2.map((t) => `${t.id.padEnd(20)} ${t.label.padEnd(24)} ${t.description}`);
689
1279
  process.stdout.write(`${rows.join("\n")}
690
1280
  `);
691
1281
  });
@@ -718,6 +1308,9 @@ function registerRebootCommand(program) {
718
1308
  import { waitForLog, waitForBoot, waitForBundle, waitForTarget, describeLayoutNode } from "@oniroproject/core";
719
1309
 
720
1310
  // src/lib/target.ts
1311
+ import { parseSelector } from "@oniroproject/core";
1312
+ var SELECTOR_HELP = `An attribute selector such as "type=Button,text*=Sign" (keys text, desc, hint, id, key, type, enabled, focused, checked, selected, visible, clickable, longClickable, checkable, scrollable; operators = *= ^= $= /=regex; a [n] suffix picks the n-th match, 0-based, [-1] or [last] the last), or "xpath=//Button[@text='OK']".`;
1313
+ var WINDOW_HELP = "Restrict to one window: a window id from `dump layout --flat` ([window <id> \u2026]) or `dump windows`.";
721
1314
  function addTargetOptions(cmd, verb, opts = {}) {
722
1315
  cmd.option(
723
1316
  "--target <text>",
@@ -726,10 +1319,20 @@ function addTargetOptions(cmd, verb, opts = {}) {
726
1319
  if (opts.index !== false) {
727
1320
  cmd.option("--target-index <n>", "Pick the n-th (0-based) of several matching elements.");
728
1321
  }
1322
+ cmd.option("--selector <expr>", `Instead of --target*: ${SELECTOR_HELP}`).option("--window <id>", WINDOW_HELP);
729
1323
  return cmd;
730
1324
  }
1325
+ function parseSelectorOption(raw) {
1326
+ return raw === void 0 ? void 0 : parseSelector(raw);
1327
+ }
731
1328
  function selectorFrom(opts) {
732
- if (opts.target === void 0 && opts.targetId === void 0 && opts.targetType === void 0) return void 0;
1329
+ const plain = opts.target !== void 0 || opts.targetId !== void 0 || opts.targetType !== void 0;
1330
+ if (opts.selector !== void 0) {
1331
+ if (plain) throw new Error("Use either --selector or --target / --target-id / --target-type, not both.");
1332
+ if (opts.targetIndex !== void 0) throw new Error("--target-index does not apply to --selector; append [n] to the selector.");
1333
+ return parseSelector(opts.selector);
1334
+ }
1335
+ if (!plain) return void 0;
733
1336
  return { text: opts.target, id: opts.targetId, type: opts.targetType };
734
1337
  }
735
1338
  function targetIndexFrom(opts) {
@@ -738,6 +1341,12 @@ function targetIndexFrom(opts) {
738
1341
  if (!Number.isInteger(n) || n < 0) throw new Error(`--target-index must be a non-negative integer; got ${opts.targetIndex}.`);
739
1342
  return n;
740
1343
  }
1344
+ function windowIdFrom(opts) {
1345
+ if (opts.window === void 0) return void 0;
1346
+ const n = Number(opts.window);
1347
+ if (!Number.isInteger(n) || n < 0) throw new Error(`--window must be a window id (a non-negative integer); got ${opts.window}.`);
1348
+ return n;
1349
+ }
741
1350
 
742
1351
  // src/lib/logSince.ts
743
1352
  function parseSince(value) {
@@ -752,18 +1361,20 @@ function parseSince(value) {
752
1361
 
753
1362
  // src/commands/wait.ts
754
1363
  function registerWaitCommand(program) {
755
- const wait = program.command("wait").description(
756
- "Wait for a device condition: a log line (--log), boot (--boot), a bundle to start (--bundle), or an on-screen element (--target / --target-id / --target-type)."
1364
+ const wait2 = program.command("wait").description(
1365
+ "Wait for a device condition: a log line (--log), boot (--boot), a bundle to start (--bundle), or an on-screen element (--target / --target-id / --target-type, or --selector)."
757
1366
  ).option("--log <pattern>", "Resolve when a hilog line (tag: message) logged after the command starts matches this regex.").option(
758
1367
  "--since <seconds>",
759
1368
  'With --log: also match lines logged up to N seconds before the command started, measured on the device clock; "all" matches everything still in the device log buffer.'
760
1369
  ).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).");
761
- addTargetOptions(wait, "Resolve when", { index: false });
762
- wait.option("--gone", "With --target*: resolve when no uncovered element matches any more.").option("--interval <ms>", "With --target*: poll interval, ms (each poll is a layout dump).", "500").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log or --target*: emit the matched entry/element as JSON.").action(async (opts) => {
1370
+ addTargetOptions(wait2, "Resolve when", { index: false });
1371
+ wait2.option("--gone", "With --target* or --selector: resolve when no uncovered element matches any more.").option("--interval <ms>", "With --target* or --selector: poll interval, ms (each poll is a layout dump).", "500").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log, --target* or --selector: emit the matched entry/element as JSON.").action(async (opts) => {
763
1372
  const { config, logger } = getRuntime();
764
1373
  const timeoutMs = Number(opts.timeout);
765
1374
  const selector = selectorFrom(opts);
1375
+ const windowId = windowIdFrom(opts);
766
1376
  if (opts.since !== void 0 && !opts.log) throw new Error("--since applies only to --log.");
1377
+ if (windowId !== void 0 && !selector) throw new Error("--window applies only to --target* and --selector.");
767
1378
  if (opts.log) {
768
1379
  const entry = await waitForLog({
769
1380
  config,
@@ -790,6 +1401,7 @@ function registerWaitCommand(program) {
790
1401
  config,
791
1402
  selector,
792
1403
  gone: opts.gone,
1404
+ windowId,
793
1405
  timeoutMs,
794
1406
  pollMs: Number(opts.interval) || 500,
795
1407
  deviceSerial: opts.device,
@@ -809,7 +1421,9 @@ function registerWaitCommand(program) {
809
1421
  logger.info(`Bundle ${opts.bundle} is running.`);
810
1422
  return;
811
1423
  }
812
- throw new Error("Specify one of --log <pattern>, --boot, --bundle <bundle>, or --target <text> (or --target-id / --target-type).");
1424
+ throw new Error(
1425
+ "Specify one of --log <pattern>, --boot, --bundle <bundle>, --target <text> (or --target-id / --target-type), or --selector <expr>."
1426
+ );
813
1427
  });
814
1428
  }
815
1429
 
@@ -846,171 +1460,62 @@ function registerWatchCommand(program) {
846
1460
  // src/commands/screenshot.ts
847
1461
  import * as fs3 from "fs";
848
1462
  import * as path7 from "path";
849
- import { takeScreenshot, captureBurst, locateTarget, describeLayoutNode as describeLayoutNode2 } from "@oniroproject/core";
850
-
851
- // src/lib/screenshotImage.ts
852
- import sharp from "sharp";
853
- function buildGridSvg(width, height) {
854
- const lineColor = "rgba(255,80,80,0.55)";
855
- const labelBg = "rgba(0,0,0,0.7)";
856
- const labelFg = "#fff";
857
- const fontSize = Math.max(10, Math.round(Math.min(width, height) / 60));
858
- const padding = Math.round(fontSize * 0.4);
859
- const labels = [];
860
- const lines = [];
861
- for (let i = 0; i <= 10; i++) {
862
- const f = i / 10;
863
- const x = Math.round(f * (width - 1));
864
- const y = Math.round(f * (height - 1));
865
- if (i > 0 && i < 10) {
866
- lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="${height}" stroke="${lineColor}" stroke-width="1"/>`);
867
- lines.push(`<line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${lineColor}" stroke-width="1"/>`);
868
- }
869
- const tag = f.toFixed(1);
870
- const tx = i === 0 ? padding : i === 10 ? width - padding : x;
871
- const anchorX = i === 0 ? "start" : i === 10 ? "end" : "middle";
872
- labels.push(
873
- `<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>`
874
- );
875
- const ty = i === 0 ? fontSize + padding : i === 10 ? height - padding : y + fontSize / 3;
876
- labels.push(
877
- `<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>`
878
- );
879
- }
880
- labels.push(
881
- `<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>`
882
- );
883
- labels.push(
884
- `<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>`
885
- );
886
- return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${lines.join("")}${labels.join("")}</svg>`;
887
- }
888
- async function renderWithGrid(jpeg, opts) {
889
- const meta = await sharp(jpeg).metadata();
890
- const originalWidth = meta.width ?? 0;
891
- const originalHeight = meta.height ?? 0;
892
- const longest = Math.max(originalWidth, originalHeight) || 1;
893
- const scale = Math.min(1, opts.maxDim / longest);
894
- const width = Math.max(1, Math.round(originalWidth * scale));
895
- const height = Math.max(1, Math.round(originalHeight * scale));
896
- const resized = await sharp(jpeg).resize(width, height).jpeg({ quality: 80 }).toBuffer();
897
- if (!opts.grid) {
898
- return { buf: resized, width, height, originalWidth, originalHeight };
899
- }
900
- const svg = buildGridSvg(width, height);
901
- const buf = await sharp(resized).composite([{ input: Buffer.from(svg) }]).jpeg({ quality: 80 }).toBuffer();
902
- return { buf, width, height, originalWidth, originalHeight };
903
- }
904
- function parseCropRect(spec) {
905
- const parts = spec.split(",").map((s) => Number(s.trim()));
906
- if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) {
907
- throw new Error(`--crop expects four 0-1 fractions "x1,y1,x2,y2"; got "${spec}".`);
908
- }
909
- const [x1, y1, x2, y2] = parts;
910
- if (!(x2 > x1 && y2 > y1)) throw new Error(`--crop needs x1 < x2 and y1 < y2; got "${spec}".`);
911
- return [x1, y1, x2, y2];
912
- }
913
- function padCropRect(r, pad) {
914
- return [Math.max(0, r[0] - pad), Math.max(0, r[1] - pad), Math.min(1, r[2] + pad), Math.min(1, r[3] + pad)];
915
- }
916
- async function cropImage(jpeg, rect, opts) {
917
- const meta = await sharp(jpeg).metadata();
918
- const w = meta.width ?? 1;
919
- const h = meta.height ?? 1;
920
- const left = Math.min(w - 1, Math.floor(rect[0] * w));
921
- const top = Math.min(h - 1, Math.floor(rect[1] * h));
922
- const cropWidth = Math.max(1, Math.min(w, Math.ceil(rect[2] * w)) - left);
923
- const cropHeight = Math.max(1, Math.min(h, Math.ceil(rect[3] * h)) - top);
924
- const scale = Math.min(1, opts.maxDim / Math.max(cropWidth, cropHeight));
925
- const width = Math.max(1, Math.round(cropWidth * scale));
926
- const height = Math.max(1, Math.round(cropHeight * scale));
927
- const buf = await sharp(jpeg).extract({ left, top, width: cropWidth, height: cropHeight }).resize(width, height).jpeg({ quality: 85 }).toBuffer();
928
- return { buf, width, height, left, top, cropWidth, cropHeight };
929
- }
930
- function buildTileLabelSvg(width, height, idx, diff) {
931
- const fontSize = Math.max(12, Math.round(Math.min(width, height) / 16));
932
- const pad = Math.round(fontSize * 0.4);
933
- let label = `#${idx}`;
934
- if (typeof diff === "number") label += ` \u0394${diff.toFixed(2)}`;
935
- const boxW = Math.round(label.length * fontSize * 0.62) + pad * 2;
936
- const boxH = fontSize + pad * 2;
937
- 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>`;
938
- }
939
- async function buildContactSheet(frames, opts) {
940
- if (frames.length === 0) throw new Error("buildContactSheet: no frames");
941
- const count = frames.length;
942
- const cols = Math.max(1, opts.cols ?? Math.ceil(Math.sqrt(count)));
943
- const rows = Math.ceil(count / cols);
944
- const gap = opts.gap ?? 8;
945
- const meta0 = await sharp(frames[0]).metadata();
946
- const fw = meta0.width ?? 1;
947
- const fh = meta0.height ?? 1;
948
- const longest = Math.max(fw, fh) || 1;
949
- const tileScale = Math.min(1, opts.maxDim / cols / longest);
950
- const tileWidth = Math.max(1, Math.round(fw * tileScale));
951
- const tileHeight = Math.max(1, Math.round(fh * tileScale));
952
- const tiles = await Promise.all(
953
- frames.map((frame, i) => {
954
- const label = Buffer.from(buildTileLabelSvg(tileWidth, tileHeight, i, opts.diffs?.[i]));
955
- return sharp(frame).resize(tileWidth, tileHeight, { fit: "fill" }).composite([{ input: label, top: 0, left: 0 }]).png().toBuffer();
956
- })
957
- );
958
- const width = cols * tileWidth + (cols + 1) * gap;
959
- const height = rows * tileHeight + (rows + 1) * gap;
960
- const placements = tiles.map((input, i) => {
961
- const r = Math.floor(i / cols);
962
- const c = i % cols;
963
- return { input, left: gap + c * (tileWidth + gap), top: gap + r * (tileHeight + gap) };
964
- });
965
- const buf = await sharp({
966
- create: { width, height, channels: 3, background: { r: 20, g: 20, b: 20 } }
967
- }).composite(placements).jpeg({ quality: 80 }).toBuffer();
968
- return { buf, width, height, cols, rows, tileWidth, tileHeight, count };
969
- }
970
- async function frameDiffs(frames, opts = {}) {
971
- if (frames.length === 0) return [];
972
- const sample = opts.sample ?? 32;
973
- const thumbs = await Promise.all(
974
- frames.map((f) => sharp(f).greyscale().resize(sample, sample, { fit: "fill" }).raw().toBuffer())
975
- );
976
- const out = [0];
977
- for (let i = 1; i < thumbs.length; i++) {
978
- const a = thumbs[i - 1];
979
- const b = thumbs[i];
980
- if (!a || !b) {
981
- out.push(0);
982
- continue;
983
- }
984
- const n = Math.min(a.length, b.length) || 1;
985
- let s = 0;
986
- for (let k = 0; k < n; k++) s += Math.abs((a[k] ?? 0) - (b[k] ?? 0));
987
- out.push(+(s / n / 255).toFixed(4));
988
- }
989
- return out;
990
- }
991
-
992
- // src/commands/screenshot.ts
993
- var DEFAULT_CONTACT_SHEET_FRAMES = 8;
994
- var TARGET_CROP_PAD = 0.02;
995
- function clampMaxDim(raw) {
996
- const n = Number(raw);
997
- if (!Number.isFinite(n)) return 1024;
998
- return Math.max(256, Math.min(4096, Math.round(n)));
1463
+ import { takeScreenshot, captureBurst, locateTarget, describeLayoutNode as describeLayoutNode2, dumpLayout, listLayoutWindows } from "@oniroproject/core";
1464
+ var DEFAULT_CONTACT_SHEET_FRAMES = 8;
1465
+ var TARGET_CROP_PAD = 0.02;
1466
+ function clampMaxDim(raw) {
1467
+ const n = Number(raw);
1468
+ if (!Number.isFinite(n)) return 1024;
1469
+ return Math.max(256, Math.min(4096, Math.round(n)));
999
1470
  }
1000
1471
  function registerScreenshotCommand(program) {
1001
- const screenshot = program.command("screenshot").description(
1002
- "Capture the screen. Default: full-resolution raw JPEG. --grid: downscaled JPEG with a 10x10 grid (axes 0.0-1.0) for picking tap coordinates. --crop / --target: a full-resolution crop of one region or element, for checking detail cheaply. --contact-sheet: capture a burst and tile it into one image with per-frame change diffs."
1472
+ const screenshot2 = program.command("screenshot").description(
1473
+ "Capture the screen. Default: full-resolution raw JPEG. --grid: downscaled JPEG with a 10x10 grid (axes 0.0-1.0) for picking tap coordinates. --crop / --target / --selector / --window: a full-resolution crop of one region, element or window, for checking detail cheaply. --contact-sheet: capture a burst and tile it into one image with per-frame change diffs. --diff: compare the screen with a baseline JPEG (perceptual-hash distance, pixel-change ratio)."
1003
1474
  ).option("-o, --output <file>", "Output file path.", "screenshot.jpeg").option("--grid", "Downscale (to --max-dim) and overlay a 10x10 coordinate grid.").option("--max-dim <px>", "Longest-side cap for --grid / --crop / --target / --contact-sheet (256-4096).", "1024").option("--crop <x1,y1,x2,y2>", "Crop to this region, given as 0-1 fractions of the screen.");
1004
- addTargetOptions(screenshot, "Crop to");
1005
- screenshot.option("--contact-sheet", "Capture a burst and composite it into one tiled contact sheet.").option("--burst <count>", "Capture N frames; writes <base>-<i><ext> (or feeds --contact-sheet).").option("--interval <ms>", "Delay between burst frames, ms.", "50").option("--json", "Emit the full result object as JSON on stdout (with --contact-sheet).").option("--device <serial>", "Target device serial.").action(async (opts, command) => {
1475
+ addTargetOptions(screenshot2, "Crop to");
1476
+ screenshot2.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(
1477
+ "--diff <baseline.jpeg>",
1478
+ "Capture the screen (written to --output) and compare it with this baseline: pHash and dHash distance, pixel-change ratio, mean change."
1479
+ ).option("--json", "Emit the full result object as JSON on stdout (with --contact-sheet or --diff).").option("--device <serial>", "Target device serial.").action(async (opts, command) => {
1006
1480
  const { config, logger } = getRuntime();
1007
1481
  const maxDim = clampMaxDim(opts.maxDim);
1008
1482
  const intervalMs = Number(opts.interval) || 50;
1009
1483
  const selector = selectorFrom(opts);
1010
- if (opts.crop !== void 0 || selector) {
1011
- if (opts.crop !== void 0 && selector) throw new Error("Use either --crop or --target*, not both.");
1484
+ const windowId = windowIdFrom(opts);
1485
+ if (opts.diff !== void 0) {
1486
+ if (opts.crop !== void 0 || selector || windowId !== void 0 || opts.grid || opts.contactSheet || opts.burst) {
1487
+ throw new Error(
1488
+ "--diff cannot be combined with --crop, --target* / --selector / --window, --grid, --contact-sheet, or --burst."
1489
+ );
1490
+ }
1491
+ let baseline;
1492
+ try {
1493
+ baseline = fs3.readFileSync(opts.diff);
1494
+ } catch (err) {
1495
+ throw new Error(`Cannot read the baseline ${opts.diff}: ${err.message}`);
1496
+ }
1497
+ const shot2 = await takeScreenshot({ config, deviceSerial: opts.device, logger });
1498
+ fs3.writeFileSync(opts.output, shot2.pixels);
1499
+ const diff = await diffScreenshots(baseline, shot2.pixels);
1500
+ const result = { baseline: opts.diff, current: opts.output, ...diff };
1501
+ if (opts.json) {
1502
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1503
+ `);
1504
+ } else {
1505
+ process.stdout.write(
1506
+ `${diff.same ? "same" : "different"}: pHash ${diff.phashDistance}/64, dHash ${diff.dhashDistance}/64, ${(diff.pixelChangeRatio * 100).toFixed(2)}% of pixels changed, mean change ${diff.meanDiff.toFixed(4)}${diff.sizeMismatch ? " (sizes differ)" : ""}
1507
+ `
1508
+ );
1509
+ }
1510
+ logger.info(`Wrote ${opts.output} (${shot2.width}x${shot2.height}).`);
1511
+ return;
1512
+ }
1513
+ if (opts.crop !== void 0 || selector || windowId !== void 0) {
1514
+ if (opts.crop !== void 0 && (selector || windowId !== void 0)) {
1515
+ throw new Error("Use either --crop or --target* / --selector / --window, not both.");
1516
+ }
1012
1517
  if (opts.grid || opts.contactSheet || opts.burst) {
1013
- throw new Error("--crop / --target cannot be combined with --grid, --contact-sheet, or --burst.");
1518
+ throw new Error("--crop / --target / --selector / --window cannot be combined with --grid, --contact-sheet, or --burst.");
1014
1519
  }
1015
1520
  let rect;
1016
1521
  let label;
@@ -1019,6 +1524,7 @@ function registerScreenshotCommand(program) {
1019
1524
  config,
1020
1525
  selector,
1021
1526
  index: targetIndexFrom(opts),
1527
+ windowId,
1022
1528
  hideKeyboard: false,
1023
1529
  deviceSerial: opts.device,
1024
1530
  logger
@@ -1026,6 +1532,12 @@ function registerScreenshotCommand(program) {
1026
1532
  if (!located.node.b) throw new Error(`${describeLayoutNode2(located.node)} has no bounds to crop to.`);
1027
1533
  rect = padCropRect(located.node.b, TARGET_CROP_PAD);
1028
1534
  label = describeLayoutNode2(located.node);
1535
+ } else if (windowId !== void 0) {
1536
+ const { tree } = await dumpLayout({ config, windowId, deviceSerial: opts.device, logger });
1537
+ const win = listLayoutWindows(tree).find((w) => w.windowId === windowId);
1538
+ if (!win?.b) throw new Error(`Window ${windowId} has no bounds to crop to.`);
1539
+ rect = win.b;
1540
+ label = `window ${windowId}${win.bundle ? ` (${win.bundle})` : ""}`;
1029
1541
  } else {
1030
1542
  rect = parseCropRect(opts.crop);
1031
1543
  label = rect.map((n) => n.toFixed(3)).join(",");
@@ -1108,7 +1620,16 @@ function registerScreenshotCommand(program) {
1108
1620
  }
1109
1621
 
1110
1622
  // src/commands/dump.ts
1111
- import { dumpLayout } from "@oniroproject/core";
1623
+ import * as fs4 from "fs";
1624
+ import {
1625
+ diffLayouts,
1626
+ dumpLayout as dumpLayout2,
1627
+ findLayoutNodes,
1628
+ listWindows,
1629
+ parseLayoutBaseline,
1630
+ selectableTree,
1631
+ summarizeLayout
1632
+ } from "@oniroproject/core";
1112
1633
 
1113
1634
  // src/lib/layoutText.ts
1114
1635
  import { flattenLayout } from "@oniroproject/core";
@@ -1126,6 +1647,7 @@ function flags(n) {
1126
1647
  if (n.sel) out.push("selected");
1127
1648
  if (n.focus) out.push("focus");
1128
1649
  if (n.disabled) out.push("disabled");
1650
+ if (n.hidden) out.push("hidden");
1129
1651
  if (n.covered) out.push("covered");
1130
1652
  return out;
1131
1653
  }
@@ -1144,6 +1666,9 @@ function isListed(n) {
1144
1666
  const labelled = n.text !== void 0 || n.desc !== void 0 || n.hint !== void 0;
1145
1667
  return labelled || !!(n.click || n.longClick || n.scroll || n.check || n.focus);
1146
1668
  }
1669
+ function windowLabel(windowId, bundle) {
1670
+ return [windowId, bundle].filter((x) => x !== void 0).join(" ");
1671
+ }
1147
1672
  function formatFlatLayout(result) {
1148
1673
  const lines = [];
1149
1674
  const d = result.display;
@@ -1152,13 +1677,15 @@ function formatFlatLayout(result) {
1152
1677
  );
1153
1678
  const keyboardBundle = result.keyboard?.shown ? result.keyboard.bundle : void 0;
1154
1679
  let window;
1680
+ let windowId;
1155
1681
  for (const n of flattenLayout(result.tree)) {
1156
- if (n.window !== window) {
1682
+ if (n.window !== window || n.windowId !== windowId) {
1157
1683
  window = n.window;
1684
+ windowId = n.windowId;
1158
1685
  if (window !== void 0 && window === keyboardBundle) {
1159
- lines.push(`[keyboard ${window}] up \u2014 dismiss with input --type hideKeyboard`);
1160
- } else if (window !== void 0) {
1161
- lines.push(`[window ${window}]`);
1686
+ lines.push(`[keyboard ${windowLabel(windowId, window)}] up \u2014 dismiss with input --type hideKeyboard`);
1687
+ } else if (window !== void 0 || windowId !== void 0) {
1688
+ lines.push(`[window ${windowLabel(windowId, window)}]`);
1162
1689
  }
1163
1690
  }
1164
1691
  if (window !== void 0 && window === keyboardBundle) continue;
@@ -1168,26 +1695,98 @@ function formatFlatLayout(result) {
1168
1695
  return `${lines.join("\n")}
1169
1696
  `;
1170
1697
  }
1698
+ function formatSelectorMatches(display, selector, matches) {
1699
+ const lines = [
1700
+ `# display ${display ? `${display.width}x${display.height}` : "unknown"}; ${matches.length} element(s) match ${JSON.stringify(selector)}${matches.length > 1 ? "; append [n] to the selector to act on one" : ""}`
1701
+ ];
1702
+ matches.forEach((n, i) => {
1703
+ const win = windowLabel(n.windowId, n.window);
1704
+ lines.push(`[${i}] ${formatLayoutLine(n)}${win ? ` (window ${win})` : ""}`);
1705
+ });
1706
+ return `${lines.join("\n")}
1707
+ `;
1708
+ }
1171
1709
 
1172
1710
  // src/commands/dump.ts
1711
+ function readBaseline(file) {
1712
+ let text;
1713
+ try {
1714
+ text = fs4.readFileSync(file, "utf8");
1715
+ } catch (err) {
1716
+ throw new Error(`Cannot read the layout baseline ${file}: ${err.message}`);
1717
+ }
1718
+ try {
1719
+ return parseLayoutBaseline(JSON.parse(text));
1720
+ } catch (err) {
1721
+ throw new Error(`${file} is not a saved \`dump layout\` JSON: ${err.message}`);
1722
+ }
1723
+ }
1724
+ function formatWindows(windows) {
1725
+ const lines = ["# id name pid type z-order [x y w h]; z-order -1 = not shown; `--window <id>` restricts dump/input/wait/screenshot"];
1726
+ for (const w of windows) {
1727
+ const rect = w.rect ? ` [${w.rect.join(" ")}]` : "";
1728
+ lines.push(`${w.id} ${w.name} pid=${w.pid} type=${w.type} z=${w.zOrder}${rect}${w.focused ? " focused" : ""}`);
1729
+ }
1730
+ return `${lines.join("\n")}
1731
+ `;
1732
+ }
1173
1733
  function registerDumpCommand(program) {
1174
1734
  program.command("dump [target]").description(
1175
- "Dump device state. Currently supports `dump layout` (the default target): JSON by default, or one line per element with --flat."
1176
- ).option("--bundle <bundle>", "Filter the layout to a single window/bundle.").option("--raw", "Return the unpruned layout tree.").option("--flat", "One line per element (type, label, center as 0-1 fractions, flags) instead of a JSON tree.").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
1735
+ "Dump device state. `dump layout` (the default target): JSON by default, or one line per element with --flat; --selector lists only the matching elements, --summary counts elements per window, --diff compares the live layout with one saved earlier. `dump windows`: the window list (ids for --window)."
1736
+ ).option("--bundle <bundle>", "Filter the layout to a single window/bundle.").option("--raw", "Return the unpruned layout tree.").option("--flat", "One line per element (type, label, center as 0-1 fractions, flags) instead of a JSON tree.").option("--summary", "Per window: element, content, clickable and text-node counts (JSON).").option(
1737
+ "--diff <baseline.json>",
1738
+ "Compare with a saved `dump layout` JSON: matched elements with IoU, missing, extra, and a 0-1 similarity (JSON)."
1739
+ ).option("--selector <expr>", `List only the elements matching this expression, numbered for its [n] suffix. ${SELECTOR_HELP}`).option("--window <id>", WINDOW_HELP).option("--json", "dump windows: print JSON instead of one line per window.").option("--device <serial>", "Target device serial.").action(async (target, opts) => {
1177
1740
  const { config, logger } = getRuntime();
1178
1741
  const what = target ?? "layout";
1179
- if (what !== "layout") throw new Error(`Unknown dump target "${what}". Supported: layout.`);
1742
+ if (what === "windows") {
1743
+ const windows = await listWindows({ config, deviceSerial: opts.device, logger });
1744
+ process.stdout.write(opts.json ? `${JSON.stringify(windows, null, 2)}
1745
+ ` : formatWindows(windows));
1746
+ return;
1747
+ }
1748
+ if (what !== "layout") throw new Error(`Unknown dump target "${what}". Supported: layout, windows.`);
1180
1749
  if (opts.raw && opts.flat) throw new Error("--raw and --flat cannot be combined.");
1181
- const result = await dumpLayout({
1750
+ const selector = parseSelectorOption(opts.selector);
1751
+ if (selector && opts.raw) throw new Error("--selector matches the pruned layout; it cannot be combined with --raw.");
1752
+ const modes = [opts.flat && "--flat", opts.summary && "--summary", opts.diff !== void 0 && "--diff"].filter(Boolean);
1753
+ if (modes.length > 1) throw new Error(`${modes.join(" and ")} cannot be combined.`);
1754
+ if (opts.raw && modes.length > 0) throw new Error(`--raw cannot be combined with ${modes[0]}.`);
1755
+ if (selector && (opts.summary || opts.diff !== void 0)) {
1756
+ throw new Error(`--selector cannot be combined with ${opts.summary ? "--summary" : "--diff"}.`);
1757
+ }
1758
+ const baseline = opts.diff !== void 0 ? readBaseline(opts.diff) : void 0;
1759
+ const result = await dumpLayout2({
1182
1760
  config,
1183
1761
  bundleName: opts.bundle,
1762
+ windowId: windowIdFrom(opts),
1184
1763
  prune: !opts.raw,
1185
1764
  detectKeyboard: !opts.raw,
1186
1765
  deviceSerial: opts.device,
1187
1766
  logger
1188
1767
  });
1189
- process.stdout.write(opts.flat ? formatFlatLayout(result) : `${JSON.stringify(result, null, 2)}
1190
- `);
1768
+ if (selector) {
1769
+ const matches = findLayoutNodes(selectableTree(result, windowIdFrom(opts)), selector);
1770
+ process.stdout.write(
1771
+ opts.flat ? formatSelectorMatches(result.display, selector.raw, matches) : `${JSON.stringify({ display: result.display, selector: selector.raw, matches }, null, 2)}
1772
+ `
1773
+ );
1774
+ return;
1775
+ }
1776
+ let out;
1777
+ if (opts.flat) out = formatFlatLayout(result);
1778
+ else if (opts.summary) out = `${JSON.stringify(summarizeLayout(result), null, 2)}
1779
+ `;
1780
+ else if (baseline) {
1781
+ const diff = diffLayouts(baseline, result);
1782
+ logger.info(
1783
+ `layout similarity ${diff.similarity.toFixed(3)}: ${diff.matched.length} matched (${diff.moved} moved, ${diff.changed} changed), ${diff.missing.length} missing, ${diff.extra.length} extra.`
1784
+ );
1785
+ out = `${JSON.stringify(diff, null, 2)}
1786
+ `;
1787
+ } else out = `${JSON.stringify(result, null, 2)}
1788
+ `;
1789
+ process.stdout.write(out);
1191
1790
  });
1192
1791
  }
1193
1792
 
@@ -1196,9 +1795,9 @@ import { getDeviceInfo, listDevices } from "@oniroproject/core";
1196
1795
  function formatLongDevice(d, info) {
1197
1796
  const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
1198
1797
  if (!info) return head;
1199
- const os3 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
1798
+ const os5 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
1200
1799
  const fields = [
1201
- ["os", os3 || void 0],
1800
+ ["os", os5 || void 0],
1202
1801
  ["api", info.sdkApiVersion],
1203
1802
  ["abi", info.abi],
1204
1803
  ["type", info.deviceType],
@@ -1208,34 +1807,40 @@ function formatLongDevice(d, info) {
1208
1807
  const rest = fields.filter(([, v]) => v).map(([k, v]) => `${k}=${/\s/.test(v) ? JSON.stringify(v) : v}`);
1209
1808
  return [head, ...rest].join(" ");
1210
1809
  }
1810
+ async function queryDevices(long) {
1811
+ const { config, logger } = getRuntime();
1812
+ const devices2 = await listDevices(config);
1813
+ const infos = /* @__PURE__ */ new Map();
1814
+ if (long) {
1815
+ for (const d of devices2.filter((x) => x.status === "Connected")) {
1816
+ try {
1817
+ infos.set(d.serial, await getDeviceInfo({ config, deviceSerial: d.serial, logger }));
1818
+ } catch (err) {
1819
+ logger.warn(`Could not query ${d.serial}: ${err.message}`);
1820
+ }
1821
+ }
1822
+ }
1823
+ return { devices: devices2, infos };
1824
+ }
1825
+ function devicesJson(devices2, infos, long) {
1826
+ return long ? devices2.map((d) => ({ ...d, info: infos.get(d.serial) ?? null })) : devices2;
1827
+ }
1211
1828
  function registerDevicesCommand(program) {
1212
1829
  program.command("devices").description("List connected hdc devices.").option(
1213
1830
  "--long",
1214
1831
  "Also ask each connected device for its OS family (HarmonyOS / OpenHarmony), version, API level, CPU ABI, device type and model."
1215
1832
  ).option("--json", "Emit machine-readable JSON.").action(async (opts) => {
1216
- const { config, logger } = getRuntime();
1217
- const devices = await listDevices(config);
1218
- const infos = /* @__PURE__ */ new Map();
1219
- if (opts.long) {
1220
- for (const d of devices.filter((x) => x.status === "Connected")) {
1221
- try {
1222
- infos.set(d.serial, await getDeviceInfo({ config, deviceSerial: d.serial, logger }));
1223
- } catch (err) {
1224
- logger.warn(`Could not query ${d.serial}: ${err.message}`);
1225
- }
1226
- }
1227
- }
1833
+ const { devices: devices2, infos } = await queryDevices(!!opts.long);
1228
1834
  if (opts.json) {
1229
- const out = opts.long ? devices.map((d) => ({ ...d, info: infos.get(d.serial) ?? null })) : devices;
1230
- process.stdout.write(`${JSON.stringify(out, null, 2)}
1835
+ process.stdout.write(`${JSON.stringify(devicesJson(devices2, infos, !!opts.long), null, 2)}
1231
1836
  `);
1232
1837
  return;
1233
1838
  }
1234
- if (devices.length === 0) {
1839
+ if (devices2.length === 0) {
1235
1840
  process.stdout.write("(no devices)\n");
1236
1841
  return;
1237
1842
  }
1238
- for (const d of devices) {
1843
+ for (const d of devices2) {
1239
1844
  process.stdout.write(`${opts.long ? formatLongDevice(d, infos.get(d.serial)) : `${d.serial} ${d.connection ?? ""} ${d.status}`}
1240
1845
  `);
1241
1846
  }
@@ -1263,7 +1868,8 @@ function registerFileCommand(program) {
1263
1868
  }
1264
1869
 
1265
1870
  // src/commands/lint.ts
1266
- import * as path8 from "path";
1871
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
1872
+ import * as path9 from "path";
1267
1873
  import {
1268
1874
  detectCmdToolsFlavour,
1269
1875
  detectRuntimeOs as detectRuntimeOs2,
@@ -1271,6 +1877,76 @@ import {
1271
1877
  missingProjectRootFiles,
1272
1878
  runCodelinter
1273
1879
  } from "@oniroproject/core";
1880
+
1881
+ // src/commands/check.ts
1882
+ import * as path8 from "path";
1883
+ import { checkArkts } from "@oniroproject/core";
1884
+ function arktsCheckExitCode(report) {
1885
+ if (!report.completed) return 2;
1886
+ return report.ok ? 0 : 1;
1887
+ }
1888
+ function arktsCheckSummaryLine(report) {
1889
+ const s = report.summary;
1890
+ const fixed = s.fixed ? `, ${s.fixed} fixed` : "";
1891
+ return `${s.errors} errors, ${s.warnings} warnings${fixed} in ${s.files} files (${(report.durationMs / 1e3).toFixed(1)} s).`;
1892
+ }
1893
+ async function arktsCheckOrError(projectDir, opts) {
1894
+ const { config, logger } = getRuntime();
1895
+ const dir = path8.resolve(projectDir ?? process.cwd());
1896
+ try {
1897
+ return await checkArkts({ config, projectDir: dir, files: opts.files, fix: opts.fix, sdkDir: opts.sdk, logger });
1898
+ } catch (err) {
1899
+ return { ok: false, completed: false, error: err instanceof Error ? err.message : String(err), diagnostics: [] };
1900
+ }
1901
+ }
1902
+ async function runArktsCheckCommand(projectDir, opts) {
1903
+ const { logger } = getRuntime();
1904
+ const report = await arktsCheckOrError(projectDir, opts);
1905
+ if (!("summary" in report)) {
1906
+ if (opts.json) process.stdout.write(`${JSON.stringify(report, null, 2)}
1907
+ `);
1908
+ logger.error(report.error);
1909
+ return 2;
1910
+ }
1911
+ if (opts.json) {
1912
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
1913
+ `);
1914
+ } else if (report.completed) {
1915
+ if (report.diagnostics.length) process.stdout.write(`${formatDiagnostics(report.diagnostics, Infinity)}
1916
+ `);
1917
+ for (const d of report.fixed) {
1918
+ process.stdout.write(` fixed ${d.file}:${d.line}:${d.column} ${d.message}
1919
+ `);
1920
+ }
1921
+ if (report.alsoModified.length) {
1922
+ process.stdout.write(`--fix also edited: ${report.alsoModified.join(", ")}
1923
+ `);
1924
+ }
1925
+ process.stdout.write(`${arktsCheckSummaryLine(report)}
1926
+ `);
1927
+ }
1928
+ if (!report.completed) logger.error(report.error ?? "The ArkTS checker did not complete, so nothing was checked.");
1929
+ return arktsCheckExitCode(report);
1930
+ }
1931
+ function registerCheckCommand(program) {
1932
+ const check = program.command("check").description("Static checks that need no build and no device.");
1933
+ check.command("arkts [project-dir]").description(
1934
+ "Type-check and lint ArkTS with the SDK's own ArkTS checker (what the build's CompileArkTS runs), plus the ArkUI component-model and project-config rules the build enforces later (V1/V2 decorators, @Entry root, route maps, resources, permissions), in seconds and without a build. Uses the SDK the project compiles with. Exits 0 when clean, 1 on an error, 2 when the checker could not run (then nothing is known)."
1935
+ ).option(
1936
+ "--files <files...>",
1937
+ "Check only these .ets files (project-wide rules such as main_pages.json and module.json5 are then skipped)."
1938
+ ).option(
1939
+ "--fix",
1940
+ "Apply the fixes the checker can verify (the compiler's `Did you mean` suggestions, import paths, a missing `export`, the @Component/@ComponentV2 decorator), re-check, and revert any fix that adds an error."
1941
+ ).option("--sdk <dir>", "SDK directory to check against (holding ets/), instead of the one compileSdkVersion names.").option(
1942
+ "--json",
1943
+ "Print the report as JSON: ok, completed, diagnostics (severity, source, rule, message, file, line, column), fixed, alsoModified, summary, sdk, durationMs, error."
1944
+ ).action(async (projectDir, opts) => {
1945
+ process.exitCode = await runArktsCheckCommand(projectDir, opts);
1946
+ });
1947
+ }
1948
+
1949
+ // src/commands/lint.ts
1274
1950
  function lintTotals(result) {
1275
1951
  if (result.summary) return result.summary;
1276
1952
  const count = (s) => result.findings.filter((f) => f.severity === s).length;
@@ -1281,19 +1957,34 @@ function lintTotals(result) {
1281
1957
  suggestions: count("suggestion")
1282
1958
  };
1283
1959
  }
1284
- function lintExitCode(result) {
1960
+ function lintExitCode(result, allowIncomplete = false) {
1285
1961
  if (lintTotals(result).errors > 0) return 1;
1286
- return lintFailed(result) ? result.code : 0;
1962
+ if (lintFailed(result)) return result.code;
1963
+ return result.incomplete && !allowIncomplete ? 2 : 0;
1287
1964
  }
1288
1965
  function lintFailed(result) {
1289
1966
  return result.summary === null && result.findings.length === 0 && result.code !== 0;
1290
1967
  }
1291
1968
  function registerLintCommand(program) {
1292
1969
  program.command("lint [project-dir]").description(
1293
- "Run the OpenHarmony codelinter on a project. Defaults to the current directory. Exits 1 on error-severity findings or when codelinter fails to run; warnings alone exit 0."
1294
- ).option("--files <globs...>", "Specific files/globs to lint (default: the whole project).").option("--json", "Emit the report (findings, summary, notices, incomplete) as JSON.").action(async (projectDir, opts) => {
1970
+ "Run the OpenHarmony codelinter on a project. Defaults to the current directory. Exits 1 on error-severity findings or when codelinter fails to run, 2 when codelinter reports that it skipped checks (incomplete), 0 otherwise; warnings alone exit 0. `--engine arkts` runs `check arkts` instead."
1971
+ ).option("--files <globs...>", "Specific files/globs to lint (default: the whole project).").option("--json", "Emit the report (findings, summary, notices, incomplete) as JSON.").option(
1972
+ "--engine <engine>",
1973
+ "`codelinter`, or `arkts` for the ArkTS checker of `check arkts` (same options, report and exit codes).",
1974
+ "codelinter"
1975
+ ).option(
1976
+ "--allow-incomplete",
1977
+ "Exit 0 rather than 2 when codelinter reports that it skipped checks (e.g. the tools flavour differs from runtimeOS)."
1978
+ ).action(async (projectDir, opts) => {
1979
+ if (opts.engine === "arkts") {
1980
+ process.exitCode = await runArktsCheckCommand(projectDir, { files: opts.files, json: opts.json });
1981
+ return;
1982
+ }
1983
+ if (opts.engine !== "codelinter") {
1984
+ throw new InvalidArgumentError3(`Unknown --engine ${opts.engine}: use codelinter or arkts.`);
1985
+ }
1295
1986
  const { config, logger } = getRuntime();
1296
- const dir = path8.resolve(projectDir ?? process.cwd());
1987
+ const dir = path9.resolve(projectDir ?? process.cwd());
1297
1988
  const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
1298
1989
  const totals = lintTotals(result);
1299
1990
  const failed = lintFailed(result);
@@ -1330,68 +2021,275 @@ function registerLintCommand(program) {
1330
2021
  );
1331
2022
  }
1332
2023
  }
1333
- process.exitCode = lintExitCode(result);
2024
+ if (result.incomplete && !opts.allowIncomplete && lintTotals(result).errors === 0 && !failed) {
2025
+ logger.error(
2026
+ "codelinter reported an incomplete run, so a clean result is not evidence the code is clean: exiting 2. Pass --allow-incomplete to exit 0, or use `oniro-app check arkts`, which does not depend on codelinter."
2027
+ );
2028
+ }
2029
+ process.exitCode = lintExitCode(result, opts.allowIncomplete);
1334
2030
  });
1335
2031
  }
1336
2032
 
1337
- // src/commands/input.ts
1338
- import { Option as Option3 } from "commander";
1339
- import {
1340
- performInput,
1341
- sendGesture,
1342
- getDisplaySize,
1343
- describeLayoutNode as describeLayoutNode3
1344
- } from "@oniroproject/core";
1345
- var INPUT_TYPES = [
1346
- "click",
1347
- "doubleClick",
1348
- "longClick",
1349
- "swipe",
1350
- "drag",
1351
- "fling",
1352
- "keyEvent",
1353
- "inputText",
1354
- "clearText",
1355
- "hideKeyboard"
1356
- ];
1357
- var num = (v) => v === void 0 ? void 0 : Number(v);
1358
- function registerInputCommand(program) {
1359
- const input = program.command("input").description(
1360
- "Inject a UI input event via uitest uiInput. Position it with --target (an on-screen element), --fx/--fy (0-1 fractions of the display, as in `dump layout` and the screenshot grid), or --x/--y (pixels)."
1361
- ).addOption(new Option3("--type <type>", "Input type.").choices(INPUT_TYPES).makeOptionMandatory());
1362
- addTargetOptions(input, "Act on");
1363
- input.option("--fx <fraction>", "Start X as a 0-1 fraction of the display width.").option("--fy <fraction>", "Start Y as a 0-1 fraction of the display height.").option("--fx2 <fraction>", "End X fraction (swipe/drag/fling).").option("--fy2 <fraction>", "End Y fraction (swipe/drag/fling).").option("--x <px>", "X pixel (click/swipe start).").option("--y <px>", "Y pixel (click/swipe start).").option("--x2 <px>", "End X pixel (swipe/drag/fling).").option("--y2 <px>", "End Y pixel (swipe/drag/fling).").option("--speed <px-per-s>", "Velocity for swipe/drag/fling.").option(
1364
- "--key <key>",
1365
- "Key for keyEvent: Back/Home/Power, a name (Delete, Enter, Tab, Escape, MoveHome, MoveEnd, \u2026), a letter, a KeyCode id, or a combo like Ctrl+A."
1366
- ).option("--text <text>", "Text for inputText.").option("--clear", "inputText: clear the field (Ctrl+A, Delete) before typing.").option("--hide-keyboard", "Dismiss the soft keyboard afterwards, if it is up.").option("--device <serial>", "Target device serial.").action(async (opts) => {
2033
+ // src/commands/test.ts
2034
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
2035
+ import * as path10 from "path";
2036
+ import { runTests } from "@oniroproject/core";
2037
+ function positiveMs(raw) {
2038
+ const n = Number(raw);
2039
+ if (!Number.isFinite(n) || n <= 0) throw new InvalidArgumentError4("Expected a positive number of milliseconds.");
2040
+ return Math.floor(n);
2041
+ }
2042
+ function testExitCode(report) {
2043
+ return report.outcome === "passed" ? 0 : report.outcome === "failed" ? 1 : 2;
2044
+ }
2045
+ function projectFrame(stack) {
2046
+ for (const line of stack?.split("\n") ?? []) {
2047
+ if (/oh_modules|@ohos\/hypium/.test(line)) continue;
2048
+ const m = /\(([^()]+:\d+:\d+)\)\s*$/.exec(line.trim());
2049
+ if (m) return m[1];
2050
+ }
2051
+ return void 0;
2052
+ }
2053
+ function formatTestCase(t) {
2054
+ const label = { pass: "PASS", fail: "FAIL", error: "ERROR", ignore: "SKIP", unknown: `CODE ${t.statusCode}` }[t.status];
2055
+ const time = t.durationMs !== void 0 ? ` (${t.durationMs} ms)` : "";
2056
+ let line = `${label} ${t.suite} > ${t.name}${time}`;
2057
+ if (t.status !== "pass" && t.message) line += `
2058
+ ${t.message}`;
2059
+ const at = t.status !== "pass" ? projectFrame(t.stack) : void 0;
2060
+ if (at) line += `
2061
+ at ${at}`;
2062
+ return line;
2063
+ }
2064
+ function registerTestCommand(program) {
2065
+ program.command("test [project-dir]").description(
2066
+ "Build a module's main and ohosTest HAPs, install both on the device, run the suite with `aa test` (Hypium, OpenHarmonyTestRunner) and report each case. Defaults to the current directory. Exits 0 when every case passed, 1 when a case failed or threw, 2 when the run did not complete (build failed, test module not found, timeout, no device). The test module is left installed."
2067
+ ).option("--module <module>", "Module whose src/ohosTest suite runs.", "entry").option("--product <product>", "hvigor product name.", "default").option("--class <filter>", "Run only these cases: `Suite`, `Suite#case`, or several joined by commas.").option("--timeout <ms>", "Per-case timeout passed to the runner.", positiveMs, 15e3).option("--run-timeout <ms>", "Wall-clock limit for the whole run; aa test can hang when the app dies.", positiveMs, 6e5).option("--no-build", "Use the HAPs already built instead of building them.").option("--no-wake", "Leave the screen as it is (by default it is turned on, a lock screen without a credential is swiped away, and it is kept on during the run).").option("--mode <mode>", "hvigor build mode (e.g. debug).").option("--no-deps", "Skip the automatic `ohpm install --all` when oh_modules/ is missing.").option("--device <serial>", "Target device serial.").option(
2068
+ "--json",
2069
+ "Print the report as JSON: ok, outcome, tests (suite, name, status, statusCode, durationMs, message, stack), summary, resultCode, haps, log, error."
2070
+ ).action(async (projectDir, opts) => {
1367
2071
  const { config, logger } = getRuntime();
1368
- const result = await performInput({
2072
+ const dir = path10.resolve(projectDir ?? process.cwd());
2073
+ const report = await runTests({
1369
2074
  config,
1370
- type: opts.type,
1371
- target: selectorFrom(opts),
1372
- targetIndex: targetIndexFrom(opts),
1373
- fx: num(opts.fx),
1374
- fy: num(opts.fy),
1375
- fx2: num(opts.fx2),
1376
- fy2: num(opts.fy2),
1377
- pxX: num(opts.x),
1378
- pxY: num(opts.y),
1379
- pxX2: num(opts.x2),
1380
- pxY2: num(opts.y2),
1381
- speed: num(opts.speed),
1382
- key: opts.key,
1383
- text: opts.text,
1384
- clear: opts.clear,
1385
- hideKeyboard: opts.hideKeyboard,
2075
+ projectDir: dir,
2076
+ module: opts.module,
2077
+ product: opts.product,
2078
+ testClass: opts.class,
2079
+ caseTimeoutMs: opts.timeout,
2080
+ runTimeoutMs: opts.runTimeout,
2081
+ build: opts.build,
2082
+ wakeScreen: opts.wake,
2083
+ mode: opts.mode,
2084
+ autoInstallDeps: opts.deps,
1386
2085
  deviceSerial: opts.device,
1387
2086
  logger
1388
2087
  });
1389
- if (opts.type === "hideKeyboard") {
1390
- logger.info(result.keyboardHidden ? "Dismissed the keyboard." : "The keyboard was not shown.");
1391
- return;
1392
- }
1393
- const at = result.x !== void 0 ? ` at ${result.x},${result.y}` : "";
1394
- const on = result.target ? ` on ${describeLayoutNode3(result.target)}` : "";
2088
+ if (opts.json) {
2089
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
2090
+ `);
2091
+ } else {
2092
+ for (const t of report.tests) process.stdout.write(`${formatTestCase(t)}
2093
+ `);
2094
+ const s = report.summary;
2095
+ if (report.tests.length || report.outcome !== "runner-error") {
2096
+ process.stdout.write(
2097
+ `${s.run} run: ${s.pass} passed, ${s.fail} failed, ${s.error} errors, ${s.ignore} ignored.
2098
+ `
2099
+ );
2100
+ }
2101
+ }
2102
+ for (const w of report.warnings) logger.warn(w);
2103
+ if (report.error) {
2104
+ const listed = report.diagnostics ? formatDiagnostics(report.diagnostics) : "";
2105
+ logger.error(`${report.error}${listed ? `
2106
+ ${listed}` : ""}`);
2107
+ }
2108
+ process.exitCode = testExitCode(report);
2109
+ });
2110
+ }
2111
+
2112
+ // src/commands/doctor.ts
2113
+ import * as fs5 from "fs";
2114
+ import * as path11 from "path";
2115
+ import { runDoctor } from "@oniroproject/core";
2116
+ function formatDoctorCheck(c) {
2117
+ const line = `${c.status.padEnd(5)} ${c.id.padEnd(16)} ${c.detail}${c.fixed ? " (fixed)" : ""}`;
2118
+ return c.fix && c.status !== "ok" && c.status !== "skip" ? `${line}
2119
+ ${" ".repeat(23)}fix: ${c.fix}` : line;
2120
+ }
2121
+ function doctorExitCode(report) {
2122
+ return report.ok ? 0 : 1;
2123
+ }
2124
+ function registerDoctorCommand(program) {
2125
+ program.command("doctor [project-dir]").description(
2126
+ "Check that this machine can build, check, sign, install and test: node, java, the SDK the project compiles with, the command-line tools (complete, their version, their flavour against runtimeOS), a `check arkts` self-test, hdc and a reachable device at the project's API, signing material, free disk space. Each check is ok, warn, fail or skip, with a fix. Uses the project in the current directory when there is one. Exits 1 when any check fails."
2127
+ ).option("--json", "Print the report as JSON: ok, checks (id, status, detail, fix, fixed), project, versions.").option("--fix", "Install the project's missing OpenHarmony SDK.").option("--no-device", "Skip the device check (for a machine that only builds).").option("--serial <serial>", "The device to check, when several are connected.").option("--no-arkts-check", "Skip the `check arkts` self-test.").action(async (projectDir, opts) => {
2128
+ const { config, logger, progress } = getRuntime();
2129
+ let dir;
2130
+ if (projectDir) {
2131
+ dir = path11.resolve(projectDir);
2132
+ } else if (fs5.existsSync(path11.join(process.cwd(), "build-profile.json5"))) {
2133
+ dir = process.cwd();
2134
+ }
2135
+ const report = await runDoctor({
2136
+ config,
2137
+ projectDir: dir,
2138
+ fix: opts.fix,
2139
+ device: opts.device,
2140
+ deviceSerial: opts.serial,
2141
+ arktsSelfTest: opts.arktsCheck,
2142
+ logger,
2143
+ progress
2144
+ });
2145
+ if (opts.json) {
2146
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
2147
+ `);
2148
+ } else {
2149
+ if (report.project) process.stdout.write(`Project ${report.project.dir} (${report.project.runtimeOS})
2150
+ `);
2151
+ for (const c of report.checks) process.stdout.write(`${formatDoctorCheck(c)}
2152
+ `);
2153
+ const failed = report.checks.filter((c) => c.status === "fail").length;
2154
+ process.stdout.write(failed ? `${failed} check(s) failed.
2155
+ ` : "No check failed.\n");
2156
+ }
2157
+ process.exitCode = doctorExitCode(report);
2158
+ });
2159
+ }
2160
+
2161
+ // src/commands/screens.ts
2162
+ import * as path12 from "path";
2163
+ import { inventoryScreens } from "@oniroproject/core";
2164
+ function formatTable(header, rows) {
2165
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
2166
+ const line = (cells) => cells.map((c, i) => i === cells.length - 1 ? c : c.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
2167
+ return [line(header), ...rows.map(line)].join("\n");
2168
+ }
2169
+ function formatScreens(inv) {
2170
+ const out = [];
2171
+ const multiModule = inv.modules.length > 1;
2172
+ out.push(`Pages (${inv.pages.length})`);
2173
+ if (inv.pages.length) {
2174
+ out.push(
2175
+ formatTable(
2176
+ ["NAME", ...multiModule ? ["MODULE"] : [], "KIND", "COMPONENT", "NODES", "ROUTES OUT", "FILE"],
2177
+ inv.pages.map((p) => [
2178
+ p.registered ? p.name : `${p.name} (unregistered)`,
2179
+ ...multiModule ? [p.module] : [],
2180
+ p.kind,
2181
+ p.component ?? "-",
2182
+ String(p.contentNodes),
2183
+ p.routesOut.join(", ") || "-",
2184
+ `${p.file}:${p.line}`
2185
+ ])
2186
+ )
2187
+ );
2188
+ }
2189
+ out.push("", `Routes (${inv.routes.length})`);
2190
+ if (inv.routes.length) {
2191
+ out.push(
2192
+ formatTable(
2193
+ ["FROM", "TO", "VIA", "FILE"],
2194
+ inv.routes.map((r) => [r.from, r.dynamic ? `${r.to} (dynamic)` : r.to, r.via, `${r.file}:${r.line}`])
2195
+ )
2196
+ );
2197
+ }
2198
+ out.push("", `Placeholders (${inv.placeholders.length})`);
2199
+ if (inv.placeholders.length) {
2200
+ out.push(formatTable(["FILE", "KIND", "TEXT"], inv.placeholders.map((p) => [`${p.file}:${p.line}`, p.kind, p.text])));
2201
+ }
2202
+ if (inv.warnings.length) {
2203
+ out.push("", `Warnings (${inv.warnings.length})`);
2204
+ for (const w of inv.warnings) out.push(w);
2205
+ }
2206
+ return out.join("\n");
2207
+ }
2208
+ function registerScreensCommand(program) {
2209
+ program.command("screens [project-dir]").description(
2210
+ "Static inventory of the pages and routes of a project, from its sources, without a build or a device: @Entry pages (main_pages.json, @Entry({ routeName })), NavDestination pages (route maps and .navDestination() builders that dispatch on the route name), the router.pushUrl/replaceUrl/pushNamedRoute and NavPathStack.pushPath*/replacePath*/pushDestination* call sites, the content nodes each page renders (component calls through its custom components and @Builders, layout containers excluded), and placeholder markers (TODO/FIXME, placeholder-like text, empty build()). Reads every module in build-profile.json5. Exits 0 (it is an inventory, not a gate) unless the project cannot be read."
2211
+ ).option(
2212
+ "--json",
2213
+ "Print the inventory as JSON: pages (name, module, file, line, component, entry, kind, registered, contentNodes, routesOut), routes (from, to, file, line, via, dynamic), placeholders (file, line, text, kind), warnings, modules, projectDir."
2214
+ ).action((projectDir, opts) => {
2215
+ const { logger } = getRuntime();
2216
+ const dir = path12.resolve(projectDir ?? process.cwd());
2217
+ let inv;
2218
+ try {
2219
+ inv = inventoryScreens({ projectDir: dir });
2220
+ } catch (err) {
2221
+ const error = err instanceof Error ? err.message : String(err);
2222
+ if (opts.json) process.stdout.write(`${JSON.stringify({ error }, null, 2)}
2223
+ `);
2224
+ logger.error(error);
2225
+ process.exitCode = 1;
2226
+ return;
2227
+ }
2228
+ process.stdout.write(opts.json ? `${JSON.stringify(inv, null, 2)}
2229
+ ` : `${formatScreens(inv)}
2230
+ `);
2231
+ });
2232
+ }
2233
+
2234
+ // src/commands/input.ts
2235
+ import { Option as Option3 } from "commander";
2236
+ import {
2237
+ performInput,
2238
+ sendGesture,
2239
+ getDisplaySize,
2240
+ describeLayoutNode as describeLayoutNode3
2241
+ } from "@oniroproject/core";
2242
+ var INPUT_TYPES = [
2243
+ "click",
2244
+ "doubleClick",
2245
+ "longClick",
2246
+ "swipe",
2247
+ "drag",
2248
+ "fling",
2249
+ "keyEvent",
2250
+ "inputText",
2251
+ "clearText",
2252
+ "hideKeyboard"
2253
+ ];
2254
+ var num = (v) => v === void 0 ? void 0 : Number(v);
2255
+ function registerInputCommand(program) {
2256
+ const input2 = program.command("input").description(
2257
+ "Inject a UI input event via uitest uiInput. Position it with --target or --selector (an on-screen element), --fx/--fy (0-1 fractions of the display, as in `dump layout` and the screenshot grid), or --x/--y (pixels)."
2258
+ ).addOption(new Option3("--type <type>", "Input type.").choices(INPUT_TYPES).makeOptionMandatory());
2259
+ addTargetOptions(input2, "Act on");
2260
+ input2.option("--fx <fraction>", "Start X as a 0-1 fraction of the display width.").option("--fy <fraction>", "Start Y as a 0-1 fraction of the display height.").option("--fx2 <fraction>", "End X fraction (swipe/drag/fling).").option("--fy2 <fraction>", "End Y fraction (swipe/drag/fling).").option("--x <px>", "X pixel (click/swipe start).").option("--y <px>", "Y pixel (click/swipe start).").option("--x2 <px>", "End X pixel (swipe/drag/fling).").option("--y2 <px>", "End Y pixel (swipe/drag/fling).").option("--speed <px-per-s>", "Velocity for swipe/drag/fling.").option(
2261
+ "--key <key>",
2262
+ "Key for keyEvent: Back/Home/Power, a name (Delete, Enter, Tab, Escape, MoveHome, MoveEnd, \u2026), a letter, a KeyCode id, or a combo like Ctrl+A."
2263
+ ).option("--text <text>", "Text for inputText.").option("--clear", "inputText: clear the field (Ctrl+A, Delete) before typing.").option("--hide-keyboard", "Dismiss the soft keyboard afterwards, if it is up.").option("--device <serial>", "Target device serial.").action(async (opts) => {
2264
+ const { config, logger } = getRuntime();
2265
+ const result = await performInput({
2266
+ config,
2267
+ type: opts.type,
2268
+ target: selectorFrom(opts),
2269
+ targetIndex: targetIndexFrom(opts),
2270
+ windowId: windowIdFrom(opts),
2271
+ fx: num(opts.fx),
2272
+ fy: num(opts.fy),
2273
+ fx2: num(opts.fx2),
2274
+ fy2: num(opts.fy2),
2275
+ pxX: num(opts.x),
2276
+ pxY: num(opts.y),
2277
+ pxX2: num(opts.x2),
2278
+ pxY2: num(opts.y2),
2279
+ speed: num(opts.speed),
2280
+ key: opts.key,
2281
+ text: opts.text,
2282
+ clear: opts.clear,
2283
+ hideKeyboard: opts.hideKeyboard,
2284
+ deviceSerial: opts.device,
2285
+ logger
2286
+ });
2287
+ if (opts.type === "hideKeyboard") {
2288
+ logger.info(result.keyboardHidden ? "Dismissed the keyboard." : "The keyboard was not shown.");
2289
+ return;
2290
+ }
2291
+ const at = result.x !== void 0 ? ` at ${result.x},${result.y}` : "";
2292
+ const on = result.target ? ` on ${describeLayoutNode3(result.target)}` : "";
1395
2293
  const kb = result.keyboardHidden ? " (dismissed the keyboard)" : "";
1396
2294
  logger.info(`Sent ${opts.type}${at}${on}${kb}.`);
1397
2295
  });
@@ -1419,13 +2317,14 @@ function registerInputCommand(program) {
1419
2317
  }
1420
2318
 
1421
2319
  // src/commands/logs.ts
1422
- import * as os2 from "os";
1423
- import * as path10 from "path";
1424
- import { captureLog } from "@oniroproject/core";
2320
+ import * as fs8 from "fs/promises";
2321
+ import * as os3 from "os";
2322
+ import * as path15 from "path";
2323
+ import { analyzeFaultLog, captureLog, latestCrash as latestCrash2 } from "@oniroproject/core";
1425
2324
 
1426
2325
  // src/lib/logBundle.ts
1427
- import * as fs4 from "fs/promises";
1428
- import * as path9 from "path";
2326
+ import * as fs6 from "fs/promises";
2327
+ import * as path13 from "path";
1429
2328
  import { deflateRawSync } from "zlib";
1430
2329
  import {
1431
2330
  VERSION,
@@ -1458,7 +2357,7 @@ function validateDate(date, time) {
1458
2357
  const value = new Date(Date.UTC(2e3, Number(monthText) - 1, Number(dayText), Number(hourText), Number(minuteText), Number(secondText), Number(millisecondText)));
1459
2358
  return value.getUTCMonth() === Number(monthText) - 1 && value.getUTCDate() === Number(dayText) && Number(hourText) < 24 && Number(minuteText) < 60 && Number(secondText) < 60;
1460
2359
  }
1461
- function filterHilog(input, filters) {
2360
+ function filterHilog(input2, filters) {
1462
2361
  const criteria = {};
1463
2362
  if (filters.pid) criteria.pid = filters.pid.replace(/^0+(?=\d)/, "");
1464
2363
  if (filters.tag) criteria.tag = filters.tag;
@@ -1467,7 +2366,7 @@ function filterHilog(input, filters) {
1467
2366
  let sawRecord = false;
1468
2367
  let keep = false;
1469
2368
  let output = "";
1470
- for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
2369
+ for (const line of input2.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
1471
2370
  const content = line.replace(/[\r\n]+$/, "");
1472
2371
  const match = HILOG_RECORD.exec(content);
1473
2372
  if (match?.groups) {
@@ -1520,34 +2419,34 @@ function decodeText(data, relativePath) {
1520
2419
  return text;
1521
2420
  }
1522
2421
  async function redactTree(source, destination, serials, boundedHilog) {
1523
- await fs4.mkdir(destination, { recursive: false, mode: 448 });
2422
+ await fs6.mkdir(destination, { recursive: false, mode: 448 });
1524
2423
  try {
1525
2424
  const visit = async (directory) => {
1526
- for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
1527
- const sourcePath = path9.join(directory, entry.name);
1528
- const relative2 = path9.relative(source, sourcePath);
1529
- const portableRelative = relative2.split(path9.sep).join("/");
1530
- const destinationPath = path9.join(destination, relative2);
2425
+ for (const entry of await fs6.readdir(directory, { withFileTypes: true })) {
2426
+ const sourcePath = path13.join(directory, entry.name);
2427
+ const relative2 = path13.relative(source, sourcePath);
2428
+ const portableRelative = relative2.split(path13.sep).join("/");
2429
+ const destinationPath = path13.join(destination, relative2);
1531
2430
  if (entry.isDirectory()) {
1532
- await fs4.mkdir(destinationPath, { recursive: true, mode: 448 });
2431
+ await fs6.mkdir(destinationPath, { recursive: true, mode: 448 });
1533
2432
  await visit(sourcePath);
1534
2433
  } else if (entry.isFile()) {
1535
2434
  if (portableRelative === "logs/hilog.txt") {
1536
2435
  const text2 = boundedHilog ?? "HiLog omitted from this archive because the requested duration could not be reliably applied.\n";
1537
- await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
1538
- await fs4.writeFile(destinationPath, redactLogText(text2, serials), { encoding: "utf8", mode: 384 });
2436
+ await fs6.mkdir(path13.dirname(destinationPath), { recursive: true, mode: 448 });
2437
+ await fs6.writeFile(destinationPath, redactLogText(text2, serials), { encoding: "utf8", mode: 384 });
1539
2438
  continue;
1540
2439
  }
1541
2440
  if (portableRelative === "logs/hilog-filtered.txt" && boundedHilog === null) continue;
1542
- const text = decodeText(await fs4.readFile(sourcePath), relative2);
1543
- await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
1544
- await fs4.writeFile(destinationPath, redactLogText(text, serials), { encoding: "utf8", mode: 384 });
2441
+ const text = decodeText(await fs6.readFile(sourcePath), relative2);
2442
+ await fs6.mkdir(path13.dirname(destinationPath), { recursive: true, mode: 448 });
2443
+ await fs6.writeFile(destinationPath, redactLogText(text, serials), { encoding: "utf8", mode: 384 });
1545
2444
  }
1546
2445
  }
1547
2446
  };
1548
2447
  await visit(source);
1549
2448
  } catch (error) {
1550
- await fs4.rm(destination, { recursive: true, force: true });
2449
+ await fs6.rm(destination, { recursive: true, force: true });
1551
2450
  throw error;
1552
2451
  }
1553
2452
  }
@@ -1572,8 +2471,8 @@ function dosDateTime(date) {
1572
2471
  async function writeZip(source, archivePath) {
1573
2472
  const files = [];
1574
2473
  const visit = async (directory) => {
1575
- for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
1576
- const filePath = path9.join(directory, entry.name);
2474
+ for (const entry of await fs6.readdir(directory, { withFileTypes: true })) {
2475
+ const filePath = path13.join(directory, entry.name);
1577
2476
  if (entry.isDirectory()) await visit(filePath);
1578
2477
  else if (entry.isFile()) files.push(filePath);
1579
2478
  }
@@ -1584,8 +2483,8 @@ async function writeZip(source, archivePath) {
1584
2483
  let offset = 0;
1585
2484
  const stamp = dosDateTime(/* @__PURE__ */ new Date());
1586
2485
  for (const filePath of files) {
1587
- const name = Buffer.from(path9.relative(source, filePath).split(path9.sep).join("/"), "utf8");
1588
- const data = await fs4.readFile(filePath);
2486
+ const name = Buffer.from(path13.relative(source, filePath).split(path13.sep).join("/"), "utf8");
2487
+ const data = await fs6.readFile(filePath);
1589
2488
  const compressed = deflateRawSync(data);
1590
2489
  const checksum = crc32(data);
1591
2490
  const local = Buffer.alloc(30);
@@ -1632,7 +2531,7 @@ async function writeZip(source, archivePath) {
1632
2531
  end.writeUInt32LE(centralDirectory.length, 12);
1633
2532
  end.writeUInt32LE(offset, 16);
1634
2533
  end.writeUInt16LE(0, 20);
1635
- await fs4.writeFile(archivePath, Buffer.concat([...localParts, centralDirectory, end]), { flag: "wx", mode: 384 });
2534
+ await fs6.writeFile(archivePath, Buffer.concat([...localParts, centralDirectory, end]), { flag: "wx", mode: 384 });
1636
2535
  }
1637
2536
  function commandFailure(result) {
1638
2537
  if (result.code !== 0) return `hdc exited with code ${result.code}`;
@@ -1644,12 +2543,12 @@ function errorMessage(error) {
1644
2543
  return error instanceof Error ? error.message : String(error);
1645
2544
  }
1646
2545
  async function uniqueBundleDirectory(outputDirectory, stamp) {
1647
- await fs4.mkdir(outputDirectory, { recursive: true, mode: 448 });
2546
+ await fs6.mkdir(outputDirectory, { recursive: true, mode: 448 });
1648
2547
  for (let suffix = 0; ; suffix++) {
1649
2548
  const name = `incident-${stamp}${suffix ? `-${suffix}` : ""}`;
1650
- const directory = path9.join(outputDirectory, name);
2549
+ const directory = path13.join(outputDirectory, name);
1651
2550
  try {
1652
- await fs4.mkdir(directory, { mode: 448 });
2551
+ await fs6.mkdir(directory, { mode: 448 });
1653
2552
  return directory;
1654
2553
  } catch (error) {
1655
2554
  if (error.code !== "EEXIST") throw error;
@@ -1669,7 +2568,7 @@ async function collectLogBundle(options) {
1669
2568
  }
1670
2569
  const started = /* @__PURE__ */ new Date();
1671
2570
  const stamp = started.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
1672
- const bundleDirectory = await uniqueBundleDirectory(path9.resolve(options.outputDirectory), stamp);
2571
+ const bundleDirectory = await uniqueBundleDirectory(path13.resolve(options.outputDirectory), stamp);
1673
2572
  const records = [];
1674
2573
  const serials = [];
1675
2574
  let replacementWarning = false;
@@ -1710,14 +2609,14 @@ async function collectLogBundle(options) {
1710
2609
  ]
1711
2610
  };
1712
2611
  const run = async (name, relativeOutput, args, deviceSerial) => {
1713
- const outputPath = path9.join(bundleDirectory, relativeOutput);
1714
- await fs4.mkdir(path9.dirname(outputPath), { recursive: true, mode: 448 });
2612
+ const outputPath = path13.join(bundleDirectory, relativeOutput);
2613
+ await fs6.mkdir(path13.dirname(outputPath), { recursive: true, mode: 448 });
1715
2614
  let result = null;
1716
2615
  let failure = null;
1717
2616
  try {
1718
2617
  result = await execute(args, deviceSerial);
1719
- await fs4.writeFile(outputPath, result.stdout, { encoding: "utf8", mode: 384 });
1720
- if (result.stderr) await fs4.writeFile(`${outputPath}.stderr.txt`, result.stderr, { encoding: "utf8", mode: 384 });
2618
+ await fs6.writeFile(outputPath, result.stdout, { encoding: "utf8", mode: 384 });
2619
+ if (result.stderr) await fs6.writeFile(`${outputPath}.stderr.txt`, result.stderr, { encoding: "utf8", mode: 384 });
1721
2620
  if (result.stdout.includes("\uFFFD") || result.stderr.includes("\uFFFD")) {
1722
2621
  replacementWarning = true;
1723
2622
  manifest.notes.push(`${name} output contains Unicode replacement characters; review the redacted evidence before sharing.`);
@@ -1725,7 +2624,7 @@ async function collectLogBundle(options) {
1725
2624
  failure = commandFailure(result);
1726
2625
  } catch (error) {
1727
2626
  failure = errorMessage(error);
1728
- await fs4.writeFile(outputPath, "", { encoding: "utf8", mode: 384 });
2627
+ await fs6.writeFile(outputPath, "", { encoding: "utf8", mode: 384 });
1729
2628
  }
1730
2629
  const record = {
1731
2630
  name,
@@ -1739,7 +2638,7 @@ async function collectLogBundle(options) {
1739
2638
  return record;
1740
2639
  };
1741
2640
  const targetRecord = await run("list-targets", "device/targets.txt", ["list", "targets", "-v"]);
1742
- const targetOutput = await fs4.readFile(path9.join(bundleDirectory, "device/targets.txt"), "utf8").catch(() => "");
2641
+ const targetOutput = await fs6.readFile(path13.join(bundleDirectory, "device/targets.txt"), "utf8").catch(() => "");
1743
2642
  const targets = parseDeviceList(targetOutput).filter((target) => target.status === "Connected" || target.status === "Unknown");
1744
2643
  serials.push(...parseDeviceList(targetOutput).map((target) => target.serial));
1745
2644
  const requestedDevice = resolveDeviceSerial(options.deviceSerial);
@@ -1781,9 +2680,9 @@ async function collectLogBundle(options) {
1781
2680
  const hostElapsedMs = Date.now() - captureStartedHostMs;
1782
2681
  if (deviceTimeBefore.status === "success" && hilog.status === "success" && deviceTimeAfter.status === "success") {
1783
2682
  const [beforeText, afterText, fullHilog] = await Promise.all([
1784
- fs4.readFile(path9.join(bundleDirectory, "device/device-time-before.txt"), "utf8"),
1785
- fs4.readFile(path9.join(bundleDirectory, "device/device-time-after.txt"), "utf8"),
1786
- fs4.readFile(path9.join(bundleDirectory, "logs/hilog.txt"), "utf8")
2683
+ fs6.readFile(path13.join(bundleDirectory, "device/device-time-before.txt"), "utf8"),
2684
+ fs6.readFile(path13.join(bundleDirectory, "device/device-time-after.txt"), "utf8"),
2685
+ fs6.readFile(path13.join(bundleDirectory, "logs/hilog.txt"), "utf8")
1787
2686
  ]);
1788
2687
  const window = filterHilogWindow(fullHilog, beforeText, afterText, durationSeconds, hostElapsedMs);
1789
2688
  if (window.text === null) durationFailureReason = window.reason;
@@ -1811,7 +2710,7 @@ async function collectLogBundle(options) {
1811
2710
  } else if (boundedHilog !== null && (options.pid || options.tag || options.level)) {
1812
2711
  const filtered = filterHilog(boundedHilog, { pid: options.pid, tag: options.tag, level: options.level });
1813
2712
  if (filtered) {
1814
- await fs4.writeFile(path9.join(bundleDirectory, "logs/hilog-filtered.txt"), filtered.text, { encoding: "utf8", mode: 384 });
2713
+ await fs6.writeFile(path13.join(bundleDirectory, "logs/hilog-filtered.txt"), filtered.text, { encoding: "utf8", mode: 384 });
1815
2714
  const status = manifest.filterStatus;
1816
2715
  for (const key of filtered.applied) status[key] = "applied";
1817
2716
  } else {
@@ -1827,11 +2726,11 @@ async function collectLogBundle(options) {
1827
2726
  manifest.collectionEndUtc = ended.toISOString();
1828
2727
  const files = records.flatMap((record) => [record.output, record.stderr].filter((file) => Boolean(file)));
1829
2728
  files.push("manifest.json", "notes.txt");
1830
- if (await fs4.access(path9.join(bundleDirectory, "logs/hilog-filtered.txt")).then(() => true, () => false)) {
2729
+ if (await fs6.access(path13.join(bundleDirectory, "logs/hilog-filtered.txt")).then(() => true, () => false)) {
1831
2730
  files.push("logs/hilog-filtered.txt");
1832
2731
  }
1833
2732
  manifest.files = files;
1834
- await fs4.writeFile(path9.join(bundleDirectory, "notes.txt"), "Add reproduction steps and precise event timestamps here.\n", { encoding: "utf8", mode: 384 });
2733
+ await fs6.writeFile(path13.join(bundleDirectory, "notes.txt"), "Add reproduction steps and precise event timestamps here.\n", { encoding: "utf8", mode: 384 });
1835
2734
  const durationFailure = !durationApplied;
1836
2735
  const criticalFailure = Boolean(targetError) || records.some((record) => record.name === "hilog-snapshot" && record.status !== "success");
1837
2736
  const anyCommandFailure = records.some((record) => record.status !== "success");
@@ -1842,16 +2741,16 @@ async function collectLogBundle(options) {
1842
2741
  if (options.redact) {
1843
2742
  redactedDirectory = `${bundleDirectory}-redacted`;
1844
2743
  manifest.redactionStatus = replacementWarning ? "review-required" : "success";
1845
- manifest.archiveStatus = { status: "success", path: `${path9.basename(redactedDirectory)}.zip` };
1846
- await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
2744
+ manifest.archiveStatus = { status: "success", path: `${path13.basename(redactedDirectory)}.zip` };
2745
+ await fs6.writeFile(path13.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1847
2746
  `, { encoding: "utf8", mode: 384 });
1848
2747
  await redactTree(bundleDirectory, redactedDirectory, serials, boundedHilog);
1849
2748
  archivePath = `${redactedDirectory}.zip`;
1850
2749
  await writeZip(redactedDirectory, archivePath);
1851
2750
  } else {
1852
2751
  archivePath = `${bundleDirectory}.zip`;
1853
- manifest.archiveStatus = { status: "success", path: path9.basename(archivePath) };
1854
- await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
2752
+ manifest.archiveStatus = { status: "success", path: path13.basename(archivePath) };
2753
+ await fs6.writeFile(path13.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1855
2754
  `, { encoding: "utf8", mode: 384 });
1856
2755
  await writeZip(bundleDirectory, archivePath);
1857
2756
  }
@@ -1861,9 +2760,9 @@ async function collectLogBundle(options) {
1861
2760
  manifest.redactionStatus = options.redact ? "failed" : "not-requested";
1862
2761
  manifest.archiveStatus = { status: "failed", path: null };
1863
2762
  manifest.notes.push(bundleError);
1864
- await fs4.rm(`${redactedDirectory ?? bundleDirectory}.zip`, { force: true }).catch(() => void 0);
1865
- if (redactedDirectory) await fs4.rm(redactedDirectory, { recursive: true, force: true }).catch(() => void 0);
1866
- await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
2763
+ await fs6.rm(`${redactedDirectory ?? bundleDirectory}.zip`, { force: true }).catch(() => void 0);
2764
+ if (redactedDirectory) await fs6.rm(redactedDirectory, { recursive: true, force: true }).catch(() => void 0);
2765
+ await fs6.writeFile(path13.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1867
2766
  `, { encoding: "utf8", mode: 384 });
1868
2767
  redactedDirectory = null;
1869
2768
  }
@@ -1895,7 +2794,7 @@ function parseHilogTimestamp(datePart, timePart, referenceMs) {
1895
2794
  const closest = candidates[0];
1896
2795
  return closest !== void 0 && closest <= referenceMs + 2e3 ? closest : null;
1897
2796
  }
1898
- function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationSeconds, hostElapsedMs) {
2797
+ function filterHilogWindow(input2, deviceClockBefore, deviceClockAfter, durationSeconds, hostElapsedMs) {
1899
2798
  if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
1900
2799
  return { text: null, reason: "requested duration is invalid" };
1901
2800
  }
@@ -1911,7 +2810,7 @@ function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationS
1911
2810
  let sawRecord = false;
1912
2811
  let keep = false;
1913
2812
  let output = "";
1914
- for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
2813
+ for (const line of input2.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
1915
2814
  const content = line.replace(/[\r\n]+$/, "");
1916
2815
  const match = HILOG_RECORD.exec(content);
1917
2816
  if (match?.groups) {
@@ -1936,6 +2835,53 @@ function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationS
1936
2835
  return sawRecord ? { text: output } : { text: null, reason: "no recognized timestamped HiLog records were found" };
1937
2836
  }
1938
2837
 
2838
+ // src/lib/crashReport.ts
2839
+ import * as fs7 from "fs";
2840
+ import * as path14 from "path";
2841
+ import { getBundleName as getBundleName2 } from "@oniroproject/core";
2842
+ function parseCrashSince(value) {
2843
+ if (value === void 0) return void 0;
2844
+ const trimmed = value.trim();
2845
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
2846
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
2847
+ const date = new Date(trimmed);
2848
+ if (!Number.isNaN(date.getTime())) return date;
2849
+ }
2850
+ throw new Error(`--since takes a number of seconds or an ISO 8601 time (2026-11-05T12:30:00+01:00), not "${value}".`);
2851
+ }
2852
+ function projectBundle(dir) {
2853
+ if (!fs7.existsSync(path14.join(dir, "AppScope", "app.json5"))) return void 0;
2854
+ try {
2855
+ return getBundleName2(dir);
2856
+ } catch {
2857
+ return void 0;
2858
+ }
2859
+ }
2860
+ function formatCrashVerdict(v) {
2861
+ const lines = [];
2862
+ const kind = v.subtype ? `${v.kind} (${v.subtype})` : v.kind;
2863
+ const who = [v.bundle ?? v.process, v.pid !== null ? `pid ${v.pid}` : null].filter(Boolean).join(" ");
2864
+ lines.push(`${v.crashed ? "CRASH" : "NO CRASH"} ${kind}, ${v.confidence} confidence${who ? `: ${who}` : ""}${v.time ? ` at ${v.time}` : ""}`);
2865
+ if (v.jsError) lines.push(`${v.jsError.name}: ${v.jsError.message}`);
2866
+ else if (v.reason) lines.push(`Reason: ${v.reason}`);
2867
+ if (v.lastFatalMessage) lines.push(`Last fatal message: ${v.lastFatalMessage}`);
2868
+ if (v.file) lines.push(`At ${v.file}:${v.line}:${v.column}`);
2869
+ if (v.topFrame) lines.push(`Top frame: ${v.topFrame}`);
2870
+ if (v.projectFrame && v.projectFrame !== v.topFrame) lines.push(`Project frame: ${v.projectFrame}`);
2871
+ const shown = v.stack.slice(0, 12);
2872
+ if (shown.length > 0) lines.push("Stack:", ...shown.map((f) => ` ${f}`));
2873
+ if (v.stack.length > shown.length) lines.push(` \u2026 ${v.stack.length - shown.length} more`);
2874
+ for (const edge of v.binderChain) {
2875
+ lines.push(`Binder: ${edge.from} -> ${edge.to} waits ${edge.waitSeconds.toFixed(2)} s${edge.frzState ? ` frz_state:${edge.frzState}` : ""}`);
2876
+ }
2877
+ for (const cycle of v.deadlocks) lines.push(`Binder deadlock: ${cycle}`);
2878
+ for (const note of v.notes) lines.push(`Note: ${note}`);
2879
+ if (v.path) lines.push(`Faultlog: ${v.path}`);
2880
+ if (v.rawPath && v.rawPath !== v.path) lines.push(`Saved to: ${v.rawPath}`);
2881
+ return `${lines.join("\n")}
2882
+ `;
2883
+ }
2884
+
1939
2885
  // src/commands/logs.ts
1940
2886
  function registerLogsCommand(program) {
1941
2887
  const logs = program.command("logs").description(
@@ -1943,7 +2889,7 @@ function registerLogsCommand(program) {
1943
2889
  ).option("-o, --output <file>", "File to append the lines to (required).").option("--for <ms>", "Stop after this many ms (default: run until interrupted).").option("--grep <regex>", "Keep only lines matching this regex (default: all).").option("--clear", "Clear the device log buffer first (hilog -r), so the file covers only this session.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--device <serial>", "Target device serial.").action(async (opts) => {
1944
2890
  if (!opts.output) logs.error("error: required option '-o, --output <file>' not specified");
1945
2891
  const { config, logger } = getRuntime();
1946
- const file = path10.resolve(opts.output);
2892
+ const file = path15.resolve(opts.output);
1947
2893
  const abort = new AbortController();
1948
2894
  const stop = () => abort.abort();
1949
2895
  process.once("SIGINT", stop);
@@ -1964,7 +2910,7 @@ function registerLogsCommand(program) {
1964
2910
  process.stdout.write(`${result.file}
1965
2911
  `);
1966
2912
  });
1967
- logs.command("collect").description("Create a diagnostic bundle from a connected device.").allowExcessArguments(false).option("-o, --output <directory>", "Parent directory for incident bundles.", path10.join(os2.homedir(), ".oniro", "incident-bundles")).option("--device <serial>", "Target device serial.").option("--package <bundle>", "Collect faultlog entries for this application bundle.").option("--pid <pid>", "Create a filtered HiLog view for this process.").option("--tag <tag>", "Create a filtered HiLog view for this tag.").option("--level <level>", "Create a filtered HiLog view for this level (V/D/I/W/E/F).").option("--duration <seconds>", "Keep HiLog records from the last N seconds (default 600).", "600").option("--redact", "Redact sensitive values and archive only the redacted bundle.").option("--json", "Emit bundle paths and status as JSON.").action(async (opts) => {
2913
+ logs.command("collect").description("Create a diagnostic bundle from a connected device.").allowExcessArguments(false).option("-o, --output <directory>", "Parent directory for incident bundles.", path15.join(os3.homedir(), ".oniro", "incident-bundles")).option("--device <serial>", "Target device serial.").option("--package <bundle>", "Collect faultlog entries for this application bundle.").option("--pid <pid>", "Create a filtered HiLog view for this process.").option("--tag <tag>", "Create a filtered HiLog view for this tag.").option("--level <level>", "Create a filtered HiLog view for this level (V/D/I/W/E/F).").option("--duration <seconds>", "Keep HiLog records from the last N seconds (default 600).", "600").option("--redact", "Redact sensitive values and archive only the redacted bundle.").option("--json", "Emit bundle paths and status as JSON.").action(async (opts) => {
1968
2914
  const { config } = getRuntime();
1969
2915
  const result = await collectLogBundle({
1970
2916
  config,
@@ -1985,6 +2931,34 @@ function registerLogsCommand(program) {
1985
2931
  throw new Error(`Bundle is partial; inspect manifest.json. ${result.error ?? "One or more collectors failed."}`);
1986
2932
  }
1987
2933
  });
2934
+ logs.command("crash").description(
2935
+ "Fetch the newest faultlog of an app from the device and say what happened: a native crash (cpp_crash), an uncaught ArkTS/JS error (js_crash), a freeze (app_freeze), or none. Reports the reason, the top frame, the first frame in the project's own code with file:line:column, the JS error and its stack, the native frames and, for a freeze, the binder chain. The bundle defaults to the project in the current directory, else any app. Lists through `hidumper -s 1201 -a \"-p Faultlogger\"`, and also reads faultloggerd's native crash records that hiview did not turn into a faultlog. Exits 0 whether or not there was a crash; check `crashed`."
2936
+ ).allowExcessArguments(false).option("--bundle <bundle>", "App to look for (default: the bundleName of the project in the current directory, else any process).").option("--since <s|ISO>", "Only faults from the last N seconds (device clock), or since an ISO 8601 time.").option("--wait <ms>", "Keep looking this long when there is no fault yet; hiview writes the faultlog a few seconds after the process dies (default: look once).").option("--path <file>", "Analyse this local faultlog (or faultloggerd .json crash record) instead of fetching one.").option("-o, --output <directory>", "Where to save the fetched faultlog.", path15.join(os3.homedir(), ".oniro", "faultlogs")).option("--device <serial>", "Target device serial.").option(
2937
+ "--json",
2938
+ "Print the verdict as JSON: crashed, kind, subtype, confidence, bundle, pid, time, reason, topFrame, projectFrame, file, line, column, jsError {name, message, stack}, stack, nativeFrames, jsFrames, binderChain, deadlocks, notes, name, path, rawPath."
2939
+ ).action(async (opts) => {
2940
+ const since = parseCrashSince(opts.since);
2941
+ const waitMs = opts.wait !== void 0 ? Number(opts.wait) : 0;
2942
+ if (!Number.isFinite(waitMs) || waitMs < 0) throw new Error(`--wait takes a number of milliseconds, not "${opts.wait}".`);
2943
+ let verdict;
2944
+ const bundle = opts.bundle ?? (opts.path ? void 0 : projectBundle(process.cwd()));
2945
+ if (opts.path) {
2946
+ const file = path15.resolve(opts.path);
2947
+ verdict = analyzeFaultLog(await fs8.readFile(file, "utf8"), { name: path15.basename(file), path: file, rawPath: file });
2948
+ } else {
2949
+ const { config, logger } = getRuntime();
2950
+ logger.info(`Looking for the newest fault of ${bundle ?? "any process"}${opts.since ? ` since ${opts.since}` : ""}...`);
2951
+ verdict = await latestCrash2({ config, bundle, since, waitMs, deviceSerial: opts.device, saveDir: opts.output, logger });
2952
+ }
2953
+ if (opts.json) {
2954
+ const none = { crashed: false, kind: null, bundle: bundle ?? null, since: since instanceof Date ? since.toISOString() : since ?? null };
2955
+ process.stdout.write(`${JSON.stringify(verdict ?? none, null, 2)}
2956
+ `);
2957
+ } else {
2958
+ process.stdout.write(verdict ? formatCrashVerdict(verdict) : `No fault for ${bundle ?? "any process"}${opts.since ? ` since ${opts.since}` : ""}.
2959
+ `);
2960
+ }
2961
+ });
1988
2962
  }
1989
2963
 
1990
2964
  // src/commands/hdc.ts
@@ -2003,58 +2977,1807 @@ function registerHdcCommand(program) {
2003
2977
  `);
2004
2978
  return;
2005
2979
  }
2006
- await new Promise((resolve10, reject) => {
2980
+ await new Promise((resolve20, reject) => {
2007
2981
  const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
2008
2982
  child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
2009
2983
  child.on("close", (code) => {
2010
2984
  process.exitCode = code ?? 1;
2011
- resolve10();
2985
+ resolve20();
2012
2986
  });
2013
2987
  });
2014
2988
  });
2015
2989
  }
2016
2990
 
2017
- // src/bin/oniro-app.ts
2018
- var CLI_VERSION = "0.16.1";
2019
- function buildProgram() {
2020
- const program = new Command4().name("oniro-app").description(
2021
- "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."
2022
- ).version(CLI_VERSION).showHelpAfterError().enablePositionalOptions();
2023
- registerSdkCommand(program);
2024
- registerCmdToolsCommand(program);
2025
- registerEmulatorCommand(program);
2026
- registerBuildCommand(program);
2027
- registerSignCommand(program);
2028
- registerAuthCommand(program);
2029
- registerAppCommand(program);
2030
- registerCreateCommand(program);
2031
- registerTemplatesCommand(program);
2032
- registerRebootCommand(program);
2033
- registerWaitCommand(program);
2034
- registerWatchCommand(program);
2035
- registerScreenshotCommand(program);
2036
- registerDumpCommand(program);
2037
- registerDevicesCommand(program);
2038
- registerFileCommand(program);
2039
- registerLintCommand(program);
2040
- registerInputCommand(program);
2041
- registerLogsCommand(program);
2042
- registerHdcCommand(program);
2043
- return program;
2991
+ // src/commands/api.ts
2992
+ import * as path16 from "path";
2993
+ import {
2994
+ findApiInOtherSdks,
2995
+ getApiIndexCacheDir,
2996
+ loadApiIndex,
2997
+ resolveApiSdk,
2998
+ searchApi,
2999
+ showApi,
3000
+ suggestApiSymbols,
3001
+ OniroError as OniroError4
3002
+ } from "@oniroproject/core";
3003
+ var KINDS = [
3004
+ "namespace",
3005
+ "class",
3006
+ "interface",
3007
+ "enum",
3008
+ "enumMember",
3009
+ "type",
3010
+ "function",
3011
+ "method",
3012
+ "constructor",
3013
+ "property",
3014
+ "variable",
3015
+ "component",
3016
+ "decorator",
3017
+ "macro",
3018
+ "typedef",
3019
+ "struct"
3020
+ ];
3021
+ function parseApi(value) {
3022
+ if (value === void 0) return void 0;
3023
+ const n = Number(value);
3024
+ if (!Number.isInteger(n) || n <= 0) throw new OniroError4(`--api expects an API level such as 20, got "${value}".`);
3025
+ return n;
2044
3026
  }
2045
- async function main() {
2046
- const program = buildProgram();
2047
- try {
2048
- await program.parseAsync(process.argv);
2049
- } catch (err) {
2050
- const message = err instanceof OniroError3 ? err.message : err instanceof Error ? err.message : String(err);
2051
- process.stderr.write(`[error] ${message}
3027
+ function parseKinds(values) {
3028
+ if (!values?.length) return void 0;
3029
+ const out = [];
3030
+ for (const v of values.flatMap((x) => x.split(","))) {
3031
+ const k = KINDS.find((kind) => kind.toLowerCase() === v.trim().toLowerCase());
3032
+ if (!k) throw new OniroError4(`Unknown --kind "${v}". Kinds: ${KINDS.join(", ")}.`);
3033
+ out.push(k);
3034
+ }
3035
+ return out;
3036
+ }
3037
+ function selectSdk(opts) {
3038
+ const { config, logger } = getRuntime();
3039
+ const selection = resolveApiSdk(config, {
3040
+ sdkDir: opts.sdk,
3041
+ api: parseApi(opts.api),
3042
+ projectDir: opts.project ? path16.resolve(opts.project) : process.cwd()
3043
+ });
3044
+ if (selection.note) logger.warn(selection.note);
3045
+ return selection;
3046
+ }
3047
+ function openIndex(sdkDir, rebuild) {
3048
+ const { config, logger } = getRuntime();
3049
+ return loadApiIndex(sdkDir, { cacheDir: getApiIndexCacheDir(config), rebuild, logger });
3050
+ }
3051
+ function describeIndex(index) {
3052
+ const api = index.api !== void 0 ? `API ${index.api}` : index.sdkDir;
3053
+ return index.sdkVersion ? `${api} (${index.sdkVersion})` : api;
3054
+ }
3055
+ function apiEntryFacts(e) {
3056
+ const parts = [e.kind];
3057
+ if (e.kit) parts.push(e.kit);
3058
+ if (e.since !== void 0) parts.push(`since ${e.sinceText ?? e.since}`);
3059
+ if (e.deprecated !== void 0) {
3060
+ const dep = e.deprecated ? `deprecated ${e.deprecated}` : "deprecated";
3061
+ parts.push(e.useInstead ? `${dep}, use ${e.useInstead}` : dep);
3062
+ }
3063
+ if (e.systemApi) parts.push("systemapi");
3064
+ return parts.join(" ");
3065
+ }
3066
+ function formatApiEntry(e, sdkDir) {
3067
+ const lines = [`${e.qualifiedName} ${apiEntryFacts(e)}`];
3068
+ lines.push(` ${e.signature}${e.overloads ? ` (+${e.overloads} overload${e.overloads > 1 ? "s" : ""})` : ""}`);
3069
+ if (e.doc) lines.push(` ${e.doc}`);
3070
+ const where = [`${path16.join(sdkDir, e.file)}:${e.line}`, e.module];
3071
+ if (e.import) where.push(e.import);
3072
+ else if (e.global) where.push("global, no import");
3073
+ if (e.syscap) where.push(e.syscap);
3074
+ lines.push(` ${where.join(" ")}`);
3075
+ return lines.join("\n");
3076
+ }
3077
+ function jsonEntry(e, sdkDir) {
3078
+ return { ...e, path: path16.join(sdkDir, e.file) };
3079
+ }
3080
+ function sdkJson(index, sel) {
3081
+ return { dir: index.sdkDir, api: index.api, version: index.sdkVersion, source: sel.source };
3082
+ }
3083
+ function runApiSearch(query, opts) {
3084
+ const sel = selectSdk(opts);
3085
+ const { index } = openIndex(sel.sdkDir, opts.rebuild);
3086
+ const lang = opts.lang?.toLowerCase();
3087
+ if (lang && lang !== "arkts" && lang !== "c") throw new OniroError4(`--lang expects arkts or c, got "${opts.lang}".`);
3088
+ const limit = Number(opts.limit ?? 20);
3089
+ if (!Number.isInteger(limit) || limit <= 0) throw new OniroError4(`--limit expects a positive number, got "${opts.limit}".`);
3090
+ const result = searchApi(index, query, {
3091
+ kit: opts.kit,
3092
+ kinds: parseKinds(opts.kind),
3093
+ language: lang,
3094
+ noDeprecated: opts.deprecated === false,
3095
+ noSystemApi: opts.systemapi === false,
3096
+ limit
3097
+ });
3098
+ const json = {
3099
+ sdk: sdkJson(index, sel),
3100
+ query: result.query,
3101
+ total: result.total,
3102
+ hits: result.hits.map((h) => jsonEntry(h, index.sdkDir))
3103
+ };
3104
+ return { json, index, hits: result.hits, total: result.total };
3105
+ }
3106
+ function runApiShow(symbol, opts) {
3107
+ const sel = selectSdk(opts);
3108
+ const { index } = openIndex(sel.sdkDir, opts.rebuild);
3109
+ const r = showApi(index, symbol, { kit: opts.kit });
3110
+ let alsoIn = [];
3111
+ let suggestions = [];
3112
+ if (!r.found) {
3113
+ const { config, logger } = getRuntime();
3114
+ if (opts.otherSdks !== false) {
3115
+ alsoIn = findApiInOtherSdks(config, symbol, index.sdkDir, {
3116
+ cacheDir: getApiIndexCacheDir(config),
3117
+ kit: opts.kit,
3118
+ logger
3119
+ });
3120
+ }
3121
+ suggestions = suggestApiSymbols(index, symbol);
3122
+ }
3123
+ const json = {
3124
+ sdk: sdkJson(index, sel),
3125
+ symbol,
3126
+ found: r.found,
3127
+ entries: r.entries.map((e) => jsonEntry(e, index.sdkDir))
3128
+ };
3129
+ if (!r.found) {
3130
+ json.alsoIn = alsoIn;
3131
+ json.suggestions = suggestions;
3132
+ }
3133
+ return { json, index, found: r.found, entries: r.entries, alsoIn, suggestions };
3134
+ }
3135
+ function registerApiCommand(program) {
3136
+ const api = program.command("api").description(
3137
+ "Look up the SDK's declarations: the ArkTS API (`ets/api`, `ets/arkts`, the ArkUI components in `ets/component`, with the kit that exports each symbol) and the Node-API C headers, at one API level. The index is built on first use (a few seconds) and cached under ~/.oniro/cache (ONIRO_CACHE_DIR)."
3138
+ );
3139
+ const sdkOptions = (cmd) => cmd.option(
3140
+ "--api <level>",
3141
+ "The SDK API level to read. Default: the project's compileSdkVersion (from the current directory or --project), else the newest installed SDK."
3142
+ ).option("--sdk <dir>", "An SDK API directory (holding ets/) to read instead.").option("--project <dir>", "The project whose compileSdkVersion picks the SDK (default: the current directory).").option("--rebuild", "Rebuild the index even if the cached one is current.");
3143
+ sdkOptions(
3144
+ api.command("search <query...>").description(
3145
+ "Ranked search: every word must match a symbol name (exact name > prefix > camelCase part > substring) or its doc line. A dotted word matches qualified names (`router.pushUrl`); a leading @ is ignored (`@Local`)."
3146
+ )
3147
+ ).option("--kit <name>", "Only symbols this kit exports (`ArkUI` or `@kit.ArkUI`).").option("--kind <kinds...>", `Only these kinds: ${KINDS.join(", ")}.`).option("--lang <lang>", "Only `arkts` declarations or `c` (Node-API headers).").option("--limit <n>", "At most n hits.", "20").option("--no-deprecated", "Leave out @deprecated symbols.").option("--no-systemapi", "Leave out @systemapi symbols (system apps only).").option(
3148
+ "--json",
3149
+ "Print JSON: sdk (dir, api, version, source), query, total, hits (name, qualifiedName, kind, kit, import, module, since, deprecated, deprecatedSince, useInstead, syscap, systemApi, file, path, line, signature, doc, overloads, score)."
3150
+ ).action((words, opts) => {
3151
+ const { json, index, hits, total } = runApiSearch(words.join(" "), opts);
3152
+ if (opts.json) {
3153
+ process.stdout.write(`${JSON.stringify(json, null, 2)}
3154
+ `);
3155
+ } else {
3156
+ for (const h of hits) process.stdout.write(`${formatApiEntry(h, index.sdkDir)}
3157
+
3158
+ `);
3159
+ const shown = hits.length < total ? `${hits.length} of ${total}` : `${total}`;
3160
+ process.stdout.write(`${shown} match${total === 1 ? "" : "es"} in ${describeIndex(index)}.
3161
+ `);
3162
+ }
3163
+ process.exitCode = total ? 0 : 1;
3164
+ });
3165
+ sdkOptions(
3166
+ api.command("show <symbol>").description(
3167
+ "Exact lookup: is `symbol` declared at this API level? Takes a qualified name (`NavPathStack.pushPathByName`, `router.pushUrl`, `Column`, `@Local`, `napi_create_threadsafe_function`); when none matches, names that end with it (`Preferences.get`). Prints every declaration (overloads included). Exits 1 when it is not declared, naming the other installed SDKs that declare it and close matches."
3168
+ )
3169
+ ).option("--kit <name>", "Only declarations this kit exports.").option("--no-other-sdks", "On a miss, do not look in the other installed SDKs.").option(
3170
+ "--json",
3171
+ "Print JSON: sdk, symbol, found, entries (the `search` hit fields), and on a miss alsoIn ([{api, since}]) and suggestions (qualified names)."
3172
+ ).action((symbol, opts) => {
3173
+ const { json, index, found, entries, alsoIn, suggestions } = runApiShow(symbol, opts);
3174
+ if (opts.json) {
3175
+ process.stdout.write(`${JSON.stringify(json, null, 2)}
3176
+ `);
3177
+ } else if (found) {
3178
+ for (const e of entries) process.stdout.write(`${formatApiEntry(e, index.sdkDir)}
3179
+
3180
+ `);
3181
+ process.stdout.write(
3182
+ `${symbol} is declared in ${describeIndex(index)} (${entries.length} declaration${entries.length === 1 ? "" : "s"}).
3183
+ `
3184
+ );
3185
+ } else {
3186
+ process.stdout.write(`${symbol} is not declared in ${describeIndex(index)}.
2052
3187
  `);
2053
- if (process.env.ONIRO_DEBUG === "1" && err instanceof Error && err.stack) {
2054
- process.stderr.write(`${err.stack}
3188
+ for (const a of alsoIn) {
3189
+ process.stdout.write(` declared in the installed API ${a.api} SDK${a.since !== void 0 ? ` (@since ${a.since})` : ""}
2055
3190
  `);
3191
+ }
3192
+ if (suggestions.length) process.stdout.write(` close matches: ${suggestions.join(", ")}
3193
+ `);
3194
+ }
3195
+ process.exitCode = found ? 0 : 1;
3196
+ });
3197
+ sdkOptions(
3198
+ api.command("index").description("Build (or refresh) the index of one SDK and print its size, for warming a cache.")
3199
+ ).option("--json", "Print JSON: sdk, entries, files, buildMs, cached, cachePath, loadMs.").action((opts) => {
3200
+ const sel = selectSdk(opts);
3201
+ const loaded = openIndex(sel.sdkDir, opts.rebuild);
3202
+ const { index } = loaded;
3203
+ if (opts.json) {
3204
+ const out = {
3205
+ sdk: { dir: index.sdkDir, api: index.api, version: index.sdkVersion, source: sel.source },
3206
+ entries: index.entries.length,
3207
+ files: index.files,
3208
+ buildMs: index.buildMs,
3209
+ builtAt: index.builtAt,
3210
+ cached: loaded.cached,
3211
+ cachePath: loaded.cachePath,
3212
+ loadMs: loaded.loadMs
3213
+ };
3214
+ process.stdout.write(`${JSON.stringify(out, null, 2)}
3215
+ `);
3216
+ } else {
3217
+ process.stdout.write(
3218
+ `${describeIndex(index)}: ${index.entries.length} declarations from ${index.files} files, ${loaded.cached ? `cached (built ${index.builtAt})` : `built in ${index.buildMs} ms`}, ${loaded.cachePath}
3219
+ `
3220
+ );
3221
+ }
3222
+ });
3223
+ }
3224
+
3225
+ // src/commands/serve.ts
3226
+ import * as fs10 from "fs";
3227
+ import * as path19 from "path";
3228
+ import { OniroError as OniroError5 } from "@oniroproject/core";
3229
+
3230
+ // src/mcp/protocol.ts
3231
+ import * as readline2 from "readline";
3232
+
3233
+ // src/mcp/schema.ts
3234
+ function typeOk(type, v) {
3235
+ switch (type) {
3236
+ case void 0:
3237
+ return true;
3238
+ case "object":
3239
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3240
+ case "array":
3241
+ return Array.isArray(v);
3242
+ case "integer":
3243
+ return typeof v === "number" && Number.isInteger(v);
3244
+ case "number":
3245
+ return typeof v === "number" && Number.isFinite(v);
3246
+ default:
3247
+ return typeof v === type;
3248
+ }
3249
+ }
3250
+ function validateArgs(schema, value, at = "") {
3251
+ const where = at || "arguments";
3252
+ if (!typeOk(schema.type, value)) return [`${where} must be ${schema.type === "integer" ? "an integer" : `a ${schema.type}`}`];
3253
+ const out = [];
3254
+ if (schema.enum && !schema.enum.includes(value)) {
3255
+ out.push(`${where} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`);
3256
+ }
3257
+ if (typeof value === "number") {
3258
+ if (schema.minimum !== void 0 && value < schema.minimum) out.push(`${where} must be >= ${schema.minimum}`);
3259
+ if (schema.maximum !== void 0 && value > schema.maximum) out.push(`${where} must be <= ${schema.maximum}`);
3260
+ }
3261
+ if (Array.isArray(value)) {
3262
+ if (schema.minItems !== void 0 && value.length < schema.minItems) {
3263
+ out.push(`${where} needs at least ${schema.minItems} item${schema.minItems === 1 ? "" : "s"}`);
3264
+ }
3265
+ if (schema.items) value.forEach((item, i) => out.push(...validateArgs(schema.items, item, `${where}[${i}]`)));
3266
+ }
3267
+ if (schema.type === "object" && typeof value === "object" && value !== null) {
3268
+ const obj = value;
3269
+ const props = schema.properties ?? {};
3270
+ for (const key of schema.required ?? []) {
3271
+ if (obj[key] === void 0 || obj[key] === null) out.push(`${at ? `${at}.` : ""}${key} is required`);
3272
+ }
3273
+ for (const [key, v] of Object.entries(obj)) {
3274
+ const prop = props[key];
3275
+ if (!prop) {
3276
+ if (schema.additionalProperties === false) {
3277
+ out.push(`unknown property ${at ? `${at}.` : ""}${key} (expected: ${Object.keys(props).join(", ") || "none"})`);
3278
+ }
3279
+ continue;
3280
+ }
3281
+ if (v === void 0 || v === null) continue;
3282
+ out.push(...validateArgs(prop, v, `${at ? `${at}.` : ""}${key}`));
3283
+ }
3284
+ }
3285
+ return out;
3286
+ }
3287
+ function applyDefaults(schema, args) {
3288
+ const out = {};
3289
+ for (const [key, v] of Object.entries(args)) if (v !== null && v !== void 0) out[key] = v;
3290
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
3291
+ if (out[key] === void 0 && prop.default !== void 0) out[key] = prop.default;
3292
+ }
3293
+ return out;
3294
+ }
3295
+
3296
+ // src/mcp/protocol.ts
3297
+ var SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
3298
+ var STRUCTURED_CONTENT_SINCE = "2025-06-18";
3299
+ var RPC_PARSE_ERROR = -32700;
3300
+ var RPC_INVALID_REQUEST = -32600;
3301
+ var RPC_METHOD_NOT_FOUND = -32601;
3302
+ var RPC_INVALID_PARAMS = -32602;
3303
+ var RPC_INTERNAL_ERROR = -32603;
3304
+ function rpcError(id, code, message2) {
3305
+ return { jsonrpc: "2.0", id, error: { code, message: message2 } };
3306
+ }
3307
+ function isObject(v) {
3308
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3309
+ }
3310
+ function errorResult(message2, structured) {
3311
+ return {
3312
+ content: [{ type: "text", text: message2 }],
3313
+ ...structured ? { structuredContent: structured } : {},
3314
+ isError: true
3315
+ };
3316
+ }
3317
+ var McpServer = class {
3318
+ constructor(opts) {
3319
+ this.opts = opts;
3320
+ for (const t of opts.tools) this.tools.set(t.name, t);
3321
+ }
3322
+ opts;
3323
+ tools = /* @__PURE__ */ new Map();
3324
+ protocolVersion;
3325
+ /** The version agreed in `initialize`, or undefined before it. */
3326
+ get negotiatedVersion() {
3327
+ return this.protocolVersion;
3328
+ }
3329
+ /**
3330
+ * Handle one parsed JSON-RPC message (or a batch, which the 2025-03-26
3331
+ * revision allows). Returns the response to send, or undefined for a
3332
+ * notification.
3333
+ */
3334
+ async handle(message2) {
3335
+ if (Array.isArray(message2)) {
3336
+ if (message2.length === 0) return rpcError(null, RPC_INVALID_REQUEST, "Empty batch.");
3337
+ const out = (await Promise.all(message2.map((m) => this.handleOne(m)))).filter(
3338
+ (r) => r !== void 0
3339
+ );
3340
+ return out.length ? out : void 0;
3341
+ }
3342
+ return this.handleOne(message2);
3343
+ }
3344
+ async handleOne(msg) {
3345
+ if (!isObject(msg) || msg.jsonrpc !== "2.0") {
3346
+ return rpcError(isObject(msg) && isId(msg.id) ? msg.id : null, RPC_INVALID_REQUEST, "Not a JSON-RPC 2.0 message.");
3347
+ }
3348
+ const hasId = "id" in msg;
3349
+ if (typeof msg.method !== "string") {
3350
+ if (hasId && ("result" in msg || "error" in msg)) return void 0;
3351
+ return rpcError(isId(msg.id) ? msg.id : null, RPC_INVALID_REQUEST, "Missing method.");
3352
+ }
3353
+ if (!hasId) {
3354
+ this.onNotification(msg.method);
3355
+ return void 0;
3356
+ }
3357
+ if (!isId(msg.id) || msg.id === null) return rpcError(null, RPC_INVALID_REQUEST, "The id must be a string or a number.");
3358
+ const id = msg.id;
3359
+ const params = msg.params === void 0 ? {} : msg.params;
3360
+ if (!isObject(params)) return rpcError(id, RPC_INVALID_PARAMS, "params must be an object.");
3361
+ try {
3362
+ return await this.dispatch(id, msg.method, params);
3363
+ } catch (err) {
3364
+ this.opts.logger.error(`mcp: ${msg.method} failed: ${err.stack ?? String(err)}`);
3365
+ return rpcError(id, RPC_INTERNAL_ERROR, err instanceof Error ? err.message : String(err));
3366
+ }
3367
+ }
3368
+ onNotification(method) {
3369
+ if (method === "notifications/initialized") this.opts.logger.debug("mcp: client initialized");
3370
+ }
3371
+ async dispatch(id, method, params) {
3372
+ const ok = (result) => ({ jsonrpc: "2.0", id, result });
3373
+ if (method === "initialize") return ok(this.initialize(params));
3374
+ if (method === "ping") return ok({});
3375
+ if (this.protocolVersion === void 0) {
3376
+ return rpcError(id, RPC_INVALID_REQUEST, `Received ${method} before initialize.`);
3377
+ }
3378
+ if (method === "tools/list") {
3379
+ return ok({
3380
+ tools: [...this.tools.values()].map((t) => ({
3381
+ name: t.name,
3382
+ ...t.title ? { title: t.title } : {},
3383
+ description: t.description,
3384
+ inputSchema: t.inputSchema,
3385
+ ...t.annotations ? { annotations: t.annotations } : {}
3386
+ }))
3387
+ });
3388
+ }
3389
+ if (method === "tools/call") {
3390
+ const name = params.name;
3391
+ if (typeof name !== "string") return rpcError(id, RPC_INVALID_PARAMS, "tools/call needs a tool name.");
3392
+ const tool = this.tools.get(name);
3393
+ if (!tool) return rpcError(id, RPC_INVALID_PARAMS, `Unknown tool: ${name}. Tools: ${[...this.tools.keys()].join(", ")}.`);
3394
+ return ok(this.shape(await this.callTool(tool, params.arguments)));
3395
+ }
3396
+ return rpcError(id, RPC_METHOD_NOT_FOUND, `Method not found: ${method}`);
3397
+ }
3398
+ initialize(params) {
3399
+ const asked = typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
3400
+ const version = asked && SUPPORTED_PROTOCOL_VERSIONS.includes(asked) ? asked : SUPPORTED_PROTOCOL_VERSIONS[0];
3401
+ this.protocolVersion = version;
3402
+ const client = isObject(params.clientInfo) ? `${params.clientInfo.name ?? "?"} ${params.clientInfo.version ?? ""}` : "client";
3403
+ this.opts.logger.info(`mcp: ${client.trim()} connected (protocol ${version}, asked ${asked ?? "none"})`);
3404
+ return {
3405
+ protocolVersion: version,
3406
+ capabilities: { tools: { listChanged: false } },
3407
+ serverInfo: { name: this.opts.name, version: this.opts.version },
3408
+ ...this.opts.instructions ? { instructions: this.opts.instructions } : {}
3409
+ };
3410
+ }
3411
+ /** Validate, fill defaults, run. A tool that throws or gets bad arguments yields an `isError` result, not an RPC error. */
3412
+ async callTool(tool, rawArgs) {
3413
+ const args = rawArgs === void 0 || rawArgs === null ? {} : rawArgs;
3414
+ if (!isObject(args)) return errorResult(`Invalid arguments for ${tool.name}: arguments must be an object.`);
3415
+ const problems = validateArgs(tool.inputSchema, args);
3416
+ if (problems.length) return errorResult(`Invalid arguments for ${tool.name}: ${problems.join("; ")}.`);
3417
+ const started = Date.now();
3418
+ this.opts.logger.info(`mcp: ${tool.name} ${JSON.stringify(args)}`);
3419
+ try {
3420
+ const result = await tool.call(applyDefaults(tool.inputSchema, args), this.opts.context);
3421
+ this.opts.logger.info(`mcp: ${tool.name} ${result.isError ? "failed" : "done"} in ${Date.now() - started} ms`);
3422
+ return result;
3423
+ } catch (err) {
3424
+ const message2 = err instanceof Error ? err.message : String(err);
3425
+ this.opts.logger.info(`mcp: ${tool.name} threw in ${Date.now() - started} ms: ${message2}`);
3426
+ if (process.env.ONIRO_DEBUG === "1" && err instanceof Error && err.stack) this.opts.logger.debug(err.stack);
3427
+ return errorResult(message2, { error: message2 });
3428
+ }
3429
+ }
3430
+ /** Clients before 2025-06-18 know no `structuredContent`; the text block carries the same JSON. */
3431
+ shape(result) {
3432
+ if (result.structuredContent && (this.protocolVersion ?? "") < STRUCTURED_CONTENT_SINCE) {
3433
+ const { structuredContent: _dropped, ...rest } = result;
3434
+ return rest;
3435
+ }
3436
+ return result;
3437
+ }
3438
+ };
3439
+ function isId(v) {
3440
+ return typeof v === "string" || typeof v === "number" || v === null;
3441
+ }
3442
+ async function serveStdio(server, opts = {}) {
3443
+ const input2 = opts.input ?? process.stdin;
3444
+ const write = opts.write ?? ((line) => void process.stdout.write(line));
3445
+ const send = (msg) => write(`${JSON.stringify(msg)}
3446
+ `);
3447
+ const pending = /* @__PURE__ */ new Set();
3448
+ const rl = readline2.createInterface({ input: input2, crlfDelay: Infinity, terminal: false });
3449
+ for await (const line of rl) {
3450
+ const text = line.trim();
3451
+ if (!text) continue;
3452
+ let message2;
3453
+ try {
3454
+ message2 = JSON.parse(text);
3455
+ } catch {
3456
+ send(rpcError(null, RPC_PARSE_ERROR, "Parse error: each line must be one JSON-RPC message."));
3457
+ continue;
3458
+ }
3459
+ const p = server.handle(message2).then(
3460
+ (response) => {
3461
+ if (response !== void 0) send(response);
3462
+ },
3463
+ () => {
3464
+ }
3465
+ );
3466
+ pending.add(p);
3467
+ void p.finally(() => pending.delete(p));
3468
+ }
3469
+ await Promise.all(pending);
3470
+ }
3471
+
3472
+ // src/mcp/tools.ts
3473
+ import * as fs9 from "fs";
3474
+ import * as os4 from "os";
3475
+ import * as path18 from "path";
3476
+ import {
3477
+ analyzeFaultLog as analyzeFaultLog2,
3478
+ captureBurst as captureBurst2,
3479
+ describeLayoutNode as describeLayoutNode4,
3480
+ findLayoutNodes as findLayoutNodes2,
3481
+ listLayoutWindows as listLayoutWindows2,
3482
+ selectableTree as selectableTree2,
3483
+ diffLayouts as diffLayouts2,
3484
+ dumpLayout as dumpLayout3,
3485
+ inventoryScreens as inventoryScreens2,
3486
+ latestCrash as latestCrash3,
3487
+ locateTarget as locateTarget2,
3488
+ parseLayoutBaseline as parseLayoutBaseline2,
3489
+ parseSelector as parseSelector2,
3490
+ performInput as performInput2,
3491
+ runDoctor as runDoctor2,
3492
+ runTests as runTests2,
3493
+ summarizeLayout as summarizeLayout2,
3494
+ takeScreenshot as takeScreenshot2,
3495
+ waitForBoot as waitForBoot2,
3496
+ waitForBundle as waitForBundle2,
3497
+ waitForLog as waitForLog2,
3498
+ waitForTarget as waitForTarget2,
3499
+ DEFAULT_SMOKE_DELAY_MS as DEFAULT_SMOKE_DELAY_MS2,
3500
+ DEFAULT_TRACE_BUFFER_KB as DEFAULT_TRACE_BUFFER_KB2,
3501
+ DEFAULT_TRACE_CATEGORIES as DEFAULT_TRACE_CATEGORIES2,
3502
+ evaluatePerfGate as evaluatePerfGate2,
3503
+ perfTrace as perfTrace2,
3504
+ readFlowFile as readFlowFile2
3505
+ } from "@oniroproject/core";
3506
+
3507
+ // src/commands/perf.ts
3508
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
3509
+ import * as path17 from "path";
3510
+ import {
3511
+ DEFAULT_PERF_FLOW_TAIL_MS,
3512
+ DEFAULT_PERF_FOR_MS,
3513
+ DEFAULT_TRACE_BUFFER_KB,
3514
+ DEFAULT_TRACE_CATEGORIES,
3515
+ evaluatePerfGate,
3516
+ parseTraceFile,
3517
+ perfSliceFilter,
3518
+ perfTrace,
3519
+ readFlowFile,
3520
+ summarizeTrace
3521
+ } from "@oniroproject/core";
3522
+
3523
+ // src/lib/interrupt.ts
3524
+ async function withInterrupt(logger, what, fn) {
3525
+ const controller = new AbortController();
3526
+ const onSigint = () => {
3527
+ if (controller.signal.aborted) process.exit(130);
3528
+ logger.info(`${what}\u2026 (Ctrl+C again to quit at once)`);
3529
+ controller.abort();
3530
+ };
3531
+ process.on("SIGINT", onSigint);
3532
+ try {
3533
+ return await fn(controller.signal);
3534
+ } finally {
3535
+ process.off("SIGINT", onSigint);
3536
+ }
3537
+ }
3538
+
3539
+ // src/commands/perf.ts
3540
+ function positiveMs2(raw) {
3541
+ const n = Number(raw);
3542
+ if (!Number.isFinite(n) || n <= 0) throw new InvalidArgumentError5("Expected a positive number of milliseconds.");
3543
+ return Math.floor(n);
3544
+ }
3545
+ function rate(raw) {
3546
+ const n = Number(raw);
3547
+ if (!Number.isFinite(n) || n < 0 || n > 1) throw new InvalidArgumentError5("Expected a rate between 0 and 1, e.g. 0.05.");
3548
+ return n;
3549
+ }
3550
+ function positiveInt(raw) {
3551
+ const n = Number(raw);
3552
+ if (!Number.isInteger(n) || n <= 0) throw new InvalidArgumentError5("Expected a positive integer.");
3553
+ return n;
3554
+ }
3555
+ function positiveNumber(raw) {
3556
+ const n = Number(raw);
3557
+ if (!Number.isFinite(n) || n <= 0) throw new InvalidArgumentError5("Expected a positive number.");
3558
+ return n;
3559
+ }
3560
+ function list(raw) {
3561
+ return raw.split(/[,\s]+/).filter(Boolean);
3562
+ }
3563
+ function perfExitCode(report, gate) {
3564
+ if (!report.ok) return 2;
3565
+ return gate.passed ? 0 : 1;
3566
+ }
3567
+ var ms = (n) => `${Math.round(n * 10) / 10} ms`;
3568
+ function formatPerfReport(r, gate) {
3569
+ const lines = [
3570
+ `${r.bundle}${r.pid !== null ? ` (pid ${r.pid})` : ""}: ${(r.durationMs / 1e3).toFixed(1)} s captured` + (r.refreshRateHz ? `, display ${r.refreshRateHz} Hz (${ms(1e3 / r.refreshRateHz)} per frame)` : "")
3571
+ ];
3572
+ const f = r.frames;
3573
+ if (f) {
3574
+ lines.push(
3575
+ ` frames: ${f.total}, janky ${f.janky} (${(f.jankRate * 100).toFixed(1)}%), p50 ${ms(f.p50Ms)}, p95 ${ms(f.p95Ms)}, max ${ms(f.maxMs)}, ${f.missedVsyncs} vsync(s) missed`
3576
+ );
3577
+ if (f.longest.length) lines.push(` longest: ${f.longest.map((l) => `${ms(l.ms)} at +${(l.atMs / 1e3).toFixed(2)} s`).join(", ")}`);
3578
+ }
3579
+ if (r.startup) {
3580
+ const s = r.startup;
3581
+ lines.push(
3582
+ ` startup: cold ${ms(s.coldMs)} (${s.from} \u2192 ${s.to})` + (s.processMs !== null ? `, process up after ${ms(s.processMs)}` : "") + (s.firstFrameMs !== null ? `, first frame ${ms(s.firstFrameMs)}` : "")
3583
+ );
3584
+ }
3585
+ if (r.mainThread) {
3586
+ lines.push(` main thread: ${ms(r.mainThread.busyMs)} in traced work${r.mainThread.cpuMs !== null ? `, ${ms(r.mainThread.cpuMs)} on CPU` : ""}`);
3587
+ }
3588
+ for (const s of r.scenes) {
3589
+ const parts = [
3590
+ s.maxFrameMs !== void 0 ? `max frame ${s.maxFrameMs} ms` : "",
3591
+ s.maxHitchMs !== void 0 ? `max hitch ${s.maxHitchMs} ms` : "",
3592
+ s.hitchMsPerS !== void 0 ? `hitch ${Math.round(s.hitchMsPerS * 10) / 10} ms/s` : "",
3593
+ s.responseMs !== void 0 ? `response ${s.responseMs} ms` : ""
3594
+ ].filter(Boolean);
3595
+ lines.push(` RenderService ${s.scene}${s.page ? ` ${s.page}` : ""}: ${parts.join(", ")}`);
3596
+ }
3597
+ if (r.flow) {
3598
+ lines.push(
3599
+ r.flow.outcome === "passed" ? ` flow: passed (${r.flow.steps.length} step(s))` : ` flow: ${r.flow.outcome} at step ${r.flow.failedStep ?? "?"} (${r.flow.reason ?? r.flow.error ?? ""}): ${r.flow.message ?? ""}`
3600
+ );
3601
+ }
3602
+ if (r.hiperf) {
3603
+ if ("error" in r.hiperf) lines.push(` hiperf: ${r.hiperf.error}`);
3604
+ else {
3605
+ lines.push(` hiperf: ${r.hiperf.samples} sample(s)${r.hiperf.event ? ` of ${r.hiperf.event}` : ""}${r.hiperf.data ? `, data ${r.hiperf.data}` : ""}`);
3606
+ for (const t of r.hiperf.top.slice(0, 10)) lines.push(` ${t.percent.toFixed(2).padStart(6)}% ${t.func} (${t.dso})`);
3607
+ }
3608
+ }
3609
+ if (r.trace) lines.push(` trace: ${r.trace}`);
3610
+ for (const n of r.notes) lines.push(` note: ${n}`);
3611
+ if (gate && !gate.passed) for (const fail of gate.failures) lines.push(`GATE FAILED: ${fail}`);
3612
+ return lines.join("\n");
3613
+ }
3614
+ function registerPerfCommand(program) {
3615
+ const perf = program.command("perf").description("Measure app performance on the device: frames, jank and startup from a hitrace capture.");
3616
+ perf.command("trace").description(
3617
+ "Capture a hitrace trace while the app cold-launches (--launch), a flow is replayed (--flow), or for a fixed time (--for); pull it and summarise it for the bundle: its UI frames (janky when one takes longer than the display's refresh interval, from its vsync to done), p50/p95/max frame time, cold-start time (launch request to the first frame composited), main-thread load and RenderService jank reports. Exits 0 when the capture completed, 1 when --max-jank-rate or --max-startup-ms is exceeded (or the flow diverged), 2 when it could not capture."
3618
+ ).option("--flow <file>", "Replay this flow file (see `replay`) while tracing; its bundle is the default --bundle.").option("--launch", "Force-stop the app, start tracing, then cold-launch it and report its startup time.").option(
3619
+ "--for <ms>",
3620
+ `Without --flow: how long to capture (after the launch with --launch), default ${DEFAULT_PERF_FOR_MS}. With --flow: how long to keep capturing after it, default ${DEFAULT_PERF_FLOW_TAIL_MS}.`,
3621
+ positiveMs2
3622
+ ).option("--bundle <bundle>", "The app to measure (default: the flow's bundle, else the project in the current directory).").option("--ability <name>", "Ability --launch starts (default: the main ability).").option("--out <file>", "Where to save the trace (default: a file in the system temp dir).").option("--categories <list>", `hitrace categories, comma-separated (default ${DEFAULT_TRACE_CATEGORIES.join(",")}; add sched for main-thread CPU time).`, list).option("--buffer-kb <kb>", "hitrace buffer size in KB.", positiveInt, DEFAULT_TRACE_BUFFER_KB).option("--hiperf", "Also sample the app's process with hiperf during the capture and list its top functions.").option("--max-jank-rate <r>", "Exit 1 when the share of janky frames is above this (0..1).", rate).option("--max-startup-ms <ms>", "Exit 1 when the cold start takes longer than this (needs --launch).", positiveMs2).option("--step-timeout <ms>", "With --flow: how long to wait for a step's selector and expect.", positiveMs2).option("--fallback", "With --flow: act on a step's fallback point when its selector is not found.").option("--no-wake", "Leave the screen as it is (by default, with --launch or --flow, it is woken, a credential-less lock screen swiped away, and kept on).").option("--device <serial>", "Target device serial.").option("--json", "Print the report as JSON: ok, bundle, durationMs, trace, refreshRateHz, frames, startup, flow, pid, mainThread, scenes, hiperf, notes, gate, error.").action(async (opts) => {
3623
+ const { config, logger } = getRuntime();
3624
+ const thresholds = { maxJankRate: opts.maxJankRate, maxStartupMs: opts.maxStartupMs };
3625
+ let report;
3626
+ try {
3627
+ const flow = opts.flow ? readFlowFile(path17.resolve(opts.flow)) : void 0;
3628
+ report = await withInterrupt(logger, "stopping the trace", (abortSignal) => perfTrace({
3629
+ config,
3630
+ logger,
3631
+ abortSignal,
3632
+ deviceSerial: opts.device,
3633
+ bundle: opts.bundle ?? flow?.bundle ?? projectBundle(process.cwd()),
3634
+ ability: opts.ability,
3635
+ launch: opts.launch,
3636
+ flow,
3637
+ replay: {
3638
+ stepTimeoutMs: opts.stepTimeout,
3639
+ fallback: opts.fallback,
3640
+ onStep: opts.json ? void 0 : (s) => logger.info(`flow step ${s.index} ${s.action} ${s.ok ? "ok" : "FAILED"}`)
3641
+ },
3642
+ forMs: opts.for,
3643
+ out: opts.out ? path17.resolve(opts.out) : void 0,
3644
+ categories: opts.categories,
3645
+ bufferKb: opts.bufferKb,
3646
+ hiperf: opts.hiperf,
3647
+ wake: opts.wake
3648
+ }));
3649
+ } catch (err) {
3650
+ report = emptyReport(opts.bundle ?? "", err.message);
3651
+ }
3652
+ const gate = evaluatePerfGate(report, thresholds);
3653
+ if (opts.json) process.stdout.write(`${JSON.stringify({ ...report, gate }, null, 2)}
3654
+ `);
3655
+ else if (report.ok) process.stdout.write(`${formatPerfReport(report, gate)}
3656
+ `);
3657
+ if (report.error) logger.error(report.error);
3658
+ process.exitCode = perfExitCode(report, gate);
3659
+ });
3660
+ perf.command("summary <trace>").description(
3661
+ "Summarise a hitrace text trace taken earlier (by `perf trace --out`, or `hitrace -o` by hand) for a bundle, the same way `perf trace` does. Exit codes as `perf trace`."
3662
+ ).option("--bundle <bundle>", "The app to summarise (default: the project in the current directory).").option("--launched", "The trace holds a cold launch: report startup and leave the first frame out of the frame stats.").option("--pid <pid>", "The app's pid, when the trace holds several of its processes.", positiveInt).option("--refresh-rate <hz>", "Refresh rate to use when the trace has no vsync period.", positiveNumber).option("--max-jank-rate <r>", "Exit 1 when the share of janky frames is above this (0..1).", rate).option("--max-startup-ms <ms>", "Exit 1 when the cold start takes longer than this.", positiveMs2).option("--json", "Print the summary as JSON.").action(async (trace, opts) => {
3663
+ const { logger } = getRuntime();
3664
+ const bundle = opts.bundle ?? projectBundle(process.cwd());
3665
+ const file = path17.resolve(trace);
3666
+ let report;
3667
+ if (!bundle) report = emptyReport("", "No bundle to summarise: pass --bundle.");
3668
+ else {
3669
+ try {
3670
+ const model = await parseTraceFile(file, { keepSlice: perfSliceFilter });
3671
+ const s = summarizeTrace(model, { bundle, launched: opts.launched, pid: opts.pid, refreshRateHz: opts.refreshRate });
3672
+ report = { ...emptyReport(bundle), ...s, ok: true, trace: file };
3673
+ } catch (err) {
3674
+ report = emptyReport(bundle, `Could not read ${file}: ${err.message}`);
3675
+ }
3676
+ }
3677
+ const gate = evaluatePerfGate(report, { maxJankRate: opts.maxJankRate, maxStartupMs: opts.maxStartupMs });
3678
+ if (opts.json) process.stdout.write(`${JSON.stringify({ ...report, gate }, null, 2)}
3679
+ `);
3680
+ else if (report.ok) process.stdout.write(`${formatPerfReport(report, gate)}
3681
+ `);
3682
+ if (report.error) logger.error(report.error);
3683
+ process.exitCode = perfExitCode(report, gate);
3684
+ });
3685
+ }
3686
+ function emptyReport(bundle, error) {
3687
+ return {
3688
+ ok: false,
3689
+ bundle,
3690
+ durationMs: 0,
3691
+ trace: null,
3692
+ refreshRateHz: null,
3693
+ frames: null,
3694
+ startup: null,
3695
+ flow: null,
3696
+ pid: null,
3697
+ mainThread: null,
3698
+ scenes: [],
3699
+ categories: [],
3700
+ notes: [],
3701
+ ...error ? { error } : {}
3702
+ };
3703
+ }
3704
+
3705
+ // src/mcp/target.ts
3706
+ function targetProperties(verb, opts = {}) {
3707
+ const props = {
3708
+ target: {
3709
+ type: "string",
3710
+ description: `${verb} the element whose text, description, or placeholder matches (case-insensitive; exact matches win, else substring). Covered elements are skipped.`
3711
+ },
3712
+ targetId: { type: "string", description: "Match the element by its id or key." },
3713
+ targetType: { type: "string", description: "Restrict the match to a component type (e.g. Button, TextInput)." },
3714
+ selector: {
3715
+ type: "string",
3716
+ description: 'Instead of target*: an exact selector. Attributes as comma-separated key<op>value clauses (keys text, desc, hint, id, key, type, enabled, focused, checked, selected, visible, clickable, scrollable; ops = *= ^= $= /=regex) with an optional [n] index, e.g. "type=Button,text*=Sign[1]"; or "xpath=//Button[@text=\\"OK\\"]".'
3717
+ },
3718
+ window: windowProperty
3719
+ };
3720
+ if (opts.index !== false) {
3721
+ props.targetIndex = { type: "integer", minimum: 0, description: "Pick the n-th (0-based) of several matching elements." };
3722
+ }
3723
+ return props;
3724
+ }
3725
+ var windowProperty = {
3726
+ type: "integer",
3727
+ minimum: 0,
3728
+ description: "Only this window (an id from `dump windows`, or the [window <id> ...] header of a flat dump)."
3729
+ };
3730
+ function selectorFromArgs(args) {
3731
+ return selectorFrom({
3732
+ target: args.target,
3733
+ targetId: args.targetId,
3734
+ targetType: args.targetType,
3735
+ targetIndex: args.targetIndex === void 0 ? void 0 : String(args.targetIndex),
3736
+ selector: args.selector
3737
+ });
3738
+ }
3739
+ function targetIndexFromArgs(args) {
3740
+ return args.targetIndex;
3741
+ }
3742
+ function windowFromArgs(args) {
3743
+ return windowIdFrom({ window: args.window === void 0 ? void 0 : String(args.window) });
3744
+ }
3745
+
3746
+ // src/mcp/tools.ts
3747
+ var str = (description, extra = {}) => ({ type: "string", description, ...extra });
3748
+ var bool = (description, dflt) => ({
3749
+ type: "boolean",
3750
+ description,
3751
+ ...dflt !== void 0 ? { default: dflt } : {}
3752
+ });
3753
+ var num2 = (description, extra = {}) => ({ type: "number", description, ...extra });
3754
+ var int = (description, extra = {}) => ({ type: "integer", description, ...extra });
3755
+ var PROJECT = {
3756
+ project: str("Project directory; a relative path resolves against the server's --project (default: the server's --project).")
3757
+ };
3758
+ var DEVICE = {
3759
+ device: str("Target device serial (default: the server's --device, else the only connected device).")
3760
+ };
3761
+ function object(properties, required = []) {
3762
+ return { type: "object", properties, ...required.length ? { required } : {}, additionalProperties: false };
3763
+ }
3764
+ function projectOf(a, ctx) {
3765
+ return typeof a.project === "string" ? path18.resolve(ctx.projectDir, a.project) : ctx.projectDir;
3766
+ }
3767
+ function deviceOf(a, ctx) {
3768
+ return a.device ?? ctx.deviceSerial;
3769
+ }
3770
+ function pathArg(a, key, ctx) {
3771
+ const v = a[key];
3772
+ return typeof v === "string" ? path18.resolve(projectOf(a, ctx), v) : void 0;
3773
+ }
3774
+ function jsonResult(json, isError = false, extra = []) {
3775
+ return {
3776
+ content: [{ type: "text", text: JSON.stringify(json) }, ...extra],
3777
+ structuredContent: json,
3778
+ ...isError ? { isError: true } : {}
3779
+ };
3780
+ }
3781
+ function message(err) {
3782
+ return err instanceof Error ? err.message : String(err);
3783
+ }
3784
+ function jpeg(buf) {
3785
+ return { type: "image", data: buf.toString("base64"), mimeType: "image/jpeg" };
3786
+ }
3787
+ function rt() {
3788
+ const { config, logger } = getRuntime();
3789
+ return { config, logger };
3790
+ }
3791
+ var SMOKE = {
3792
+ smoke: bool(
3793
+ "After launch, wait for the process, take a screenshot after smokeDelay, and fail (isError) when the process died or the screen is blank; a crash is reported from the faultlog.",
3794
+ false
3795
+ ),
3796
+ smokeDelay: int("With smoke: wait between the process appearing and the screenshot, ms.", {
3797
+ minimum: 0,
3798
+ default: DEFAULT_SMOKE_DELAY_MS2
3799
+ }),
3800
+ smokeScreenshot: str("With smoke: where to write the screenshot (default: a file in the system temp dir).")
3801
+ };
3802
+ var INPUT_TYPES2 = [
3803
+ "click",
3804
+ "doubleClick",
3805
+ "longClick",
3806
+ "swipe",
3807
+ "drag",
3808
+ "fling",
3809
+ "keyEvent",
3810
+ "inputText",
3811
+ "clearText",
3812
+ "hideKeyboard"
3813
+ ];
3814
+ var build = {
3815
+ name: "build",
3816
+ title: "Build the app",
3817
+ description: "Build an OpenHarmony or HarmonyOS app via hvigorw (`oniro-app build --json`). Returns ok, exitCode, durationMs, failedTask, diagnostics (severity, source, code, rule, message, file, line, column, task), summary, discoveredHaps, warnings, error and rawTail. An OpenHarmony project fails early, naming the release to install, when the SDK for its compileSdkVersion is missing. isError when the build fails.",
3818
+ inputSchema: object({
3819
+ ...PROJECT,
3820
+ product: str("hvigor product name.", { default: "default" }),
3821
+ module: str("Restrict the build to a specific module."),
3822
+ mode: str("hvigor build mode (e.g. release, debug)."),
3823
+ task: str("hvigor task to run.", { default: "assembleHap" }),
3824
+ deps: bool("Run `ohpm install --all` when oh_modules/ is missing (false = --no-deps).", true),
3825
+ parallel: bool("Build modules in parallel (false = --no-parallel).", true),
3826
+ installSdk: bool("Install the OpenHarmony SDK the project compiles against when it is missing, then build.", false)
3827
+ }),
3828
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3829
+ changesDevice: false,
3830
+ async call(a, ctx) {
3831
+ try {
3832
+ const report = await runBuild(projectOf(a, ctx), {
3833
+ product: a.product,
3834
+ module: a.module,
3835
+ mode: a.mode,
3836
+ task: a.task,
3837
+ deps: a.deps,
3838
+ parallel: a.parallel,
3839
+ installSdk: a.installSdk
3840
+ });
3841
+ return jsonResult(report, !report.ok);
3842
+ } catch (err) {
3843
+ return jsonResult({ ok: false, exitCode: 1, error: message(err), diagnostics: [] }, true);
3844
+ }
3845
+ }
3846
+ };
3847
+ var checkArktsTool = {
3848
+ name: "check_arkts",
3849
+ title: "Check ArkTS",
3850
+ description: "Type-check and lint ArkTS with the SDK's own ArkTS checker (what the build's CompileArkTS runs), plus the ArkUI component-model and project-config rules the build enforces later (V1/V2 decorators, @Entry root, route maps, resources, permissions), in seconds and without a build (`oniro-app check arkts --json`). Returns ok, completed, diagnostics (severity, source, rule, message, file, line, column), fixed, alsoModified, summary, sdk, durationMs, error. isError when it found an error or could not run (completed: false means nothing is known).",
3851
+ inputSchema: object({
3852
+ ...PROJECT,
3853
+ files: {
3854
+ type: "array",
3855
+ items: { type: "string" },
3856
+ minItems: 1,
3857
+ description: "Check only these .ets files (project-wide rules such as main_pages.json and module.json5 are then skipped)."
3858
+ },
3859
+ fix: bool(
3860
+ "Apply the fixes the checker can verify (the compiler's `Did you mean` suggestions, import paths, a missing `export`, the @Component/@ComponentV2 decorator), re-check, and revert any fix that adds an error.",
3861
+ false
3862
+ ),
3863
+ sdk: str("SDK directory to check against (holding ets/), instead of the one compileSdkVersion names.")
3864
+ }),
3865
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3866
+ changesDevice: false,
3867
+ async call(a, ctx) {
3868
+ const report = await arktsCheckOrError(projectOf(a, ctx), {
3869
+ files: a.files,
3870
+ fix: a.fix,
3871
+ sdk: pathArg(a, "sdk", ctx)
3872
+ });
3873
+ return jsonResult(report, arktsCheckExitCode(report) !== 0);
3874
+ }
3875
+ };
3876
+ var test = {
3877
+ name: "test",
3878
+ title: "Run the ohosTest suite",
3879
+ description: "Build a module's main and ohosTest HAPs, install both on the device, run the suite with `aa test` (Hypium) and report each case (`oniro-app test --json`). Returns ok, outcome, tests (suite, name, status, statusCode, durationMs, message, stack), summary, resultCode, haps, log, error. isError when a case failed or the run did not complete. Installs on the device; the test module is left installed.",
3880
+ inputSchema: object({
3881
+ ...PROJECT,
3882
+ module: str("Module whose src/ohosTest suite runs.", { default: "entry" }),
3883
+ product: str("hvigor product name.", { default: "default" }),
3884
+ class: str("Run only these cases: `Suite`, `Suite#case`, or several joined by commas."),
3885
+ timeout: int("Per-case timeout passed to the runner, ms.", { minimum: 1, default: 15e3 }),
3886
+ runTimeout: int("Wall-clock limit for the whole run, ms; aa test can hang when the app dies.", { minimum: 1, default: 6e5 }),
3887
+ build: bool("Build the HAPs first (false = --no-build: use the HAPs already built).", true),
3888
+ wake: bool(
3889
+ "Turn the screen on, swipe a credential-less lock screen away and keep it on during the run (false = --no-wake).",
3890
+ true
3891
+ ),
3892
+ mode: str("hvigor build mode (e.g. debug)."),
3893
+ deps: bool("Run `ohpm install --all` when oh_modules/ is missing (false = --no-deps).", true),
3894
+ ...DEVICE
3895
+ }),
3896
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
3897
+ changesDevice: true,
3898
+ async call(a, ctx) {
3899
+ const { config, logger } = rt();
3900
+ const report = await runTests2({
3901
+ config,
3902
+ projectDir: projectOf(a, ctx),
3903
+ module: a.module,
3904
+ product: a.product,
3905
+ testClass: a.class,
3906
+ caseTimeoutMs: a.timeout,
3907
+ runTimeoutMs: a.runTimeout,
3908
+ build: a.build,
3909
+ wakeScreen: a.wake,
3910
+ mode: a.mode,
3911
+ autoInstallDeps: a.deps,
3912
+ deviceSerial: deviceOf(a, ctx),
3913
+ logger
3914
+ });
3915
+ return jsonResult(report, testExitCode(report) !== 0);
3916
+ }
3917
+ };
3918
+ var apply = {
3919
+ name: "apply",
3920
+ title: "Install and apply a HAP",
3921
+ description: "Install the HAP of the project (or `hap`) and verify the running process took the change (`oniro-app app apply --json`). Refuses a HAP of another bundle. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart. Returns method, preInstallPid, postInstallPid, replaced, cacheCleared, and with smoke a `smoke` verdict (passed, failures, pid, alive, blankScreen, blankScore, crash, screenshot). isError when the smoke check fails.",
3922
+ inputSchema: object({
3923
+ ...PROJECT,
3924
+ hap: str(
3925
+ "An explicit .hap file (relative to the project); its own bundleName is the one applied unless `bundle` is given."
3926
+ ),
3927
+ bundle: str("Bundle name to apply changes to. Defaults to the bundleName in the .hap file, else in the project AppScope/app.json5."),
3928
+ module: str("Module to resolve the HAP from (multi-module projects)."),
3929
+ installedHap: str("Local path to the currently-installed HAP, to enable the asset-cache reboot."),
3930
+ system: bool("Treat as a persistent/system bundle (refuses uninstall on sign-info mismatch).", false),
3931
+ allowUninstall: bool("Permit uninstalling a system bundle on sign-info mismatch (dangerous).", false),
3932
+ ...SMOKE,
3933
+ ...DEVICE
3934
+ }),
3935
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
3936
+ changesDevice: true,
3937
+ async call(a, ctx) {
3938
+ const hap = pathArg(a, "hap", ctx);
3939
+ const report = await applyWithSmoke(rt(), {
3940
+ // A HAP goes in as the command's positional .hap, so its own bundle name is the one applied.
3941
+ target: hap ?? projectOf(a, ctx),
3942
+ bundle: a.bundle,
3943
+ module: a.module,
3944
+ installedHap: pathArg(a, "installedHap", ctx),
3945
+ system: a.system,
3946
+ allowUninstall: a.allowUninstall,
3947
+ smoke: a.smoke,
3948
+ smokeDelay: a.smokeDelay,
3949
+ smokeScreenshot: pathArg(a, "smokeScreenshot", ctx),
3950
+ device: deviceOf(a, ctx)
3951
+ });
3952
+ return jsonResult(report, report.smoke ? !report.smoke.passed : false);
3953
+ }
3954
+ };
3955
+ var launch = {
3956
+ name: "launch",
3957
+ title: "Launch the app",
3958
+ description: "Launch an app on the device (`oniro-app app launch --json`). Project mode (default) resolves the bundle and main ability from the project; bundle mode (`bundle`) launches an already-installed app. With smoke, waits for the process, takes a screenshot and reports whether it died, crashed (from the faultlog) or shows a blank screen. Returns bundle and, with smoke, `smoke` (passed, failures, pid, alive, blankScreen, blankScore, crash, screenshot). isError when the smoke check fails.",
3959
+ inputSchema: object({
3960
+ ...PROJECT,
3961
+ bundle: str("Launch an already-installed app by bundle name (no project needed)."),
3962
+ module: str("Module folder name (project mode).", { default: "entry" }),
3963
+ ability: str(
3964
+ "Explicit ability to launch (defaults to the module mainElement in project mode, or the installed bundle main ability in bundle mode)."
3965
+ ),
3966
+ ...SMOKE,
3967
+ ...DEVICE
3968
+ }),
3969
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
3970
+ changesDevice: true,
3971
+ async call(a, ctx) {
3972
+ const report = await launchWithSmoke(rt(), {
3973
+ projectDir: projectOf(a, ctx),
3974
+ bundle: a.bundle,
3975
+ module: a.module,
3976
+ ability: a.ability,
3977
+ smoke: a.smoke,
3978
+ smokeDelay: a.smokeDelay,
3979
+ smokeScreenshot: pathArg(a, "smokeScreenshot", ctx),
3980
+ device: deviceOf(a, ctx)
3981
+ });
3982
+ return jsonResult(report, report.smoke ? !report.smoke.passed : false);
3983
+ }
3984
+ };
3985
+ function readLayoutBaseline(file) {
3986
+ let text;
3987
+ try {
3988
+ text = fs9.readFileSync(file, "utf8");
3989
+ } catch (err) {
3990
+ throw new Error(`Cannot read the layout baseline ${file}: ${message(err)}`);
3991
+ }
3992
+ try {
3993
+ return parseLayoutBaseline2(JSON.parse(text));
3994
+ } catch (err) {
3995
+ throw new Error(`${file} is not a saved \`dump layout\` JSON: ${message(err)}`);
3996
+ }
3997
+ }
3998
+ var dumpLayoutTool = {
3999
+ name: "dump_layout",
4000
+ title: "Dump the UI layout",
4001
+ description: "Dump the on-screen UI layout (`oniro-app dump layout`). Default: the pruned JSON tree (large; prefer flat or summary). flat: one line per labelled or interactive element (type, label, center as 0-1 fractions, flags), as text. summary: per window element, content, clickable and text-node counts. diff: compare with a layout saved earlier (via `output`): matched elements with IoU, missing, extra, and a 0-1 similarity.",
4002
+ inputSchema: object({
4003
+ bundle: str("Filter the layout to a single window/bundle."),
4004
+ raw: bool("Return the unpruned layout tree.", false),
4005
+ flat: bool("One line per element (type, label, center as 0-1 fractions, flags) instead of a JSON tree.", false),
4006
+ summary: bool("Per window: element, content, clickable and text-node counts (JSON).", false),
4007
+ diff: str(
4008
+ "Compare with a saved `dump layout` JSON (relative to the project): matched elements with IoU, missing, extra, and a 0-1 similarity (JSON)."
4009
+ ),
4010
+ output: str("Also write what is returned (the JSON, or the flat text) to this file, e.g. as a later diff baseline."),
4011
+ selector: str(
4012
+ "Return only the elements matching this selector (attribute clauses or xpath=..., as in input), numbered for its [n] suffix: { display, selector, matches }."
4013
+ ),
4014
+ window: windowProperty,
4015
+ ...PROJECT,
4016
+ ...DEVICE
4017
+ }),
4018
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4019
+ changesDevice: false,
4020
+ async call(a, ctx) {
4021
+ const { config, logger } = rt();
4022
+ if (a.raw && a.flat) throw new Error("raw and flat cannot be combined.");
4023
+ const modes = [a.flat && "flat", a.summary && "summary", a.diff !== void 0 && "diff"].filter(Boolean);
4024
+ if (modes.length > 1) throw new Error(`${modes.join(" and ")} cannot be combined.`);
4025
+ if (a.raw && modes.length > 0) throw new Error(`raw cannot be combined with ${modes[0]}.`);
4026
+ const selector = a.selector === void 0 ? void 0 : parseSelector2(a.selector);
4027
+ if (selector && (a.raw || a.summary || a.diff !== void 0)) {
4028
+ throw new Error("selector cannot be combined with raw, summary or diff.");
4029
+ }
4030
+ const windowId = windowFromArgs(a);
4031
+ const diffPath = pathArg(a, "diff", ctx);
4032
+ const baseline = diffPath !== void 0 ? readLayoutBaseline(diffPath) : void 0;
4033
+ const result = await dumpLayout3({
4034
+ config,
4035
+ bundleName: a.bundle,
4036
+ windowId,
4037
+ prune: !a.raw,
4038
+ detectKeyboard: !a.raw,
4039
+ deviceSerial: deviceOf(a, ctx),
4040
+ logger
4041
+ });
4042
+ const output = pathArg(a, "output", ctx);
4043
+ if (selector) {
4044
+ const matches = findLayoutNodes2(selectableTree2(result, windowId), selector);
4045
+ const json2 = { display: result.display, selector: selector.raw, matches };
4046
+ if (output) fs9.writeFileSync(output, `${JSON.stringify(json2, null, 2)}
4047
+ `);
4048
+ return jsonResult(json2);
4049
+ }
4050
+ if (a.flat) {
4051
+ const text = formatFlatLayout(result);
4052
+ if (output) fs9.writeFileSync(output, text);
4053
+ return { content: [{ type: "text", text }] };
4054
+ }
4055
+ let json;
4056
+ if (a.summary) json = summarizeLayout2(result);
4057
+ else if (baseline) json = diffLayouts2(baseline, result);
4058
+ else json = result;
4059
+ if (output) fs9.writeFileSync(output, `${JSON.stringify(json, null, 2)}
4060
+ `);
4061
+ return jsonResult(json);
4062
+ }
4063
+ };
4064
+ var input = {
4065
+ name: "input",
4066
+ title: "Send a UI input",
4067
+ description: "Inject a UI input event via uitest uiInput (`oniro-app input`). Position it with target/targetId/targetType (an on-screen element), fx/fy (0-1 fractions of the display, as in dump_layout flat and the screenshot grid), or x/y (pixels). Returns x, y (the resolved start point in pixels), target (the element it resolved to) and keyboardHidden.",
4068
+ inputSchema: object(
4069
+ {
4070
+ type: { type: "string", enum: INPUT_TYPES2, description: "Input type." },
4071
+ ...targetProperties("Act on"),
4072
+ fx: num2("Start X as a 0-1 fraction of the display width.", { minimum: 0, maximum: 1 }),
4073
+ fy: num2("Start Y as a 0-1 fraction of the display height.", { minimum: 0, maximum: 1 }),
4074
+ fx2: num2("End X fraction (swipe/drag/fling).", { minimum: 0, maximum: 1 }),
4075
+ fy2: num2("End Y fraction (swipe/drag/fling).", { minimum: 0, maximum: 1 }),
4076
+ x: num2("X pixel (click/swipe start)."),
4077
+ y: num2("Y pixel (click/swipe start)."),
4078
+ x2: num2("End X pixel (swipe/drag/fling)."),
4079
+ y2: num2("End Y pixel (swipe/drag/fling)."),
4080
+ speed: num2("Velocity for swipe/drag/fling, px/s."),
4081
+ key: str(
4082
+ "Key for keyEvent: Back/Home/Power, a name (Delete, Enter, Tab, Escape, MoveHome, MoveEnd, \u2026), a letter, a KeyCode id, or a combo like Ctrl+A."
4083
+ ),
4084
+ text: str("Text for inputText."),
4085
+ clear: bool("inputText: clear the field (Ctrl+A, Delete) before typing.", false),
4086
+ hideKeyboard: bool("Dismiss the soft keyboard afterwards, if it is up.", false),
4087
+ ...DEVICE
4088
+ },
4089
+ ["type"]
4090
+ ),
4091
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4092
+ changesDevice: true,
4093
+ async call(a, ctx) {
4094
+ const { config, logger } = rt();
4095
+ const result = await performInput2({
4096
+ config,
4097
+ type: a.type,
4098
+ target: selectorFromArgs(a),
4099
+ targetIndex: targetIndexFromArgs(a),
4100
+ windowId: windowFromArgs(a),
4101
+ fx: a.fx,
4102
+ fy: a.fy,
4103
+ fx2: a.fx2,
4104
+ fy2: a.fy2,
4105
+ pxX: a.x,
4106
+ pxY: a.y,
4107
+ pxX2: a.x2,
4108
+ pxY2: a.y2,
4109
+ speed: a.speed,
4110
+ key: a.key,
4111
+ text: a.text,
4112
+ clear: a.clear,
4113
+ hideKeyboard: a.hideKeyboard,
4114
+ deviceSerial: deviceOf(a, ctx),
4115
+ logger
4116
+ });
4117
+ return jsonResult(result);
4118
+ }
4119
+ };
4120
+ var DEFAULT_CONTACT_SHEET_FRAMES2 = 8;
4121
+ var TARGET_CROP_PAD2 = 0.02;
4122
+ var screenshot = {
4123
+ name: "screenshot",
4124
+ title: "Take a screenshot",
4125
+ description: "Capture the screen and return it as an image, downscaled so its longest side is at most maxDim (`oniro-app screenshot --max-dim`). grid: overlay a 10x10 grid (axes 0.0-1.0) for picking fx/fy tap coordinates. crop / target*: a full-resolution crop of one region or element, for checking detail cheaply. contactSheet: capture a burst and tile it into one image with per-frame change diffs. diff: compare the screen with a baseline JPEG (pHash/dHash distance, pixel-change ratio, mean change). output: also write the image to a file (with diff: the full-resolution capture, usable as a later baseline).",
4126
+ inputSchema: object({
4127
+ output: str("Also write the image to this file (relative to the project)."),
4128
+ grid: bool("Overlay a 10x10 coordinate grid.", false),
4129
+ maxDim: int("Longest-side cap of the returned image, px.", { minimum: 256, maximum: 4096, default: 1024 }),
4130
+ crop: str('Crop to this region, given as 0-1 fractions of the screen: "x1,y1,x2,y2".'),
4131
+ ...targetProperties("Crop to"),
4132
+ contactSheet: bool("Capture a burst and composite it into one tiled contact sheet.", false),
4133
+ burst: int("With contactSheet: the number of frames.", { minimum: 1, default: DEFAULT_CONTACT_SHEET_FRAMES2 }),
4134
+ interval: int("With contactSheet: delay between frames, ms.", { minimum: 0, default: 50 }),
4135
+ diff: str(
4136
+ "Compare the screen with this baseline JPEG (relative to the project): pHash and dHash distance, pixel-change ratio, mean change."
4137
+ ),
4138
+ ...PROJECT,
4139
+ ...DEVICE
4140
+ }),
4141
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
4142
+ changesDevice: false,
4143
+ async call(a, ctx) {
4144
+ const { config, logger } = rt();
4145
+ const deviceSerial = deviceOf(a, ctx);
4146
+ const maxDim = a.maxDim;
4147
+ const output = pathArg(a, "output", ctx);
4148
+ const selector = selectorFromArgs(a);
4149
+ const windowId = windowFromArgs(a);
4150
+ if (a.diff !== void 0) {
4151
+ if (a.crop !== void 0 || selector || windowId !== void 0 || a.grid || a.contactSheet) {
4152
+ throw new Error("diff cannot be combined with crop, target* / selector / window, grid, or contactSheet.");
4153
+ }
4154
+ const baselinePath = pathArg(a, "diff", ctx);
4155
+ let baseline;
4156
+ try {
4157
+ baseline = fs9.readFileSync(baselinePath);
4158
+ } catch (err) {
4159
+ throw new Error(`Cannot read the baseline ${baselinePath}: ${message(err)}`);
4160
+ }
4161
+ const shot2 = await takeScreenshot2({ config, deviceSerial, logger });
4162
+ if (output) fs9.writeFileSync(output, shot2.pixels);
4163
+ const diff = await diffScreenshots(baseline, shot2.pixels);
4164
+ const shown = await renderWithGrid(shot2.pixels, { maxDim });
4165
+ return jsonResult({ baseline: baselinePath, current: output ?? null, ...diff }, false, [jpeg(shown.buf)]);
4166
+ }
4167
+ if (a.crop !== void 0 || selector || windowId !== void 0) {
4168
+ if (a.crop !== void 0 && (selector || windowId !== void 0)) {
4169
+ throw new Error("Use either crop or target* / selector / window, not both.");
4170
+ }
4171
+ if (a.grid || a.contactSheet) throw new Error("crop / target cannot be combined with grid or contactSheet.");
4172
+ let rect;
4173
+ let label;
4174
+ if (selector) {
4175
+ const located = await locateTarget2({
4176
+ config,
4177
+ selector,
4178
+ index: targetIndexFromArgs(a),
4179
+ windowId,
4180
+ hideKeyboard: false,
4181
+ deviceSerial,
4182
+ logger
4183
+ });
4184
+ if (!located.node.b) throw new Error(`${describeLayoutNode4(located.node)} has no bounds to crop to.`);
4185
+ rect = padCropRect(located.node.b, TARGET_CROP_PAD2);
4186
+ label = describeLayoutNode4(located.node);
4187
+ } else if (windowId !== void 0) {
4188
+ const { tree } = await dumpLayout3({ config, windowId, deviceSerial, logger });
4189
+ const win = listLayoutWindows2(tree).find((w) => w.windowId === windowId);
4190
+ if (!win?.b) throw new Error(`Window ${windowId} has no bounds to crop to.`);
4191
+ rect = win.b;
4192
+ label = `window ${windowId}${win.bundle ? ` (${win.bundle})` : ""}`;
4193
+ } else {
4194
+ rect = parseCropRect(a.crop);
4195
+ label = rect.map((n) => n.toFixed(3)).join(",");
4196
+ }
4197
+ const shot2 = await takeScreenshot2({ config, deviceSerial, logger });
4198
+ const cropped = await cropImage(shot2.pixels, rect, { maxDim });
4199
+ if (output) fs9.writeFileSync(output, cropped.buf);
4200
+ const { buf: buf2, ...dims2 } = cropped;
4201
+ return jsonResult({ output: output ?? null, region: label, ...dims2 }, false, [jpeg(buf2)]);
4202
+ }
4203
+ if (a.contactSheet) {
4204
+ const intervalMs = a.interval;
4205
+ const frames = await captureBurst2({ config, count: a.burst, intervalMs, deviceSerial, logger });
4206
+ const diffs = await frameDiffs(frames);
4207
+ const sheet = await buildContactSheet(frames, { maxDim, diffs });
4208
+ if (output) fs9.writeFileSync(output, sheet.buf);
4209
+ const json = {
4210
+ sheet: output ?? null,
4211
+ frameCount: frames.length,
4212
+ cols: sheet.cols,
4213
+ rows: sheet.rows,
4214
+ intervalMs,
4215
+ note: "t_ms is nominal (idx*interval); diffVsPrev is 0..1 mean greyscale diff vs the previous frame.",
4216
+ frames: frames.map((_, i) => ({ idx: i, t_ms: i * intervalMs, diffVsPrev: diffs[i] ?? 0 }))
4217
+ };
4218
+ return jsonResult(json, false, [jpeg(sheet.buf)]);
4219
+ }
4220
+ const shot = await takeScreenshot2({ config, deviceSerial, logger });
4221
+ const rendered = await renderWithGrid(shot.pixels, { maxDim, grid: !!a.grid });
4222
+ if (output) fs9.writeFileSync(output, rendered.buf);
4223
+ const { buf, ...dims } = rendered;
4224
+ return jsonResult({ output: output ?? null, grid: !!a.grid, ...dims }, false, [jpeg(buf)]);
4225
+ }
4226
+ };
4227
+ var wait = {
4228
+ name: "wait",
4229
+ title: "Wait for a device condition",
4230
+ description: "Wait for a device condition (`oniro-app wait --json`): a hilog line (log), boot (boot), a bundle to start (bundle), or an on-screen element (target / targetId / targetType or selector, optionally in one window, or its absence with gone). Returns the matched log entry or element, or {booted}, {gone}, {bundle, running}. isError on timeout.",
4231
+ inputSchema: object({
4232
+ log: str("Resolve when a hilog line (tag: message) logged after the call starts matches this regex."),
4233
+ since: str(
4234
+ 'With log: also match lines logged up to N seconds before the call started, measured on the device clock; "all" matches everything still in the device log buffer.'
4235
+ ),
4236
+ boot: bool("Resolve when the device is reachable (optionally also pidOf).", false),
4237
+ bundle: str("With log: filter to this bundle. Alone: wait until this bundle is running."),
4238
+ pidOf: str("With boot: also require this process to have a pid."),
4239
+ domain: str("With log: hilog domain filter (e.g. 0xD003900)."),
4240
+ ...targetProperties("Resolve when", { index: false }),
4241
+ gone: bool("With target*: resolve when no uncovered element matches any more.", false),
4242
+ interval: int("With target*: poll interval, ms (each poll is a layout dump).", { minimum: 1, default: 500 }),
4243
+ timeout: int("Max wait, ms.", { minimum: 0, default: 3e4 }),
4244
+ ...DEVICE
4245
+ }),
4246
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4247
+ changesDevice: false,
4248
+ async call(a, ctx) {
4249
+ const { config, logger } = rt();
4250
+ const deviceSerial = deviceOf(a, ctx);
4251
+ const timeoutMs = a.timeout;
4252
+ const selector = selectorFromArgs(a);
4253
+ const windowId = windowFromArgs(a);
4254
+ if (windowId !== void 0 && !selector) throw new Error("window applies only to target* and selector.");
4255
+ if (a.since !== void 0 && !a.log) throw new Error("since applies only to log.");
4256
+ if (a.log) {
4257
+ const entry = await waitForLog2({
4258
+ config,
4259
+ pattern: new RegExp(a.log),
4260
+ timeoutMs,
4261
+ bundle: a.bundle,
4262
+ domain: a.domain,
4263
+ deviceSerial,
4264
+ lookbackMs: parseSince(a.since),
4265
+ logger
4266
+ });
4267
+ return jsonResult(entry);
4268
+ }
4269
+ if (a.boot) {
4270
+ await waitForBoot2({ config, untilPidOf: a.pidOf, timeoutMs, deviceSerial, logger });
4271
+ return jsonResult({ booted: true });
4272
+ }
4273
+ if (selector) {
4274
+ const node = await waitForTarget2({
4275
+ config,
4276
+ selector,
4277
+ gone: a.gone,
4278
+ windowId,
4279
+ timeoutMs,
4280
+ pollMs: a.interval,
4281
+ deviceSerial,
4282
+ logger
4283
+ });
4284
+ return jsonResult(node ? node : { gone: true });
4285
+ }
4286
+ if (a.bundle) {
4287
+ await waitForBundle2({ config, bundle: a.bundle, timeoutMs, deviceSerial, logger });
4288
+ return jsonResult({ bundle: a.bundle, running: true });
4289
+ }
4290
+ throw new Error("Specify one of log, boot, bundle, target (or targetId / targetType), or selector.");
4291
+ }
4292
+ };
4293
+ var logsCrash = {
4294
+ name: "logs_crash",
4295
+ title: "Explain the last crash",
4296
+ description: "Fetch the newest faultlog of an app from the device and say what happened (`oniro-app logs crash --json`): a native crash (cpp_crash), an uncaught ArkTS/JS error (js_crash), a freeze (app_freeze), or none. Returns crashed, kind, subtype, confidence, bundle, pid, time, reason, topFrame, projectFrame, file, line, column, jsError, stack, nativeFrames, jsFrames, binderChain, deadlocks, notes, path, rawPath; `{crashed: false, kind: null}` when there is no fault. Not an error when there was no crash: check `crashed`.",
4297
+ inputSchema: object({
4298
+ bundle: str("App to look for (default: the bundleName of the project, else any process)."),
4299
+ since: str("Only faults from the last N seconds (device clock), or since an ISO 8601 time."),
4300
+ wait: int(
4301
+ "Keep looking this long when there is no fault yet, ms; hiview writes the faultlog a few seconds after the process dies.",
4302
+ { minimum: 0, default: 0 }
4303
+ ),
4304
+ path: str("Analyse this local faultlog (or faultloggerd .json crash record) instead of fetching one."),
4305
+ output: str("Where to save the fetched faultlog.", { default: path18.join(os4.homedir(), ".oniro", "faultlogs") }),
4306
+ ...PROJECT,
4307
+ ...DEVICE
4308
+ }),
4309
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4310
+ changesDevice: false,
4311
+ async call(a, ctx) {
4312
+ const since = parseCrashSince(a.since);
4313
+ const local = pathArg(a, "path", ctx);
4314
+ const bundle = a.bundle ?? (local ? void 0 : projectBundle(projectOf(a, ctx)));
4315
+ let verdict;
4316
+ if (local) {
4317
+ verdict = analyzeFaultLog2(fs9.readFileSync(local, "utf8"), { name: path18.basename(local), path: local, rawPath: local });
4318
+ } else {
4319
+ const { config, logger } = rt();
4320
+ verdict = await latestCrash3({
4321
+ config,
4322
+ bundle,
4323
+ since,
4324
+ waitMs: a.wait,
4325
+ deviceSerial: deviceOf(a, ctx),
4326
+ saveDir: pathArg(a, "output", ctx),
4327
+ logger
4328
+ });
4329
+ }
4330
+ const none = { crashed: false, kind: null, bundle: bundle ?? null, since: since instanceof Date ? since.toISOString() : since ?? null };
4331
+ return jsonResult(verdict ?? none);
4332
+ }
4333
+ };
4334
+ var perfTraceTool = {
4335
+ name: "perf_trace",
4336
+ title: "Measure frames, jank and startup",
4337
+ description: "Capture a hitrace trace while the app cold-launches (`launch`), a flow file is replayed (`flow`), or for a fixed time (`for`), and summarise it (`oniro-app perf trace --json`). Returns ok, bundle, durationMs, trace (local path), refreshRateHz, frames (total, janky, jankRate, p50Ms, p95Ms, maxMs, missedVsyncs, longest), startup (coldMs, firstFrameMs, processMs, from, to) or null, flow (the replay report) or null, pid, mainThread, scenes (RenderService's own jank reports), hiperf, notes, gate (passed, failures), error. isError when the capture failed or a threshold (maxJankRate, maxStartupMs) or the flow failed. Launching force-stops the app first.",
4338
+ inputSchema: object({
4339
+ bundle: str("The app to measure (default: the flow's bundle, else the bundleName of the project)."),
4340
+ launch: bool("Force-stop the app, start tracing, then cold-launch it and report its startup time.", false),
4341
+ flow: str("A flow file (as for `replay`) to replay while tracing; relative to the project."),
4342
+ for: int("Without flow: how long to capture (after the launch with launch), ms. With flow: how long to keep capturing after it.", {
4343
+ minimum: 1
4344
+ }),
4345
+ ability: str("Ability `launch` starts (default: the main ability)."),
4346
+ out: str("Where to save the trace (default: a file in the system temp dir)."),
4347
+ categories: { type: "array", items: { type: "string" }, description: `hitrace categories (default ${DEFAULT_TRACE_CATEGORIES2.join(", ")}).` },
4348
+ bufferKb: int("hitrace buffer size, KB.", { minimum: 1, default: DEFAULT_TRACE_BUFFER_KB2 }),
4349
+ hiperf: bool("Also sample the app's process with hiperf and list its top functions.", false),
4350
+ maxJankRate: num2("Fail (isError) when the share of janky frames is above this.", { minimum: 0, maximum: 1 }),
4351
+ maxStartupMs: int("Fail (isError) when the cold start takes longer than this, ms (needs launch).", { minimum: 1 }),
4352
+ stepTimeout: int("With flow: how long to wait for a step's selector and expect, ms.", { minimum: 1 }),
4353
+ fallback: bool("With flow: act on a step's fallback point when its selector is not found.", false),
4354
+ wake: bool("With launch or flow: wake the screen and swipe a credential-less lock screen away (false = --no-wake).", true),
4355
+ ...PROJECT,
4356
+ ...DEVICE
4357
+ }),
4358
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4359
+ // It force-stops and launches the app, and a flow taps and swipes.
4360
+ changesDevice: true,
4361
+ async call(a, ctx) {
4362
+ const { config, logger } = rt();
4363
+ const flowFile = pathArg(a, "flow", ctx);
4364
+ const flow = flowFile ? readFlowFile2(flowFile) : void 0;
4365
+ const report = await perfTrace2({
4366
+ config,
4367
+ logger,
4368
+ deviceSerial: deviceOf(a, ctx),
4369
+ bundle: a.bundle ?? flow?.bundle ?? projectBundle(projectOf(a, ctx)),
4370
+ ability: a.ability,
4371
+ launch: a.launch,
4372
+ flow,
4373
+ replay: { stepTimeoutMs: a.stepTimeout, fallback: a.fallback },
4374
+ forMs: a.for,
4375
+ out: pathArg(a, "out", ctx),
4376
+ categories: a.categories,
4377
+ bufferKb: a.bufferKb,
4378
+ hiperf: a.hiperf,
4379
+ wake: a.wake
4380
+ });
4381
+ const gate = evaluatePerfGate2(report, { maxJankRate: a.maxJankRate, maxStartupMs: a.maxStartupMs });
4382
+ return jsonResult({ ...report, gate }, perfExitCode(report, gate) !== 0);
4383
+ }
4384
+ };
4385
+ var screens = {
4386
+ name: "screens",
4387
+ title: "Inventory pages and routes",
4388
+ description: "Static inventory of the pages and routes of a project, from its sources, without a build or a device (`oniro-app screens --json`): @Entry and NavDestination pages, the router/NavPathStack call sites between them, the content nodes each page renders, and placeholder markers (TODO/FIXME, placeholder-like text, empty build()). Returns pages (name, module, file, line, component, entry, kind, registered, contentNodes, routesOut), routes (from, to, file, line, via, dynamic), placeholders (file, line, text, kind), warnings, modules, projectDir.",
4389
+ inputSchema: object({ ...PROJECT }),
4390
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4391
+ changesDevice: false,
4392
+ async call(a, ctx) {
4393
+ try {
4394
+ return jsonResult(inventoryScreens2({ projectDir: projectOf(a, ctx) }));
4395
+ } catch (err) {
4396
+ return jsonResult({ error: message(err) }, true);
4397
+ }
4398
+ }
4399
+ };
4400
+ var SDK_PROPS = {
4401
+ api: int(
4402
+ "The SDK API level to read. Default: the project's compileSdkVersion, else the newest installed SDK.",
4403
+ { minimum: 1 }
4404
+ ),
4405
+ sdk: str("An SDK API directory (holding ets/) to read instead."),
4406
+ project: str("The project whose compileSdkVersion picks the SDK (default: the server's --project)."),
4407
+ rebuild: bool("Rebuild the index even if the cached one is current.", false)
4408
+ };
4409
+ var API_KINDS = [
4410
+ "namespace",
4411
+ "class",
4412
+ "interface",
4413
+ "enum",
4414
+ "enumMember",
4415
+ "type",
4416
+ "function",
4417
+ "method",
4418
+ "constructor",
4419
+ "property",
4420
+ "variable",
4421
+ "component",
4422
+ "decorator",
4423
+ "macro",
4424
+ "typedef",
4425
+ "struct"
4426
+ ];
4427
+ function sdkOpts(a, ctx) {
4428
+ return {
4429
+ api: a.api !== void 0 ? String(a.api) : void 0,
4430
+ sdk: pathArg(a, "sdk", ctx),
4431
+ project: projectOf(a, ctx),
4432
+ rebuild: a.rebuild
4433
+ };
4434
+ }
4435
+ var apiSearch = {
4436
+ name: "api_search",
4437
+ title: "Search the SDK API",
4438
+ description: "Ranked search of the SDK's declarations at one API level (`oniro-app api search --json`): the ArkTS API, the ArkUI components and decorators, and the Node-API C headers. Every word must match a symbol name (exact > prefix > camelCase part > substring) or its doc line; a dotted word matches qualified names (`router.pushUrl`). Returns sdk, query, total, hits (name, qualifiedName, kind, kit, import, module, since, deprecated, useInstead, syscap, systemApi, file, path, line, signature, doc, overloads, score). isError when nothing matches.",
4439
+ inputSchema: object(
4440
+ {
4441
+ query: str('The search words, e.g. "navigation push" or "router.pushUrl".'),
4442
+ kit: str("Only symbols this kit exports (`ArkUI` or `@kit.ArkUI`)."),
4443
+ kind: { type: "array", items: { type: "string", enum: API_KINDS }, description: "Only these kinds." },
4444
+ lang: { type: "string", enum: ["arkts", "c"], description: "Only `arkts` declarations or `c` (Node-API headers)." },
4445
+ limit: int("At most n hits.", { minimum: 1, default: 20 }),
4446
+ deprecated: bool("Include @deprecated symbols (false = --no-deprecated).", true),
4447
+ systemapi: bool("Include @systemapi symbols, for system apps only (false = --no-systemapi).", true),
4448
+ ...SDK_PROPS
4449
+ },
4450
+ ["query"]
4451
+ ),
4452
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4453
+ changesDevice: false,
4454
+ async call(a, ctx) {
4455
+ const run = runApiSearch(a.query, {
4456
+ ...sdkOpts(a, ctx),
4457
+ kit: a.kit,
4458
+ kind: a.kind,
4459
+ lang: a.lang,
4460
+ limit: String(a.limit),
4461
+ deprecated: a.deprecated,
4462
+ systemapi: a.systemapi
4463
+ });
4464
+ return jsonResult(run.json, run.total === 0);
4465
+ }
4466
+ };
4467
+ var apiShow = {
4468
+ name: "api_show",
4469
+ title: "Look up one SDK symbol",
4470
+ description: "Exact lookup (`oniro-app api show --json`): is `symbol` declared at this API level? Takes a qualified name (`NavPathStack.pushPathByName`, `router.pushUrl`, `Column`, `@Local`, `napi_create_threadsafe_function`); when none matches, names that end with it. Returns sdk, symbol, found, entries (the api_search hit fields, every overload), and on a miss alsoIn ([{api, since}], the other installed SDKs that declare it) and suggestions. isError when it is not declared.",
4471
+ inputSchema: object(
4472
+ {
4473
+ symbol: str("The symbol, e.g. `NavPathStack.pushPathByName`."),
4474
+ kit: str("Only declarations this kit exports."),
4475
+ otherSdks: bool("On a miss, look in the other installed SDKs (false = --no-other-sdks).", true),
4476
+ ...SDK_PROPS
4477
+ },
4478
+ ["symbol"]
4479
+ ),
4480
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4481
+ changesDevice: false,
4482
+ async call(a, ctx) {
4483
+ const run = runApiShow(a.symbol, {
4484
+ ...sdkOpts(a, ctx),
4485
+ kit: a.kit,
4486
+ otherSdks: a.otherSdks
4487
+ });
4488
+ return jsonResult(run.json, !run.found);
4489
+ }
4490
+ };
4491
+ var doctor = {
4492
+ name: "doctor",
4493
+ title: "Check the toolchain",
4494
+ description: "Check that this machine can build, check, sign, install and test (`oniro-app doctor --json`): node, java, the SDK the project compiles with, the command-line tools, a `check arkts` self-test, hdc and a reachable device at the project's API, signing material, free disk space. Returns ok, checks (id, status ok/warn/fail/skip, detail, fix, fixed), project, versions. isError when a check fails.",
4495
+ inputSchema: object({
4496
+ project: str("Project directory (default: the server's --project when it holds build-profile.json5)."),
4497
+ fix: bool("Install the project's missing OpenHarmony SDK.", false),
4498
+ device: bool("Check the device (false = --no-device, for a machine that only builds).", true),
4499
+ serial: str("The device to check, when several are connected (default: the server's --device)."),
4500
+ arktsCheck: bool("Run the `check arkts` self-test (false = --no-arkts-check).", true)
4501
+ }),
4502
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4503
+ changesDevice: false,
4504
+ async call(a, ctx) {
4505
+ const { config, logger, progress } = getRuntime();
4506
+ let dir;
4507
+ if (typeof a.project === "string") dir = projectOf(a, ctx);
4508
+ else if (fs9.existsSync(path18.join(ctx.projectDir, "build-profile.json5"))) dir = ctx.projectDir;
4509
+ const report = await runDoctor2({
4510
+ config,
4511
+ projectDir: dir,
4512
+ fix: a.fix,
4513
+ device: a.device,
4514
+ deviceSerial: a.serial ?? ctx.deviceSerial,
4515
+ arktsSelfTest: a.arktsCheck,
4516
+ logger,
4517
+ progress
4518
+ });
4519
+ return jsonResult(report, doctorExitCode(report) !== 0);
4520
+ }
4521
+ };
4522
+ var devices = {
4523
+ name: "devices",
4524
+ title: "List devices",
4525
+ description: "List the connected hdc devices (`oniro-app devices --json`), as `{devices: [{serial, connection, status}]}`. long: also ask each connected device for its OS family, version, API level, CPU ABI, device type and model (`info`).",
4526
+ inputSchema: object({
4527
+ long: bool(
4528
+ "Also ask each connected device for its OS family (HarmonyOS / OpenHarmony), version, API level, CPU ABI, device type and model.",
4529
+ false
4530
+ )
4531
+ }),
4532
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4533
+ changesDevice: false,
4534
+ async call(a) {
4535
+ const long = a.long;
4536
+ const { devices: list2, infos } = await queryDevices(long);
4537
+ return jsonResult({ devices: devicesJson(list2, infos, long) });
4538
+ }
4539
+ };
4540
+ var ALL_TOOLS = [
4541
+ build,
4542
+ checkArktsTool,
4543
+ test,
4544
+ apply,
4545
+ launch,
4546
+ dumpLayoutTool,
4547
+ input,
4548
+ screenshot,
4549
+ wait,
4550
+ logsCrash,
4551
+ perfTraceTool,
4552
+ screens,
4553
+ apiSearch,
4554
+ apiShow,
4555
+ doctor,
4556
+ devices
4557
+ ];
4558
+ var TOOL_NAMES = ALL_TOOLS.map((t) => t.name);
4559
+ function selectTools(names, readOnly) {
4560
+ if (names) {
4561
+ const unknown = names.filter((n) => !TOOL_NAMES.includes(n));
4562
+ if (unknown.length) throw new Error(`Unknown tool${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}. Tools: ${TOOL_NAMES.join(", ")}.`);
4563
+ }
4564
+ return ALL_TOOLS.filter((t) => (!names || names.includes(t.name)) && !(readOnly && t.changesDevice));
4565
+ }
4566
+
4567
+ // src/commands/serve.ts
4568
+ function parseToolList(raw) {
4569
+ if (raw === void 0) return void 0;
4570
+ const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
4571
+ if (!names.length) throw new OniroError5("--tools needs at least one tool name.");
4572
+ return names;
4573
+ }
4574
+ function instructions(ctx, toolCount) {
4575
+ return `oniro-app: build, check, test, deploy and inspect the OpenHarmony/HarmonyOS app in ${ctx.projectDir}${ctx.deviceSerial ? ` on device ${ctx.deviceSerial}` : ""} (${toolCount} tools). Each tool runs the matching \`oniro-app\` command and returns its \`--json\` object, as structured content and as JSON text; isError is true when the command would exit non-zero (a failed build, check, test or smoke check, a missing symbol) or could not run. Relative paths resolve against the project. A typical round: check_arkts on the changed files, build, apply with smoke, then screenshot and dump_layout (flat) to see the result, input to drive it, logs_crash when it died.`;
4576
+ }
4577
+ function registerServeCommand(program) {
4578
+ const serve = program.command("serve").description("Serve oniro-app to other programs.");
4579
+ serve.command("mcp").description(
4580
+ `A Model Context Protocol server on stdio (newline-delimited JSON-RPC 2.0) exposing the gate commands as typed tools for agents: ${TOOL_NAMES.join(", ")}. Each tool returns the JSON the command's --json prints. stdout carries only the protocol; logs go to stderr. Register it with Claude Code: \`claude mcp add oniro-app -- oniro-app serve mcp --project <dir>\`.`
4581
+ ).allowExcessArguments(false).option("--project <dir>", "Project the tools work on unless a call names another (default: the current directory).").option("--device <serial>", "Device the tools target unless a call names another (default: the only connected one).").option("--tools <list>", `Expose only these tools, comma-separated (of ${TOOL_NAMES.join(", ")}).`).option("--read-only", "Leave out the tools that change the device: apply, launch, input, test.").action(async (opts) => {
4582
+ const { logger } = getRuntime();
4583
+ const projectDir = path19.resolve(opts.project ?? process.cwd());
4584
+ if (!fs10.existsSync(projectDir)) throw new OniroError5(`--project ${projectDir} does not exist.`);
4585
+ const tools = selectTools(parseToolList(opts.tools), !!opts.readOnly);
4586
+ if (!tools.length) throw new OniroError5("No tool left to serve: --read-only removed every tool --tools named.");
4587
+ const context = { projectDir, deviceSerial: opts.device };
4588
+ const server = new McpServer({
4589
+ name: "oniro-app",
4590
+ version: program.version() ?? "0.0.0",
4591
+ tools,
4592
+ context,
4593
+ instructions: instructions(context, tools.length),
4594
+ logger
4595
+ });
4596
+ const protocolWrite = process.stdout.write.bind(process.stdout);
4597
+ process.stdout.write = ((chunk, ...rest) => process.stderr.write(chunk, ...rest));
4598
+ process.stdout.on("error", () => process.exit(0));
4599
+ logger.info(`oniro-app MCP server on stdio: project ${projectDir}, tools ${tools.map((t) => t.name).join(", ")}`);
4600
+ await serveStdio(server, { write: (line) => void protocolWrite(line) });
4601
+ logger.info("stdin closed; MCP server exiting.");
4602
+ protocolWrite("", () => process.exit(0));
4603
+ });
4604
+ }
4605
+
4606
+ // src/commands/record.ts
4607
+ import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
4608
+ import * as fs11 from "fs";
4609
+ import * as path20 from "path";
4610
+ import { formatFlow, recordFlow } from "@oniroproject/core";
4611
+ function positiveMs3(raw) {
4612
+ const n = Number(raw);
4613
+ if (!Number.isFinite(n) || n <= 0) throw new InvalidArgumentError6("Expected a positive number of milliseconds.");
4614
+ return Math.floor(n);
4615
+ }
4616
+ function registerRecordCommand(program) {
4617
+ program.command("record").description(
4618
+ "Record what is done on the device into a flow file for `replay`. Runs uitest's recorder (`uitest uiRecord record -l`), which logs every tap, swipe and key with the widget under the finger and dumps the layout after each one; each event becomes a step with the most specific stable selector for that widget in the layout on screen when it happened, a fallback point, and an `expect` element that appeared after it. Typing collapses into one inputText step per field. Stops on Ctrl+C or after --for."
4619
+ ).requiredOption("--out <file>", "Where to write the flow (JSON).").option("--for <ms>", "Stop after this long instead of waiting for Ctrl+C.", positiveMs3).option("--bundle <bundle>", "The app being recorded (default: the foreground app when recording starts).").option("--no-wake", "Leave the screen as it is (by default it is turned on, a lock screen without a credential is swiped away, and it is kept on while recording).").option("--raw <dir>", "Also keep uitest's raw recording (events, layouts, touch times) in this directory, for bug reports.").option("--device <serial>", "Target device serial.").option("--json", "Print {out, steps, events, warnings} as JSON.").action(async (opts) => {
4620
+ const { config, logger } = getRuntime();
4621
+ await withInterrupt(logger, "stopping the recorder", async (abortSignal) => {
4622
+ const result = await recordFlow({
4623
+ config,
4624
+ durationMs: opts.for,
4625
+ abortSignal,
4626
+ bundle: opts.bundle,
4627
+ wake: opts.wake,
4628
+ rawDir: opts.raw,
4629
+ deviceSerial: opts.device,
4630
+ logger,
4631
+ onEvents: (n) => logger.info(`${n} event line(s) recorded`)
4632
+ });
4633
+ const out = path20.resolve(opts.out);
4634
+ fs11.mkdirSync(path20.dirname(out), { recursive: true });
4635
+ fs11.writeFileSync(out, formatFlow(result.flow));
4636
+ for (const w of result.warnings) logger.warn(w);
4637
+ if (opts.json) {
4638
+ process.stdout.write(
4639
+ `${JSON.stringify({ out, steps: result.flow.steps.length, events: result.events, warnings: result.warnings }, null, 2)}
4640
+ `
4641
+ );
4642
+ } else {
4643
+ process.stdout.write(`Wrote ${result.flow.steps.length} step(s) to ${out}.
4644
+ `);
4645
+ }
4646
+ });
4647
+ });
4648
+ }
4649
+
4650
+ // src/commands/replay.ts
4651
+ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
4652
+ import * as path21 from "path";
4653
+ import { readFlowFile as readFlowFile3, replayFlow } from "@oniroproject/core";
4654
+ function stepIndex(raw) {
4655
+ const n = Number(raw);
4656
+ if (!Number.isInteger(n) || n < 0) throw new InvalidArgumentError7("Expected a step index (0-based).");
4657
+ return n;
4658
+ }
4659
+ function positiveMs4(raw) {
4660
+ const n = Number(raw);
4661
+ if (!Number.isFinite(n) || n <= 0) throw new InvalidArgumentError7("Expected a positive number of milliseconds.");
4662
+ return Math.floor(n);
4663
+ }
4664
+ function replayExitCode(report) {
4665
+ return report.outcome === "passed" ? 0 : report.outcome === "diverged" ? 1 : 2;
4666
+ }
4667
+ function formatReplayStep(s) {
4668
+ const what = s.selector ? ` ${s.selector}` : "";
4669
+ const how = s.usedFallback ? " (at the recorded point)" : "";
4670
+ return `[${s.index}] ${s.action}${what}${how} ${s.ok ? "ok" : "FAILED"} ${(s.durationMs / 1e3).toFixed(1)} s`;
4671
+ }
4672
+ function formatDivergence(r) {
4673
+ const lines = [`Diverged at step ${r.failedStep} (${r.reason}): ${r.message}`];
4674
+ if (r.screenshot) lines.push(` screenshot: ${r.screenshot}`);
4675
+ for (const n of r.nearMatches ?? []) lines.push(` near match: ${n.element} (differs in ${n.differs})`);
4676
+ if (r.layout) {
4677
+ const where = r.layout.appOnScreen ? `on screen in ${r.bundle}` : "on screen";
4678
+ lines.push(` ${where} (${r.layout.elementCount} element(s)${r.layout.keyboard ? ", keyboard up" : ""}):`);
4679
+ for (const e of r.layout.elements) lines.push(` ${e}`);
4680
+ if (r.layout.elementCount > r.layout.elements.length) lines.push(` \u2026 ${r.layout.elementCount - r.layout.elements.length} more`);
4681
+ }
4682
+ return lines.join("\n");
4683
+ }
4684
+ function registerReplayCommand(program) {
4685
+ program.command("replay <flow>").description(
4686
+ "Replay a flow file (written by `record` or by hand) on the device: for each step, wait for its selector, act on it the way `input --selector` does, then wait for its `expect`. Stops at the first step that fails and reports the step, why (not-found, expect-not-found, wrong-screen, ambiguous, covered, action-failed), a screenshot and what was on screen. Exits 0 when every step ran, 1 when the flow diverged, 2 when it could not run (invalid flow file, no device)."
4687
+ ).option("--from <i>", "First step to run (0-based).", stepIndex).option("--to <i>", "Last step to run (0-based, inclusive).", stepIndex).option("--step-timeout <ms>", "How long to wait for a step's selector and its expect (a step's timeoutMs overrides it).", positiveMs4, 1e4).option("--screenshots <dir>", "Save a screenshot after every step (step-<i>.jpeg), and the divergence screenshot, here.").option("--fallback", "When a step's selector is not found, act on its recorded fallback point instead of stopping.").option("--launch", "Force-stop and start the flow's app (its bundle and ability) before the first step.").option("--no-wake", "Leave the screen as it is (by default it is turned on, a lock screen without a credential is swiped away, and it is kept on during the run).").option("--device <serial>", "Target device serial.").option(
4688
+ "--json",
4689
+ "Print the report as JSON: ok, outcome, steps, failedStep, reason, message, selector, screenshot, layout, nearMatches, error, warnings."
4690
+ ).action(async (flowPath, opts) => {
4691
+ const { config, logger } = getRuntime();
4692
+ let report;
4693
+ try {
4694
+ const flow = readFlowFile3(path21.resolve(flowPath));
4695
+ report = await withInterrupt(logger, "stopping the replay", (abortSignal) => replayFlow({
4696
+ config,
4697
+ flow,
4698
+ abortSignal,
4699
+ from: opts.from,
4700
+ to: opts.to,
4701
+ stepTimeoutMs: opts.stepTimeout,
4702
+ screenshotsDir: opts.screenshots,
4703
+ fallback: opts.fallback,
4704
+ launch: opts.launch,
4705
+ wake: opts.wake,
4706
+ deviceSerial: opts.device,
4707
+ logger,
4708
+ onStep: opts.json ? void 0 : (s) => process.stdout.write(`${formatReplayStep(s)}
4709
+ `)
4710
+ }));
4711
+ } catch (err) {
4712
+ report = { ok: false, outcome: "error", range: [0, 0], steps: [], error: err.message, warnings: [] };
4713
+ }
4714
+ if (opts.json) {
4715
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
4716
+ `);
4717
+ } else if (report.outcome === "passed") {
4718
+ process.stdout.write(`Flow passed: ${report.steps.length} step(s).
4719
+ `);
4720
+ } else if (report.outcome === "diverged") {
4721
+ process.stdout.write(`${formatDivergence(report)}
4722
+ `);
4723
+ }
4724
+ for (const w of report.warnings) logger.warn(w);
4725
+ if (report.error) logger.error(report.error);
4726
+ process.exitCode = replayExitCode(report);
4727
+ });
4728
+ }
4729
+
4730
+ // src/bin/oniro-app.ts
4731
+ var CLI_VERSION = "0.18.0";
4732
+ function buildProgram() {
4733
+ const program = new Command11().name("oniro-app").description(
4734
+ "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."
4735
+ ).version(CLI_VERSION).showHelpAfterError().enablePositionalOptions().exitOverride();
4736
+ registerSdkCommand(program);
4737
+ registerCmdToolsCommand(program);
4738
+ registerEmulatorCommand(program);
4739
+ registerBuildCommand(program);
4740
+ registerSignCommand(program);
4741
+ registerAuthCommand(program);
4742
+ registerAppCommand(program);
4743
+ registerCreateCommand(program);
4744
+ registerTemplatesCommand(program);
4745
+ registerRebootCommand(program);
4746
+ registerWaitCommand(program);
4747
+ registerWatchCommand(program);
4748
+ registerScreenshotCommand(program);
4749
+ registerDumpCommand(program);
4750
+ registerDevicesCommand(program);
4751
+ registerFileCommand(program);
4752
+ registerLintCommand(program);
4753
+ registerCheckCommand(program);
4754
+ registerDoctorCommand(program);
4755
+ registerScreensCommand(program);
4756
+ registerApiCommand(program);
4757
+ registerTestCommand(program);
4758
+ registerInputCommand(program);
4759
+ registerRecordCommand(program);
4760
+ registerReplayCommand(program);
4761
+ registerPerfCommand(program);
4762
+ registerLogsCommand(program);
4763
+ registerHdcCommand(program);
4764
+ registerServeCommand(program);
4765
+ return program;
4766
+ }
4767
+ async function main() {
4768
+ const program = buildProgram();
4769
+ try {
4770
+ await program.parseAsync(process.argv);
4771
+ } catch (err) {
4772
+ if (err instanceof CommanderError) {
4773
+ process.exitCode = err.exitCode;
4774
+ return;
2056
4775
  }
2057
- process.exit(1);
4776
+ const message2 = err instanceof OniroError6 ? err.message : err instanceof Error ? err.message : String(err);
4777
+ const stack = process.env.ONIRO_DEBUG === "1" && err instanceof Error && err.stack ? `${err.stack}
4778
+ ` : "";
4779
+ process.stderr.write(`[error] ${message2}
4780
+ ${stack}`, () => process.exit(1));
2058
4781
  }
2059
4782
  }
2060
4783
  void main();