@matter/nodejs-shell 0.17.9 → 0.18.0-alpha.0-20260807-335c3291b

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.
Files changed (52) hide show
  1. package/README.md +11 -1
  2. package/dist/esm/MatterNode.js +237 -134
  3. package/dist/esm/MatterNode.js.map +1 -1
  4. package/dist/esm/app.js +13 -7
  5. package/dist/esm/app.js.map +1 -1
  6. package/dist/esm/shell/cmd_cluster-attributes.js +64 -45
  7. package/dist/esm/shell/cmd_cluster-attributes.js.map +2 -2
  8. package/dist/esm/shell/cmd_cluster-commands.js +11 -9
  9. package/dist/esm/shell/cmd_cluster-commands.js.map +1 -1
  10. package/dist/esm/shell/cmd_cluster-events.js +36 -9
  11. package/dist/esm/shell/cmd_cluster-events.js.map +1 -1
  12. package/dist/esm/shell/cmd_commission.js +119 -93
  13. package/dist/esm/shell/cmd_commission.js.map +1 -1
  14. package/dist/esm/shell/cmd_discover.js +23 -21
  15. package/dist/esm/shell/cmd_discover.js.map +1 -1
  16. package/dist/esm/shell/cmd_icd.js +46 -30
  17. package/dist/esm/shell/cmd_icd.js.map +1 -1
  18. package/dist/esm/shell/cmd_identify.js +11 -7
  19. package/dist/esm/shell/cmd_identify.js.map +1 -1
  20. package/dist/esm/shell/cmd_nodes.js +189 -168
  21. package/dist/esm/shell/cmd_nodes.js.map +1 -1
  22. package/dist/esm/shell/cmd_session.js +3 -4
  23. package/dist/esm/shell/cmd_session.js.map +1 -1
  24. package/dist/esm/shell/cmd_subscribe.js +26 -12
  25. package/dist/esm/shell/cmd_subscribe.js.map +1 -1
  26. package/dist/esm/util/ClusterEndpoint.js +61 -0
  27. package/dist/esm/util/ClusterEndpoint.js.map +6 -0
  28. package/dist/esm/util/awaitSeeded.js +31 -0
  29. package/dist/esm/util/awaitSeeded.js.map +6 -0
  30. package/dist/esm/util/diagnosticLogging.js +99 -0
  31. package/dist/esm/util/diagnosticLogging.js.map +6 -0
  32. package/dist/esm/util/legacyStorageMigration.js +188 -0
  33. package/dist/esm/util/legacyStorageMigration.js.map +6 -0
  34. package/package.json +10 -11
  35. package/src/MatterNode.ts +298 -159
  36. package/src/app.ts +17 -9
  37. package/src/shell/cmd_cluster-attributes.ts +77 -53
  38. package/src/shell/cmd_cluster-commands.ts +12 -13
  39. package/src/shell/cmd_cluster-events.ts +38 -11
  40. package/src/shell/cmd_commission.ts +136 -113
  41. package/src/shell/cmd_discover.ts +36 -22
  42. package/src/shell/cmd_icd.ts +52 -32
  43. package/src/shell/cmd_identify.ts +11 -7
  44. package/src/shell/cmd_nodes.ts +226 -199
  45. package/src/shell/cmd_session.ts +3 -5
  46. package/src/shell/cmd_subscribe.ts +37 -14
  47. package/src/shell/webassets/index.html +1 -1
  48. package/src/tsconfig.json +0 -3
  49. package/src/util/ClusterEndpoint.ts +107 -0
  50. package/src/util/awaitSeeded.ts +43 -0
  51. package/src/util/diagnosticLogging.ts +117 -0
  52. package/src/util/legacyStorageMigration.ts +275 -0
@@ -4,15 +4,23 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Diagnostic, Logger, MatterError } from "@matter/general";
7
+ import { Diagnostic, ImplementationError, Logger, Seconds } from "@matter/general";
8
+ import { ClientNode, CommissioningClient } from "@matter/node";
9
+ import { BasicInformationClient } from "@matter/node/behaviors/basic-information";
10
+ import { DescriptorClient } from "@matter/node/behaviors/descriptor";
8
11
  import { DiscoveryCapabilitiesSchema, ManualPairingCodeCodec, NodeId, QrCode, QrPairingCodeCodec } from "@matter/types";
9
- import { BasicInformation, Descriptor, GeneralCommissioning } from "@matter/types/clusters";
10
- import { NodeCommissioningOptions } from "@project-chip/matter.js";
12
+ import { GeneralCommissioning } from "@matter/types/clusters";
11
13
  import type { Argv } from "yargs";
12
- import { createDiagnosticCallbacks, MatterNode } from "../MatterNode.js";
14
+ import { MatterNode } from "../MatterNode.js";
15
+ import { awaitSeeded } from "../util/awaitSeeded.js";
13
16
 
14
17
  const logger = Logger.get("Commission");
15
18
 
19
+ /** Look up a commissioned peer by node id across the controller's default admin fabric. */
20
+ function findCommissionedNode(theNode: MatterNode, nodeId: NodeId): ClientNode | undefined {
21
+ return theNode.node.peers.commissioned.find(peer => peer.peerAddress?.nodeId === nodeId);
22
+ }
23
+
16
24
  export default function commands(theNode: MatterNode) {
17
25
  return {
18
26
  command: "commission",
@@ -23,9 +31,53 @@ export default function commands(theNode: MatterNode) {
23
31
  .command("pair", "Pair with a matter device", yargs => {
24
32
  return (
25
33
  yargs
34
+ // Options are registered before the command below so their types flow into its handler's argv.
35
+ .options({
36
+ pairingCode: {
37
+ describe: "pairing code",
38
+ default: undefined,
39
+ type: "string",
40
+ },
41
+ qrCode: {
42
+ describe: "QR code string (MT:...)",
43
+ default: undefined,
44
+ type: "string",
45
+ },
46
+ qrCodeIndex: {
47
+ describe: "Index of QR code entry if multiple (1..n)",
48
+ default: 1,
49
+ type: "number",
50
+ },
51
+ setupPinCode: {
52
+ describe: "setup pin code",
53
+ default: 20202021,
54
+ type: "number",
55
+ },
56
+ instanceId: {
57
+ alias: "i",
58
+ describe: "instance id",
59
+ type: "string",
60
+ },
61
+ discriminator: {
62
+ alias: "d",
63
+ description: "Long discriminator",
64
+ type: "number",
65
+ },
66
+ shortDiscriminator: {
67
+ alias: "s",
68
+ description: "Short discriminator",
69
+ type: "number",
70
+ },
71
+ ble: {
72
+ alias: "b",
73
+ description: "Also discover over BLE",
74
+ type: "boolean",
75
+ default: false,
76
+ },
77
+ })
26
78
  // Pair
27
79
  .command(
28
- "* [node-id] [ip:[port]]",
80
+ "* [node-id] [ip] [port]",
29
81
  "Commission a matter device",
30
82
  yargs => {
31
83
  return yargs
@@ -46,17 +98,16 @@ export default function commands(theNode: MatterNode) {
46
98
  });
47
99
  },
48
100
  async argv => {
49
- const {
50
- pairingCode,
51
- qrCode,
52
- nodeId: nodeIdStr,
53
- ipPort,
54
- ip,
55
- ble = false,
56
- instanceId,
57
- } = argv;
101
+ const { pairingCode, qrCode, nodeId: nodeIdStr, port, ip, ble, instanceId } = argv;
58
102
  let { setupPinCode, discriminator, shortDiscriminator, qrCodeIndex } = argv;
59
103
 
104
+ if ((ip === undefined) !== (port === undefined)) {
105
+ console.log(
106
+ "Known-address commissioning requires both <ip> and <port>; provide both or neither (to use discovery).",
107
+ );
108
+ return;
109
+ }
110
+
60
111
  if (typeof pairingCode === "string" && pairingCode.length > 0) {
61
112
  const { shortDiscriminator: pairingCodeShortDiscriminator, passcode } =
62
113
  ManualPairingCodeCodec.decode(pairingCode);
@@ -104,32 +155,6 @@ export default function commands(theNode: MatterNode) {
104
155
 
105
156
  const nodeId = nodeIdStr !== undefined ? NodeId(BigInt(nodeIdStr)) : undefined;
106
157
  await theNode.start();
107
- if (theNode.commissioningController === undefined) {
108
- throw new Error("CommissioningController not initialized");
109
- }
110
-
111
- const options = {
112
- discovery: {
113
- knownAddress:
114
- ip !== undefined && ipPort !== undefined
115
- ? { ip, port: ipPort, type: "udp" }
116
- : undefined,
117
- identifierData:
118
- instanceId !== undefined
119
- ? { instanceId }
120
- : discriminator !== undefined
121
- ? { longDiscriminator: discriminator }
122
- : shortDiscriminator !== undefined
123
- ? { shortDiscriminator }
124
- : {},
125
- discoveryCapabilities: {
126
- ble,
127
- onIpNetwork: true,
128
- },
129
- },
130
- passcode: setupPinCode,
131
- ...createDiagnosticCallbacks(),
132
- } as NodeCommissioningOptions;
133
158
 
134
159
  // Attestation policy: strict rejects errors but allows warnings/info
135
160
  const strictAttestation = await theNode.Store.get<boolean>(
@@ -137,8 +162,12 @@ export default function commands(theNode: MatterNode) {
137
162
  false,
138
163
  );
139
164
 
140
- options.commissioning = {
141
- nodeId: nodeId !== undefined ? NodeId(nodeId) : undefined,
165
+ const commissioningOptions: CommissioningClient.CommissioningOptions = {
166
+ passcode: setupPinCode,
167
+ discriminator,
168
+ nodeId,
169
+ // The shell subscribes on demand via the `subscribe` command, not at commission.
170
+ autoSubscribe: false,
142
171
  regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor, // Set to the most restrictive if relevant
143
172
  regulatoryCountryCode: "XX",
144
173
  onAttestationFailure: findings => {
@@ -156,103 +185,82 @@ export default function commands(theNode: MatterNode) {
156
185
  },
157
186
  };
158
187
 
159
- console.log(Diagnostic.json(options));
188
+ // Keep secrets out of the debug dump (CodeQL: clear-text logging of sensitive
189
+ // info). Omit the passcode entirely — spreading then overriding still flows it
190
+ // into the logged object — and mask network credentials.
191
+ const { passcode: _passcode, wifiNetwork, ...loggable } = commissioningOptions;
192
+ console.log(
193
+ Diagnostic.json({
194
+ ...loggable,
195
+ ...(wifiNetwork && {
196
+ wifiNetwork: { ...wifiNetwork, wifiCredentials: "<redacted>" },
197
+ }),
198
+ }),
199
+ );
160
200
 
161
201
  await theNode.certificateService();
162
202
 
163
203
  const wifiSsid = await theNode.Store.get<string>("WiFiSsid", "");
164
204
  const wifiCredentials = await theNode.Store.get<string>("WiFiPassword", "");
165
205
  if (wifiSsid.length > 0 && wifiCredentials.length > 0) {
166
- options.commissioning.wifiNetwork = { wifiSsid, wifiCredentials };
206
+ commissioningOptions.wifiNetwork = { wifiSsid, wifiCredentials };
167
207
  }
168
208
  const threadOperationalDataset = await theNode.Store.get<string>(
169
209
  "ThreadOperationalDataset",
170
210
  "",
171
211
  );
172
212
  if (threadOperationalDataset.length > 0) {
173
- options.commissioning.threadNetwork = {
213
+ commissioningOptions.threadNetwork = {
174
214
  networkName: await theNode.Store.get<string>("ThreadName", ""),
175
215
  operationalDataset: threadOperationalDataset,
176
216
  };
177
217
  }
178
218
 
179
- const commissionedNodeId =
180
- await theNode.commissioningController.commissionNode(options);
219
+ let node: ClientNode;
220
+ if (ip !== undefined && port !== undefined) {
221
+ // Known address: locate/create the peer node directly instead of running mDNS
222
+ // discovery. Mirrors MatterController's own known-address commissioning path.
223
+ node = await theNode.node.peers.forDescriptor({
224
+ addresses: [{ ip, port, type: "udp" }],
225
+ });
226
+ await node.commission(commissioningOptions);
227
+ } else {
228
+ const identifierData =
229
+ instanceId !== undefined
230
+ ? { instanceId }
231
+ : shortDiscriminator !== undefined
232
+ ? { shortDiscriminator }
233
+ : {};
234
+ node = await theNode.node.peers.commission({
235
+ ...identifierData,
236
+ ...commissioningOptions,
237
+ discoveryCapabilities: { ble, onIpNetwork: true },
238
+ });
239
+ }
181
240
 
182
- console.log("Commissioned Node:", commissionedNodeId);
241
+ console.log("Commissioned Node:", node.peerAddress?.nodeId.toString());
183
242
 
184
- const node = theNode.commissioningController.getPairedNode(commissionedNodeId);
185
- if (node === undefined) {
186
- // Should not happen
187
- throw new MatterError("Node not found after commissioning.");
243
+ if (!(await awaitSeeded(node))) {
244
+ return;
188
245
  }
189
246
 
190
- // Important: This is a temporary API to proof the methods working and this will change soon and is NOT stable!
191
- // It is provided to proof the concept
192
-
193
- // Example to initialize a ClusterClient and access concrete fields as API methods
194
- const descriptor = node.getRootClusterClient(Descriptor);
247
+ // Example to read cluster state directly instead of via a ClusterClient
248
+ const descriptor = node.maybeStateOf(DescriptorClient);
195
249
  if (descriptor !== undefined) {
196
- console.log(await descriptor.attributes.deviceTypeList.get()); // you can call that way
197
- console.log(await descriptor.getServerListAttribute()); // or more convenient that way
250
+ console.log(descriptor.deviceTypeList);
251
+ console.log(descriptor.serverList);
198
252
  } else {
199
253
  console.log("No Descriptor Cluster found. This should never happen!");
200
254
  }
201
255
 
202
- // Example to subscribe to a field and get the value
203
- const info = node.getRootClusterClient(BasicInformation);
256
+ const info = node.maybeStateOf(BasicInformationClient);
204
257
  if (info !== undefined) {
205
- console.log(await info.getProductNameAttribute()); // This call is executed remotely
206
- //console.log(await info.subscribeProductNameAttribute(value => console.log("productName", value), 5, 30));
207
- //console.log(await info.getProductNameAttribute()); // This call is resolved locally because we have subscribed to the value!
258
+ console.log(info.productName);
208
259
  } else {
209
260
  console.log("No BasicInformation Cluster found. This should never happen!");
210
261
  }
211
262
  },
212
263
  )
213
- .options({
214
- pairingCode: {
215
- describe: "pairing code",
216
- default: undefined,
217
- type: "string",
218
- },
219
- qrCode: {
220
- describe: "QR code string (MT:...)",
221
- default: undefined,
222
- type: "string",
223
- },
224
- qrCodeIndex: {
225
- describe: "Index of QR code entry if multiple (1..n)",
226
- default: 1,
227
- type: "number",
228
- },
229
- setupPinCode: {
230
- describe: "setup pin code",
231
- default: 20202021,
232
- type: "number",
233
- },
234
- instanceId: {
235
- alias: "i",
236
- describe: "instance id",
237
- type: "string",
238
- },
239
- discriminator: {
240
- alias: "d",
241
- description: "Long discriminator",
242
- type: "number",
243
- },
244
- shortDiscriminator: {
245
- alias: "s",
246
- description: "Short discriminator",
247
- type: "number",
248
- },
249
- ble: {
250
- alias: "b",
251
- description: "Also discover over BLE",
252
- type: "boolean",
253
- default: false,
254
- },
255
- })
256
264
  );
257
265
  })
258
266
  .command(
@@ -275,8 +283,11 @@ export default function commands(theNode: MatterNode) {
275
283
  const { nodeId, timeout } = argv;
276
284
  await theNode.start();
277
285
  const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
286
+ if (!(await awaitSeeded(node))) {
287
+ return;
288
+ }
278
289
 
279
- await node.openBasicCommissioningWindow(timeout);
290
+ await node.openBasicCommissioningWindow(Seconds(timeout));
280
291
 
281
292
  console.log(`Basic Commissioning Window for node ${nodeId} opened`);
282
293
  },
@@ -301,10 +312,14 @@ export default function commands(theNode: MatterNode) {
301
312
  await theNode.start();
302
313
  const { nodeId, timeout } = argv;
303
314
  const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
304
- const data = await node.openEnhancedCommissioningWindow(timeout);
315
+ if (!(await awaitSeeded(node))) {
316
+ return;
317
+ }
318
+ const { qrPairingCode, manualPairingCode } = await node.openEnhancedCommissioningWindow(
319
+ Seconds(timeout),
320
+ );
305
321
 
306
322
  console.log(`Enhanced Commissioning Window for node ${nodeId} opened`);
307
- const { qrPairingCode, manualPairingCode } = data;
308
323
 
309
324
  console.log(QrCode.get(qrPairingCode));
310
325
  console.log(
@@ -333,11 +348,19 @@ export default function commands(theNode: MatterNode) {
333
348
  },
334
349
  async argv => {
335
350
  await theNode.start();
336
- const { nodeId, force } = argv;
351
+ const { nodeId: nodeIdStr, force } = argv;
337
352
  if (force) {
338
- await theNode.controller.removeNode(NodeId(BigInt(nodeId)), !force);
353
+ // Force: skip the on-device decommission attempt and just drop the peer locally.
354
+ const node = findCommissionedNode(theNode, NodeId(BigInt(nodeIdStr)));
355
+ if (node === undefined) {
356
+ throw new ImplementationError(`Node ${nodeIdStr} not commissioned`);
357
+ }
358
+ await node.delete();
339
359
  } else {
340
- const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
360
+ const node = (await theNode.connectAndGetNodes(nodeIdStr, { autoSubscribe: false }))[0];
361
+ if (!(await awaitSeeded(node))) {
362
+ return;
363
+ }
341
364
  await node.decommission();
342
365
  }
343
366
  },
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Diagnostic, Seconds } from "@matter/general";
7
+ import { ChannelType, Diagnostic, Seconds } from "@matter/general";
8
8
  import { CommissionableDeviceIdentifiers } from "@matter/protocol";
9
9
  import { ManualPairingCodeCodec, VendorId } from "@matter/types";
10
10
  import type { Argv } from "yargs";
@@ -88,9 +88,6 @@ export default function commands(theNode: MatterNode) {
88
88
  }
89
89
 
90
90
  await theNode.start();
91
- if (theNode.commissioningController === undefined) {
92
- throw new Error("CommissioningController not initialized");
93
- }
94
91
 
95
92
  const identifierData: CommissionableDeviceIdentifiers =
96
93
  discriminator !== undefined
@@ -111,25 +108,42 @@ export default function commands(theNode: MatterNode) {
111
108
  )} for ${once ? "first match or " : ""}${timeoutSeconds} seconds.`,
112
109
  );
113
110
 
114
- const results = await theNode.commissioningController.discoverCommissionableDevices(
115
- identifierData,
116
- {
117
- ble,
118
- onIpNetwork: true,
119
- },
120
- device => {
121
- console.log(`Discovered device ${Diagnostic.json(device)}`);
122
- if (once) {
123
- theNode.commissioningController?.cancelCommissionableDeviceDiscovery(
124
- identifierData,
125
- { ble, onIpNetwork: true },
126
- );
127
- }
128
- },
129
- Seconds(timeoutSeconds),
130
- );
111
+ // scannerFilter mirrors the discoveryCapabilities->filter mapping in CommissioningDiscovery.
112
+ const discovery = theNode.node.peers.discover({
113
+ ...identifierData,
114
+ timeout: Seconds(timeoutSeconds),
115
+ scannerFilter: scanner =>
116
+ scanner.type === ChannelType.UDP || (ble && scanner.type === ChannelType.BLE),
117
+ });
118
+ // A re-advertising device fires `discovered` again for the same node; dedupe by its
119
+ // canonical identity so the log doesn't spam duplicate lines.
120
+ const seen = new Set<string>();
121
+ discovery.discovered.on(node => {
122
+ const { deviceIdentifier, addresses } = node.state.commissioning;
123
+ // Prefer the stable device identifier; the address fallback is sorted so re-advertisement
124
+ // in a different order does not read as a new device (ServerAddress carries no volatile fields).
125
+ const id =
126
+ deviceIdentifier ||
127
+ (addresses ?? [])
128
+ .map(a => JSON.stringify(a))
129
+ .sort()
130
+ .join("|");
131
+ if (seen.has(id)) {
132
+ return;
133
+ }
134
+ seen.add(id);
135
+ console.log(`Discovered device ${Diagnostic.json(node.state.commissioning)}`);
136
+ if (once) {
137
+ discovery.stop();
138
+ }
139
+ });
140
+
141
+ const results = await discovery;
131
142
 
132
- console.log(`Discovered ${results.length} devices`, results);
143
+ console.log(
144
+ `Discovered ${results.length} devices`,
145
+ results.map(node => node.state.commissioning),
146
+ );
133
147
  },
134
148
  ),
135
149
  handler: async (argv: any) => {
@@ -4,22 +4,26 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Duration, Millis, ObserverGroup } from "@matter/general";
8
- import { IcdClient, IcdMultiAdminError } from "@matter/node";
7
+ import { Duration, ImplementationError, Millis, ObserverGroup } from "@matter/general";
8
+ import { ClientNode, IcdClient, IcdMultiAdminError, NodeConnectionState } from "@matter/node";
9
9
  import { IcdManagementClient } from "@matter/node/behaviors/icd-management";
10
10
  import { NodeId, SubjectId, VendorId } from "@matter/types";
11
11
  import { IcdManagement } from "@matter/types/clusters/icd-management";
12
- import { NodeStates, PairedNode } from "@project-chip/matter.js/device";
13
12
  import type { Argv } from "yargs";
14
13
  import { MatterNode } from "../MatterNode.js";
14
+ import { awaitSeeded } from "../util/awaitSeeded.js";
15
15
 
16
16
  const watchers = new Map<string, ObserverGroup>();
17
17
 
18
- async function printStatus(paired: PairedNode) {
19
- const clientNode = paired.node;
18
+ /** Look up a commissioned peer by node id without connecting or awaiting remote initialization. */
19
+ function findCommissionedNode(theNode: MatterNode, nodeId: NodeId): ClientNode | undefined {
20
+ return theNode.node.peers.commissioned.find(peer => peer.peerAddress?.nodeId === nodeId);
21
+ }
22
+
23
+ async function printStatus(nodeId: NodeId, clientNode: ClientNode) {
20
24
  if (!clientNode.behaviors.has(IcdClient)) {
21
- if (!paired.initialized) {
22
- console.log(`node ${paired.nodeId}: uninitialized (no cached data yet)`);
25
+ if (!clientNode.lifecycle.isSeeded) {
26
+ console.log(`node ${nodeId}: uninitialized (no cached data yet)`);
23
27
  }
24
28
  return;
25
29
  }
@@ -31,10 +35,10 @@ async function printStatus(paired: PairedNode) {
31
35
  mgmt === undefined
32
36
  ? "n/a"
33
37
  : (IcdManagement.OperatingMode[mgmt.operatingMode ?? -1] ?? mgmt.operatingMode ?? "unknown");
34
- const marker = paired.initialized ? "" : " [uninitialized]";
38
+ const marker = clientNode.lifecycle.isSeeded ? "" : " [uninitialized]";
35
39
  console.log(
36
40
  [
37
- `node ${paired.nodeId}:${marker}`,
41
+ `node ${nodeId}:${marker}`,
38
42
  ` registered=${icd.registered}`,
39
43
  ` available=${icd.available}`,
40
44
  ` awake=${awake}`,
@@ -58,7 +62,12 @@ function selectsAll(nodeIdStr: string) {
58
62
  /** Resolve a node-id argument to concrete ids: `all` expands to every commissioned node, otherwise a single id. */
59
63
  async function targetNodeIds(theNode: MatterNode, nodeIdStr: string): Promise<NodeId[]> {
60
64
  await theNode.start();
61
- return selectsAll(nodeIdStr) ? theNode.controller.getCommissionedNodes() : [NodeId(BigInt(nodeIdStr))];
65
+ if (!selectsAll(nodeIdStr)) {
66
+ return [NodeId(BigInt(nodeIdStr))];
67
+ }
68
+ return theNode.node.peers.commissioned
69
+ .map(peer => peer.peerAddress?.nodeId)
70
+ .filter((nodeId): nodeId is NodeId => nodeId !== undefined);
62
71
  }
63
72
 
64
73
  /**
@@ -84,19 +93,20 @@ async function eachNode(theNode: MatterNode, nodeIdStr: string, action: (nodeId:
84
93
  }
85
94
  }
86
95
 
87
- async function pairedNodeFor(theNode: MatterNode, nodeId: NodeId) {
88
- const paired = (await theNode.connectAndGetNodes(String(nodeId)))[0];
89
- if (paired === undefined) {
90
- throw new Error(`Node ${nodeId} not found / not connected`);
96
+ /** Connect to `nodeId` and verify it exposes IcdManagement, throwing an {@link ImplementationError} otherwise. */
97
+ async function connectedIcdNode(theNode: MatterNode, nodeId: NodeId): Promise<ClientNode> {
98
+ const clientNode = (await theNode.connectAndGetNodes(String(nodeId)))[0];
99
+ if (clientNode === undefined) {
100
+ throw new ImplementationError(`Node ${nodeId} not found / not connected`);
91
101
  }
92
- if (!paired.node.behaviors.has(IcdClient)) {
93
- throw new Error(`Node ${nodeId} is not an ICD device (no IcdManagement cluster)`);
102
+ // Behaviors populate as part of seeding; await it so a not-yet-seeded peer isn't misread as non-ICD.
103
+ if (!(await awaitSeeded(clientNode, { quiet: true }))) {
104
+ throw new ImplementationError(`Node ${nodeId} did not become ready (offline?)`);
94
105
  }
95
- return paired;
96
- }
97
-
98
- async function clientNodeFor(theNode: MatterNode, nodeId: NodeId) {
99
- return (await pairedNodeFor(theNode, nodeId)).node;
106
+ if (!clientNode.behaviors.has(IcdClient)) {
107
+ throw new ImplementationError(`Node ${nodeId} is not an ICD device (no IcdManagement cluster)`);
108
+ }
109
+ return clientNode;
100
110
  }
101
111
 
102
112
  function stopWatch(nodeIdStr: string) {
@@ -120,8 +130,7 @@ async function startWatch(theNode: MatterNode, nodeId: NodeId) {
120
130
  watchers.get(key)?.close();
121
131
  watchers.delete(key);
122
132
 
123
- const paired = await pairedNodeFor(theNode, nodeId);
124
- const clientNode = paired.node;
133
+ const clientNode = await connectedIcdNode(theNode, nodeId);
125
134
  const observers = new ObserverGroup();
126
135
  const stamp = (msg: string) => console.log(`[icd ${nodeId}] ${msg}`);
127
136
  // Event emitters discard emit()'s promise, so a rejection from this async read must not escape.
@@ -145,10 +154,12 @@ async function startWatch(theNode: MatterNode, nodeId: NodeId) {
145
154
  observers.on(events.checkInMissed, async () => stamp(`check-in MISSED ${await wakefulnessSuffix()}`));
146
155
  observers.on(events.keyRefreshed, () => stamp("key refreshed"));
147
156
  observers.on(events.available$Changed, (value: boolean) => stamp(`available=${value}`));
148
- observers.on(paired.events.stateChanged, state => stamp(`connection=${NodeStates[state] ?? state}`));
157
+ observers.on(clientNode.lifecycle.connectionStateChanged, state =>
158
+ stamp(`connection=${NodeConnectionState[state] ?? state}`),
159
+ );
149
160
  watchers.set(key, observers);
150
161
  // Drop the watcher when the node goes away so it doesn't leak in the map.
151
- clientNode.lifecycle.destroyed.once(() => {
162
+ observers.on(clientNode.lifecycle.destroyed, () => {
152
163
  watchers.get(key)?.close();
153
164
  watchers.delete(key);
154
165
  });
@@ -198,7 +209,7 @@ export default function commands(theNode: MatterNode) {
198
209
  ignoredVendors: argv.ignoreVendor?.map((v: string | number) => VendorId(Number(v))),
199
210
  };
200
211
  await eachNode(theNode, argv.nodeId, async nodeId => {
201
- const clientNode = await clientNodeFor(theNode, nodeId);
212
+ const clientNode = await connectedIcdNode(theNode, nodeId);
202
213
  try {
203
214
  await clientNode.act(agent => agent.get(IcdClient).register(options));
204
215
  console.log(`Registered as Check-In client on node ${nodeId}`);
@@ -235,9 +246,14 @@ export default function commands(theNode: MatterNode) {
235
246
  if (argv.force) {
236
247
  // A registered-but-unreachable LIT peer parks every connecting/peer-I/O path on its
237
248
  // never-coming Check-In; reach it via the non-connecting accessor and forget() locally.
238
- const clientNode = (await theNode.controller.getNode(nodeId)).node;
249
+ const clientNode = findCommissionedNode(theNode, nodeId);
250
+ if (clientNode === undefined) {
251
+ throw new ImplementationError(`Node ${nodeId} is not commissioned`);
252
+ }
239
253
  if (!clientNode.behaviors.has(IcdClient)) {
240
- throw new Error(`Node ${nodeId} is not an ICD device (no IcdManagement cluster)`);
254
+ throw new ImplementationError(
255
+ `Node ${nodeId} is not an ICD device (no IcdManagement cluster)`,
256
+ );
241
257
  }
242
258
  await clientNode.act(agent => agent.get(IcdClient).forget());
243
259
  console.log(
@@ -245,7 +261,7 @@ export default function commands(theNode: MatterNode) {
245
261
  );
246
262
  return;
247
263
  }
248
- const clientNode = await clientNodeFor(theNode, nodeId);
264
+ const clientNode = await connectedIcdNode(theNode, nodeId);
249
265
  await clientNode.act(agent => agent.get(IcdClient).unregister());
250
266
  console.log(`Unregistered Check-In client on node ${nodeId}`);
251
267
  });
@@ -269,7 +285,7 @@ export default function commands(theNode: MatterNode) {
269
285
  }),
270
286
  handler: async (argv: any) => {
271
287
  await eachNode(theNode, argv.nodeId, async nodeId => {
272
- const clientNode = await clientNodeFor(theNode, nodeId);
288
+ const clientNode = await connectedIcdNode(theNode, nodeId);
273
289
  const promised = await clientNode.act(agent =>
274
290
  agent.get(IcdClient).stayActive(Millis(argv.durationMs)),
275
291
  );
@@ -283,7 +299,7 @@ export default function commands(theNode: MatterNode) {
283
299
  builder: (y: Argv) =>
284
300
  y.positional("node-id", { describe: "node id, or 'all'", type: "string", demandOption: true }),
285
301
  handler: async (argv: any) => {
286
- // getNode() never awaits remote init, unlike connectAndGetNodes(), so a peer stuck mid-read can't hang this command.
302
+ // findCommissionedNode never connects/awaits remote init, so a peer stuck mid-read can't hang this command.
287
303
  const nodeIds = await targetNodeIds(theNode, argv.nodeId);
288
304
  if (nodeIds.length === 0) {
289
305
  console.log("No commissioned nodes.");
@@ -292,7 +308,11 @@ export default function commands(theNode: MatterNode) {
292
308
  // Diagnostic command: report an unreachable node inline rather than aborting, even for a single id.
293
309
  for (const nodeId of nodeIds) {
294
310
  try {
295
- await printStatus(await theNode.controller.getNode(nodeId));
311
+ const clientNode = findCommissionedNode(theNode, nodeId);
312
+ if (clientNode === undefined) {
313
+ throw new ImplementationError(`Node ${nodeId} is not commissioned`);
314
+ }
315
+ await printStatus(nodeId, clientNode);
296
316
  } catch (e) {
297
317
  console.log(
298
318
  `node ${nodeId}: unavailable (${e instanceof Error ? e.message : String(e)})`,