@onekeyfe/hardware-cli 1.2.0-alpha.7 → 1.2.0-alpha.71

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,42 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.program = exports.runFirmwareUpdateV4WithRetry = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
4
5
  const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
5
7
  const commander_1 = require("commander");
6
8
  const hd_core_1 = require("@onekeyfe/hd-core");
7
9
  const hd_shared_1 = require("@onekeyfe/hd-shared");
8
10
  const chains_1 = require("./chains");
11
+ const deviceSelection_1 = require("./deviceSelection");
12
+ const deviceStateCommands_1 = require("./deviceStateCommands");
9
13
  const sdk_1 = require("./sdk");
10
14
  const session_1 = require("./session");
11
- function extractPassphraseSession(payload) {
12
- if (typeof payload === 'string') {
13
- return { passphraseState: payload };
14
- }
15
- if (!payload || typeof payload !== 'object') {
16
- return {};
17
- }
18
- const statePayload = payload;
19
- let passphraseState;
20
- if (typeof statePayload.passphrase_state === 'string') {
21
- passphraseState = statePayload.passphrase_state;
22
- }
23
- else if (typeof statePayload.passphraseState === 'string') {
24
- passphraseState = statePayload.passphraseState;
25
- }
26
- let sessionId;
27
- if (typeof statePayload.session_id === 'string') {
28
- sessionId = statePayload.session_id;
29
- }
30
- else if (typeof statePayload.sessionId === 'string') {
31
- sessionId = statePayload.sessionId;
32
- }
33
- return { passphraseState, sessionId };
34
- }
35
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'));
36
18
  program
37
19
  .name('onekey-hw')
38
20
  .description('OneKey hardware wallet CLI for AI agent integration')
39
- .version('1.1.26-alpha.1');
21
+ .version(cliVersion);
40
22
  // ============================================================
41
23
  // Global Options
42
24
  // ============================================================
@@ -54,50 +36,103 @@ program
54
36
  .description('Search for connected OneKey hardware wallet devices')
55
37
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
56
38
  const result = await sdk.searchDevices();
57
- // USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
58
- if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
59
- for (const device of result.payload) {
60
- if (device.connectId) {
61
- try {
62
- const features = await sdk.getFeatures(device.connectId);
63
- if (features?.success && features.payload) {
64
- device.features = features.payload;
65
- device.name = features.payload.label || features.payload.bleName || device.name;
66
- const devType = features.payload.deviceType?.toLowerCase();
67
- if (devType) {
68
- device.deviceType = devType;
69
- }
70
- }
71
- }
72
- catch {
73
- // Features fetch failed — device may need PIN, continue with basic info
74
- }
75
- }
76
- }
77
- }
78
39
  outputResult(globalOpts, result);
79
40
  }));
80
41
  program
81
42
  .command('get-features')
82
43
  .description('Get device features (firmware, unlock state, passphrase protection, etc.)')
83
44
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
84
- // Resolve connectId: explicit flag wins, else pick the first attached device
85
- let { connectId } = globalOpts;
86
- if (!connectId) {
87
- const searchResult = await sdk.searchDevices();
88
- if (!searchResult?.success ||
89
- !Array.isArray(searchResult.payload) ||
90
- searchResult.payload.length === 0) {
91
- outputResult(globalOpts, {
92
- success: false,
93
- payload: { error: 'No device found', code: 'NO_DEVICE' },
94
- });
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)
95
86
  return;
87
+ const progress = Number(event.payload.progress);
88
+ if (!Number.isFinite(progress))
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);
98
+ }
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;
96
105
  }
97
- connectId = searchResult.payload[0].connectId ?? undefined;
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
+ });
98
120
  }
99
- const result = await sdk.getFeatures(connectId || '');
100
- 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
+ });
101
136
  }));
102
137
  // ============================================================
103
138
  // Signing Commands
@@ -412,21 +447,45 @@ program
412
447
  code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
413
448
  },
414
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;
415
474
  program
416
475
  .command('firmware-update-ble')
417
476
  .description('Run Protocol V2 firmware update over BLE')
418
477
  .action(() => respondAndExit({
419
478
  success: false,
420
479
  payload: {
421
- error: 'Use `onekey-hw --transport ble firmware-update-v4-debug` for BLE Protocol V2 firmware update debugging.',
422
- code: 'USE_FIRMWARE_UPDATE_V4_DEBUG',
480
+ error: 'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
481
+ code: 'USE_FIRMWARE_UPDATE_V4',
423
482
  },
424
483
  }));
425
484
  program
426
- .command('firmware-update-v4-debug')
427
- .description('Debug Protocol V2 firmware update through sdk.firmwareUpdateV4')
485
+ .command('firmware-update-v4')
486
+ .description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
428
487
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
429
- .option('--resource <path>', 'FW_MGMT_TARGET_CRATE resource package path')
488
+ .option('--resource-file <spec...>', 'Resource direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
430
489
  .option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
431
490
  .option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
432
491
  .option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
@@ -439,8 +498,8 @@ program
439
498
  .option('--forced-update-res', 'Force resource update')
440
499
  .option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
441
500
  .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
442
- const params = buildFirmwareUpdateV4DebugParams(opts);
443
- const result = await runFirmwareUpdateV4DebugWithRetry({
501
+ const params = buildFirmwareUpdateV4Params(opts);
502
+ const result = await runFirmwareUpdateV4WithRetry({
444
503
  sdk,
445
504
  globalOpts,
446
505
  params,
@@ -563,7 +622,7 @@ program
563
622
  const sessionCmd = program.command('session').description('Manage device passphrase session cache');
564
623
  sessionCmd
565
624
  .command('connect')
566
- .description('Connect device and establish passphrase session (cached for subsequent commands)')
625
+ .description('Connect device and select a hidden wallet for this invocation')
567
626
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
568
627
  // 1. Search for device
569
628
  const searchResult = await sdk.searchDevices();
@@ -574,7 +633,14 @@ sessionCmd
574
633
  });
575
634
  return;
576
635
  }
577
- const device = searchResult.payload[0];
636
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
637
+ if (!device) {
638
+ outputResult(globalOpts, {
639
+ success: false,
640
+ payload: { error: 'No matching device found', code: 'NO_DEVICE' },
641
+ });
642
+ return;
643
+ }
578
644
  const connectId = device.connectId || globalOpts.connectId;
579
645
  // 2. Unlock if locked — getPassphraseState below talks to a live
580
646
  // device session, which a locked device will reject with an obscure
@@ -583,56 +649,33 @@ sessionCmd
583
649
  process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
584
650
  await unlockWithRetry(sdk, connectId);
585
651
  }
586
- // 3. Get passphraseState (triggers 1/2/3 selection)
587
- const psResult = await sdk.getPassphraseState(connectId, {
588
- initSession: true,
589
- useEmptyPassphrase: false,
652
+ // 3. Open a hidden wallet session (triggers 1/2/3 selection).
653
+ const sessionResult = await sdk.openWalletSession(connectId, {
654
+ mode: 'select-hidden',
590
655
  });
591
- if (!psResult.success) {
592
- outputResult(globalOpts, psResult);
656
+ if (!sessionResult.success) {
657
+ outputResult(globalOpts, sessionResult);
593
658
  return;
594
659
  }
595
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
596
- if (!passphraseState) {
660
+ if (sessionResult.payload.walletType !== 'hidden') {
597
661
  outputResult(globalOpts, {
598
662
  success: false,
599
- payload: { error: 'getPassphraseState did not return passphraseState' },
663
+ payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
600
664
  });
601
665
  return;
602
666
  }
667
+ const { deviceId, passphraseState } = sessionResult.payload;
603
668
  // 4. Get address to verify + extract deviceId
604
- const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
669
+ const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
605
670
  path: "m/44'/60'/0'/0/0",
606
671
  showOnOneKey: false,
607
672
  passphraseState,
608
673
  });
609
- // 5. Fetch the now-active session_id via getFeatures.
610
- //
611
- // IMPORTANT: pass `passphraseState` here. Without it, the SDK's
612
- // connectStateChange guard (core/index.ts) would see the payload's
613
- // passphraseState flip from mnNy → undefined, clear the cached Device,
614
- // and call Initialize again with no passphrase_state / no session_id.
615
- // That Initialize resets the device to the standard wallet and returns
616
- // a *standard-wallet* session_id — which we'd then save in the keychain
617
- // paired with the hidden-wallet passphraseState. On the next CLI run
618
- // the mismatch would trigger PassphraseRequest (1/2/3 again).
619
- const featResult = await sdk.getFeatures(connectId, {
620
- passphraseState,
621
- skipPassphraseCheck: true,
622
- });
623
- const featPayload = featResult?.success ? featResult.payload : undefined;
624
- const deviceId = featPayload?.deviceId || device.deviceId || '';
625
- const sessionId = passphraseSessionId || featPayload?.sessionId || '';
626
- // 6. Save to keychain
627
- if (passphraseState && deviceId && sessionId) {
628
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
629
- }
630
674
  outputResult(globalOpts, {
631
675
  success: true,
632
676
  payload: {
633
677
  passphraseState,
634
678
  deviceId,
635
- ...(sessionId ? { sessionId } : {}),
636
679
  ...(addrResult?.success ? { address: addrResult.payload.address } : {}),
637
680
  },
638
681
  });
@@ -642,8 +685,7 @@ sessionCmd
642
685
  .description('Clear cached device session')
643
686
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
644
687
  const searchResult = await sdk.searchDevices();
645
- const device = // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
646
- searchResult?.payload?.[0];
688
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult?.payload ?? [], globalOpts.connectId);
647
689
  const deviceId = device?.deviceId || device?.features?.device_id;
648
690
  if (deviceId) {
649
691
  await (0, session_1.clearSessionFromKeychain)(deviceId);
@@ -720,8 +762,8 @@ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
720
762
  * Prepare passphrase session before SDK calls.
721
763
  *
722
764
  * 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
723
- * 2. Try keychain → preloadSessionCache → use cached session
724
- * 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
765
+ * 2. Try a legacy keychain entry → preloadSessionCache → use cached session
766
+ * 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
725
767
  *
726
768
  * After this, globalOpts.passphraseState is set and getCommonParams will include it.
727
769
  */
@@ -733,7 +775,7 @@ globalOpts) {
733
775
  return globalOpts.passphraseState;
734
776
  }
735
777
  // Errors from the SDK calls below (PIN cancelled, transport broken,
736
- // getPassphraseState rejection) intentionally propagate to runCommand's
778
+ // openWalletSession rejection) intentionally propagate to runCommand's
737
779
  // catch block, which renders them as structured `{ success: false,
738
780
  // payload: { error, code } }` output instead of silently falling through
739
781
  // to a confusing downstream error 112 / 114.
@@ -744,18 +786,22 @@ globalOpts) {
744
786
  searchResult.payload.length === 0) {
745
787
  return undefined;
746
788
  }
747
- const device = searchResult.payload[0];
748
- const connectId = device.connectId || globalOpts.connectId || '';
789
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
790
+ if (!device) {
791
+ throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
792
+ }
793
+ const selectedDevice = device;
794
+ const connectId = selectedDevice.connectId || globalOpts.connectId || '';
749
795
  if (!globalOpts.connectId && connectId) {
750
796
  globalOpts.connectId = connectId;
751
797
  }
752
798
  // ── Step 2: Get features if searchDevices didn't populate them ──
753
799
  // getFeatures failures here are non-fatal — we fall through to Step 3
754
800
  // which will fail with a clearer error if the device is truly unreachable.
755
- let deviceId = device.features?.deviceId || device.deviceId || '';
756
- let deviceType = (0, hd_core_1.getDeviceType)(device.features);
757
- let unlocked = device.features?.unlocked;
758
- let passphraseProtection = device.features?.passphraseProtection;
801
+ let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
802
+ let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
803
+ let unlocked = selectedDevice.features?.unlocked;
804
+ let passphraseProtection = selectedDevice.features?.passphraseProtection;
759
805
  if (!deviceId || unlocked == null || passphraseProtection == null) {
760
806
  try {
761
807
  const featResult = await sdk.getFeatures(connectId);
@@ -790,7 +836,7 @@ globalOpts) {
790
836
  if (passphraseProtection === false && deviceType !== hd_shared_1.EDeviceType.Pro2) {
791
837
  return undefined;
792
838
  }
793
- // ── Step 5: Try keychain session reuse ───────────────────────────
839
+ // ── Step 5: Try legacy keychain session reuse ────────────────────
794
840
  // Only attempt if device was already unlocked — locking invalidates
795
841
  // all passphrase sessions, so cached session_id is useless after unlock.
796
842
  if (!wasLocked && deviceId) {
@@ -800,38 +846,22 @@ globalOpts) {
800
846
  return cached;
801
847
  }
802
848
  }
803
- // ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
804
- const psResult = await sdk.getPassphraseState(connectId, {
805
- initSession: true,
806
- useEmptyPassphrase: false,
849
+ // ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
850
+ const sessionResult = await sdk.openWalletSession(connectId, {
851
+ mode: 'select-hidden',
807
852
  });
808
- if (psResult.success && psResult.payload) {
809
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
810
- if (!passphraseState) {
853
+ if (sessionResult.success && sessionResult.payload) {
854
+ if (sessionResult.payload.walletType !== 'hidden') {
811
855
  return undefined;
812
856
  }
857
+ const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
858
+ globalOpts.deviceId = sessionDeviceId;
813
859
  globalOpts.passphraseState = passphraseState;
814
- // Save session to keychain for next invocation.
815
- //
816
- // Pass passphraseState to keep connectStateChange=false — otherwise
817
- // Initialize would be re-run without passphrase_state, resetting the
818
- // device to the standard wallet and returning a mismatched session_id.
819
- // See the matching comment in `session connect`.
820
- if (deviceId) {
821
- const featAfter = await sdk.getFeatures(connectId, {
822
- passphraseState,
823
- skipPassphraseCheck: true,
824
- });
825
- const sessionId = passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
826
- if (sessionId) {
827
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
828
- await (0, session_1.preloadSessionFromKeychain)(deviceId);
829
- }
830
- }
831
860
  return passphraseState;
832
861
  }
833
862
  return undefined;
834
863
  }
864
+ exports.prepareSession = prepareSession;
835
865
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
836
866
  function outputResult(_globalOpts, result) {
837
867
  console.log(JSON.stringify(result, null, 2));
@@ -841,8 +871,8 @@ function outputResult(_globalOpts, result) {
841
871
  !result.success) {
842
872
  process.exitCode = 1;
843
873
  }
844
- // No process.exit here — runCommand() below handles dispose + exit so SDK
845
- // async cleanup (USB release, event listener teardown) finishes first.
874
+ // No process.exit here — runCommand() waits for SDK cleanup, then lets Node
875
+ // exit naturally so leaked USB handles remain observable.
846
876
  }
847
877
  async function runCommand(options, handler) {
848
878
  const globalOpts = program.opts();
@@ -875,9 +905,7 @@ async function runCommand(options, handler) {
875
905
  // promise reference. Idempotent, safe to call even if init failed.
876
906
  await (0, sdk_1.disposeSDK)();
877
907
  }
878
- // SDK event listeners can keep the event loop alive after dispose.
879
- // setImmediate lets any trailing stdout/stderr writes flush first.
880
- setImmediate(() => process.exit(process.exitCode ?? 0));
908
+ // disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
881
909
  }
882
910
  /** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
883
911
  function respondAndExit(result) {
@@ -902,9 +930,51 @@ function readBinaryParam(path) {
902
930
  const buffer = (0, node_fs_1.readFileSync)(path);
903
931
  return new Uint8Array(buffer).buffer;
904
932
  }
905
- function getFirmwareUpdateV4DebugTotalBytes(params) {
933
+ async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName) {
934
+ if (explicitConnectId && !deviceName)
935
+ return explicitConnectId;
936
+ const searchResult = await sdk.searchDevices();
937
+ if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
938
+ throw new Error('Unable to scan BLE devices');
939
+ }
940
+ const devices = searchResult.payload;
941
+ const normalizedName = deviceName?.trim().toLowerCase();
942
+ const matches = normalizedName
943
+ ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
944
+ : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
945
+ if (matches.length === 0) {
946
+ throw new Error(normalizedName
947
+ ? `BLE device not found by name: ${deviceName}`
948
+ : 'No Classic/Pure BLE device found');
949
+ }
950
+ if (matches.length > 1) {
951
+ throw new Error(normalizedName
952
+ ? `Multiple BLE devices found by name: ${deviceName}`
953
+ : 'Multiple Classic/Pure BLE devices found; specify --device-name');
954
+ }
955
+ const [{ connectId, name }] = matches;
956
+ if (!connectId)
957
+ throw new Error(`BLE device has no connect ID: ${name}`);
958
+ return connectId;
959
+ }
960
+ function parseResourceFileParam(spec) {
961
+ const sep = spec.indexOf(':');
962
+ if (sep <= 0 || sep === spec.length - 1) {
963
+ throw new Error(`Invalid --resource-file value: "${spec}". Expected <localPath>:<devicePath>`);
964
+ }
965
+ const localPath = spec.slice(0, sep);
966
+ const devicePath = spec.slice(sep + 1);
967
+ if (!devicePath.startsWith('vol')) {
968
+ throw new Error(`Invalid --resource-file device path: "${devicePath}". Expected a vol*:/... path`);
969
+ }
970
+ return {
971
+ binary: readBinaryParam(localPath),
972
+ devicePath,
973
+ };
974
+ }
975
+ function getFirmwareUpdateV4TotalBytes(params) {
906
976
  return [
907
- ...(params.resourceBinaries ?? []),
977
+ ...(params.resourceFiles?.map(item => item.binary) ?? []),
908
978
  params.bootloaderBinary,
909
979
  params.applicationP1Binary,
910
980
  params.applicationP2Binary,
@@ -915,17 +985,17 @@ function getFirmwareUpdateV4DebugTotalBytes(params) {
915
985
  params.se04Binary,
916
986
  ].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
917
987
  }
918
- function getFirmwareUpdateV4DebugErrorText(result) {
988
+ function getFirmwareUpdateV4ErrorText(result) {
919
989
  if (!result || typeof result !== 'object')
920
990
  return '';
921
- const payload = result.payload;
991
+ const { payload } = result;
922
992
  if (!payload || typeof payload !== 'object')
923
993
  return '';
924
- const error = payload.error;
994
+ const { error } = payload;
925
995
  return typeof error === 'string' ? error : '';
926
996
  }
927
997
  function isProtocolV2UsbProbeTransientResult(result) {
928
- const error = getFirmwareUpdateV4DebugErrorText(result);
998
+ const error = getFirmwareUpdateV4ErrorText(result);
929
999
  return (error.includes('Device protocol mismatch') &&
930
1000
  error.includes('expected V2') &&
931
1001
  error.includes('did not respond to expected protocol'));
@@ -933,22 +1003,33 @@ function isProtocolV2UsbProbeTransientResult(result) {
933
1003
  function isSuccessResult(result) {
934
1004
  return (!!result && typeof result === 'object' && result.success === true);
935
1005
  }
936
- function getFirmwareDebugPayload(message) {
1006
+ function getFirmwareUpdatePayload(message) {
937
1007
  if (!message || typeof message !== 'object')
938
1008
  return undefined;
939
1009
  return message.payload;
940
1010
  }
941
- function formatFirmwareDebugProgress(progress) {
1011
+ function formatFirmwareProgress(progress) {
942
1012
  if (!Number.isFinite(progress))
943
1013
  return '0%';
944
1014
  return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
945
1015
  }
946
- function formatFirmwareDebugBytes(bytes) {
1016
+ function formatFirmwareBytes(bytes) {
947
1017
  if (!Number.isFinite(bytes) || bytes <= 0)
948
1018
  return '';
949
1019
  return `${(bytes / 1024).toFixed(1)} KiB`;
950
1020
  }
951
- function maybePrintFirmwareDebugProgress({ progressType, progress, payload, lastPrintedProgress, }) {
1021
+ function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }) {
1022
+ const elapsedMs = Math.max(endedAt - startedAt, 0);
1023
+ return {
1024
+ totalBytes,
1025
+ transferredBytes,
1026
+ totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
1027
+ transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
1028
+ lastProgress,
1029
+ };
1030
+ }
1031
+ exports.buildWallpaperUploadMetrics = buildWallpaperUploadMetrics;
1032
+ function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
952
1033
  const printableProgress = Math.floor(progress / 10) * 10;
953
1034
  if (printableProgress <= lastPrintedProgress && progress < 100) {
954
1035
  return lastPrintedProgress;
@@ -957,15 +1038,15 @@ function maybePrintFirmwareDebugProgress({ progressType, progress, payload, last
957
1038
  const totalBytes = Number(payload.totalBytes);
958
1039
  const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
959
1040
  const sizeText = Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
960
- ? ` ${formatFirmwareDebugBytes(transferredBytes)}/${formatFirmwareDebugBytes(totalBytes)}`
1041
+ ? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
961
1042
  : '';
962
1043
  const speedText = Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
963
1044
  ? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
964
1045
  : '';
965
- process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareDebugProgress(progress)}${sizeText}${speedText}\n`);
1046
+ process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(progress)}${sizeText}${speedText}\n`);
966
1047
  return progress >= 100 ? 100 : printableProgress;
967
1048
  }
968
- function buildFirmwareUpdateV4DebugMetrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
1049
+ function buildFirmwareUpdateV4Metrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
969
1050
  const totalElapsedMs = Date.now() - totalStartedAt;
970
1051
  const transferElapsedMs = transferStartedAt !== undefined && transferEndedAt !== undefined
971
1052
  ? transferEndedAt - transferStartedAt
@@ -990,137 +1071,145 @@ function buildFirmwareUpdateV4DebugMetrics({ attempt, maxAttempts, totalBytes, t
990
1071
  totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
991
1072
  };
992
1073
  }
993
- async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retries, }) {
994
- const totalBytes = getFirmwareUpdateV4DebugTotalBytes(params);
1074
+ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }) {
1075
+ const totalBytes = getFirmwareUpdateV4TotalBytes(params);
995
1076
  const maxAttempts = Math.max((retries ?? 2) + 1, 1);
996
1077
  let currentSdk = sdk;
997
- let lastResult;
998
1078
  let retried = false;
999
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1000
- let progressEvents = 0;
1001
- let lastProgress = -1;
1002
- let transferStartedAt;
1003
- let transferEndedAt;
1004
- let installProgressEvents = 0;
1005
- let lastInstallProgress = -1;
1006
- let installStartedAt;
1007
- let installEndedAt;
1008
- let lastPrintedTransferProgress = -10;
1009
- let lastPrintedInstallProgress = -10;
1010
- const totalStartedAt = Date.now();
1011
- const connectId = retried && globalOpts.transport === 'usb' && globalOpts.connectId
1012
- ? undefined
1013
- : globalOpts.connectId;
1014
- const onUiEvent = (message) => {
1015
- if (!message || typeof message !== 'object')
1016
- return;
1017
- const messageType = message.type;
1018
- const payload = getFirmwareDebugPayload(message);
1019
- if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1020
- const tipMessage = payload?.data?.message;
1021
- if (typeof tipMessage === 'string') {
1022
- process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1023
- }
1024
- return;
1025
- }
1026
- if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1027
- const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1028
- process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1029
- return;
1030
- }
1031
- if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1032
- return;
1033
- const progress = Number(payload.progress);
1034
- if (!Number.isFinite(progress))
1035
- return;
1036
- if (payload.progressType === 'transferData') {
1037
- progressEvents += 1;
1038
- lastProgress = Math.max(lastProgress, progress);
1039
- transferStartedAt ?? (transferStartedAt = Date.now());
1040
- lastPrintedTransferProgress = maybePrintFirmwareDebugProgress({
1041
- progressType: 'transfer',
1042
- progress,
1043
- payload,
1044
- lastPrintedProgress: lastPrintedTransferProgress,
1045
- });
1046
- if (progress >= 100) {
1047
- transferEndedAt ?? (transferEndedAt = Date.now());
1048
- }
1049
- return;
1050
- }
1051
- if (payload.progressType === 'installingFirmware') {
1052
- installProgressEvents += 1;
1053
- lastInstallProgress = Math.max(lastInstallProgress, progress);
1054
- installStartedAt ?? (installStartedAt = Date.now());
1055
- lastPrintedInstallProgress = maybePrintFirmwareDebugProgress({
1056
- progressType: 'install',
1057
- progress,
1058
- payload,
1059
- lastPrintedProgress: lastPrintedInstallProgress,
1060
- });
1061
- if (progress >= 100) {
1062
- installEndedAt ?? (installEndedAt = Date.now());
1063
- }
1079
+ let attempt = 1;
1080
+ let { connectId } = globalOpts;
1081
+ if (globalOpts.transport === 'usb') {
1082
+ for (; attempt <= maxAttempts; attempt += 1) {
1083
+ const probeResult = await currentSdk.getDeviceState(connectId, {
1084
+ scope: 'runtime',
1085
+ connectProtocol: 'V2',
1086
+ retryCount: 0,
1087
+ });
1088
+ if (isSuccessResult(probeResult))
1089
+ break;
1090
+ if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
1091
+ return probeResult;
1064
1092
  }
1065
- };
1066
- currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1067
- try {
1068
- lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1069
- }
1070
- finally {
1071
- currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1093
+ retried = true;
1094
+ process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
1095
+ await (0, sdk_1.disposeSDK)();
1096
+ await new Promise(resolve => {
1097
+ setTimeout(resolve, 3000);
1098
+ });
1099
+ currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1100
+ if (globalOpts.connectId)
1101
+ connectId = undefined;
1072
1102
  }
1073
- if (installStartedAt !== undefined && installEndedAt === undefined) {
1074
- installEndedAt = Date.now();
1103
+ }
1104
+ let progressEvents = 0;
1105
+ let lastProgress = -1;
1106
+ let transferStartedAt;
1107
+ let transferEndedAt;
1108
+ let installProgressEvents = 0;
1109
+ let lastInstallProgress = -1;
1110
+ let installStartedAt;
1111
+ let installEndedAt;
1112
+ let lastPrintedTransferProgress = -10;
1113
+ let lastPrintedInstallProgress = -10;
1114
+ const totalStartedAt = Date.now();
1115
+ const onUiEvent = (message) => {
1116
+ if (!message || typeof message !== 'object')
1117
+ return;
1118
+ const messageType = message.type;
1119
+ const payload = getFirmwareUpdatePayload(message);
1120
+ if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1121
+ const tipMessage = payload?.data?.message;
1122
+ if (typeof tipMessage === 'string') {
1123
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1124
+ }
1125
+ return;
1075
1126
  }
1076
- const debugMetrics = buildFirmwareUpdateV4DebugMetrics({
1077
- attempt,
1078
- maxAttempts,
1079
- totalBytes,
1080
- totalStartedAt,
1081
- transferStartedAt,
1082
- transferEndedAt,
1083
- installStartedAt,
1084
- installEndedAt,
1085
- progressEvents,
1086
- lastProgress,
1087
- installProgressEvents,
1088
- lastInstallProgress,
1089
- retried,
1090
- });
1091
- if (lastResult && typeof lastResult === 'object') {
1092
- const payload = (lastResult.payload ?? {});
1093
- lastResult = {
1094
- ...lastResult,
1095
- payload: {
1096
- ...payload,
1097
- _debug: debugMetrics,
1098
- },
1099
- };
1127
+ if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1128
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1129
+ process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1130
+ return;
1100
1131
  }
1101
- if (isSuccessResult(lastResult)) {
1102
- return lastResult;
1132
+ if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1133
+ return;
1134
+ const progress = Number(payload.progress);
1135
+ if (!Number.isFinite(progress))
1136
+ return;
1137
+ if (payload.progressType === 'transferData') {
1138
+ progressEvents += 1;
1139
+ lastProgress = Math.max(lastProgress, progress);
1140
+ transferStartedAt ?? (transferStartedAt = Date.now());
1141
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1142
+ progressType: 'transfer',
1143
+ progress,
1144
+ payload,
1145
+ lastPrintedProgress: lastPrintedTransferProgress,
1146
+ });
1147
+ if (progress >= 100) {
1148
+ transferEndedAt ?? (transferEndedAt = Date.now());
1149
+ }
1150
+ return;
1103
1151
  }
1104
- if (attempt >= maxAttempts ||
1105
- globalOpts.transport !== 'usb' ||
1106
- !isProtocolV2UsbProbeTransientResult(lastResult)) {
1107
- return lastResult;
1152
+ if (payload.progressType === 'installingFirmware') {
1153
+ installProgressEvents += 1;
1154
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1155
+ installStartedAt ?? (installStartedAt = Date.now());
1156
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1157
+ progressType: 'install',
1158
+ progress,
1159
+ payload,
1160
+ lastPrintedProgress: lastPrintedInstallProgress,
1161
+ });
1162
+ if (progress >= 100) {
1163
+ installEndedAt ?? (installEndedAt = Date.now());
1164
+ }
1108
1165
  }
1109
- retried = true;
1110
- process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
1111
- await (0, sdk_1.disposeSDK)();
1112
- await new Promise(resolve => setTimeout(resolve, 3000));
1113
- currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1166
+ };
1167
+ currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1168
+ let result;
1169
+ try {
1170
+ result = await currentSdk.firmwareUpdateV4(connectId, params);
1171
+ }
1172
+ finally {
1173
+ currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1174
+ }
1175
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1176
+ installEndedAt = Date.now();
1114
1177
  }
1115
- return lastResult;
1178
+ const metrics = buildFirmwareUpdateV4Metrics({
1179
+ attempt,
1180
+ maxAttempts,
1181
+ totalBytes,
1182
+ totalStartedAt,
1183
+ transferStartedAt,
1184
+ transferEndedAt,
1185
+ installStartedAt,
1186
+ installEndedAt,
1187
+ progressEvents,
1188
+ lastProgress,
1189
+ installProgressEvents,
1190
+ lastInstallProgress,
1191
+ retried,
1192
+ });
1193
+ if (result && typeof result === 'object') {
1194
+ const payload = (result.payload ?? {});
1195
+ return {
1196
+ ...result,
1197
+ payload: {
1198
+ ...payload,
1199
+ metrics,
1200
+ },
1201
+ };
1202
+ }
1203
+ return result;
1116
1204
  }
1117
- function buildFirmwareUpdateV4DebugParams(opts) {
1205
+ exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
1206
+ function buildFirmwareUpdateV4Params(opts) {
1118
1207
  const params = {
1119
1208
  platform: 'desktop',
1120
1209
  connectProtocol: 'V2',
1121
1210
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
1122
1211
  forcedUpdateRes: opts.forcedUpdateRes,
1123
- resourceBinaries: opts.resource ? [readBinaryParam(opts.resource)] : undefined,
1212
+ resourceFiles: opts.resourceFile?.map(parseResourceFileParam),
1124
1213
  romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
1125
1214
  bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
1126
1215
  applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
@@ -1132,7 +1221,7 @@ function buildFirmwareUpdateV4DebugParams(opts) {
1132
1221
  se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
1133
1222
  };
1134
1223
  const hasPayload = [
1135
- params.resourceBinaries,
1224
+ params.resourceFiles,
1136
1225
  params.romloaderBinary,
1137
1226
  params.bootloaderBinary,
1138
1227
  params.applicationP1Binary,
@@ -1144,7 +1233,7 @@ function buildFirmwareUpdateV4DebugParams(opts) {
1144
1233
  params.se04Binary,
1145
1234
  ].some(Boolean);
1146
1235
  if (!hasPayload) {
1147
- const err = new Error('firmware-update-v4-debug requires at least one binary path');
1236
+ const err = new Error('firmware-update-v4 requires at least one binary path');
1148
1237
  err.code = 'MISSING_FIRMWARE_BINARY';
1149
1238
  throw err;
1150
1239
  }
@@ -1160,4 +1249,6 @@ function safeParseInt(input, label) {
1160
1249
  }
1161
1250
  return num;
1162
1251
  }
1163
- program.parse();
1252
+ if (require.main === module) {
1253
+ program.parse();
1254
+ }