@onekeyfe/hardware-cli 1.2.0-alpha.3 → 1.2.0-alpha.30

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/cli.js CHANGED
@@ -1,48 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.program = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
4
7
  const commander_1 = require("commander");
8
+ const hd_core_1 = require("@onekeyfe/hd-core");
9
+ const hd_shared_1 = require("@onekeyfe/hd-shared");
10
+ const chains_1 = require("./chains");
11
+ const deviceSelection_1 = require("./deviceSelection");
12
+ const deviceStateCommands_1 = require("./deviceStateCommands");
5
13
  const sdk_1 = require("./sdk");
6
14
  const session_1 = require("./session");
7
- const chains_1 = require("./chains");
8
- const hd_shared_1 = require("@onekeyfe/hd-shared");
9
- const hd_core_1 = require("@onekeyfe/hd-core");
10
- function extractPassphraseSession(payload) {
11
- if (typeof payload === 'string') {
12
- return { passphraseState: payload };
13
- }
14
- if (!payload || typeof payload !== 'object') {
15
- return {};
16
- }
17
- const statePayload = payload;
18
- let passphraseState;
19
- if (typeof statePayload.passphrase_state === 'string') {
20
- passphraseState = statePayload.passphrase_state;
21
- }
22
- else if (typeof statePayload.passphraseState === 'string') {
23
- passphraseState = statePayload.passphraseState;
24
- }
25
- let sessionId;
26
- if (typeof statePayload.session_id === 'string') {
27
- sessionId = statePayload.session_id;
28
- }
29
- else if (typeof statePayload.sessionId === 'string') {
30
- sessionId = statePayload.sessionId;
31
- }
32
- return { passphraseState, sessionId };
33
- }
34
15
  const program = new commander_1.Command();
16
+ exports.program = program;
17
+ const { version: cliVersion } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
35
18
  program
36
19
  .name('onekey-hw')
37
20
  .description('OneKey hardware wallet CLI for AI agent integration')
38
- .version('1.1.26-alpha.1');
21
+ .version(cliVersion);
39
22
  // ============================================================
40
23
  // Global Options
41
24
  // ============================================================
42
25
  program.option('--connect-id <id>', 'Device connection ID (USB: serial, iOS: uuid, Android: MAC)');
43
26
  program.option('--device-id <id>', 'Persistent device ID from getFeatures (changes when seed changes)');
27
+ program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
44
28
  program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
45
29
  program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
30
+ program.option('--debug', 'Enable SDK debug logs');
46
31
  // ============================================================
47
32
  // Device Commands
48
33
  // ============================================================
@@ -51,50 +36,103 @@ program
51
36
  .description('Search for connected OneKey hardware wallet devices')
52
37
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
53
38
  const result = await sdk.searchDevices();
54
- // Auto-fetch features for each discovered device (doesn't require PIN)
55
- if (result?.success && Array.isArray(result.payload)) {
56
- for (const device of result.payload) {
57
- if (device.connectId) {
58
- try {
59
- const features = await sdk.getFeatures(device.connectId);
60
- if (features?.success && features.payload) {
61
- device.features = features.payload;
62
- device.name = features.payload.label || features.payload.bleName || device.name;
63
- const devType = features.payload.deviceType?.toLowerCase();
64
- if (devType) {
65
- device.deviceType = devType;
66
- }
67
- }
68
- }
69
- catch {
70
- // Features fetch failed — device may need PIN, continue with basic info
71
- }
72
- }
73
- }
74
- }
75
39
  outputResult(globalOpts, result);
76
40
  }));
77
41
  program
78
42
  .command('get-features')
79
43
  .description('Get device features (firmware, unlock state, passphrase protection, etc.)')
80
44
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
81
- // Resolve connectId: explicit flag wins, else pick the first attached device
82
- let { connectId } = globalOpts;
83
- if (!connectId) {
84
- const searchResult = await sdk.searchDevices();
85
- if (!searchResult?.success ||
86
- !Array.isArray(searchResult.payload) ||
87
- searchResult.payload.length === 0) {
88
- outputResult(globalOpts, {
89
- success: false,
90
- payload: { error: 'No device found', code: 'NO_DEVICE' },
91
- });
45
+ const result = await (0, deviceStateCommands_1.getCompatibleFeatures)(sdk, globalOpts.connectId);
46
+ outputResult(globalOpts, result);
47
+ }));
48
+ program
49
+ .command('get-state')
50
+ .description('Get canonical device state for Protocol V1 and Protocol V2 devices')
51
+ .option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
52
+ .action((opts) => runCommand({}, async ({ sdk, globalOpts }) => {
53
+ const supportedScopes = ['runtime', 'settings', 'firmware'];
54
+ if (!supportedScopes.includes(opts.scope)) {
55
+ const error = new Error(`Unsupported device state scope: ${opts.scope}`);
56
+ error.code = 'INVALID_DEVICE_STATE_SCOPE';
57
+ throw error;
58
+ }
59
+ const result = await (0, deviceStateCommands_1.getCanonicalDeviceState)(sdk, globalOpts.connectId, opts.scope);
60
+ outputResult(globalOpts, result);
61
+ }));
62
+ program
63
+ .command('upload-wallpaper')
64
+ .description('Upload and activate a Pro2 wallpaper')
65
+ .requiredOption('--rgba <path>', '604x1024 raw RGBA file')
66
+ .option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
67
+ .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
68
+ .action(opts => runCommand({}, async ({ sdk, globalOpts, params }) => {
69
+ const rgba = readBinaryParam(opts.rgba);
70
+ const expectedBytes = 604 * 1024 * 4;
71
+ if (rgba.byteLength !== expectedBytes) {
72
+ throw new Error(`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`);
73
+ }
74
+ let transferStartedAt;
75
+ let transferEndedAt;
76
+ let lastProgress = -1;
77
+ let lastPrintedProgress = -10;
78
+ let progressTotalBytes = 0;
79
+ let transferredBytes = 0;
80
+ const totalStartedAt = Date.now();
81
+ const onUiEvent = (message) => {
82
+ if (!message || typeof message !== 'object')
83
+ return;
84
+ const event = message;
85
+ if (event.type !== hd_core_1.UI_REQUEST.DEVICE_PROGRESS || !event.payload)
86
+ return;
87
+ const progress = Number(event.payload.progress);
88
+ if (!Number.isFinite(progress))
92
89
  return;
90
+ transferStartedAt ?? (transferStartedAt = Date.now());
91
+ lastProgress = Math.max(lastProgress, progress);
92
+ const totalBytes = Number(event.payload.totalBytes);
93
+ if (Number.isFinite(totalBytes) && totalBytes > 0)
94
+ progressTotalBytes = totalBytes;
95
+ const confirmedBytes = Number(event.payload.transferredBytes);
96
+ if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
97
+ transferredBytes = Math.max(transferredBytes, confirmedBytes);
93
98
  }
94
- connectId = searchResult.payload[0].connectId ?? undefined;
99
+ const printableProgress = Math.floor(progress / 10) * 10;
100
+ if (printableProgress > lastPrintedProgress || progress >= 100) {
101
+ const rate = Number(event.payload.rateBytesPerSecond);
102
+ const rateText = Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
103
+ process.stderr.write(`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`);
104
+ lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
105
+ }
106
+ if (progress >= 100)
107
+ transferEndedAt ?? (transferEndedAt = Date.now());
108
+ };
109
+ sdk.on(hd_core_1.UI_EVENT, onUiEvent);
110
+ let result;
111
+ try {
112
+ result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
113
+ ...params,
114
+ width: 604,
115
+ height: 1024,
116
+ rgba,
117
+ fileName: opts.fileName,
118
+ chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
119
+ });
95
120
  }
96
- const result = await sdk.getFeatures(connectId || '');
97
- outputResult(globalOpts, result);
121
+ finally {
122
+ sdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
123
+ }
124
+ const endedAt = transferEndedAt ?? Date.now();
125
+ const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
126
+ outputResult(globalOpts, {
127
+ ...result,
128
+ metrics: buildWallpaperUploadMetrics({
129
+ totalBytes,
130
+ transferredBytes: result?.success ? totalBytes : transferredBytes,
131
+ startedAt: transferStartedAt ?? totalStartedAt,
132
+ endedAt,
133
+ lastProgress,
134
+ }),
135
+ });
98
136
  }));
99
137
  // ============================================================
100
138
  // Signing Commands
@@ -409,16 +447,66 @@ program
409
447
  code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
410
448
  },
411
449
  }));
450
+ program
451
+ .command('firmware-update-legacy')
452
+ .description('Update Classic/Pure firmware through the legacy protocol')
453
+ .requiredOption('--binary <path>', 'Local firmware binary path')
454
+ .option('--device-name <name>', 'BLE advertising name, for example K1514')
455
+ .option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
456
+ .option('--no-reboot', 'Do not reboot the device after a successful update')
457
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
458
+ if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
459
+ throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
460
+ }
461
+ const connectId = await resolveLegacyFirmwareConnectId(sdk, globalOpts.connectId, opts.deviceName);
462
+ const result = await sdk.firmwareUpdate(connectId, {
463
+ binary: readBinaryParam(opts.binary),
464
+ updateType: opts.updateType,
465
+ rebootOnSuccess: opts.reboot,
466
+ timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
467
+ });
468
+ outputResult(globalOpts, result);
469
+ }));
470
+ function getLegacyFirmwareConnectTimeout(transport) {
471
+ return transport === 'usb' ? 90000 : undefined;
472
+ }
473
+ exports.getLegacyFirmwareConnectTimeout = getLegacyFirmwareConnectTimeout;
412
474
  program
413
475
  .command('firmware-update-ble')
414
- .description('BLE firmware update is not supported via CLI')
476
+ .description('Run Protocol V2 firmware update over BLE')
415
477
  .action(() => respondAndExit({
416
478
  success: false,
417
479
  payload: {
418
- error: 'BLE firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
419
- code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
480
+ error: 'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
481
+ code: 'USE_FIRMWARE_UPDATE_V4',
420
482
  },
421
483
  }));
484
+ program
485
+ .command('firmware-update-v4')
486
+ .description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
487
+ .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
488
+ .option('--resource-bundle <spec...>', 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
489
+ .option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
490
+ .option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
491
+ .option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
492
+ .option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
493
+ .option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
494
+ .option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
495
+ .option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
496
+ .option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
497
+ .option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
498
+ .option('--forced-update-res', 'Force resource update')
499
+ .option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
500
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
501
+ const params = buildFirmwareUpdateV4Params(opts);
502
+ const result = await runFirmwareUpdateV4WithRetry({
503
+ sdk,
504
+ globalOpts,
505
+ params,
506
+ retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
507
+ });
508
+ outputResult(globalOpts, result);
509
+ }));
422
510
  program
423
511
  .command('bootloader-check')
424
512
  .description('Check bootloader version and status')
@@ -534,7 +622,7 @@ program
534
622
  const sessionCmd = program.command('session').description('Manage device passphrase session cache');
535
623
  sessionCmd
536
624
  .command('connect')
537
- .description('Connect device and establish passphrase session (cached for subsequent commands)')
625
+ .description('Connect device and select a hidden wallet for this invocation')
538
626
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
539
627
  // 1. Search for device
540
628
  const searchResult = await sdk.searchDevices();
@@ -554,56 +642,33 @@ sessionCmd
554
642
  process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
555
643
  await unlockWithRetry(sdk, connectId);
556
644
  }
557
- // 3. Get passphraseState (triggers 1/2/3 selection)
558
- const psResult = await sdk.getPassphraseState(connectId, {
559
- initSession: true,
560
- useEmptyPassphrase: false,
645
+ // 3. Open a hidden wallet session (triggers 1/2/3 selection).
646
+ const sessionResult = await sdk.openWalletSession(connectId, {
647
+ mode: 'select-hidden',
561
648
  });
562
- if (!psResult.success) {
563
- outputResult(globalOpts, psResult);
649
+ if (!sessionResult.success) {
650
+ outputResult(globalOpts, sessionResult);
564
651
  return;
565
652
  }
566
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
567
- if (!passphraseState) {
653
+ if (sessionResult.payload.walletType !== 'hidden') {
568
654
  outputResult(globalOpts, {
569
655
  success: false,
570
- payload: { error: 'getPassphraseState did not return passphraseState' },
656
+ payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
571
657
  });
572
658
  return;
573
659
  }
660
+ const { deviceId, passphraseState } = sessionResult.payload;
574
661
  // 4. Get address to verify + extract deviceId
575
- const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
662
+ const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
576
663
  path: "m/44'/60'/0'/0/0",
577
664
  showOnOneKey: false,
578
665
  passphraseState,
579
666
  });
580
- // 5. Fetch the now-active session_id via getFeatures.
581
- //
582
- // IMPORTANT: pass `passphraseState` here. Without it, the SDK's
583
- // connectStateChange guard (core/index.ts) would see the payload's
584
- // passphraseState flip from mnNy → undefined, clear the cached Device,
585
- // and call Initialize again with no passphrase_state / no session_id.
586
- // That Initialize resets the device to the standard wallet and returns
587
- // a *standard-wallet* session_id — which we'd then save in the keychain
588
- // paired with the hidden-wallet passphraseState. On the next CLI run
589
- // the mismatch would trigger PassphraseRequest (1/2/3 again).
590
- const featResult = await sdk.getFeatures(connectId, {
591
- passphraseState,
592
- skipPassphraseCheck: true,
593
- });
594
- const featPayload = featResult?.success ? featResult.payload : undefined;
595
- const deviceId = featPayload?.deviceId || device.deviceId || '';
596
- const sessionId = passphraseSessionId || featPayload?.sessionId || '';
597
- // 6. Save to keychain
598
- if (passphraseState && deviceId && sessionId) {
599
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
600
- }
601
667
  outputResult(globalOpts, {
602
668
  success: true,
603
669
  payload: {
604
670
  passphraseState,
605
671
  deviceId,
606
- ...(sessionId ? { sessionId } : {}),
607
672
  ...(addrResult?.success ? { address: addrResult.payload.address } : {}),
608
673
  },
609
674
  });
@@ -691,8 +756,8 @@ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
691
756
  * Prepare passphrase session before SDK calls.
692
757
  *
693
758
  * 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
694
- * 2. Try keychain → preloadSessionCache → use cached session
695
- * 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
759
+ * 2. Try a legacy keychain entry → preloadSessionCache → use cached session
760
+ * 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
696
761
  *
697
762
  * After this, globalOpts.passphraseState is set and getCommonParams will include it.
698
763
  */
@@ -704,7 +769,7 @@ globalOpts) {
704
769
  return globalOpts.passphraseState;
705
770
  }
706
771
  // Errors from the SDK calls below (PIN cancelled, transport broken,
707
- // getPassphraseState rejection) intentionally propagate to runCommand's
772
+ // openWalletSession rejection) intentionally propagate to runCommand's
708
773
  // catch block, which renders them as structured `{ success: false,
709
774
  // payload: { error, code } }` output instead of silently falling through
710
775
  // to a confusing downstream error 112 / 114.
@@ -715,18 +780,22 @@ globalOpts) {
715
780
  searchResult.payload.length === 0) {
716
781
  return undefined;
717
782
  }
718
- const device = searchResult.payload[0];
719
- const connectId = device.connectId || globalOpts.connectId || '';
783
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
784
+ if (!device) {
785
+ throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
786
+ }
787
+ const selectedDevice = device;
788
+ const connectId = selectedDevice.connectId || globalOpts.connectId || '';
720
789
  if (!globalOpts.connectId && connectId) {
721
790
  globalOpts.connectId = connectId;
722
791
  }
723
792
  // ── Step 2: Get features if searchDevices didn't populate them ──
724
793
  // getFeatures failures here are non-fatal — we fall through to Step 3
725
794
  // which will fail with a clearer error if the device is truly unreachable.
726
- let deviceId = device.features?.deviceId || device.deviceId || '';
727
- let deviceType = (0, hd_core_1.getDeviceType)(device.features);
728
- let unlocked = device.features?.unlocked;
729
- let passphraseProtection = device.features?.passphraseProtection;
795
+ let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
796
+ let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
797
+ let unlocked = selectedDevice.features?.unlocked;
798
+ let passphraseProtection = selectedDevice.features?.passphraseProtection;
730
799
  if (!deviceId || unlocked == null || passphraseProtection == null) {
731
800
  try {
732
801
  const featResult = await sdk.getFeatures(connectId);
@@ -761,7 +830,7 @@ globalOpts) {
761
830
  if (passphraseProtection === false && deviceType !== hd_shared_1.EDeviceType.Pro2) {
762
831
  return undefined;
763
832
  }
764
- // ── Step 5: Try keychain session reuse ───────────────────────────
833
+ // ── Step 5: Try legacy keychain session reuse ────────────────────
765
834
  // Only attempt if device was already unlocked — locking invalidates
766
835
  // all passphrase sessions, so cached session_id is useless after unlock.
767
836
  if (!wasLocked && deviceId) {
@@ -771,38 +840,22 @@ globalOpts) {
771
840
  return cached;
772
841
  }
773
842
  }
774
- // ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
775
- const psResult = await sdk.getPassphraseState(connectId, {
776
- initSession: true,
777
- useEmptyPassphrase: false,
843
+ // ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
844
+ const sessionResult = await sdk.openWalletSession(connectId, {
845
+ mode: 'select-hidden',
778
846
  });
779
- if (psResult.success && psResult.payload) {
780
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
781
- if (!passphraseState) {
847
+ if (sessionResult.success && sessionResult.payload) {
848
+ if (sessionResult.payload.walletType !== 'hidden') {
782
849
  return undefined;
783
850
  }
851
+ const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
852
+ globalOpts.deviceId = sessionDeviceId;
784
853
  globalOpts.passphraseState = passphraseState;
785
- // Save session to keychain for next invocation.
786
- //
787
- // Pass passphraseState to keep connectStateChange=false — otherwise
788
- // Initialize would be re-run without passphrase_state, resetting the
789
- // device to the standard wallet and returning a mismatched session_id.
790
- // See the matching comment in `session connect`.
791
- if (deviceId) {
792
- const featAfter = await sdk.getFeatures(connectId, {
793
- passphraseState,
794
- skipPassphraseCheck: true,
795
- });
796
- const sessionId = passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
797
- if (sessionId) {
798
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
799
- await (0, session_1.preloadSessionFromKeychain)(deviceId);
800
- }
801
- }
802
854
  return passphraseState;
803
855
  }
804
856
  return undefined;
805
857
  }
858
+ exports.prepareSession = prepareSession;
806
859
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
807
860
  function outputResult(_globalOpts, result) {
808
861
  console.log(JSON.stringify(result, null, 2));
@@ -812,12 +865,15 @@ function outputResult(_globalOpts, result) {
812
865
  !result.success) {
813
866
  process.exitCode = 1;
814
867
  }
815
- // No process.exit here — runCommand() below handles dispose + exit so SDK
816
- // async cleanup (USB release, event listener teardown) finishes first.
868
+ // No process.exit here — runCommand() waits for SDK cleanup, then lets Node
869
+ // exit naturally so leaked USB handles remain observable.
817
870
  }
818
871
  async function runCommand(options, handler) {
819
872
  const globalOpts = program.opts();
820
873
  try {
874
+ if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
875
+ throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
876
+ }
821
877
  const sdk = await (0, sdk_1.createSDK)(globalOpts);
822
878
  if (options.needsSession) {
823
879
  await prepareSession(sdk, globalOpts);
@@ -843,9 +899,7 @@ async function runCommand(options, handler) {
843
899
  // promise reference. Idempotent, safe to call even if init failed.
844
900
  await (0, sdk_1.disposeSDK)();
845
901
  }
846
- // SDK event listeners can keep the event loop alive after dispose.
847
- // setImmediate lets any trailing stdout/stderr writes flush first.
848
- setImmediate(() => process.exit(process.exitCode ?? 0));
902
+ // disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
849
903
  }
850
904
  /** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
851
905
  function respondAndExit(result) {
@@ -866,6 +920,313 @@ function safeJsonParse(input, label) {
866
920
  throw err;
867
921
  }
868
922
  }
923
+ function readBinaryParam(path) {
924
+ const buffer = (0, node_fs_1.readFileSync)(path);
925
+ return new Uint8Array(buffer).buffer;
926
+ }
927
+ async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName) {
928
+ if (explicitConnectId && !deviceName)
929
+ return explicitConnectId;
930
+ const searchResult = await sdk.searchDevices();
931
+ if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
932
+ throw new Error('Unable to scan BLE devices');
933
+ }
934
+ const devices = searchResult.payload;
935
+ const normalizedName = deviceName?.trim().toLowerCase();
936
+ const matches = normalizedName
937
+ ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
938
+ : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
939
+ if (matches.length === 0) {
940
+ throw new Error(normalizedName
941
+ ? `BLE device not found by name: ${deviceName}`
942
+ : 'No Classic/Pure BLE device found');
943
+ }
944
+ if (matches.length > 1) {
945
+ throw new Error(normalizedName
946
+ ? `Multiple BLE devices found by name: ${deviceName}`
947
+ : 'Multiple Classic/Pure BLE devices found; specify --device-name');
948
+ }
949
+ const [{ connectId, name }] = matches;
950
+ if (!connectId)
951
+ throw new Error(`BLE device has no connect ID: ${name}`);
952
+ return connectId;
953
+ }
954
+ function parseResourceBundleParam(spec) {
955
+ const sep = spec.indexOf(':');
956
+ if (sep <= 0 || sep === spec.length - 1) {
957
+ throw new Error(`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`);
958
+ }
959
+ const localPath = spec.slice(0, sep);
960
+ const devicePath = spec.slice(sep + 1);
961
+ if (!devicePath.startsWith('vol')) {
962
+ throw new Error(`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`);
963
+ }
964
+ return {
965
+ binary: readBinaryParam(localPath),
966
+ devicePath,
967
+ };
968
+ }
969
+ function getFirmwareUpdateV4TotalBytes(params) {
970
+ return [
971
+ ...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
972
+ params.bootloaderBinary,
973
+ params.applicationP1Binary,
974
+ params.applicationP2Binary,
975
+ params.coprocessorBinary,
976
+ params.se01Binary,
977
+ params.se02Binary,
978
+ params.se03Binary,
979
+ params.se04Binary,
980
+ ].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
981
+ }
982
+ function getFirmwareUpdateV4ErrorText(result) {
983
+ if (!result || typeof result !== 'object')
984
+ return '';
985
+ const { payload } = result;
986
+ if (!payload || typeof payload !== 'object')
987
+ return '';
988
+ const { error } = payload;
989
+ return typeof error === 'string' ? error : '';
990
+ }
991
+ function isProtocolV2UsbProbeTransientResult(result) {
992
+ const error = getFirmwareUpdateV4ErrorText(result);
993
+ return (error.includes('Device protocol mismatch') &&
994
+ error.includes('expected V2') &&
995
+ error.includes('did not respond to expected protocol'));
996
+ }
997
+ function isSuccessResult(result) {
998
+ return (!!result && typeof result === 'object' && result.success === true);
999
+ }
1000
+ function getFirmwareUpdatePayload(message) {
1001
+ if (!message || typeof message !== 'object')
1002
+ return undefined;
1003
+ return message.payload;
1004
+ }
1005
+ function formatFirmwareProgress(progress) {
1006
+ if (!Number.isFinite(progress))
1007
+ return '0%';
1008
+ return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
1009
+ }
1010
+ function formatFirmwareBytes(bytes) {
1011
+ if (!Number.isFinite(bytes) || bytes <= 0)
1012
+ return '';
1013
+ return `${(bytes / 1024).toFixed(1)} KiB`;
1014
+ }
1015
+ function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }) {
1016
+ const elapsedMs = Math.max(endedAt - startedAt, 0);
1017
+ return {
1018
+ totalBytes,
1019
+ transferredBytes,
1020
+ totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
1021
+ transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
1022
+ lastProgress,
1023
+ };
1024
+ }
1025
+ exports.buildWallpaperUploadMetrics = buildWallpaperUploadMetrics;
1026
+ function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
1027
+ const printableProgress = Math.floor(progress / 10) * 10;
1028
+ if (printableProgress <= lastPrintedProgress && progress < 100) {
1029
+ return lastPrintedProgress;
1030
+ }
1031
+ const transferredBytes = Number(payload.transferredBytes);
1032
+ const totalBytes = Number(payload.totalBytes);
1033
+ const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
1034
+ const sizeText = Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
1035
+ ? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
1036
+ : '';
1037
+ const speedText = Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
1038
+ ? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
1039
+ : '';
1040
+ process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(progress)}${sizeText}${speedText}\n`);
1041
+ return progress >= 100 ? 100 : printableProgress;
1042
+ }
1043
+ function buildFirmwareUpdateV4Metrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
1044
+ const totalElapsedMs = Date.now() - totalStartedAt;
1045
+ const transferElapsedMs = transferStartedAt !== undefined && transferEndedAt !== undefined
1046
+ ? transferEndedAt - transferStartedAt
1047
+ : undefined;
1048
+ const installElapsedMs = installStartedAt !== undefined && installEndedAt !== undefined
1049
+ ? installEndedAt - installStartedAt
1050
+ : undefined;
1051
+ return {
1052
+ attempt,
1053
+ maxAttempts,
1054
+ retried,
1055
+ totalBytes,
1056
+ progressEvents,
1057
+ lastProgress,
1058
+ installProgressEvents,
1059
+ lastInstallProgress,
1060
+ transferSeconds: transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
1061
+ transferKiBPerSecond: transferElapsedMs !== undefined && transferElapsedMs > 0
1062
+ ? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
1063
+ : null,
1064
+ installSeconds: installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
1065
+ totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
1066
+ };
1067
+ }
1068
+ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }) {
1069
+ const totalBytes = getFirmwareUpdateV4TotalBytes(params);
1070
+ const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1071
+ let currentSdk = sdk;
1072
+ let lastResult;
1073
+ let retried = false;
1074
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1075
+ let progressEvents = 0;
1076
+ let lastProgress = -1;
1077
+ let transferStartedAt;
1078
+ let transferEndedAt;
1079
+ let installProgressEvents = 0;
1080
+ let lastInstallProgress = -1;
1081
+ let installStartedAt;
1082
+ let installEndedAt;
1083
+ let lastPrintedTransferProgress = -10;
1084
+ let lastPrintedInstallProgress = -10;
1085
+ const totalStartedAt = Date.now();
1086
+ const connectId = retried && globalOpts.transport === 'usb' && globalOpts.connectId
1087
+ ? undefined
1088
+ : globalOpts.connectId;
1089
+ const onUiEvent = (message) => {
1090
+ if (!message || typeof message !== 'object')
1091
+ return;
1092
+ const messageType = message.type;
1093
+ const payload = getFirmwareUpdatePayload(message);
1094
+ if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1095
+ const tipMessage = payload?.data?.message;
1096
+ if (typeof tipMessage === 'string') {
1097
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1098
+ }
1099
+ return;
1100
+ }
1101
+ if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1102
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1103
+ process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1104
+ return;
1105
+ }
1106
+ if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1107
+ return;
1108
+ const progress = Number(payload.progress);
1109
+ if (!Number.isFinite(progress))
1110
+ return;
1111
+ if (payload.progressType === 'transferData') {
1112
+ progressEvents += 1;
1113
+ lastProgress = Math.max(lastProgress, progress);
1114
+ transferStartedAt ?? (transferStartedAt = Date.now());
1115
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1116
+ progressType: 'transfer',
1117
+ progress,
1118
+ payload,
1119
+ lastPrintedProgress: lastPrintedTransferProgress,
1120
+ });
1121
+ if (progress >= 100) {
1122
+ transferEndedAt ?? (transferEndedAt = Date.now());
1123
+ }
1124
+ return;
1125
+ }
1126
+ if (payload.progressType === 'installingFirmware') {
1127
+ installProgressEvents += 1;
1128
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1129
+ installStartedAt ?? (installStartedAt = Date.now());
1130
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1131
+ progressType: 'install',
1132
+ progress,
1133
+ payload,
1134
+ lastPrintedProgress: lastPrintedInstallProgress,
1135
+ });
1136
+ if (progress >= 100) {
1137
+ installEndedAt ?? (installEndedAt = Date.now());
1138
+ }
1139
+ }
1140
+ };
1141
+ currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1142
+ try {
1143
+ lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1144
+ }
1145
+ finally {
1146
+ currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1147
+ }
1148
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1149
+ installEndedAt = Date.now();
1150
+ }
1151
+ const metrics = buildFirmwareUpdateV4Metrics({
1152
+ attempt,
1153
+ maxAttempts,
1154
+ totalBytes,
1155
+ totalStartedAt,
1156
+ transferStartedAt,
1157
+ transferEndedAt,
1158
+ installStartedAt,
1159
+ installEndedAt,
1160
+ progressEvents,
1161
+ lastProgress,
1162
+ installProgressEvents,
1163
+ lastInstallProgress,
1164
+ retried,
1165
+ });
1166
+ if (lastResult && typeof lastResult === 'object') {
1167
+ const payload = (lastResult.payload ?? {});
1168
+ lastResult = {
1169
+ ...lastResult,
1170
+ payload: {
1171
+ ...payload,
1172
+ metrics,
1173
+ },
1174
+ };
1175
+ }
1176
+ if (isSuccessResult(lastResult)) {
1177
+ return lastResult;
1178
+ }
1179
+ if (attempt >= maxAttempts ||
1180
+ globalOpts.transport !== 'usb' ||
1181
+ !isProtocolV2UsbProbeTransientResult(lastResult)) {
1182
+ return lastResult;
1183
+ }
1184
+ retried = true;
1185
+ process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
1186
+ await (0, sdk_1.disposeSDK)();
1187
+ await new Promise(resolve => {
1188
+ setTimeout(resolve, 3000);
1189
+ });
1190
+ currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1191
+ }
1192
+ return lastResult;
1193
+ }
1194
+ function buildFirmwareUpdateV4Params(opts) {
1195
+ const params = {
1196
+ platform: 'desktop',
1197
+ connectProtocol: 'V2',
1198
+ chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
1199
+ forcedUpdateRes: opts.forcedUpdateRes,
1200
+ resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
1201
+ romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
1202
+ bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
1203
+ applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
1204
+ applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
1205
+ coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
1206
+ se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
1207
+ se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
1208
+ se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
1209
+ se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
1210
+ };
1211
+ const hasPayload = [
1212
+ params.resourceBundleFiles,
1213
+ params.romloaderBinary,
1214
+ params.bootloaderBinary,
1215
+ params.applicationP1Binary,
1216
+ params.applicationP2Binary,
1217
+ params.coprocessorBinary,
1218
+ params.se01Binary,
1219
+ params.se02Binary,
1220
+ params.se03Binary,
1221
+ params.se04Binary,
1222
+ ].some(Boolean);
1223
+ if (!hasPayload) {
1224
+ const err = new Error('firmware-update-v4 requires at least one binary path');
1225
+ err.code = 'MISSING_FIRMWARE_BINARY';
1226
+ throw err;
1227
+ }
1228
+ return params;
1229
+ }
869
1230
  /**
870
1231
  * #9 FIX: Safe parseInt with NaN check
871
1232
  */
@@ -876,4 +1237,6 @@ function safeParseInt(input, label) {
876
1237
  }
877
1238
  return num;
878
1239
  }
879
- program.parse();
1240
+ if (require.main === module) {
1241
+ program.parse();
1242
+ }