@matter/nodejs-shell 0.16.0-alpha.0-20251112-dba1973d5 → 0.16.0-alpha.0-20251125-16883ca92

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.
@@ -0,0 +1,550 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Crypto, Diagnostic, Environment } from "#general";
8
+ import { OtaImageReader, PersistedFileDesignator } from "#protocol";
9
+ import { createReadStream, createWriteStream, statSync, WriteStream } from "node:fs";
10
+ import { basename, dirname, extname, join } from "node:path";
11
+ import { Readable } from "node:stream";
12
+ import type { Argv } from "yargs";
13
+ import { MatterNode } from "../MatterNode.js";
14
+
15
+ /**
16
+ * Parse a hex string to a number, handling optional 0x prefix.
17
+ * Exits the process with an error if the value is invalid.
18
+ */
19
+ function parseHexId(value: string, type: "vendor" | "product"): number {
20
+ const hexStr = value.replace(/^0x/i, "");
21
+ const parsed = parseInt(hexStr, 16);
22
+ if (isNaN(parsed)) {
23
+ console.error(`Error: Invalid ${type} ID "${value}"`);
24
+ process.exit(1);
25
+ }
26
+ return parsed;
27
+ }
28
+
29
+ function createWritableStream(writeStream: WriteStream) {
30
+ return new WritableStream({
31
+ write(chunk) {
32
+ return new Promise((resolve, reject) => {
33
+ writeStream.write(chunk, (error: Error | null | undefined) => {
34
+ if (error) reject(error);
35
+ else resolve();
36
+ });
37
+ });
38
+ },
39
+ close() {
40
+ return new Promise((resolve, reject) => {
41
+ writeStream.end((error: Error | null | undefined) => {
42
+ if (error) reject(error);
43
+ else resolve();
44
+ });
45
+ });
46
+ },
47
+ });
48
+ }
49
+
50
+ export default function commands(theNode: MatterNode) {
51
+ return {
52
+ command: "ota",
53
+ describe: "OTA update operations",
54
+ builder: (yargs: Argv) =>
55
+ yargs
56
+ .command(
57
+ "info <file>",
58
+ "Display OTA image information from a file or storage key",
59
+ yargs => {
60
+ return yargs.positional("file", {
61
+ describe:
62
+ "File path (with file:// prefix for absolute paths) or storage key (without prefix)",
63
+ type: "string",
64
+ demandOption: true,
65
+ });
66
+ },
67
+ async argv => {
68
+ const { file } = argv;
69
+ const fileArg = file;
70
+
71
+ let updateInfo;
72
+
73
+ if (fileArg.startsWith("file://")) {
74
+ // Absolute file path outside storage
75
+ const filePath = fileArg.slice(7); // Remove "file://" prefix
76
+
77
+ // Create a Node.js readable stream and convert to Web ReadableStream
78
+ const nodeStream = createReadStream(filePath);
79
+ const webStream = Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;
80
+
81
+ updateInfo = await theNode.otaService.updateInfoFromStream(webStream, fileArg);
82
+ } else {
83
+ // Read file from storage using PersistedFileDesignator
84
+ const fileDesignator = await theNode.otaService.fileDesignatorForUpdate(fileArg);
85
+ const blob = await fileDesignator.openBlob();
86
+ const reader = blob.stream().getReader();
87
+
88
+ // Parse header to get update info
89
+ const header = await OtaImageReader.header(reader);
90
+
91
+ // Create update info structure from header
92
+ updateInfo = {
93
+ vid: header.vendorId,
94
+ pid: header.productId,
95
+ softwareVersion: header.softwareVersion,
96
+ softwareVersionString: header.softwareVersionString,
97
+ payloadSize: header.payloadSize,
98
+ imageDigestType: header.imageDigestType,
99
+ imageDigest: header.imageDigest,
100
+ minApplicableSoftwareVersion: header.minApplicableSoftwareVersion,
101
+ maxApplicableSoftwareVersion: header.maxApplicableSoftwareVersion,
102
+ releaseNotesUrl: header.releaseNotesUrl,
103
+ storageKey: fileArg,
104
+ };
105
+ }
106
+
107
+ // Display the information in formatted JSON
108
+ console.log(Diagnostic.json(updateInfo));
109
+ },
110
+ )
111
+ .command(
112
+ "extract <file>",
113
+ "Extract and validate payload from an OTA image file",
114
+ yargs => {
115
+ return yargs.positional("file", {
116
+ describe: "Absolute path to the OTA image file",
117
+ type: "string",
118
+ demandOption: true,
119
+ });
120
+ },
121
+ async argv => {
122
+ const { file } = argv as { file: string };
123
+
124
+ // Get crypto from the environment
125
+ const crypto = Environment.default.get(Crypto);
126
+
127
+ // Generate output filename by adding "-payload" before the extension
128
+ const dir = dirname(file);
129
+ const ext = extname(file);
130
+ const base = basename(file, ext);
131
+ const outputFile = join(dir, `${base}-payload${ext}`);
132
+
133
+ console.log(`Reading OTA image from: ${file}`);
134
+ console.log(`Extracting payload to: ${outputFile}`);
135
+
136
+ // Read the OTA file
137
+ const response = await fetch(`file://${file}`, { method: "GET" });
138
+
139
+ if (!response.ok) {
140
+ throw new Error(`Failed to read OTA file: ${response.status} ${response.statusText}`);
141
+ }
142
+
143
+ if (!response.body) {
144
+ throw new Error("No response body received");
145
+ }
146
+
147
+ // Create output stream for the payload
148
+ const writeStream = createWriteStream(outputFile);
149
+ const writableStream = createWritableStream(writeStream);
150
+
151
+ const payloadWriter = writableStream.getWriter();
152
+
153
+ // Extract and validate payload
154
+ const reader = response.body.getReader();
155
+ const header = await OtaImageReader.extractPayload(reader, payloadWriter, crypto);
156
+
157
+ console.log(`\nPayload extracted successfully!`);
158
+ console.log(`Vendor ID: ${Diagnostic.hex(header.vendorId, 4)}`);
159
+ console.log(`Product ID: 0x${Diagnostic.hex(header.productId, 4)}`);
160
+ console.log(`Software Version: ${header.softwareVersion}`);
161
+ console.log(`Software Version String: ${header.softwareVersionString}`);
162
+ console.log(`Payload Size: ${header.payloadSize} bytes`);
163
+ console.log(`Output file: ${outputFile}`);
164
+ },
165
+ )
166
+ .command(
167
+ "verify <file>",
168
+ "Verify an OTA image file (validates header and payload checksums)",
169
+ yargs => {
170
+ return yargs.positional("file", {
171
+ describe: "Path to the OTA image file (with file:// prefix) or storage key",
172
+ type: "string",
173
+ demandOption: true,
174
+ });
175
+ },
176
+ async argv => {
177
+ const { file } = argv;
178
+ const fileArg = file;
179
+
180
+ // Get crypto from the environment
181
+ const crypto = Environment.default.get(Crypto);
182
+
183
+ console.log(`Verifying OTA image: ${fileArg}\n`);
184
+
185
+ let header;
186
+ let source: string;
187
+
188
+ if (fileArg.startsWith("file://")) {
189
+ // Absolute file path outside storage
190
+ const filePath = fileArg.slice(7); // Remove "file://" prefix
191
+ source = filePath;
192
+
193
+ // Create a Node.js readable stream and convert to Web ReadableStream
194
+ const nodeStream = createReadStream(filePath);
195
+ const webStream = Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;
196
+ const reader = webStream.getReader();
197
+
198
+ // Validate the entire file (header + payload with checksums)
199
+ header = await OtaImageReader.file(reader, crypto);
200
+ } else {
201
+ // Storage key - read from OTA storage
202
+ source = `storage:${fileArg}`;
203
+ const fileDesignator = await theNode.otaService.fileDesignatorForUpdate(fileArg);
204
+ const blob = await fileDesignator.openBlob();
205
+ const reader = blob.stream().getReader();
206
+
207
+ // Validate the entire file (header + payload with checksums)
208
+ header = await OtaImageReader.file(reader, crypto);
209
+ }
210
+
211
+ console.log(`✓ OTA image is valid!\n`);
212
+ console.log(`File: ${source}`);
213
+ console.log(`Vendor ID: ${Diagnostic.hex(header.vendorId, 4)}`);
214
+ console.log(`Product ID: ${Diagnostic.hex(header.productId, 4)}`);
215
+ console.log(`Software Version: ${header.softwareVersion}`);
216
+ console.log(`Software Version String: ${header.softwareVersionString}`);
217
+ console.log(`Payload Size: ${header.payloadSize} bytes`);
218
+ console.log(`Digest Algorithm: ${header.imageDigestType}`);
219
+ console.log(`Digest: ${header.imageDigest}`);
220
+ if (header.minApplicableSoftwareVersion !== undefined) {
221
+ console.log(`Min Applicable Version: ${header.minApplicableSoftwareVersion}`);
222
+ }
223
+ if (header.maxApplicableSoftwareVersion !== undefined) {
224
+ console.log(`Max Applicable Version: ${header.maxApplicableSoftwareVersion}`);
225
+ }
226
+ if (header.releaseNotesUrl) {
227
+ console.log(`Release Notes: ${header.releaseNotesUrl}`);
228
+ }
229
+ },
230
+ )
231
+ .command(
232
+ "list",
233
+ "List downloaded OTA images in storage",
234
+ yargs => {
235
+ return yargs
236
+ .option("vid", {
237
+ alias: "vendor-id",
238
+ describe: "Filter by vendor ID (hex, e.g., 0xFFF1 or FFF1)",
239
+ type: "string",
240
+ })
241
+ .option("pid", {
242
+ alias: "product-id",
243
+ describe: "Filter by product ID (hex, e.g., 0x8000 or 8000) - requires --vid",
244
+ type: "string",
245
+ })
246
+ .option("mode", {
247
+ describe: "Filter by mode (prod or test)",
248
+ type: "string",
249
+ choices: ["prod", "test"],
250
+ });
251
+ },
252
+ async argv => {
253
+ const { vid, pid, mode } = argv;
254
+
255
+ // Validate filter options
256
+ if (pid && !vid) {
257
+ console.error("Error: --pid requires --vid to be specified");
258
+ process.exit(1);
259
+ }
260
+
261
+ // Parse vendor and product IDs from hex strings
262
+ const vendorId = vid ? parseHexId(vid, "vendor") : undefined;
263
+ const productId = pid ? parseHexId(pid, "product") : undefined;
264
+ const isProduction = mode ? mode === "prod" : undefined;
265
+
266
+ // Get list of downloaded updates
267
+ const updates = await theNode.otaService.find({
268
+ vendorId,
269
+ productId,
270
+ isProduction,
271
+ });
272
+
273
+ if (updates.length === 0) {
274
+ console.log("No OTA images found in storage matching the criteria.");
275
+ return;
276
+ }
277
+
278
+ // Display results in a table format
279
+ console.log(
280
+ `Found ${updates.length} OTA image${updates.length === 1 ? "" : "s"} in storage:\n`,
281
+ );
282
+ console.log(
283
+ "Filename".padEnd(35) +
284
+ "VID".padEnd(8) +
285
+ "PID".padEnd(8) +
286
+ "Version".padEnd(12) +
287
+ "Mode".padEnd(8) +
288
+ "Size",
289
+ );
290
+ console.log("-".repeat(100));
291
+
292
+ for (const update of updates) {
293
+ const vidHex = `0x${update.vendorId.toString(16).toUpperCase()}`;
294
+ const pidHex = `0x${update.productId.toString(16).toUpperCase()}`;
295
+ const sizeKB = (update.size / 1024).toFixed(2);
296
+
297
+ console.log(
298
+ update.filename.padEnd(35) +
299
+ vidHex.padEnd(8) +
300
+ pidHex.padEnd(8) +
301
+ `${update.softwareVersion}`.padEnd(12) +
302
+ update.mode.padEnd(8) +
303
+ `${sizeKB} KB`,
304
+ );
305
+ }
306
+ },
307
+ )
308
+ .command(
309
+ "add <file>",
310
+ "Add an OTA image file to storage",
311
+ yargs => {
312
+ return yargs
313
+ .positional("file", {
314
+ describe: "Absolute path to the OTA image file",
315
+ type: "string",
316
+ demandOption: true,
317
+ })
318
+ .option("mode", {
319
+ describe: "Mode for the OTA file (prod or test)",
320
+ type: "string",
321
+ choices: ["prod", "test"],
322
+ default: "prod",
323
+ });
324
+ },
325
+ async argv => {
326
+ const { file, mode } = argv;
327
+ let filePath = file;
328
+ const isProduction = mode === "prod";
329
+
330
+ if (filePath.startsWith("file://")) {
331
+ filePath = filePath.slice(7); // Remove "file://" prefix
332
+ } else if (!filePath.startsWith("/")) {
333
+ console.error("Error: File path must be absolute or start with file://");
334
+ return;
335
+ }
336
+ console.log(`Reading OTA image from: ${filePath}`);
337
+
338
+ // Create update info from the file (validates the file)
339
+ let localFile = false;
340
+ let updateInfo;
341
+ if (filePath.toLowerCase().startsWith("https://")) {
342
+ // Remote HTTPS file
343
+ updateInfo = await theNode.otaService.createUpdateInfoFromFile(filePath);
344
+ } else {
345
+ // Local file - use stream
346
+ const nodeStream = createReadStream(filePath);
347
+ const webStream = Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;
348
+ const fileUrl = `file://${filePath}`;
349
+ localFile = true;
350
+ updateInfo = await theNode.otaService.updateInfoFromStream(webStream, fileUrl);
351
+ }
352
+
353
+ console.log(`Validated OTA image:`);
354
+ console.log(` Vendor ID: 0x${updateInfo.vid.toString(16).toUpperCase()}`);
355
+ console.log(` Product ID: 0x${updateInfo.pid.toString(16).toUpperCase()}`);
356
+ console.log(` Software Version: ${updateInfo.softwareVersion}`);
357
+ console.log(` Software Version String: ${updateInfo.softwareVersionString}`);
358
+ console.log(` Mode: ${isProduction ? "production" : "test"}`);
359
+
360
+ // Download (copy to storage) using the existing logic
361
+ let fd: PersistedFileDesignator;
362
+ if (localFile) {
363
+ const nodeStream = createReadStream(filePath);
364
+ const webStream = Readable.toWeb(nodeStream) as ReadableStream<Uint8Array>;
365
+ fd = await theNode.otaService.store(webStream, updateInfo, isProduction);
366
+ } else {
367
+ fd = await theNode.otaService.downloadUpdate(updateInfo, isProduction);
368
+ }
369
+
370
+ console.log(`\nOTA image added to storage successfully: ${fd.text}`);
371
+ },
372
+ )
373
+ .command(
374
+ "delete [keyname]",
375
+ "Delete OTA image(s) from storage",
376
+ yargs => {
377
+ return yargs
378
+ .positional("keyname", {
379
+ describe: "Storage key name to delete",
380
+ type: "string",
381
+ })
382
+ .option("vid", {
383
+ alias: "vendor-id",
384
+ describe: "Delete by vendor ID (hex, e.g., 0xFFF1 or FFF1)",
385
+ type: "string",
386
+ conflicts: "keyname",
387
+ })
388
+ .option("pid", {
389
+ alias: "product-id",
390
+ describe: "Delete by product ID (hex, e.g., 0x8000 or 8000) - requires --vid",
391
+ type: "string",
392
+ requires: "vid",
393
+ })
394
+ .option("mode", {
395
+ describe: "Mode (prod or test) - requires --vid",
396
+ type: "string",
397
+ choices: ["prod", "test"],
398
+ default: "prod",
399
+ requires: "vid",
400
+ })
401
+ .check(argv => {
402
+ if (!argv.keyname && !argv.vid) {
403
+ throw new Error("Either keyname or --vid must be provided");
404
+ }
405
+ if (argv.pid && !argv.vid) {
406
+ throw new Error("--pid requires --vid to be specified");
407
+ }
408
+ return true;
409
+ });
410
+ },
411
+ async argv => {
412
+ const { keyname, vid, pid, mode } = argv;
413
+
414
+ if (keyname) {
415
+ // Delete by keyname
416
+ await theNode.otaService.delete({
417
+ filename: keyname,
418
+ });
419
+ console.log(`Deleted OTA image: ${keyname}`);
420
+ } else {
421
+ // Delete by vendor ID, product ID (optional), and mode
422
+ const vendorId = parseHexId(vid as string, "vendor");
423
+ const productId = pid ? parseHexId(pid, "product") : undefined;
424
+ const isProduction = mode === "prod";
425
+
426
+ const deletedCount = await theNode.otaService.delete({
427
+ vendorId,
428
+ productId,
429
+ isProduction,
430
+ });
431
+
432
+ if (productId !== undefined) {
433
+ console.log(
434
+ `Deleted OTA image for VID: 0x${vendorId.toString(16).toUpperCase()}, PID: 0x${productId.toString(16).toUpperCase()}, mode: ${mode}`,
435
+ );
436
+ } else {
437
+ console.log(
438
+ `Deleted ${deletedCount} OTA image(s) for VID: 0x${vendorId.toString(16).toUpperCase()}, mode: ${mode}`,
439
+ );
440
+ }
441
+ }
442
+ },
443
+ )
444
+ .command(
445
+ "copy <source> <target>",
446
+ "Copy OTA image from storage to filesystem",
447
+ yargs => {
448
+ return yargs
449
+ .positional("source", {
450
+ describe: "Storage key name OR vendor ID (if using --pid and --mode)",
451
+ type: "string",
452
+ demandOption: true,
453
+ })
454
+ .positional("target", {
455
+ describe: "Target filesystem path (file or directory)",
456
+ type: "string",
457
+ demandOption: true,
458
+ })
459
+ .option("pid", {
460
+ alias: "product-id",
461
+ describe: "Product ID when source is vendor ID (hex, e.g., 0x8000 or 8000)",
462
+ type: "string",
463
+ })
464
+ .option("mode", {
465
+ describe: "Mode when using vendor/product ID (prod or test)",
466
+ type: "string",
467
+ choices: ["prod", "test"],
468
+ })
469
+ .check(argv => {
470
+ if ((argv.pid || argv.mode) && !(argv.pid && argv.mode)) {
471
+ throw new Error("Both --pid and --mode must be provided together");
472
+ }
473
+ return true;
474
+ });
475
+ },
476
+ async argv => {
477
+ const { source, target, pid, mode } = argv;
478
+ const sourceArg = source;
479
+ const targetArg = target;
480
+
481
+ let keyname: string;
482
+
483
+ if (pid && mode) {
484
+ // Source is vendor ID, construct keyname
485
+ const vendorId = parseHexId(sourceArg, "vendor");
486
+ const productId = parseHexId(pid, "product");
487
+ const modeStr = mode as "prod" | "test";
488
+ keyname = `${vendorId.toString(16)}-${productId.toString(16)}-${modeStr}`;
489
+ } else {
490
+ // Source is keyname
491
+ keyname = sourceArg;
492
+ }
493
+
494
+ // Get file from storage
495
+ const fileDesignator = await theNode.otaService.fileDesignatorForUpdate(keyname);
496
+
497
+ // Determine target path
498
+ let targetPath = targetArg;
499
+ try {
500
+ const stats = statSync(targetArg);
501
+ if (stats.isDirectory()) {
502
+ // Target is a directory, use keyname as filename
503
+ targetPath = join(targetArg, keyname);
504
+ }
505
+ } catch {
506
+ // Target doesn't exist, check if parent directory exists
507
+ const parentDir = dirname(targetArg);
508
+ try {
509
+ const parentStats = statSync(parentDir);
510
+ if (parentStats.isDirectory()) {
511
+ // Parent exists and is a directory, use provided targetname
512
+ targetPath = targetArg;
513
+ } else {
514
+ console.error(`Error: Parent path is not a directory: ${parentDir}`);
515
+ process.exit(1);
516
+ }
517
+ } catch {
518
+ console.error(`Error: Parent directory does not exist: ${parentDir}`);
519
+ process.exit(1);
520
+ }
521
+ }
522
+
523
+ console.log(`Copying OTA image from storage: ${keyname}`);
524
+ console.log(`Target path: ${targetPath}`);
525
+
526
+ // Read from storage and write to filesystem
527
+ const blob = await fileDesignator.openBlob();
528
+ const reader = blob.stream().getReader();
529
+
530
+ const writeStream = createWriteStream(targetPath);
531
+ const writableStream = createWritableStream(writeStream);
532
+
533
+ const writer = writableStream.getWriter();
534
+
535
+ // Copy data
536
+ while (true) {
537
+ const { value, done } = await reader.read();
538
+ if (done) break;
539
+ await writer.write(value);
540
+ }
541
+ await writer.close();
542
+
543
+ console.log(`OTA image copied successfully to: ${targetPath}`);
544
+ },
545
+ ),
546
+ handler: async (argv: any) => {
547
+ argv.unhandled = true;
548
+ },
549
+ };
550
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Diagnostic } from "#general";
8
+ import type { Argv } from "yargs";
9
+ import { MatterNode } from "../MatterNode.js";
10
+
11
+ export default function commands(theNode: MatterNode) {
12
+ return {
13
+ command: "vendor",
14
+ describe: "Vendor information management operations",
15
+ builder: (yargs: Argv) =>
16
+ yargs
17
+ .command(
18
+ ["*", "list"],
19
+ "List all stored vendor information",
20
+ () => {},
21
+ async () => {
22
+ await theNode.start();
23
+ const vendors = [...theNode.vendorInfoService.vendors.values()];
24
+
25
+ if (vendors.length === 0) {
26
+ console.log("No vendor information found in storage.");
27
+ return;
28
+ }
29
+
30
+ console.log(`\nFound ${vendors.length} vendor(s):\n`);
31
+
32
+ // Sort vendors by ID for consistent output
33
+ vendors.sort((a, b) => a.vendorId - b.vendorId);
34
+
35
+ vendors.forEach(vendor => {
36
+ console.log(
37
+ `Vendor ID: ${vendor.vendorId} (0x${vendor.vendorId.toString(16).toUpperCase().padStart(4, "0")})`,
38
+ );
39
+ console.log(` Name: ${vendor.vendorName}`);
40
+ console.log(` Legal Name: ${vendor.companyLegalName}`);
41
+ console.log(` Preferred Name: ${vendor.companyPreferredName}`);
42
+ console.log(` Landing Page: ${vendor.vendorLandingPageUrl}`);
43
+ console.log("");
44
+ });
45
+ },
46
+ )
47
+ .command(
48
+ "get <vendor-id>",
49
+ "Display detailed information about a vendor",
50
+ yargs => {
51
+ return yargs.positional("vendor-id", {
52
+ describe: "Vendor ID (hex format like 0xFFF1 or decimal)",
53
+ type: "string",
54
+ demandOption: true,
55
+ });
56
+ },
57
+ async argv => {
58
+ const { vendorId: vendorIdStr } = argv;
59
+
60
+ let vendorId: number;
61
+ if (vendorIdStr.startsWith("0x")) {
62
+ const hexStr = vendorIdStr.replace(/^0x/i, "");
63
+ vendorId = parseInt(hexStr, 16);
64
+ } else {
65
+ vendorId = parseInt(vendorIdStr, 10);
66
+ }
67
+
68
+ if (!isFinite(vendorId)) {
69
+ console.error(`Error: Invalid vendor ID "${vendorIdStr}"`);
70
+ return;
71
+ }
72
+
73
+ await theNode.start();
74
+ const vendor = theNode.vendorInfoService.infoFor(vendorId);
75
+ if (!vendor) {
76
+ console.error(`Vendor with ID ${vendorIdStr} not found`);
77
+ return;
78
+ }
79
+
80
+ console.log("\nVendor Details:");
81
+ console.log(Diagnostic.json(vendor));
82
+ },
83
+ )
84
+ .command(
85
+ "update",
86
+ "Update vendor information from DCL",
87
+ () => {},
88
+ async () => {
89
+ await theNode.start();
90
+ console.log("Updating vendor information from DCL...");
91
+
92
+ try {
93
+ await theNode.vendorInfoService.update();
94
+ console.log(
95
+ `Successfully updated. ${theNode.vendorInfoService.vendors.size} vendor(s) now available.`,
96
+ );
97
+ } catch (error) {
98
+ console.error(
99
+ `Failed to update vendor information: ${error instanceof Error ? error.message : error}`,
100
+ );
101
+ }
102
+ },
103
+ ),
104
+ handler: async (argv: any) => {
105
+ argv.unhandled = true;
106
+ },
107
+ };
108
+ }