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