tirtc-devtools-cli 0.0.6 → 0.0.8

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 (37) hide show
  1. package/README.md +8 -1
  2. package/USAGE.md +45 -7
  3. package/dist/devtools/cli/src/config.js +2 -2
  4. package/dist/devtools/cli/src/facade.d.ts +19 -3
  5. package/dist/devtools/cli/src/facade.js +12 -3
  6. package/dist/devtools/cli/src/guide.js +1 -1
  7. package/dist/devtools/cli/src/index.js +68 -23
  8. package/dist/devtools/cli/src/progress.d.ts +19 -0
  9. package/dist/devtools/cli/src/progress.js +63 -0
  10. package/package.json +1 -1
  11. package/vendor/app-server/bin/native/macos-arm64/runtime_host_napi.node +0 -0
  12. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/error.h +1 -0
  13. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/transport.h +2 -0
  14. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_audio.a +0 -0
  15. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_credential.a +0 -0
  16. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_facade.a +0 -0
  17. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_http.a +0 -0
  18. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_logging.a +0 -0
  19. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_media.a +0 -0
  20. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_transport.a +0 -0
  21. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_video.a +0 -0
  22. package/vendor/app-server/bin/runtime/macos-arm64/manifest.txt +11 -11
  23. package/vendor/app-server/dist/host/ArtifactManager.js +17 -2
  24. package/vendor/app-server/dist/host/HostProtocol.d.ts +7 -3
  25. package/vendor/app-server/dist/host/HostProtocol.js +9 -3
  26. package/vendor/app-server/dist/host/HostServer.d.ts +6 -0
  27. package/vendor/app-server/dist/host/HostServer.js +172 -2
  28. package/vendor/app-server/dist/host/HostState.d.ts +14 -0
  29. package/vendor/app-server/dist/host/HostState.js +1 -0
  30. package/vendor/app-server/dist/host/RuntimeAdapter.d.ts +4 -2
  31. package/vendor/app-server/dist/host/RuntimeAdapter.js +51 -13
  32. package/vendor/app-server/dist/host/RuntimeSendWorker.d.ts +12 -0
  33. package/vendor/app-server/dist/host/RuntimeSendWorker.js +147 -0
  34. package/vendor/app-server/dist/host/native/RuntimeHostBridge.d.ts +13 -0
  35. package/vendor/app-server/dist/host/native/RuntimeHostBridge.js +16 -0
  36. package/vendor/app-server/dist/protocol/contract.d.ts +158 -12
  37. package/vendor/app-server/dist/protocol/contract.js +30 -5
@@ -64,6 +64,39 @@ function resolveLocalAssetsDir(source) {
64
64
  }
65
65
  return assetsDir;
66
66
  }
67
+ function normalizeCommandPayload(payload) {
68
+ if (typeof payload === 'string') {
69
+ return {
70
+ payloadEncoding: 'utf8',
71
+ payload,
72
+ };
73
+ }
74
+ if (payload instanceof Uint8Array) {
75
+ return {
76
+ payloadEncoding: 'base64',
77
+ payload: Buffer.from(payload).toString('base64'),
78
+ };
79
+ }
80
+ if (Array.isArray(payload) && payload.every((value) => typeof value === 'number')) {
81
+ return {
82
+ payloadEncoding: 'base64',
83
+ payload: Buffer.from(payload).toString('base64'),
84
+ };
85
+ }
86
+ if (typeof payload === 'object' && payload !== null && 'type' in payload && payload.type === 'Buffer') {
87
+ const data = payload.data;
88
+ if (Array.isArray(data) && data.every((value) => typeof value === 'number')) {
89
+ return {
90
+ payloadEncoding: 'base64',
91
+ payload: Buffer.from(data).toString('base64'),
92
+ };
93
+ }
94
+ }
95
+ return {
96
+ payloadEncoding: 'utf8',
97
+ payload: '',
98
+ };
99
+ }
67
100
  class HostServer {
68
101
  state;
69
102
  artifactManager;
@@ -175,6 +208,19 @@ class HostServer {
175
208
  if (family !== 'runtime') {
176
209
  return;
177
210
  }
211
+ if (kind === 'connection.accepted') {
212
+ this.noteIncomingConnection(payload);
213
+ this.activatePendingStreamsOnConnected();
214
+ return;
215
+ }
216
+ if (kind === 'connection.disconnected') {
217
+ this.handleIncomingConnectionDisconnected(payload);
218
+ return;
219
+ }
220
+ if (kind === 'command.remote.requested') {
221
+ this.handleInboundRemoteCommandRequest(payload);
222
+ return;
223
+ }
178
224
  if (kind === 'transport.remote.request_audio' ||
179
225
  kind === 'transport.remote.request_video' ||
180
226
  kind === 'transport.remote.request_iframe') {
@@ -187,6 +233,53 @@ class HostServer {
187
233
  this.maybeAutoStartStreamFromRemoteDemand(streamId, media, payload);
188
234
  }
189
235
  }
236
+ snapshotPendingConnectionSummary() {
237
+ return {
238
+ state: this.state.connection.state,
239
+ direction: this.state.connection.direction,
240
+ peerId: this.state.connection.peerId,
241
+ };
242
+ }
243
+ handleInboundRemoteCommandRequest(payload) {
244
+ const remoteRequestId = Number(payload.remoteRequestId);
245
+ const commandId = Number(payload.commandId);
246
+ if (!Number.isInteger(remoteRequestId) || remoteRequestId <= 0 || !Number.isInteger(commandId)) {
247
+ return;
248
+ }
249
+ this.noteIncomingConnection(payload);
250
+ const normalizedPayload = normalizeCommandPayload(payload.payload);
251
+ const entry = {
252
+ remoteRequestId,
253
+ commandId,
254
+ payloadEncoding: normalizedPayload.payloadEncoding,
255
+ payload: normalizedPayload.payload,
256
+ receivedAt: new Date().toISOString(),
257
+ connection: this.snapshotPendingConnectionSummary(),
258
+ };
259
+ this.state.pendingRemoteCommands.set(remoteRequestId, entry);
260
+ this.writeEvent('command', 'command.remote.requested', {
261
+ ...entry,
262
+ });
263
+ }
264
+ clearPendingRemoteCommands(reason) {
265
+ if (this.state.pendingRemoteCommands.size === 0) {
266
+ return;
267
+ }
268
+ const clearedAt = new Date().toISOString();
269
+ for (const entry of this.state.pendingRemoteCommands.values()) {
270
+ this.writeEvent('command', 'command.remote.cleared', {
271
+ remoteRequestId: entry.remoteRequestId,
272
+ commandId: entry.commandId,
273
+ payloadEncoding: entry.payloadEncoding,
274
+ payload: entry.payload,
275
+ receivedAt: entry.receivedAt,
276
+ connection: entry.connection,
277
+ reason,
278
+ clearedAt,
279
+ });
280
+ }
281
+ this.state.pendingRemoteCommands.clear();
282
+ }
190
283
  noteIncomingConnection(payload) {
191
284
  if (this.state.connection.state === 'connected') {
192
285
  return;
@@ -198,6 +291,39 @@ class HostServer {
198
291
  connectedAt: new Date().toISOString(),
199
292
  };
200
293
  }
294
+ handleIncomingConnectionDisconnected(payload) {
295
+ if (this.state.connection.state === 'idle' || this.state.connection.state === 'disconnected') {
296
+ return;
297
+ }
298
+ this.clearPendingRemoteCommands('connection_disconnect');
299
+ const disconnectedAt = new Date().toISOString();
300
+ this.state.connection = {
301
+ state: 'disconnected',
302
+ direction: 'incoming',
303
+ peerId: this.state.connection.peerId,
304
+ connectedAt: this.state.connection.connectedAt,
305
+ disconnectedAt,
306
+ reasonCode: typeof payload.reasonCode === 'string' && payload.reasonCode.length > 0
307
+ ? payload.reasonCode
308
+ : undefined,
309
+ };
310
+ for (const stream of this.state.streams.values()) {
311
+ if (stream.direction !== 'send') {
312
+ continue;
313
+ }
314
+ if (stream.state !== 'active') {
315
+ continue;
316
+ }
317
+ stream.state = 'pending_on_connection';
318
+ stream.updatedAt = disconnectedAt;
319
+ this.writeEvent('stream', 'state.pending_on_reconnect', {
320
+ streamId: stream.streamId,
321
+ media: stream.media,
322
+ direction: stream.direction,
323
+ at: disconnectedAt,
324
+ });
325
+ }
326
+ }
201
327
  maybeAutoStartStreamFromRemoteDemand(streamId, media, payload) {
202
328
  const key = streamBindingKey(streamId, media);
203
329
  const bound = this.state.streamSourceBindings.get(key);
@@ -270,8 +396,13 @@ class HostServer {
270
396
  return this.handleOutputAttach((0, HostProtocol_1.parseOutputAttachParams)(params));
271
397
  case 'output/detach':
272
398
  return this.handleOutputDetach((0, HostProtocol_1.parseOutputDetachParams)(params));
399
+ case 'command/request':
273
400
  case 'command/send':
274
401
  return this.handleCommandSend((0, HostProtocol_1.parseCommandSendParams)(params));
402
+ case 'command/reply':
403
+ return this.handleCommandReply((0, HostProtocol_1.parseCommandReplyParams)(params));
404
+ case 'command/pending/list':
405
+ return this.handleCommandPendingList();
275
406
  case 'events/subscribe':
276
407
  return this.handleEventsSubscribe((0, HostProtocol_1.parseEventsSubscribeParams)(params));
277
408
  case 'artifact/readReport':
@@ -311,6 +442,7 @@ class HostServer {
311
442
  };
312
443
  }
313
444
  async handleHostShutdown() {
445
+ this.clearPendingRemoteCommands('host_shutdown');
314
446
  return {
315
447
  acceptedAt: new Date().toISOString(),
316
448
  service: this.state.service,
@@ -381,6 +513,7 @@ class HostServer {
381
513
  };
382
514
  }
383
515
  await this.runtimeAdapter.stopService();
516
+ this.clearPendingRemoteCommands('service_stop');
384
517
  this.state.service.state = 'stopped';
385
518
  this.state.service.stoppedAt = new Date().toISOString();
386
519
  return {
@@ -507,6 +640,7 @@ class HostServer {
507
640
  };
508
641
  }
509
642
  await this.runtimeAdapter.disconnect();
643
+ this.clearPendingRemoteCommands('connection_disconnect');
510
644
  this.state.connection.state = 'disconnected';
511
645
  this.state.connection.disconnectedAt = new Date().toISOString();
512
646
  return {
@@ -631,15 +765,51 @@ class HostServer {
631
765
  };
632
766
  }
633
767
  async handleCommandSend(params) {
634
- const response = await this.runtimeAdapter.sendCommand(params.commandId, params.kind, params.payloadEncoding, params.payload, params.replyToSequenceNumber, params.awaitResponse, params.timeoutMs);
768
+ const response = await this.runtimeAdapter.sendCommand(params.commandId, params.payloadEncoding, params.payload, params.timeoutMs);
635
769
  return {
636
770
  sequenceNumber: this.commandSequence++,
637
771
  commandId: params.commandId,
638
- kind: params.kind,
639
772
  acceptedAt: new Date().toISOString(),
640
773
  response,
641
774
  };
642
775
  }
776
+ async handleCommandReply(params) {
777
+ const entry = this.state.pendingRemoteCommands.get(params.remoteRequestId);
778
+ if (!entry) {
779
+ throw new HostProtocol_1.HostProtocolError('invalid_request', `remoteRequestId ${params.remoteRequestId} is not pending`, false);
780
+ }
781
+ if (entry.commandId !== params.commandId) {
782
+ throw new HostProtocol_1.HostProtocolError('invalid_request', 'commandId does not match pending entry', false);
783
+ }
784
+ if (this.state.connection.state !== 'connected') {
785
+ throw new HostProtocol_1.HostProtocolError('connection_not_ready', 'connection is not established', true);
786
+ }
787
+ await this.runtimeAdapter.replyRemoteCommand(params.remoteRequestId, params.commandId, params.payloadEncoding, params.payload);
788
+ this.state.pendingRemoteCommands.delete(params.remoteRequestId);
789
+ const repliedAt = new Date().toISOString();
790
+ this.writeEvent('command', 'command.remote.replied', {
791
+ remoteRequestId: params.remoteRequestId,
792
+ commandId: params.commandId,
793
+ payloadEncoding: entry.payloadEncoding,
794
+ payload: entry.payload,
795
+ receivedAt: entry.receivedAt,
796
+ connection: entry.connection,
797
+ repliedAt,
798
+ });
799
+ return {
800
+ remoteRequestId: params.remoteRequestId,
801
+ commandId: params.commandId,
802
+ repliedAt,
803
+ };
804
+ }
805
+ async handleCommandPendingList() {
806
+ const items = Array.from(this.state.pendingRemoteCommands.values())
807
+ .sort((left, right) => left.receivedAt.localeCompare(right.receivedAt));
808
+ return {
809
+ items,
810
+ listedAt: new Date().toISOString(),
811
+ };
812
+ }
643
813
  async handleEventsSubscribe(params) {
644
814
  const families = params.families ?? [];
645
815
  for (const family of families) {
@@ -35,6 +35,19 @@ export interface StreamSourceBinding {
35
35
  source: StreamSource;
36
36
  updatedAt: string;
37
37
  }
38
+ export interface CommandPendingConnectionSummary {
39
+ state: 'idle' | 'connecting' | 'accepting' | 'connected' | 'disconnecting' | 'disconnected' | 'failed' | 'missed';
40
+ direction?: 'incoming' | 'outgoing';
41
+ peerId?: string;
42
+ }
43
+ export interface CommandPendingEntry {
44
+ remoteRequestId: number;
45
+ commandId: number;
46
+ payloadEncoding: 'utf8' | 'base64';
47
+ payload: string;
48
+ receivedAt: string;
49
+ connection: CommandPendingConnectionSummary;
50
+ }
38
51
  export declare class HostState {
39
52
  service: ServiceSnapshot;
40
53
  connection: ConnectionSnapshot;
@@ -42,6 +55,7 @@ export declare class HostState {
42
55
  subscriptions: Set<string>;
43
56
  internalConnections: Set<string>;
44
57
  streamSourceBindings: Map<string, StreamSourceBinding>;
58
+ pendingRemoteCommands: Map<number, CommandPendingEntry>;
45
59
  protocolVersion: string;
46
60
  serverVersion: string;
47
61
  runtimeTarget: string;
@@ -8,6 +8,7 @@ class HostState {
8
8
  subscriptions = new Set();
9
9
  internalConnections = new Set();
10
10
  streamSourceBindings = new Map();
11
+ pendingRemoteCommands = new Map();
11
12
  protocolVersion = '1.0.0';
12
13
  serverVersion = '1.0.0';
13
14
  runtimeTarget = 'runtime-backed/unknown';
@@ -39,7 +39,8 @@ export interface RuntimeAdapter {
39
39
  stopReceiveStream(streamId: number): Promise<void>;
40
40
  attachOutput(streamId: number, consumer: string, mediaView: string, format: string, delivery: string, targetPath?: string, maxFiles?: number): Promise<OutputAttachResult>;
41
41
  detachOutput(outputId: string): Promise<void>;
42
- sendCommand(commandId: number, kind: string, payloadEncoding: string, payload: string, replyToSequenceNumber?: number, awaitResponse?: boolean, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
42
+ sendCommand(commandId: number, payloadEncoding: string, payload: string, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
43
+ replyRemoteCommand(remoteRequestId: number, commandId: number, payloadEncoding: string, payload: string): Promise<void>;
43
44
  }
44
45
  type RuntimeBackedAdapterOptions = {
45
46
  runtimeBundleRoot: string;
@@ -88,6 +89,7 @@ export declare class RuntimeBackedAdapter implements RuntimeAdapter {
88
89
  stopReceiveStream(streamId: number): Promise<void>;
89
90
  attachOutput(streamId: number, consumer: string, mediaView: string, format: string, delivery: string, targetPath?: string, maxFiles?: number): Promise<OutputAttachResult>;
90
91
  detachOutput(outputId: string): Promise<void>;
91
- sendCommand(commandId: number, kind: string, payloadEncoding: string, payload: string, replyToSequenceNumber?: number, awaitResponse?: boolean, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
92
+ sendCommand(commandId: number, payloadEncoding: string, payload: string, timeoutMs?: number): Promise<RuntimeCommandResponse | undefined>;
93
+ replyRemoteCommand(remoteRequestId: number, commandId: number, payloadEncoding: string, payload: string): Promise<void>;
92
94
  }
93
95
  export {};
@@ -133,6 +133,26 @@ function resolveStreamAssetsDir(source) {
133
133
  }
134
134
  return assetsDir;
135
135
  }
136
+ function decodePayload(payloadEncoding, payload) {
137
+ if (payloadEncoding === 'base64') {
138
+ return Uint8Array.from(Buffer.from(payload, 'base64'));
139
+ }
140
+ return Uint8Array.from(Buffer.from(payload, 'utf8'));
141
+ }
142
+ function encodePayload(payload) {
143
+ const buffer = Buffer.from(payload);
144
+ const utf8Text = buffer.toString('utf8');
145
+ if (Buffer.from(utf8Text, 'utf8').equals(buffer)) {
146
+ return {
147
+ payloadEncoding: 'utf8',
148
+ payload: utf8Text,
149
+ };
150
+ }
151
+ return {
152
+ payloadEncoding: 'base64',
153
+ payload: buffer.toString('base64'),
154
+ };
155
+ }
136
156
  class RuntimeBackedAdapter {
137
157
  runtimeTarget;
138
158
  hostEndpoint = 'stdio://runtime-backed';
@@ -163,6 +183,15 @@ class RuntimeBackedAdapter {
163
183
  onLogLine: (payload) => {
164
184
  this.emit('logs', 'runtime.line', payload);
165
185
  },
186
+ onRuntimeEvent: (kind, payload) => {
187
+ if (kind === 'connection.accepted') {
188
+ this.connected = true;
189
+ }
190
+ else if (kind === 'connection.disconnected') {
191
+ this.connected = false;
192
+ }
193
+ this.emit('runtime', kind, payload);
194
+ },
166
195
  });
167
196
  this.receiveWorker = new RuntimeReceiveWorker_1.RuntimeReceiveWorkerManager({
168
197
  repoRoot: this.repoRoot,
@@ -262,6 +291,7 @@ class RuntimeBackedAdapter {
262
291
  async startService(serviceEntry, license, timeoutMs) {
263
292
  this.assertRuntimeBundleReady();
264
293
  const resolvedServiceEntry = this.resolveServiceEntry(serviceEntry);
294
+ const keepaliveSession = await this.sendWorker.ensureServiceSession(resolvedServiceEntry, license);
265
295
  this.activeService = {
266
296
  serviceEntry: resolvedServiceEntry,
267
297
  license,
@@ -271,7 +301,8 @@ class RuntimeBackedAdapter {
271
301
  serviceEntry: resolvedServiceEntry,
272
302
  licensePresent: license.length > 0,
273
303
  timeoutMs,
274
- senderBootstrapStarted: false,
304
+ senderBootstrapStarted: keepaliveSession.started,
305
+ workerPid: keepaliveSession.pid,
275
306
  at: nowIsoString(),
276
307
  });
277
308
  return resolvedServiceEntry;
@@ -404,6 +435,7 @@ class RuntimeBackedAdapter {
404
435
  if (!this.connected) {
405
436
  throw new Error('connection is not established');
406
437
  }
438
+ await this.sendWorker.sendStreamMessage(streamId, decodePayload('utf8', payload));
407
439
  this.emit('stream', 'message.runtime.send', {
408
440
  streamId,
409
441
  payloadPreview: payload.slice(0, 120),
@@ -567,32 +599,38 @@ class RuntimeBackedAdapter {
567
599
  await this.refreshReceiveWorker();
568
600
  this.emit('stream', 'output.detach.requested', { outputId, at: nowIsoString() });
569
601
  }
570
- async sendCommand(commandId, kind, payloadEncoding, payload, replyToSequenceNumber, awaitResponse, timeoutMs) {
602
+ async sendCommand(commandId, payloadEncoding, payload, timeoutMs) {
571
603
  if (!this.connected) {
572
604
  throw new Error('connection is not established');
573
605
  }
574
606
  this.emit('command', 'send.requested', {
575
607
  commandId,
576
- kind,
577
608
  payloadEncoding,
578
- replyToSequenceNumber,
579
- awaitResponse: Boolean(awaitResponse),
580
609
  timeoutMs,
581
610
  at: nowIsoString(),
582
611
  });
583
- if (!awaitResponse) {
584
- return undefined;
585
- }
586
612
  const sequenceNumber = this.commandSequence;
587
613
  this.commandSequence += 1;
614
+ const response = await this.sendWorker.requestCommand(commandId, decodePayload(payloadEncoding, payload), timeoutMs ?? 15000);
615
+ const encodedResponse = encodePayload(response.payload);
588
616
  return {
589
617
  sequenceNumber,
590
- payloadEncoding: 'runtime-evidence',
591
- payload: JSON.stringify({
592
- evidence: 'runtime-backed-callbacks',
593
- commandRxCount: 1,
594
- }),
618
+ payloadEncoding: encodedResponse.payloadEncoding,
619
+ payload: encodedResponse.payload,
595
620
  };
596
621
  }
622
+ async replyRemoteCommand(remoteRequestId, commandId, payloadEncoding, payload) {
623
+ if (!this.connected) {
624
+ throw new Error('connection is not established');
625
+ }
626
+ this.emit('command', 'reply.requested', {
627
+ remoteRequestId,
628
+ commandId,
629
+ payloadEncoding,
630
+ payloadLength: payload.length,
631
+ at: nowIsoString(),
632
+ });
633
+ await this.sendWorker.replyRemoteCommand(remoteRequestId, commandId, decodePayload(payloadEncoding, payload));
634
+ }
597
635
  }
598
636
  exports.RuntimeBackedAdapter = RuntimeBackedAdapter;
@@ -7,6 +7,7 @@ type SendWorkerManagerOptions = {
7
7
  line: string;
8
8
  pid?: number;
9
9
  }) => void;
10
+ onRuntimeEvent?: (kind: string, payload: Record<string, unknown>) => void;
10
11
  };
11
12
  type SendWorkerStartOptions = {
12
13
  key: string;
@@ -20,9 +21,14 @@ export declare class RuntimeSendWorkerManager {
20
21
  private readonly repoRoot;
21
22
  private readonly bindings;
22
23
  private readonly onLogLine?;
24
+ private readonly onRuntimeEvent?;
23
25
  private readonly bridge;
24
26
  private sharedSession?;
27
+ private nextCommandRequestId;
28
+ private readonly pendingCommandRequests;
25
29
  constructor(options: SendWorkerManagerOptions);
30
+ private handleNativeEvent;
31
+ private rejectPendingCommandRequests;
26
32
  private emitLog;
27
33
  private resolveFixtureAssetsDir;
28
34
  private buildDesiredConfig;
@@ -42,5 +48,11 @@ export declare class RuntimeSendWorkerManager {
42
48
  }>;
43
49
  stopStream(key: string): Promise<void>;
44
50
  stopAll(): Promise<void>;
51
+ sendStreamMessage(streamId: number, payload: Uint8Array): Promise<void>;
52
+ requestCommand(commandId: number, payload: Uint8Array, timeoutMs: number): Promise<{
53
+ commandId: number;
54
+ payload: Uint8Array;
55
+ }>;
56
+ replyRemoteCommand(remoteRequestId: number, commandId: number, payload: Uint8Array): Promise<void>;
45
57
  }
46
58
  export {};
@@ -43,6 +43,25 @@ function pathExists(filePath) {
43
43
  function nowIsoString() {
44
44
  return new Date().toISOString();
45
45
  }
46
+ function toSafeInteger(value) {
47
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
48
+ return value;
49
+ }
50
+ if (typeof value === 'bigint') {
51
+ const normalized = Number(value);
52
+ if (Number.isSafeInteger(normalized)) {
53
+ return normalized;
54
+ }
55
+ return undefined;
56
+ }
57
+ if (typeof value === 'string' && value.trim().length > 0) {
58
+ const normalized = Number(value);
59
+ if (Number.isSafeInteger(normalized)) {
60
+ return normalized;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
46
65
  function ensureValidAssetsDir(assetsDir) {
47
66
  const resolved = path.resolve(assetsDir);
48
67
  let stats;
@@ -125,15 +144,90 @@ class RuntimeSendWorkerManager {
125
144
  repoRoot;
126
145
  bindings = new Map();
127
146
  onLogLine;
147
+ onRuntimeEvent;
128
148
  bridge;
129
149
  sharedSession;
150
+ nextCommandRequestId = 1;
151
+ pendingCommandRequests = new Map();
130
152
  constructor(options) {
131
153
  this.repoRoot = options.repoRoot;
132
154
  this.onLogLine = options.onLogLine;
155
+ this.onRuntimeEvent = options.onRuntimeEvent;
133
156
  this.bridge = new RuntimeHostBridge_1.RuntimeHostBridge({
134
157
  repoRoot: options.repoRoot,
135
158
  platform: options.platform,
136
159
  });
160
+ this.bridge.setUplinkEventHandler((event) => {
161
+ this.handleNativeEvent(event);
162
+ });
163
+ }
164
+ handleNativeEvent(event) {
165
+ this.emitLog('stdout', '[runtime-backed sender] native event kind=' + event.kind +
166
+ ' requestId=' + String(event.requestId ?? '') +
167
+ ' remoteRequestId=' + String(event.remoteRequestId ?? '') +
168
+ ' commandId=' + String(event.commandId ?? '') +
169
+ ' errorCode=' + String(event.errorCode ?? '') +
170
+ ' reasonCode=' + String(event.reasonCode ?? '') +
171
+ ' payloadLength=' + String(event.payload?.byteLength ?? 0));
172
+ if (event.kind === 'command.response') {
173
+ const requestId = Number(event.requestId);
174
+ if (!Number.isInteger(requestId)) {
175
+ return;
176
+ }
177
+ const pending = this.pendingCommandRequests.get(requestId);
178
+ if (!pending) {
179
+ return;
180
+ }
181
+ this.pendingCommandRequests.delete(requestId);
182
+ if (pending.timer) {
183
+ clearTimeout(pending.timer);
184
+ }
185
+ if ((event.errorCode ?? 0) !== 0) {
186
+ pending.reject(new Error(`uplink command request failed error=${String(event.errorCode ?? -1)}`));
187
+ return;
188
+ }
189
+ pending.resolve({
190
+ commandId: Number.isInteger(event.commandId) ? Number(event.commandId) : pending.commandId,
191
+ payload: event.payload ?? new Uint8Array(),
192
+ });
193
+ return;
194
+ }
195
+ if (!this.onRuntimeEvent) {
196
+ return;
197
+ }
198
+ if (event.kind === 'connection.accepted') {
199
+ this.onRuntimeEvent(event.kind, { at: nowIsoString() });
200
+ return;
201
+ }
202
+ if (event.kind === 'connection.disconnected') {
203
+ this.onRuntimeEvent(event.kind, {
204
+ at: nowIsoString(),
205
+ reasonCode: String(event.reasonCode ?? 0),
206
+ });
207
+ return;
208
+ }
209
+ if (event.kind === 'command.remote.requested') {
210
+ const remoteRequestId = toSafeInteger(event.remoteRequestId);
211
+ const commandId = toSafeInteger(event.commandId);
212
+ this.onRuntimeEvent(event.kind, {
213
+ at: nowIsoString(),
214
+ remoteRequestId: remoteRequestId ?? String(event.remoteRequestId ?? ''),
215
+ commandId: commandId ?? event.commandId,
216
+ payload: event.payload,
217
+ });
218
+ }
219
+ }
220
+ rejectPendingCommandRequests(message) {
221
+ if (this.pendingCommandRequests.size === 0) {
222
+ return;
223
+ }
224
+ for (const [requestId, pending] of this.pendingCommandRequests.entries()) {
225
+ if (pending.timer) {
226
+ clearTimeout(pending.timer);
227
+ }
228
+ pending.reject(new Error(`${message} requestId=${requestId}`));
229
+ }
230
+ this.pendingCommandRequests.clear();
137
231
  }
138
232
  emitLog(stream, line) {
139
233
  const normalized = line.trim();
@@ -201,6 +295,7 @@ class RuntimeSendWorkerManager {
201
295
  }
202
296
  this.bridge.stopUplink();
203
297
  this.sharedSession = undefined;
298
+ this.rejectPendingCommandRequests('uplink session stopped');
204
299
  this.emitLog('stdout', '[runtime-backed sender] session stopped');
205
300
  }
206
301
  async ensureSharedSession(serviceEntry, license, keepAliveWhenNoStreams) {
@@ -287,5 +382,57 @@ class RuntimeSendWorkerManager {
287
382
  this.bindings.clear();
288
383
  await this.stopSharedSession();
289
384
  }
385
+ async sendStreamMessage(streamId, payload) {
386
+ if (!this.sharedSession) {
387
+ throw new Error('uplink session is not started');
388
+ }
389
+ try {
390
+ this.bridge.sendUplinkStreamMessage(streamId, payload);
391
+ this.emitLog('stdout', '[runtime-backed sender] send_stream_message accepted streamId=' +
392
+ String(streamId) + ' payloadLength=' + String(payload.byteLength));
393
+ }
394
+ catch (error) {
395
+ const message = error instanceof Error ? error.message : String(error);
396
+ this.emitLog('stderr', '[runtime-backed sender] send_stream_message failed streamId=' +
397
+ String(streamId) + ' payloadLength=' + String(payload.byteLength) + ' error=' + message);
398
+ throw error;
399
+ }
400
+ }
401
+ async requestCommand(commandId, payload, timeoutMs) {
402
+ if (!this.sharedSession) {
403
+ throw new Error('uplink session is not started');
404
+ }
405
+ const requestId = this.nextCommandRequestId++;
406
+ return await new Promise((resolve, reject) => {
407
+ const pending = {
408
+ commandId,
409
+ resolve,
410
+ reject,
411
+ };
412
+ pending.timer = setTimeout(() => {
413
+ if (!this.pendingCommandRequests.delete(requestId)) {
414
+ return;
415
+ }
416
+ reject(new Error(`uplink command response timeout requestId=${requestId}`));
417
+ }, Math.max(timeoutMs * 2, timeoutMs + 2000));
418
+ this.pendingCommandRequests.set(requestId, pending);
419
+ try {
420
+ this.bridge.requestUplinkCommand(requestId, commandId, payload, timeoutMs);
421
+ }
422
+ catch (error) {
423
+ this.pendingCommandRequests.delete(requestId);
424
+ if (pending.timer) {
425
+ clearTimeout(pending.timer);
426
+ }
427
+ reject(error instanceof Error ? error : new Error(String(error)));
428
+ }
429
+ });
430
+ }
431
+ async replyRemoteCommand(remoteRequestId, commandId, payload) {
432
+ if (!this.sharedSession) {
433
+ throw new Error('uplink session is not started');
434
+ }
435
+ this.bridge.replyUplinkRemoteCommand(remoteRequestId, commandId, payload);
436
+ }
290
437
  }
291
438
  exports.RuntimeSendWorkerManager = RuntimeSendWorkerManager;
@@ -1,3 +1,12 @@
1
+ export type UplinkNativeEvent = {
2
+ kind: string;
3
+ requestId?: number;
4
+ remoteRequestId?: number;
5
+ commandId?: number;
6
+ errorCode?: number;
7
+ reasonCode?: number;
8
+ payload?: Uint8Array;
9
+ };
1
10
  type RuntimeHostBridgeOptions = {
2
11
  repoRoot: string;
3
12
  platform: string;
@@ -13,6 +22,10 @@ export declare class RuntimeHostBridge {
13
22
  private ensureBinding;
14
23
  startUplink(args: string[]): number;
15
24
  stopUplink(): void;
25
+ setUplinkEventHandler(handler: ((event: UplinkNativeEvent) => void) | undefined): void;
26
+ sendUplinkStreamMessage(streamId: number, payload: Uint8Array): void;
27
+ requestUplinkCommand(requestId: number, commandId: number, payload: Uint8Array, timeoutMs: number): void;
28
+ replyUplinkRemoteCommand(remoteRequestId: number, commandId: number, payload: Uint8Array): void;
16
29
  startDownlink(args: string[]): number;
17
30
  stopDownlink(): void;
18
31
  }