@oniroproject/oniro-app 0.14.0 → 0.16.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
@@ -2,17 +2,19 @@
2
2
 
3
3
  // src/bin/oniro-app.ts
4
4
  import { Command as Command4 } from "commander";
5
- import { OniroError as OniroError2 } from "@oniroproject/core";
5
+ import { OniroError as OniroError3 } from "@oniroproject/core";
6
6
 
7
7
  // src/commands/sdk.ts
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
10
  import {
11
11
  ALL_SDKS,
12
+ OHOS_DAILY_BUILDS_URL,
12
13
  downloadAndInstallSdk,
13
14
  getOsFolder,
14
15
  getSdkRootDir,
15
16
  getSupportedSdksForUi,
17
+ installSdkFromArchive,
16
18
  removeSdk
17
19
  } from "@oniroproject/core";
18
20
 
@@ -104,13 +106,42 @@ function getRuntime() {
104
106
  }
105
107
 
106
108
  // src/commands/sdk.ts
109
+ 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>\`.`;
107
110
  function registerSdkCommand(program) {
108
111
  const sdk = program.command("sdk").description("Manage OpenHarmony SDK installs.");
109
- sdk.command("install <version>").description("Download and install an OpenHarmony SDK. Skips if already installed.").option("--force", "Reinstall even if the SDK is already present.").option(
112
+ sdk.command("install [version]").description(
113
+ "Download and install an OpenHarmony SDK (skips if already installed), or install one from a local archive with --from-archive."
114
+ ).option(
115
+ "--from-archive <path>",
116
+ "Install from a local SDK .tar.gz instead of downloading, e.g. the full SDK. The API level is read from the archive."
117
+ ).option("--force", "Reinstall even if the SDK is already present.").option(
110
118
  "--tmp-dir <path>",
111
119
  "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."
112
- ).action(async (version, opts) => {
120
+ ).addHelpText("after", `
121
+ ${FULL_SDK_TIP}`).action(async (version, opts) => {
113
122
  const { config, logger, progress } = getRuntime();
123
+ if (opts.fromArchive) {
124
+ if (version) throw new Error("Pass either a version or --from-archive, not both.");
125
+ const archivePath = path.resolve(opts.fromArchive);
126
+ logger.info(`Installing OpenHarmony SDK from ${archivePath}...`);
127
+ const installed = await installSdkFromArchive({
128
+ config,
129
+ archivePath,
130
+ force: opts.force,
131
+ progress,
132
+ logger,
133
+ tmpDir: opts.tmpDir
134
+ });
135
+ const label = installed.version ? `SDK ${installed.version} (api ${installed.api})` : `SDK api ${installed.api}`;
136
+ logger.info(`${label} installed at ${installed.dir}.`);
137
+ return;
138
+ }
139
+ if (!version) {
140
+ throw new Error(
141
+ `Name an SDK version (${ALL_SDKS.map((s) => s.version).join(", ")}) or pass --from-archive <path>.
142
+ ` + FULL_SDK_TIP
143
+ );
144
+ }
114
145
  const release = ALL_SDKS.find((s) => s.version === version);
115
146
  if (!release) {
116
147
  throw new Error(
@@ -488,7 +519,17 @@ function registerAuthCommand(program) {
488
519
 
489
520
  // src/commands/app.ts
490
521
  import * as path4 from "path";
491
- import { installApp, launchApp, uninstallApp, forceStop, applyChanges, getBundleName } from "@oniroproject/core";
522
+ import {
523
+ installApp,
524
+ launchApp,
525
+ uninstallApp,
526
+ forceStop,
527
+ applyChanges,
528
+ existingHapOrUnsigned,
529
+ getBundleName,
530
+ readHapBundleName,
531
+ OniroError as OniroError2
532
+ } from "@oniroproject/core";
492
533
  function registerAppCommand(program) {
493
534
  const app = program.command("app").description("Install, launch, and manage built apps on a connected device/emulator.");
494
535
  app.command("install [project-dir-or-hap]").description(
@@ -516,16 +557,29 @@ function registerAppCommand(program) {
516
557
  }
517
558
  logger.info("App launched.");
518
559
  });
519
- app.command("apply [project-dir]").description(
520
- "Install a HAP and verify the running process took the change. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
521
- ).option("--bundle <bundle>", "Bundle name to apply changes to. Defaults to the bundleName in the project AppScope/app.json5.").option("--module <module>", "Module to resolve the HAP from (multi-module projects).").option("--hap <path>", "Explicit .hap path (relative to project-dir or absolute).").option("--installed-hap <path>", "Local path to the currently-installed HAP, to enable the asset-cache reboot.").option("--system", "Treat as a persistent/system bundle (refuses uninstall on sign-info mismatch).").option("--allow-uninstall", "Permit uninstalling a system bundle on sign-info mismatch (dangerous).").option("--device <serial>", "Target device serial.").option("--json", "Emit the result as JSON.").action(async (projectDir, opts) => {
560
+ app.command("apply [project-dir-or-hap]").description(
561
+ "Install a HAP (a project dir, default the current one, or a .hap file) and verify the running process took the change. Refuses a HAP of another bundle. Handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart."
562
+ ).option(
563
+ "--bundle <bundle>",
564
+ "Bundle name to apply changes to. Defaults to the bundleName in the .hap file, else in the project AppScope/app.json5."
565
+ ).option("--module <module>", "Module to resolve the HAP from (multi-module projects).").option("--hap <path>", "Explicit .hap path (relative to project-dir or absolute).").option("--installed-hap <path>", "Local path to the currently-installed HAP, to enable the asset-cache reboot.").option("--system", "Treat as a persistent/system bundle (refuses uninstall on sign-info mismatch).").option("--allow-uninstall", "Permit uninstalling a system bundle on sign-info mismatch (dangerous).").option("--device <serial>", "Target device serial.").option("--json", "Emit the result as JSON.").action(async (target, opts) => {
522
566
  const { config, logger } = getRuntime();
523
- const dir = path4.resolve(projectDir ?? process.cwd());
524
- const bundle = opts.bundle ?? getBundleName(dir);
567
+ const isHap = target !== void 0 && /\.hap$/i.test(target);
568
+ if (isHap && opts.hap) throw new OniroError2("Pass the HAP either as the argument or with --hap, not both.");
569
+ const dir = isHap ? process.cwd() : path4.resolve(target ?? process.cwd());
570
+ const hapPath = isHap ? path4.resolve(target) : opts.hap;
571
+ let bundle = opts.bundle;
572
+ if (!bundle && isHap) {
573
+ const hap = existingHapOrUnsigned(hapPath);
574
+ if (!hap) throw new OniroError2(`HAP not found at ${hapPath}.`);
575
+ bundle = await readHapBundleName(hap.path).catch(() => null) ?? void 0;
576
+ if (!bundle) throw new OniroError2(`Could not read the bundle name from ${hap.path}. Pass --bundle.`);
577
+ }
578
+ bundle ??= getBundleName(dir);
525
579
  const result = await applyChanges({
526
580
  config,
527
581
  bundle,
528
- hapPath: opts.hap,
582
+ hapPath,
529
583
  projectDir: dir,
530
584
  module: opts.module,
531
585
  installedHapPath: opts.installedHap,
@@ -685,16 +739,31 @@ function targetIndexFrom(opts) {
685
739
  return n;
686
740
  }
687
741
 
742
+ // src/lib/logSince.ts
743
+ function parseSince(value) {
744
+ if (value === void 0) return void 0;
745
+ if (value === "all") return Infinity;
746
+ const seconds = Number(value);
747
+ if (value.trim() === "" || !Number.isFinite(seconds) || seconds < 0) {
748
+ throw new Error(`--since takes a number of seconds or "all", not "${value}".`);
749
+ }
750
+ return seconds * 1e3;
751
+ }
752
+
688
753
  // src/commands/wait.ts
689
754
  function registerWaitCommand(program) {
690
755
  const wait = program.command("wait").description(
691
756
  "Wait for a device condition: a log line (--log), boot (--boot), a bundle to start (--bundle), or an on-screen element (--target / --target-id / --target-type)."
692
- ).option("--log <pattern>", "Resolve when a hilog line (tag: message) matches this regex.").option("--boot", "Resolve when the device is reachable (optionally also --pid-of).").option("--bundle <bundle>", "With --log: filter to this bundle. Alone: wait until this bundle is running.").option("--pid-of <name>", "With --boot: also require this process to have a pid.").option("--domain <domain>", "With --log: hilog domain filter (e.g. 0xD003900).");
757
+ ).option("--log <pattern>", "Resolve when a hilog line (tag: message) logged after the command starts matches this regex.").option(
758
+ "--since <seconds>",
759
+ 'With --log: also match lines logged up to N seconds before the command started, measured on the device clock; "all" matches everything still in the device log buffer.'
760
+ ).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).");
693
761
  addTargetOptions(wait, "Resolve when", { index: false });
694
762
  wait.option("--gone", "With --target*: resolve when no uncovered element matches any more.").option("--interval <ms>", "With --target*: poll interval, ms (each poll is a layout dump).", "500").option("--timeout <ms>", "Max wait, ms.", "30000").option("--device <serial>", "Target device serial.").option("--json", "With --log or --target*: emit the matched entry/element as JSON.").action(async (opts) => {
695
763
  const { config, logger } = getRuntime();
696
764
  const timeoutMs = Number(opts.timeout);
697
765
  const selector = selectorFrom(opts);
766
+ if (opts.since !== void 0 && !opts.log) throw new Error("--since applies only to --log.");
698
767
  if (opts.log) {
699
768
  const entry = await waitForLog({
700
769
  config,
@@ -703,11 +772,12 @@ function registerWaitCommand(program) {
703
772
  bundle: opts.bundle,
704
773
  domain: opts.domain,
705
774
  deviceSerial: opts.device,
775
+ lookbackMs: parseSince(opts.since),
706
776
  logger
707
777
  });
708
778
  if (opts.json) process.stdout.write(`${JSON.stringify(entry, null, 2)}
709
779
  `);
710
- else logger.info(`matched: ${entry.tag}: ${entry.message}`);
780
+ else logger.info(`matched: ${entry.time} ${entry.tag}: ${entry.message}`);
711
781
  return;
712
782
  }
713
783
  if (opts.boot) {
@@ -746,7 +816,10 @@ function registerWaitCommand(program) {
746
816
  // src/commands/watch.ts
747
817
  import { watchLog } from "@oniroproject/core";
748
818
  function registerWatchCommand(program) {
749
- program.command("watch").description("Collect hilog lines matching a pattern for a fixed duration.").option("--log <pattern>", "Regex tested against each line (tag: message).").option("--for <ms>", "Duration to watch, ms.", "10000").option("--bundle <bundle>", "Filter to this bundle.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--no-dedup", "Do not collapse consecutive duplicate lines.").option("--device <serial>", "Target device serial.").option("--json", "Emit the entries as JSON.").action(async (opts) => {
819
+ program.command("watch").description("Collect the hilog lines matching a pattern that are logged during a fixed duration.").option("--log <pattern>", "Regex tested against each line (tag: message).").option("--for <ms>", "Duration to watch, ms.", "10000").option(
820
+ "--since <seconds>",
821
+ 'Also collect lines logged up to N seconds before the command started, measured on the device clock; "all" adds everything still in the device log buffer.'
822
+ ).option("--bundle <bundle>", "Filter to this bundle.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--no-dedup", "Do not collapse consecutive duplicate lines.").option("--device <serial>", "Target device serial.").option("--json", "Emit the entries as JSON.").action(async (opts) => {
750
823
  const { config, logger } = getRuntime();
751
824
  if (!opts.log) throw new Error("watch requires --log <pattern>.");
752
825
  const entries = await watchLog({
@@ -757,6 +830,7 @@ function registerWatchCommand(program) {
757
830
  domain: opts.domain,
758
831
  dedup: opts.dedup,
759
832
  deviceSerial: opts.device,
833
+ lookbackMs: parseSince(opts.since),
760
834
  logger
761
835
  });
762
836
  if (opts.json) {
@@ -1122,9 +1196,9 @@ import { getDeviceInfo, listDevices } from "@oniroproject/core";
1122
1196
  function formatLongDevice(d, info) {
1123
1197
  const head = `${d.serial} ${d.connection ?? ""} ${d.status}`;
1124
1198
  if (!info) return head;
1125
- const os2 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
1199
+ const os3 = [info.osFamily, info.osFamily === "HarmonyOS" ? info.osDistVersion : void 0].filter(Boolean).join(" ");
1126
1200
  const fields = [
1127
- ["os", os2 || void 0],
1201
+ ["os", os3 || void 0],
1128
1202
  ["api", info.sdkApiVersion],
1129
1203
  ["abi", info.abi],
1130
1204
  ["type", info.deviceType],
@@ -1194,6 +1268,7 @@ import {
1194
1268
  detectCmdToolsFlavour,
1195
1269
  detectRuntimeOs as detectRuntimeOs2,
1196
1270
  getCmdToolsPath,
1271
+ missingProjectRootFiles,
1197
1272
  runCodelinter
1198
1273
  } from "@oniroproject/core";
1199
1274
  function lintTotals(result) {
@@ -1208,8 +1283,10 @@ function lintTotals(result) {
1208
1283
  }
1209
1284
  function lintExitCode(result) {
1210
1285
  if (lintTotals(result).errors > 0) return 1;
1211
- const noReport = result.summary === null && result.findings.length === 0;
1212
- return noReport && result.code !== 0 ? result.code : 0;
1286
+ return lintFailed(result) ? result.code : 0;
1287
+ }
1288
+ function lintFailed(result) {
1289
+ return result.summary === null && result.findings.length === 0 && result.code !== 0;
1213
1290
  }
1214
1291
  function registerLintCommand(program) {
1215
1292
  program.command("lint [project-dir]").description(
@@ -1219,10 +1296,11 @@ function registerLintCommand(program) {
1219
1296
  const dir = path8.resolve(projectDir ?? process.cwd());
1220
1297
  const result = await runCodelinter({ config, projectDir: dir, files: opts.files, logger });
1221
1298
  const totals = lintTotals(result);
1299
+ const failed = lintFailed(result);
1222
1300
  if (opts.json) {
1223
1301
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1224
1302
  `);
1225
- } else {
1303
+ } else if (!failed) {
1226
1304
  for (const f of result.findings) {
1227
1305
  const at = `${f.file}:${f.line}${f.column ? `:${f.column}` : ""}`;
1228
1306
  process.stdout.write(`${at} ${f.severity} ${f.message}${f.code ? ` [${f.code}]` : ""}
@@ -1233,9 +1311,13 @@ function registerLintCommand(program) {
1233
1311
  `
1234
1312
  );
1235
1313
  }
1236
- if (result.summary === null && result.findings.length === 0 && result.code !== 0) {
1237
- logger.error(`codelinter exited ${result.code} without a report:
1238
- ${result.raw.trim().split("\n").slice(-15).join("\n")}`);
1314
+ if (failed) {
1315
+ const missing = missingProjectRootFiles(dir);
1316
+ const why = missing.length ? ` It needs a DevEco project root, and ${dir} has no ${missing.join(", ")}.` : "";
1317
+ logger.error(
1318
+ `codelinter exited ${result.code} without a report, so nothing was linted.${why}
1319
+ ` + result.raw.trim().split("\n").slice(-15).join("\n")
1320
+ );
1239
1321
  }
1240
1322
  if (result.incomplete) {
1241
1323
  for (const n of result.notices) logger.warn(`codelinter: ${n}`);
@@ -1337,14 +1419,531 @@ function registerInputCommand(program) {
1337
1419
  }
1338
1420
 
1339
1421
  // src/commands/logs.ts
1340
- import * as path9 from "path";
1422
+ import * as os2 from "os";
1423
+ import * as path10 from "path";
1341
1424
  import { captureLog } from "@oniroproject/core";
1425
+
1426
+ // src/lib/logBundle.ts
1427
+ import * as fs4 from "fs/promises";
1428
+ import * as path9 from "path";
1429
+ import { deflateRawSync } from "zlib";
1430
+ import {
1431
+ VERSION,
1432
+ hdcExec,
1433
+ parseDeviceList,
1434
+ resolveDeviceSerial,
1435
+ shellChecked
1436
+ } from "@oniroproject/core";
1437
+ var LEVELS = {
1438
+ V: "V",
1439
+ VERBOSE: "V",
1440
+ D: "D",
1441
+ DEBUG: "D",
1442
+ I: "I",
1443
+ INFO: "I",
1444
+ W: "W",
1445
+ WARN: "W",
1446
+ WARNING: "W",
1447
+ E: "E",
1448
+ ERROR: "E",
1449
+ F: "F",
1450
+ FATAL: "F"
1451
+ };
1452
+ var HILOG_RECORD = /^(?<date>\d{2}-\d{2})[ \t]+(?<time>\d{2}:\d{2}:\d{2}\.\d{3})[ \t]+(?<pid>\d+)[ \t]+\d+[ \t]+(?<level>[VDIWEF])[ \t]+(?<tag>\S+?)(?:[ \t]+[NH](?::(?:[ \t]+.*)?|[ \t]+.*)?|:(?:[ \t]+.*)?)$/;
1453
+ var FAILURE_TEXT = /^(?:\s*(?:\[(?:fail|error)\]|(?:error|failed|failure):|permission denied\b|no devices? connected\b|too many arguments\b|unknown option\b|invalid option\b|command not found\b)|\s*(?:hdc|device)\b.*\b(?:error|failed|no device|not connected|offline)\b)/im;
1454
+ function validateDate(date, time) {
1455
+ const [, monthText, dayText] = /^(\d{2})-(\d{2})$/.exec(date) ?? [];
1456
+ const [, hourText, minuteText, secondText, millisecondText] = /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/.exec(time) ?? [];
1457
+ if (!monthText || !dayText || !hourText || !minuteText || !secondText || !millisecondText) return false;
1458
+ const value = new Date(Date.UTC(2e3, Number(monthText) - 1, Number(dayText), Number(hourText), Number(minuteText), Number(secondText), Number(millisecondText)));
1459
+ return value.getUTCMonth() === Number(monthText) - 1 && value.getUTCDate() === Number(dayText) && Number(hourText) < 24 && Number(minuteText) < 60 && Number(secondText) < 60;
1460
+ }
1461
+ function filterHilog(input, filters) {
1462
+ const criteria = {};
1463
+ if (filters.pid) criteria.pid = filters.pid.replace(/^0+(?=\d)/, "");
1464
+ if (filters.tag) criteria.tag = filters.tag;
1465
+ if (filters.level) criteria.level = LEVELS[filters.level.toUpperCase()] ?? filters.level.toUpperCase();
1466
+ if (Object.keys(criteria).length === 0) return null;
1467
+ let sawRecord = false;
1468
+ let keep = false;
1469
+ let output = "";
1470
+ for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
1471
+ const content = line.replace(/[\r\n]+$/, "");
1472
+ const match = HILOG_RECORD.exec(content);
1473
+ if (match?.groups) {
1474
+ if (!validateDate(match.groups.date, match.groups.time)) return null;
1475
+ sawRecord = true;
1476
+ keep = Object.entries(criteria).every(([key, value]) => {
1477
+ const field = key === "tag" ? match.groups.tag.slice(match.groups.tag.lastIndexOf("/") + 1) : match.groups[key];
1478
+ return key === "pid" ? field?.replace(/^0+(?=\d)/, "") === value : field === value;
1479
+ });
1480
+ } else if (/^\s*\[?\d{1,4}[-/]/.test(content) || !sawRecord && content.trim()) {
1481
+ return null;
1482
+ }
1483
+ if (keep) output += line;
1484
+ }
1485
+ return sawRecord ? { text: output, applied: Object.keys(criteria) } : null;
1486
+ }
1487
+ function redactLogText(value, deviceSerials = []) {
1488
+ let result = value.replace(/(\bBearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]").replace(/(\bBasic\s+)[A-Za-z0-9+/=]+/gi, "$1[REDACTED]").replace(/((?:token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]").replace(/(cookie\s*[=:]\s*)[^\r\n]+/gi, "$1[REDACTED]").replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]").replace(/((?:phone|mobile|telephone|tel)(?:number)?\s*[=:]\s*)\+?[\d ()-]{8,}\d/gi, "$1[REDACTED_PHONE]").replace(/(?<![\w.])(?:\+\d{1,3}[ -]?)?\(?\d{2,4}\)?[ -]\d{3,4}[ -]\d{3,4}(?![\w.])/g, "[REDACTED_PHONE]").replace(/\b(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\b/g, "[REDACTED_MAC]").replace(/([A-Z]:\\Users\\)[^\\\r\n "']+/gi, "$1[REDACTED_USER]").replace(/(\/(?:Users|home)\/)[^/\r\n "']+/gi, "$1[REDACTED_USER]").replace(/("(?:target|deviceSerial)"\s*:\s*")[^"]+(")/gi, "$1[REDACTED_DEVICE]$2").replace(/(--device\s+|-t\s+)([A-Za-z0-9._:-]+)/gi, "$1[REDACTED_DEVICE]");
1489
+ for (const serial of deviceSerials) {
1490
+ if (serial) result = result.split(serial).join("[REDACTED_DEVICE]");
1491
+ }
1492
+ const ipv4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
1493
+ result = result.replace(ipv4, (address, offset, whole) => {
1494
+ const prefix = whole.slice(Math.max(0, offset - 48), offset);
1495
+ const suffix = whole.slice(offset + address.length);
1496
+ const contextual = /(?:ip|addr(?:ess)?|host|peer|remote|local|endpoint|server|client|target|device(?:id)?|serial)[^\r\n]{0,24}$/i.test(prefix);
1497
+ const hasPort = /^:\d{1,5}(?!\d)/.test(suffix);
1498
+ const octets = address.split(".").map(Number);
1499
+ return octets.every((part) => part <= 255) && (contextual || hasPort) ? "[REDACTED_IP]" : address;
1500
+ });
1501
+ return result;
1502
+ }
1503
+ function decodeText(data, relativePath) {
1504
+ let text;
1505
+ if (data.subarray(0, 2).equals(Buffer.from([255, 254]))) text = data.toString("utf16le").slice(1);
1506
+ else if (data.subarray(0, 2).equals(Buffer.from([254, 255]))) {
1507
+ const swapped = Buffer.from(data.subarray(2));
1508
+ for (let i = 0; i + 1 < swapped.length; i += 2) [swapped[i], swapped[i + 1]] = [swapped[i + 1], swapped[i]];
1509
+ text = swapped.toString("utf16le");
1510
+ } else {
1511
+ const utf8 = data.subarray(0, 3).equals(Buffer.from([239, 187, 191])) ? data.subarray(3) : data;
1512
+ if (utf8.includes(0)) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
1513
+ try {
1514
+ text = new TextDecoder("utf-8", { fatal: true }).decode(utf8);
1515
+ } catch {
1516
+ throw new Error(`Non-UTF-8 data cannot be safely redacted: ${relativePath}`);
1517
+ }
1518
+ }
1519
+ if (text.includes("\0")) throw new Error(`Binary data cannot be safely redacted: ${relativePath}`);
1520
+ return text;
1521
+ }
1522
+ async function redactTree(source, destination, serials, boundedHilog) {
1523
+ await fs4.mkdir(destination, { recursive: false, mode: 448 });
1524
+ try {
1525
+ const visit = async (directory) => {
1526
+ for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
1527
+ const sourcePath = path9.join(directory, entry.name);
1528
+ const relative2 = path9.relative(source, sourcePath);
1529
+ const portableRelative = relative2.split(path9.sep).join("/");
1530
+ const destinationPath = path9.join(destination, relative2);
1531
+ if (entry.isDirectory()) {
1532
+ await fs4.mkdir(destinationPath, { recursive: true, mode: 448 });
1533
+ await visit(sourcePath);
1534
+ } else if (entry.isFile()) {
1535
+ if (portableRelative === "logs/hilog.txt") {
1536
+ const text2 = boundedHilog ?? "HiLog omitted from this archive because the requested duration could not be reliably applied.\n";
1537
+ await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
1538
+ await fs4.writeFile(destinationPath, redactLogText(text2, serials), { encoding: "utf8", mode: 384 });
1539
+ continue;
1540
+ }
1541
+ if (portableRelative === "logs/hilog-filtered.txt" && boundedHilog === null) continue;
1542
+ const text = decodeText(await fs4.readFile(sourcePath), relative2);
1543
+ await fs4.mkdir(path9.dirname(destinationPath), { recursive: true, mode: 448 });
1544
+ await fs4.writeFile(destinationPath, redactLogText(text, serials), { encoding: "utf8", mode: 384 });
1545
+ }
1546
+ }
1547
+ };
1548
+ await visit(source);
1549
+ } catch (error) {
1550
+ await fs4.rm(destination, { recursive: true, force: true });
1551
+ throw error;
1552
+ }
1553
+ }
1554
+ var CRC_TABLE = new Uint32Array(256);
1555
+ for (let i = 0; i < CRC_TABLE.length; i++) {
1556
+ let value = i;
1557
+ for (let bit = 0; bit < 8; bit++) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
1558
+ CRC_TABLE[i] = value >>> 0;
1559
+ }
1560
+ function crc32(data) {
1561
+ let crc = 4294967295;
1562
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
1563
+ return (crc ^ 4294967295) >>> 0;
1564
+ }
1565
+ function dosDateTime(date) {
1566
+ const year = Math.max(1980, date.getFullYear());
1567
+ return {
1568
+ date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(),
1569
+ time: date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2)
1570
+ };
1571
+ }
1572
+ async function writeZip(source, archivePath) {
1573
+ const files = [];
1574
+ const visit = async (directory) => {
1575
+ for (const entry of await fs4.readdir(directory, { withFileTypes: true })) {
1576
+ const filePath = path9.join(directory, entry.name);
1577
+ if (entry.isDirectory()) await visit(filePath);
1578
+ else if (entry.isFile()) files.push(filePath);
1579
+ }
1580
+ };
1581
+ await visit(source);
1582
+ const localParts = [];
1583
+ const centralParts = [];
1584
+ let offset = 0;
1585
+ const stamp = dosDateTime(/* @__PURE__ */ new Date());
1586
+ for (const filePath of files) {
1587
+ const name = Buffer.from(path9.relative(source, filePath).split(path9.sep).join("/"), "utf8");
1588
+ const data = await fs4.readFile(filePath);
1589
+ const compressed = deflateRawSync(data);
1590
+ const checksum = crc32(data);
1591
+ const local = Buffer.alloc(30);
1592
+ local.writeUInt32LE(67324752, 0);
1593
+ local.writeUInt16LE(20, 4);
1594
+ local.writeUInt16LE(2048, 6);
1595
+ local.writeUInt16LE(8, 8);
1596
+ local.writeUInt16LE(stamp.time, 10);
1597
+ local.writeUInt16LE(stamp.date, 12);
1598
+ local.writeUInt32LE(checksum, 14);
1599
+ local.writeUInt32LE(compressed.length, 18);
1600
+ local.writeUInt32LE(data.length, 22);
1601
+ local.writeUInt16LE(name.length, 26);
1602
+ local.writeUInt16LE(0, 28);
1603
+ localParts.push(local, name, compressed);
1604
+ const central = Buffer.alloc(46);
1605
+ central.writeUInt32LE(33639248, 0);
1606
+ central.writeUInt16LE(20, 4);
1607
+ central.writeUInt16LE(20, 6);
1608
+ central.writeUInt16LE(2048, 8);
1609
+ central.writeUInt16LE(8, 10);
1610
+ central.writeUInt16LE(stamp.time, 12);
1611
+ central.writeUInt16LE(stamp.date, 14);
1612
+ central.writeUInt32LE(checksum, 16);
1613
+ central.writeUInt32LE(compressed.length, 20);
1614
+ central.writeUInt32LE(data.length, 24);
1615
+ central.writeUInt16LE(name.length, 28);
1616
+ central.writeUInt16LE(0, 30);
1617
+ central.writeUInt16LE(0, 32);
1618
+ central.writeUInt16LE(0, 34);
1619
+ central.writeUInt16LE(0, 36);
1620
+ central.writeUInt32LE(0, 38);
1621
+ central.writeUInt32LE(offset, 42);
1622
+ centralParts.push(central, name);
1623
+ offset += local.length + name.length + compressed.length;
1624
+ }
1625
+ const centralDirectory = Buffer.concat(centralParts);
1626
+ const end = Buffer.alloc(22);
1627
+ end.writeUInt32LE(101010256, 0);
1628
+ end.writeUInt16LE(0, 4);
1629
+ end.writeUInt16LE(0, 6);
1630
+ end.writeUInt16LE(files.length, 8);
1631
+ end.writeUInt16LE(files.length, 10);
1632
+ end.writeUInt32LE(centralDirectory.length, 12);
1633
+ end.writeUInt32LE(offset, 16);
1634
+ end.writeUInt16LE(0, 20);
1635
+ await fs4.writeFile(archivePath, Buffer.concat([...localParts, centralDirectory, end]), { flag: "wx", mode: 384 });
1636
+ }
1637
+ function commandFailure(result) {
1638
+ if (result.code !== 0) return `hdc exited with code ${result.code}`;
1639
+ if (result.stderr.trim()) return "hdc wrote to stderr";
1640
+ const contentError = result.stdout.split(/\r?\n/).find((line) => FAILURE_TEXT.test(line))?.trim() ?? null;
1641
+ return contentError ? `command output indicates failure: ${contentError}` : null;
1642
+ }
1643
+ function errorMessage(error) {
1644
+ return error instanceof Error ? error.message : String(error);
1645
+ }
1646
+ async function uniqueBundleDirectory(outputDirectory, stamp) {
1647
+ await fs4.mkdir(outputDirectory, { recursive: true, mode: 448 });
1648
+ for (let suffix = 0; ; suffix++) {
1649
+ const name = `incident-${stamp}${suffix ? `-${suffix}` : ""}`;
1650
+ const directory = path9.join(outputDirectory, name);
1651
+ try {
1652
+ await fs4.mkdir(directory, { mode: 448 });
1653
+ return directory;
1654
+ } catch (error) {
1655
+ if (error.code !== "EEXIST") throw error;
1656
+ }
1657
+ }
1658
+ }
1659
+ async function collectLogBundle(options) {
1660
+ if (options.packageName && !/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/.test(options.packageName)) {
1661
+ throw new Error("packageName must be a reverse-DNS bundle name.");
1662
+ }
1663
+ if (options.pid && !/^[1-9]\d*$/.test(options.pid)) throw new Error("pid must be a positive integer.");
1664
+ if (options.tag && !/^[^\s/\x00-\x1f\x7f]+$/.test(options.tag)) throw new Error("tag must be a single HiLog tag.");
1665
+ if (options.level && !LEVELS[options.level.toUpperCase()]) throw new Error("level is not a recognized HiLog level.");
1666
+ const durationSeconds = options.durationSeconds ?? 600;
1667
+ if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
1668
+ throw new Error("durationSeconds must be an integer between 1 and 31536000.");
1669
+ }
1670
+ const started = /* @__PURE__ */ new Date();
1671
+ const stamp = started.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
1672
+ const bundleDirectory = await uniqueBundleDirectory(path9.resolve(options.outputDirectory), stamp);
1673
+ const records = [];
1674
+ const serials = [];
1675
+ let replacementWarning = false;
1676
+ const execute = options.execute ?? ((args, deviceSerial) => args[0] === "shell" ? shellChecked({ config: options.config, command: args.slice(1).join(" "), deviceSerial }) : hdcExec({ config: options.config, args, deviceSerial }));
1677
+ const manifest = {
1678
+ schemaVersion: "1.0",
1679
+ collectorVersion: VERSION,
1680
+ collectionStartUtc: started.toISOString(),
1681
+ collectionEndUtc: null,
1682
+ host: { os: process.platform },
1683
+ target: null,
1684
+ packageName: options.packageName ?? null,
1685
+ pid: options.pid ?? null,
1686
+ filters: {
1687
+ tag: options.tag ?? null,
1688
+ level: options.level ?? null,
1689
+ pid: options.pid ?? null,
1690
+ packageName: options.packageName ?? null,
1691
+ durationSeconds
1692
+ },
1693
+ filterStatus: {
1694
+ duration: "not-applied",
1695
+ packageName: options.packageName ? "failed" : "not-requested",
1696
+ pid: options.pid ? "not-applied" : "not-requested",
1697
+ tag: options.tag ? "not-applied" : "not-requested",
1698
+ level: options.level ? "not-applied" : "not-requested"
1699
+ },
1700
+ hilogWindowApplied: false,
1701
+ redactionRequested: options.redact,
1702
+ redactionStatus: options.redact ? "pending" : "not-requested",
1703
+ archiveStatus: { status: "pending", path: null },
1704
+ commands: records,
1705
+ files: [],
1706
+ notes: [
1707
+ "Collection is read-only on the target device.",
1708
+ "hilog -x captures the current device buffer; duration is applied locally against device time when log timestamps are recognized.",
1709
+ "Redaction is heuristic and does not guarantee that a bundle is safe to publish."
1710
+ ]
1711
+ };
1712
+ const run = async (name, relativeOutput, args, deviceSerial) => {
1713
+ const outputPath = path9.join(bundleDirectory, relativeOutput);
1714
+ await fs4.mkdir(path9.dirname(outputPath), { recursive: true, mode: 448 });
1715
+ let result = null;
1716
+ let failure = null;
1717
+ try {
1718
+ result = await execute(args, deviceSerial);
1719
+ await fs4.writeFile(outputPath, result.stdout, { encoding: "utf8", mode: 384 });
1720
+ if (result.stderr) await fs4.writeFile(`${outputPath}.stderr.txt`, result.stderr, { encoding: "utf8", mode: 384 });
1721
+ if (result.stdout.includes("\uFFFD") || result.stderr.includes("\uFFFD")) {
1722
+ replacementWarning = true;
1723
+ manifest.notes.push(`${name} output contains Unicode replacement characters; review the redacted evidence before sharing.`);
1724
+ }
1725
+ failure = commandFailure(result);
1726
+ } catch (error) {
1727
+ failure = errorMessage(error);
1728
+ await fs4.writeFile(outputPath, "", { encoding: "utf8", mode: 384 });
1729
+ }
1730
+ const record = {
1731
+ name,
1732
+ status: failure ? "failed" : "success",
1733
+ output: relativeOutput,
1734
+ stderr: result?.stderr ? `${relativeOutput}.stderr.txt` : null,
1735
+ exitCode: result?.code ?? null,
1736
+ error: failure
1737
+ };
1738
+ records.push(record);
1739
+ return record;
1740
+ };
1741
+ const targetRecord = await run("list-targets", "device/targets.txt", ["list", "targets", "-v"]);
1742
+ const targetOutput = await fs4.readFile(path9.join(bundleDirectory, "device/targets.txt"), "utf8").catch(() => "");
1743
+ const targets = parseDeviceList(targetOutput).filter((target) => target.status === "Connected" || target.status === "Unknown");
1744
+ serials.push(...parseDeviceList(targetOutput).map((target) => target.serial));
1745
+ const requestedDevice = resolveDeviceSerial(options.deviceSerial);
1746
+ let selectedDevice = null;
1747
+ let targetError = null;
1748
+ if (targetRecord.status !== "success") targetError = targetRecord.error ?? "hdc target discovery failed";
1749
+ else if (requestedDevice) {
1750
+ selectedDevice = targets.find((target) => target.serial === requestedDevice)?.serial ?? null;
1751
+ if (!selectedDevice) targetError = "selected device was not discovered as connected";
1752
+ } else if (targets.length === 0) targetError = "hdc did not discover a connected device";
1753
+ else if (targets.length > 1) targetError = "multiple devices are connected; specify --device";
1754
+ else selectedDevice = targets[0].serial;
1755
+ let boundedHilog = null;
1756
+ let durationApplied = false;
1757
+ let durationFailureReason = "device clock or HiLog collection failed";
1758
+ if (targetError) {
1759
+ targetRecord.status = "failed";
1760
+ targetRecord.error = targetError;
1761
+ manifest.notes.push("Device collection stopped because target discovery did not identify exactly one selected connected device.");
1762
+ } else {
1763
+ manifest.target = selectedDevice;
1764
+ const shell = (args) => run(args[0], args[1], args.slice(2), selectedDevice);
1765
+ const captureStartedHostMs = Date.now();
1766
+ const deviceTimeBefore = await shell([
1767
+ "device-time-before",
1768
+ "device/device-time-before.txt",
1769
+ "shell",
1770
+ "date",
1771
+ "+%Y-%m-%dT%H:%M:%S"
1772
+ ]);
1773
+ const hilog = await shell(["hilog-snapshot", "logs/hilog.txt", "shell", "hilog", "-x"]);
1774
+ const deviceTimeAfter = await shell([
1775
+ "device-time-after",
1776
+ "device/device-time-after.txt",
1777
+ "shell",
1778
+ "date",
1779
+ "+%Y-%m-%dT%H:%M:%S"
1780
+ ]);
1781
+ const hostElapsedMs = Date.now() - captureStartedHostMs;
1782
+ if (deviceTimeBefore.status === "success" && hilog.status === "success" && deviceTimeAfter.status === "success") {
1783
+ const [beforeText, afterText, fullHilog] = await Promise.all([
1784
+ fs4.readFile(path9.join(bundleDirectory, "device/device-time-before.txt"), "utf8"),
1785
+ fs4.readFile(path9.join(bundleDirectory, "device/device-time-after.txt"), "utf8"),
1786
+ fs4.readFile(path9.join(bundleDirectory, "logs/hilog.txt"), "utf8")
1787
+ ]);
1788
+ const window = filterHilogWindow(fullHilog, beforeText, afterText, durationSeconds, hostElapsedMs);
1789
+ if (window.text === null) durationFailureReason = window.reason;
1790
+ else boundedHilog = window.text;
1791
+ } else if (deviceTimeBefore.status !== "success" || deviceTimeAfter.status !== "success") {
1792
+ durationFailureReason = "device clock could not be read";
1793
+ } else if (hilog.status !== "success") {
1794
+ durationFailureReason = "HiLog snapshot collection failed";
1795
+ }
1796
+ durationApplied = boundedHilog !== null;
1797
+ manifest.hilogWindowApplied = durationApplied;
1798
+ const faultArgs = options.packageName ? ["faultlog", "logs/faultlog.txt", "shell", "hidumper", "-e", "--print", options.packageName, "-n", "5"] : ["faultlog", "logs/faultlog.txt", "shell", "hidumper", "-e", "--list", "-n", "20"];
1799
+ const faultlog = await shell(faultArgs);
1800
+ manifest.filterStatus = {
1801
+ duration: durationApplied ? "applied" : "not-applied",
1802
+ packageName: options.packageName ? faultlog.status === "success" ? "applied" : "failed" : "not-requested",
1803
+ pid: options.pid ? "not-applied" : "not-requested",
1804
+ tag: options.tag ? "not-applied" : "not-requested",
1805
+ level: options.level ? "not-applied" : "not-requested"
1806
+ };
1807
+ if (!durationApplied) {
1808
+ manifest.notes.push(
1809
+ `HiLog duration was not applied (${durationFailureReason}); the full snapshot is kept only in the local raw bundle and omitted from the redacted archive.`
1810
+ );
1811
+ } else if (boundedHilog !== null && (options.pid || options.tag || options.level)) {
1812
+ const filtered = filterHilog(boundedHilog, { pid: options.pid, tag: options.tag, level: options.level });
1813
+ if (filtered) {
1814
+ await fs4.writeFile(path9.join(bundleDirectory, "logs/hilog-filtered.txt"), filtered.text, { encoding: "utf8", mode: 384 });
1815
+ const status = manifest.filterStatus;
1816
+ for (const key of filtered.applied) status[key] = "applied";
1817
+ } else {
1818
+ manifest.notes.push("Requested HiLog PID/tag/level filters were not applied because the windowed snapshot format could not be parsed reliably.");
1819
+ }
1820
+ }
1821
+ await shell(["device-name", "device/device-info.txt", "shell", "param", "get", "const.product.name"]);
1822
+ await shell(["process-list", "device/process-list.txt", "shell", "ps", "-ef"]);
1823
+ await shell(["memory", "device/memory.txt", "shell", "hidumper", "--mem"]);
1824
+ await shell(["threads", "device/threads.txt", "shell", "hidumper", "-p"]);
1825
+ }
1826
+ const ended = /* @__PURE__ */ new Date();
1827
+ manifest.collectionEndUtc = ended.toISOString();
1828
+ const files = records.flatMap((record) => [record.output, record.stderr].filter((file) => Boolean(file)));
1829
+ files.push("manifest.json", "notes.txt");
1830
+ if (await fs4.access(path9.join(bundleDirectory, "logs/hilog-filtered.txt")).then(() => true, () => false)) {
1831
+ files.push("logs/hilog-filtered.txt");
1832
+ }
1833
+ manifest.files = files;
1834
+ await fs4.writeFile(path9.join(bundleDirectory, "notes.txt"), "Add reproduction steps and precise event timestamps here.\n", { encoding: "utf8", mode: 384 });
1835
+ const durationFailure = !durationApplied;
1836
+ const criticalFailure = Boolean(targetError) || records.some((record) => record.name === "hilog-snapshot" && record.status !== "success");
1837
+ const anyCommandFailure = records.some((record) => record.status !== "success");
1838
+ let redactedDirectory = null;
1839
+ let archivePath = null;
1840
+ let bundleError;
1841
+ try {
1842
+ if (options.redact) {
1843
+ redactedDirectory = `${bundleDirectory}-redacted`;
1844
+ manifest.redactionStatus = replacementWarning ? "review-required" : "success";
1845
+ manifest.archiveStatus = { status: "success", path: `${path9.basename(redactedDirectory)}.zip` };
1846
+ await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1847
+ `, { encoding: "utf8", mode: 384 });
1848
+ await redactTree(bundleDirectory, redactedDirectory, serials, boundedHilog);
1849
+ archivePath = `${redactedDirectory}.zip`;
1850
+ await writeZip(redactedDirectory, archivePath);
1851
+ } else {
1852
+ archivePath = `${bundleDirectory}.zip`;
1853
+ manifest.archiveStatus = { status: "success", path: path9.basename(archivePath) };
1854
+ await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1855
+ `, { encoding: "utf8", mode: 384 });
1856
+ await writeZip(bundleDirectory, archivePath);
1857
+ }
1858
+ } catch (error) {
1859
+ bundleError = options.redact ? `redaction or archive creation failed: ${errorMessage(error)}` : `archive creation failed: ${errorMessage(error)}`;
1860
+ archivePath = null;
1861
+ manifest.redactionStatus = options.redact ? "failed" : "not-requested";
1862
+ manifest.archiveStatus = { status: "failed", path: null };
1863
+ manifest.notes.push(bundleError);
1864
+ await fs4.rm(`${redactedDirectory ?? bundleDirectory}.zip`, { force: true }).catch(() => void 0);
1865
+ if (redactedDirectory) await fs4.rm(redactedDirectory, { recursive: true, force: true }).catch(() => void 0);
1866
+ await fs4.writeFile(path9.join(bundleDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
1867
+ `, { encoding: "utf8", mode: 384 });
1868
+ redactedDirectory = null;
1869
+ }
1870
+ return {
1871
+ bundleDirectory,
1872
+ archivePath,
1873
+ redactedDirectory,
1874
+ complete: !criticalFailure && !anyCommandFailure && !bundleError && !replacementWarning && !durationFailure,
1875
+ error: bundleError ?? (criticalFailure ? targetError ?? "HiLog collection failed" : replacementWarning ? "Captured output contains Unicode replacement characters; review the manifest before sharing." : durationFailure ? `HiLog duration was not applied (${durationFailureReason}); the redacted archive omits the full snapshot.` : void 0)
1876
+ };
1877
+ }
1878
+ function parseDeviceClock(value) {
1879
+ const match = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\s*$/.exec(value);
1880
+ if (!match) return null;
1881
+ const [, year, month, day, hour, minute, second] = match;
1882
+ const timestamp = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
1883
+ const date = new Date(timestamp);
1884
+ return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day) && Number(hour) < 24 && Number(minute) < 60 && Number(second) < 60 ? timestamp : null;
1885
+ }
1886
+ function parseHilogTimestamp(datePart, timePart, referenceMs) {
1887
+ if (!validateDate(datePart, timePart)) return null;
1888
+ const [month, day] = datePart.split("-").map(Number);
1889
+ const [hour, minute, second, millisecond] = timePart.split(/[:.]/).map(Number);
1890
+ const referenceYear = new Date(referenceMs).getUTCFullYear();
1891
+ const candidates = [referenceYear - 1, referenceYear, referenceYear + 1].map((year) => Date.UTC(year, month - 1, day, hour, minute, second, millisecond)).filter((timestamp) => {
1892
+ const candidate = new Date(timestamp);
1893
+ return candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
1894
+ }).sort((left, right) => Math.abs(left - referenceMs) - Math.abs(right - referenceMs));
1895
+ const closest = candidates[0];
1896
+ return closest !== void 0 && closest <= referenceMs + 2e3 ? closest : null;
1897
+ }
1898
+ function filterHilogWindow(input, deviceClockBefore, deviceClockAfter, durationSeconds, hostElapsedMs) {
1899
+ if (!Number.isSafeInteger(durationSeconds) || durationSeconds < 1 || durationSeconds > 31536e3) {
1900
+ return { text: null, reason: "requested duration is invalid" };
1901
+ }
1902
+ const beforeMs = parseDeviceClock(deviceClockBefore);
1903
+ const afterMs = parseDeviceClock(deviceClockAfter);
1904
+ if (beforeMs === null || afterMs === null) return { text: null, reason: "device clock output was not recognized" };
1905
+ const deviceElapsedMs = afterMs - beforeMs;
1906
+ if (deviceElapsedMs < -1e3 || deviceElapsedMs > Math.max(0, hostElapsedMs) + 5e3) {
1907
+ return { text: null, reason: "device clock changed inconsistently during capture" };
1908
+ }
1909
+ const windowEnd = afterMs + 999;
1910
+ const windowStart = windowEnd - durationSeconds * 1e3;
1911
+ let sawRecord = false;
1912
+ let keep = false;
1913
+ let output = "";
1914
+ for (const line of input.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
1915
+ const content = line.replace(/[\r\n]+$/, "");
1916
+ const match = HILOG_RECORD.exec(content);
1917
+ if (match?.groups) {
1918
+ if (!validateDate(match.groups.date, match.groups.time)) {
1919
+ return { text: null, reason: "HiLog contains an invalid timestamp" };
1920
+ }
1921
+ const timestamp = parseHilogTimestamp(match.groups.date, match.groups.time, afterMs);
1922
+ if (timestamp === null || timestamp > windowEnd) {
1923
+ return { text: null, reason: "HiLog timestamps do not agree with the device clock" };
1924
+ }
1925
+ sawRecord = true;
1926
+ keep = timestamp >= windowStart;
1927
+ } else if (/^-{3,}\s*beginning of\b/i.test(content.trim())) {
1928
+ continue;
1929
+ } else if (/^\s*\[?\d{1,4}[-/]/.test(content) || /^\s*\d{2}:\d{2}:\d{2}/.test(content)) {
1930
+ return { text: null, reason: "HiLog contains timestamped lines in an unrecognized format" };
1931
+ } else if (!sawRecord && content.trim()) {
1932
+ return { text: null, reason: "HiLog contains unclassified lines before the first record" };
1933
+ }
1934
+ if (keep) output += line;
1935
+ }
1936
+ return sawRecord ? { text: output } : { text: null, reason: "no recognized timestamped HiLog records were found" };
1937
+ }
1938
+
1939
+ // src/commands/logs.ts
1342
1940
  function registerLogsCommand(program) {
1343
- program.command("logs").description(
1941
+ const logs = program.command("logs").description(
1344
1942
  "Capture hilog to a file for a whole session: the device's small log buffer wraps within seconds under chatty components, so a dump at the end loses lines. Runs until --for elapses or the process is interrupted; start it in the background, e.g. `oniro-app logs --clear -o hilog_full.txt &`. Every line is flushed as it arrives and the stream reconnects after a reboot."
1345
- ).requiredOption("-o, --output <file>", "File to append the lines to.").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) => {
1943
+ ).option("-o, --output <file>", "File to append the lines to (required).").option("--for <ms>", "Stop after this many ms (default: run until interrupted).").option("--grep <regex>", "Keep only lines matching this regex (default: all).").option("--clear", "Clear the device log buffer first (hilog -r), so the file covers only this session.").option("--domain <domain>", "hilog domain filter (e.g. 0xD003900).").option("--device <serial>", "Target device serial.").action(async (opts) => {
1944
+ if (!opts.output) logs.error("error: required option '-o, --output <file>' not specified");
1346
1945
  const { config, logger } = getRuntime();
1347
- const file = path9.resolve(opts.output);
1946
+ const file = path10.resolve(opts.output);
1348
1947
  const abort = new AbortController();
1349
1948
  const stop = () => abort.abort();
1350
1949
  process.once("SIGINT", stop);
@@ -1365,6 +1964,27 @@ function registerLogsCommand(program) {
1365
1964
  process.stdout.write(`${result.file}
1366
1965
  `);
1367
1966
  });
1967
+ logs.command("collect").description("Create a diagnostic bundle from a connected device.").allowExcessArguments(false).option("-o, --output <directory>", "Parent directory for incident bundles.", path10.join(os2.homedir(), ".oniro", "incident-bundles")).option("--device <serial>", "Target device serial.").option("--package <bundle>", "Collect faultlog entries for this application bundle.").option("--pid <pid>", "Create a filtered HiLog view for this process.").option("--tag <tag>", "Create a filtered HiLog view for this tag.").option("--level <level>", "Create a filtered HiLog view for this level (V/D/I/W/E/F).").option("--duration <seconds>", "Keep HiLog records from the last N seconds (default 600).", "600").option("--redact", "Redact sensitive values and archive only the redacted bundle.").option("--json", "Emit bundle paths and status as JSON.").action(async (opts) => {
1968
+ const { config } = getRuntime();
1969
+ const result = await collectLogBundle({
1970
+ config,
1971
+ outputDirectory: opts.output,
1972
+ deviceSerial: opts.device,
1973
+ packageName: opts.package,
1974
+ pid: opts.pid,
1975
+ tag: opts.tag,
1976
+ level: opts.level,
1977
+ durationSeconds: Number(opts.duration),
1978
+ redact: Boolean(opts.redact)
1979
+ });
1980
+ const output = opts.json ? `${JSON.stringify({ ...result, status: result.complete ? "complete" : "partial" }, null, 2)}
1981
+ ` : `${result.archivePath ?? result.bundleDirectory}
1982
+ `;
1983
+ process.stdout.write(output);
1984
+ if (!result.complete) {
1985
+ throw new Error(`Bundle is partial; inspect manifest.json. ${result.error ?? "One or more collectors failed."}`);
1986
+ }
1987
+ });
1368
1988
  }
1369
1989
 
1370
1990
  // src/commands/hdc.ts
@@ -1383,19 +2003,19 @@ function registerHdcCommand(program) {
1383
2003
  `);
1384
2004
  return;
1385
2005
  }
1386
- await new Promise((resolve8, reject) => {
2006
+ await new Promise((resolve10, reject) => {
1387
2007
  const child = spawnCompat(hdc, hdcPassthroughArgv(args, opts.device), { stdio: "inherit" });
1388
2008
  child.on("error", (err) => reject(new Error(`Failed to run ${hdc}: ${err.message}`)));
1389
2009
  child.on("close", (code) => {
1390
2010
  process.exitCode = code ?? 1;
1391
- resolve8();
2011
+ resolve10();
1392
2012
  });
1393
2013
  });
1394
2014
  });
1395
2015
  }
1396
2016
 
1397
2017
  // src/bin/oniro-app.ts
1398
- var CLI_VERSION = "0.14.0";
2018
+ var CLI_VERSION = "0.16.0";
1399
2019
  function buildProgram() {
1400
2020
  const program = new Command4().name("oniro-app").description(
1401
2021
  "Cross-platform CLI for Oniro/OpenHarmony app development.\n\nDesigned for non-interactive use (CI, scripts, agents): every command\ntakes explicit flags, results go to stdout, progress/logs go to stderr,\nand a non-zero exit code indicates failure."
@@ -1427,7 +2047,7 @@ async function main() {
1427
2047
  try {
1428
2048
  await program.parseAsync(process.argv);
1429
2049
  } catch (err) {
1430
- const message = err instanceof OniroError2 ? err.message : err instanceof Error ? err.message : String(err);
2050
+ const message = err instanceof OniroError3 ? err.message : err instanceof Error ? err.message : String(err);
1431
2051
  process.stderr.write(`[error] ${message}
1432
2052
  `);
1433
2053
  if (process.env.ONIRO_DEBUG === "1" && err instanceof Error && err.stack) {