@stim-cli/server 1.11.0 → 1.13.0

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/README.md CHANGED
@@ -107,12 +107,13 @@ closes its open connections.
107
107
  ## Scopes
108
108
 
109
109
  A paired device has the `read` capability, which serves state, or also
110
- `control`, which runs [actions](#actions). Pairing grants `read` only, unless
110
+ `control`, which runs [actions](#actions) and [controls devices](#control). Pairing grants `read` only, unless
111
111
  the pairing code came from `stim-server pair --control`. On the Mac,
112
112
  `stim-server devices grant <id> --control` adds control to a paired device and
113
113
  `--read` takes it away. Nothing a client sends changes its own capabilities.
114
- The server checks the device's capabilities in `devices.json` on every action,
115
- so taking control away applies to open connections at once. `hello` reports
114
+ The server checks the device's capabilities in `devices.json` on every action
115
+ and control session, and ends a device's control sessions when it loses
116
+ `control`, so taking control away applies to open connections at once. `hello` reports
116
117
  the capabilities and actions of the connection's device when it connects; a
117
118
  connection sees a new grant after it reconnects.
118
119
 
@@ -164,23 +165,31 @@ Events are `{ "event", "subscription", ... }`.
164
165
  `$STIM_HOME/server/helpers/`, named by a hash of the sources and the
165
166
  compiler version. For a simulator it renders the display's framebuffer
166
167
  (CoreSimulator's IOSurface) when the display reports damage, turned
167
- upright; for an emulator it keeps one gRPC `streamScreenshot` call open,
168
+ upright. An iPhone Duo lights one of its two panels, the cover while
169
+ folded and the inner panel while unfolded, and leaves the other black, so
170
+ the helper streams whichever panel is lit; its frames and video carry
171
+ `posture`, and the size changes with the panel. For an emulator it keeps one gRPC `streamScreenshot` call open,
168
172
  found through the discovery file and token the emulator writes when Stim
169
173
  boots it. It scales frames to fit the largest `maxEdge` and paces them to
170
174
  the highest `fps` its subscribers asked for. All subscribers of a device
171
- share one helper, which sends a new subscriber the latest frame and exits
172
- with the last subscriber or when the server's end of its stdin closes.
175
+ share one helper, which sends a new subscriber the latest frame. It keeps
176
+ running for 10 seconds after the last subscriber leaves, so a client that
177
+ subscribes for one frame at a time reuses it and gets the latest frame at
178
+ once, and exits after that or when the server's end of its stdin closes.
173
179
 
174
180
  A client that decodes H.264 adds `video: ["h264"]`. When the helper is
175
181
  built, the result carries `video: "h264"`, `fps` may go up to 60, and
176
182
  frames arrive as binary WebSocket messages instead of `frame` events: a
177
183
  big-endian header (u8 version 1, u8 flags with bit 0 set on a keyframe,
184
+ and on an iPhone Duo bit 1 while folded or bit 2 while unfolded,
178
185
  u16 header length, u32 sequence number of the messages sent on this
179
186
  subscription, f64 capture time in milliseconds since the epoch on the
180
187
  Mac's clock, u16 width, u16 height, u8 subscription id length and the
181
188
  ASCII id), then one Annex-B access unit. Every keyframe carries its SPS
182
189
  and PPS, and the stream has no B-frames, so each access unit is shown as
183
- it arrives. The helper encodes with VideoToolbox in real time: Main
190
+ it arrives. A change of size, such as a rotation or a Duo fold, restarts
191
+ the encoder, and the next access unit is a keyframe with the new SPS and
192
+ PPS. The helper encodes with VideoToolbox in real time: Main
184
193
  profile, a keyframe at least every 2 seconds, straight from the
185
194
  simulator's IOSurface or from the emulator's RGBA frames, only when the
186
195
  screen changed. A subscriber starts at a keyframe, and `frames.keyframe`
@@ -194,13 +203,13 @@ Events are `{ "event", "subscription", ... }`.
194
203
  subscribers of the same device still get at most the `fps` they asked
195
204
  for. Watching video needs only `read`. Without the helper, the result has
196
205
  no `video`, `fps` above 30 is lowered to 30, and `frame` events arrive as
197
- before; an iPhone Duo and a helper that fails before its first frame also
198
- fall back to `frame` events within a video subscription.
206
+ before; a helper that fails before its first frame also falls back to
207
+ `frame` events within a video subscription.
199
208
 
200
209
  Without the helper (the compiler is missing or fails, which the server
201
- retries every 5 minutes, or the helper fails before its first frame), for a
202
- subscription made while it is still being built, and for an iPhone Duo,
203
- frames come from screenshots, and `fps` and `maxEdge` only cap the rate. Simulators are
210
+ retries every 5 minutes, or the helper fails before its first frame), and
211
+ for a subscription made while it is still being built, frames come from
212
+ screenshots, and `fps` and `maxEdge` only cap the rate. Simulators are
204
213
  captured with `xcrun simctl io <udid> screenshot`, of the primary display,
205
214
  or of the default display when that `simctl` does not accept `primary`. An
206
215
  iPhone Duo lights one of two panels: the capture follows the lit one
@@ -221,12 +230,15 @@ Events are `{ "event", "subscription", ... }`.
221
230
  (`default` when absent), and returns the payload of
222
231
  `stim <platform> --plan --json` run in the workspace: the fingerprint, the
223
232
  cache result the next build would get (`local`, `remote` or `false`), the
224
- prebuild decision, and `expectedMs` with its `basis`. It builds, boots and
233
+ prebuild decision, `expectedMs` with its `basis`, and on a predicted cold
234
+ build its `missReason`. It builds, boots and
225
235
  installs nothing and writes no Stim state; a remote cache check can
226
236
  download the artifact into a temporary directory, so it gets 150 seconds
227
237
  instead of 60. A plan predicting that the build would refuse is a result
228
238
  whose `refusal` holds the code, message and remedy. A plan that cannot be
229
- computed is a `stim-failed` error.
239
+ computed is a `stim-failed` error. One plan runs per workspace at a time,
240
+ across all connections; later requests wait their turn, and the 150
241
+ seconds start when the plan starts.
230
242
  - `machine.get` returns cheap machine usage, read in the server process
231
243
  without running `stim`: `volumes`, one per volume that holds a Stim
232
244
  workspace, Stim home, or the simulators, with `mount`, `holds`, `freeBytes`
@@ -297,6 +309,80 @@ and `durationMs` when it ran. Strings from the client and error messages are
297
309
  cut to 256 characters. `stim-server log` prints them, with control characters
298
310
  replaced by `?`.
299
311
 
312
+ ## Control
313
+
314
+ A device with `control` can drive a simulator or emulator that `stim status`
315
+ lists as owned by a workspace. Nothing it sends reaches any other device.
316
+
317
+ - `control.begin` takes `workspace`, `platform`, `slot` (`default` when
318
+ absent) and `takeOver`, and returns `{ "session", "platform", "lease",
319
+ "postures" }`. `postures` lists what `input.posture` takes for the device:
320
+ `folded` and `unfolded` for an iPhone Duo, `folded`, `half-open` and
321
+ `unfolded` for an emulator with a hinge, such as a `pixel_fold` AVD, and
322
+ none otherwise.
323
+ The server refuses with `device-busy` when status reports the device driven
324
+ (agent-device, a `stim device lock`, a test runner) or another client
325
+ controls it, naming the driver. With `takeOver: true` it proceeds anyway;
326
+ another client's session then ends with `taken-over`.
327
+ - The session holds a `stim device lock <platform> <id> --for 2m` lease,
328
+ renewed every minute, so `stim status` shows the device as driven by
329
+ `stim device lock` and agents leave it alone. `lease` carries its
330
+ `grantedAt`, the `since` of that driver, so a client can tell its own lease
331
+ from an agent's. The server releases the lease with `stim device unlock`
332
+ when the session ends, unless the lease was already held before the session
333
+ began, as when it took the device over from an agent in the same workspace;
334
+ it renews only a lease it took. Leases belong to the workspace, not to the
335
+ session: an agent in the same workspace that runs `stim device lock` on the
336
+ device during a session shares the lease, and the session's end releases
337
+ it.
338
+ - `input.touch` takes `session`, `phase` (`down`, `move`, `up`), `x` and `y`
339
+ as fractions of the upright screen, and `display` (0, the main display).
340
+ `input.text` takes up to 256 printable ASCII characters, where `\n` presses
341
+ Return, `\t` Tab and `\b` Delete. `input.button` takes `home` or `lock`,
342
+ and on Android also `back` or `app-switch`. `input.rotate` takes
343
+ `direction` (`left` or `right`) and turns the device a quarter turn.
344
+ `input.posture` takes one of the session's `postures`. Each answers `{}` once the input
345
+ is handed to the device: when it goes through the helper, that is when the
346
+ helper receives it, so a failure there shows only in the server's log. A connection may send 120 inputs a second and type 40
347
+ characters a second, with a burst of 256, and rotate or change posture twice a
348
+ second; more fail with `limit-exceeded`.
349
+ - `control.end` ends a session. The server also ends it with a
350
+ `control-ended` event `{ "session", "reason", "message" }` after 5 minutes
351
+ without input (`idle`), when another client takes the device over
352
+ (`taken-over`), when the device stops or changes owner (`device-gone`),
353
+ when the paired device loses `control` (`forbidden`), or when input cannot
354
+ reach the device (`failed`). Closing the connection ends its sessions.
355
+
356
+ Input goes through the device's `stim-frames` helper, the process that
357
+ streams its frames:
358
+
359
+ - Simulators take touches, keys and buttons through the simulator's
360
+ CoreDevice HID service (`dtuhidd`), the one Xcode's Device Hub uses, so
361
+ input keeps working while Device Hub or Siniulator shows the simulator.
362
+ With an Xcode whose simulators have no such service, input goes through
363
+ SimulatorKit's legacy HID client instead. The first input starts that service, as Device
364
+ Hub does, and from then until the simulator reboots it ignores tools that
365
+ still use SimulatorKit's legacy HID client. Text is typed key by key on a US
366
+ layout. `lock` is the side button.
367
+ - Emulators take touches through the emulator's gRPC `sendTouch`. Text and
368
+ buttons go through gRPC `sendKey` when the emulator reports a hardware
369
+ keyboard, which AVDs Stim creates have. An emulator without one drops key
370
+ events, so for it text and buttons go through `adb -s <serial> shell input`.
371
+ - Rotation and posture go the way Stim Desktop sends them: a simulator turns
372
+ through the orientation message Simulator.app sends it, and an emulator
373
+ through gRPC `setPhysicalModel` for rotation and `setPosture` for its hinge.
374
+ An iPhone Duo folds with `sim-fold`, which the server builds from Stim
375
+ Desktop's sources on the first fold and runs inside the simulator with
376
+ `xcrun simctl spawn`; it answers `{}` once the fold finished, and fails
377
+ with `action-failed` when the fold does not finish within 40 seconds. `sim-fold`
378
+ swaps the posture, so the server runs it only when the Duo's latest frame
379
+ shows the other posture, and refuses the request until a frame showed one.
380
+
381
+ Every session start, takeover and end, and every refused `control.begin`,
382
+ appends a line to the action log, with `action` set to `control.begin`,
383
+ `control.take-over` or `control.end`, and a `reason` that says why the session
384
+ ended or whom it took the device from. Inputs are not logged.
385
+
300
386
  The error codes `unauthorized`, `pairing-expired`, and `protocol-unsupported`
301
387
  refuse the client until it pairs again or updates; clients retry the others.
302
388
 
@@ -16,7 +16,14 @@ const METHODS = [
16
16
  "machine.get",
17
17
  "machine.history",
18
18
  "unsubscribe",
19
- "action"
19
+ "action",
20
+ "control.begin",
21
+ "control.end",
22
+ "input.touch",
23
+ "input.text",
24
+ "input.button",
25
+ "input.rotate",
26
+ "input.posture"
20
27
  ];
21
28
  /**
22
29
  * `unauthorized`, `pairing-expired` and `protocol-unsupported` refuse the client until it pairs again or
@@ -41,7 +48,9 @@ const ERROR_CODES = [
41
48
  "forbidden",
42
49
  "unknown-action",
43
50
  "action-busy",
44
- "action-failed"
51
+ "action-failed",
52
+ "device-busy",
53
+ "unknown-session"
45
54
  ];
46
55
  const LOG_SOURCES = [
47
56
  "metro",
@@ -58,6 +67,12 @@ const LOG_LEVELS = [
58
67
  "fatal"
59
68
  ];
60
69
  const MAX_LOG_TAIL = 5e3;
70
+ /** `reload` also reaches the workspace's Stim-owned Chrome page. */
71
+ const RELOAD_PLATFORMS = [
72
+ "ios",
73
+ "android",
74
+ "web"
75
+ ];
61
76
  const FRAME_FPS = {
62
77
  default: 5,
63
78
  max: 30,
@@ -72,8 +87,38 @@ const VIDEO_CODECS = ["h264"];
72
87
  const VIDEO_HEADER_VERSION = 1;
73
88
  /** The keyframe bit of a {@link VideoPacket}'s flags. */
74
89
  const VIDEO_KEYFRAME = 1;
90
+ /** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
91
+ const VIDEO_FOLDED = 2;
92
+ const VIDEO_UNFOLDED = 4;
75
93
  /** Each action runs one fixed `stim` command in the workspace. */
76
94
  const ACTIONS = ["reload", "stop"];
95
+ const TOUCH_PHASES = [
96
+ "down",
97
+ "move",
98
+ "up"
99
+ ];
100
+ const MAX_INPUT_TEXT = 256;
101
+ /** `home` and `lock` on both platforms; `back` and `app-switch` on Android only. */
102
+ const INPUT_BUTTONS = [
103
+ "home",
104
+ "lock",
105
+ "back",
106
+ "app-switch"
107
+ ];
108
+ /** `left` turns the device a quarter turn counterclockwise, `right` clockwise. */
109
+ const ROTATE_DIRECTIONS = ["left", "right"];
110
+ const DEVICE_POSTURES = [
111
+ "folded",
112
+ "half-open",
113
+ "unfolded"
114
+ ];
115
+ const CONTROL_END_REASONS = [
116
+ "idle",
117
+ "taken-over",
118
+ "device-gone",
119
+ "forbidden",
120
+ "failed"
121
+ ];
77
122
  const requestId = { type: ["integer", "string"] };
78
123
  const protocolError = {
79
124
  type: "object",
@@ -103,6 +148,17 @@ function request(method, params) {
103
148
  }
104
149
  };
105
150
  }
151
+ function session(properties, required = []) {
152
+ return {
153
+ type: "object",
154
+ required: ["session", ...required],
155
+ additionalProperties: false,
156
+ properties: {
157
+ session: { type: "string" },
158
+ ...properties
159
+ }
160
+ };
161
+ }
106
162
  function optionalParams(method, params) {
107
163
  return {
108
164
  type: "object",
@@ -216,7 +272,12 @@ function protocolJsonSchema() {
216
272
  ts: { type: "number" },
217
273
  src: { type: "string" },
218
274
  level: { type: "string" },
219
- msg: { type: "string" }
275
+ msg: { type: "string" },
276
+ context: {
277
+ type: "array",
278
+ items: { type: "string" },
279
+ description: "With `errors`: the code frame and stack lines Expo printed after this error."
280
+ }
220
281
  }
221
282
  },
222
283
  LogFilter: {
@@ -236,10 +297,11 @@ function protocolJsonSchema() {
236
297
  level: { enum: [...LOG_LEVELS] },
237
298
  slot: {
238
299
  type: "string",
239
- minLength: 1
300
+ pattern: "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"
240
301
  },
241
302
  grep: {
242
303
  type: "string",
304
+ pattern: "^[^\\u0000]*$",
243
305
  description: "A regular expression matched against each message."
244
306
  },
245
307
  errors: { type: "boolean" },
@@ -295,7 +357,7 @@ function protocolJsonSchema() {
295
357
  type: "string",
296
358
  description: "An environment path from a status payload."
297
359
  },
298
- platform: { enum: ["ios", "android"] }
360
+ platform: { enum: [...RELOAD_PLATFORMS] }
299
361
  }
300
362
  }, {
301
363
  type: "object",
@@ -326,6 +388,61 @@ function protocolJsonSchema() {
326
388
  }
327
389
  }
328
390
  },
391
+ ControlBeginParams: {
392
+ type: "object",
393
+ required: ["workspace", "platform"],
394
+ additionalProperties: false,
395
+ properties: {
396
+ workspace: {
397
+ type: "string",
398
+ description: "An environment path from a status payload."
399
+ },
400
+ platform: { enum: ["ios", "android"] },
401
+ slot: {
402
+ type: "string",
403
+ minLength: 1,
404
+ default: "default"
405
+ },
406
+ takeOver: {
407
+ type: "boolean",
408
+ default: false
409
+ }
410
+ }
411
+ },
412
+ ControlBeginResult: {
413
+ type: "object",
414
+ required: [
415
+ "session",
416
+ "platform",
417
+ "lease",
418
+ "postures"
419
+ ],
420
+ additionalProperties: false,
421
+ properties: {
422
+ session: { type: "string" },
423
+ platform: { enum: ["ios", "android"] },
424
+ lease: { oneOf: [{ type: "null" }, {
425
+ type: "object",
426
+ required: ["grantedAt", "expiresAt"],
427
+ additionalProperties: false,
428
+ properties: {
429
+ grantedAt: {
430
+ type: ["string", "null"],
431
+ format: "date-time"
432
+ },
433
+ expiresAt: {
434
+ type: "string",
435
+ format: "date-time"
436
+ }
437
+ }
438
+ }] },
439
+ postures: {
440
+ type: "array",
441
+ items: { enum: [...DEVICE_POSTURES] },
442
+ uniqueItems: true
443
+ }
444
+ }
445
+ },
329
446
  BuildPlanParams: {
330
447
  type: "object",
331
448
  required: ["workspace", "platform"],
@@ -495,7 +612,41 @@ function protocolJsonSchema() {
495
612
  additionalProperties: false,
496
613
  properties: { subscription: { type: "string" } }
497
614
  }),
498
- request("action", { $ref: "#/$defs/ActionParams" })
615
+ request("action", { $ref: "#/$defs/ActionParams" }),
616
+ request("control.begin", { $ref: "#/$defs/ControlBeginParams" }),
617
+ request("control.end", session({})),
618
+ request("input.touch", session({
619
+ phase: { enum: [...TOUCH_PHASES] },
620
+ x: {
621
+ type: "number",
622
+ minimum: 0,
623
+ maximum: 1
624
+ },
625
+ y: {
626
+ type: "number",
627
+ minimum: 0,
628
+ maximum: 1
629
+ },
630
+ display: {
631
+ type: "integer",
632
+ minimum: 0,
633
+ maximum: 3,
634
+ default: 0
635
+ }
636
+ }, [
637
+ "phase",
638
+ "x",
639
+ "y"
640
+ ])),
641
+ request("input.text", session({ text: {
642
+ type: "string",
643
+ minLength: 1,
644
+ maxLength: 256,
645
+ pattern: "^[\\x20-\\x7e\\n\\t\\b]+$"
646
+ } }, ["text"])),
647
+ request("input.button", session({ button: { enum: [...INPUT_BUTTONS] } }, ["button"])),
648
+ request("input.rotate", session({ direction: { enum: [...ROTATE_DIRECTIONS] } }, ["direction"])),
649
+ request("input.posture", session({ posture: { enum: [...DEVICE_POSTURES] } }, ["posture"]))
499
650
  ] },
500
651
  ServerResponse: { oneOf: [{
501
652
  type: "object",
@@ -505,6 +656,7 @@ function protocolJsonSchema() {
505
656
  id: requestId,
506
657
  result: { anyOf: [
507
658
  { $ref: "#/$defs/HelloResult" },
659
+ { $ref: "#/$defs/ControlBeginResult" },
508
660
  { $ref: "#/$defs/ActionResult" },
509
661
  { $ref: "#/$defs/MachineUsage" },
510
662
  { $ref: "#/$defs/MachineHistory" },
@@ -627,6 +779,22 @@ function protocolJsonSchema() {
627
779
  delayed: { type: "boolean" }
628
780
  }
629
781
  },
782
+ {
783
+ type: "object",
784
+ required: [
785
+ "event",
786
+ "session",
787
+ "reason",
788
+ "message"
789
+ ],
790
+ additionalProperties: false,
791
+ properties: {
792
+ event: { const: "control-ended" },
793
+ session: { type: "string" },
794
+ reason: { enum: [...CONTROL_END_REASONS] },
795
+ message: { type: "string" }
796
+ }
797
+ },
630
798
  {
631
799
  type: "object",
632
800
  required: [
@@ -651,4 +819,4 @@ function protocolJsonSchema() {
651
819
  };
652
820
  }
653
821
  //#endregion
654
- export { FRAME_FPS as a, MAX_LOG_TAIL as c, PROTOCOL_VERSION as d, VIDEO_CODECS as f, protocolJsonSchema as h, FRAME_EDGE as i, METHODS as l, VIDEO_KEYFRAME as m, CAPABILITIES as n, LOG_LEVELS as o, VIDEO_HEADER_VERSION as p, ERROR_CODES as r, LOG_SOURCES as s, ACTIONS as t, PROTOCOL_SCHEMA_FILE as u };
822
+ export { VIDEO_UNFOLDED as C, VIDEO_KEYFRAME as S, ROTATE_DIRECTIONS as _, ERROR_CODES as a, VIDEO_FOLDED as b, INPUT_BUTTONS as c, MAX_INPUT_TEXT as d, MAX_LOG_TAIL as f, RELOAD_PLATFORMS as g, PROTOCOL_VERSION as h, DEVICE_POSTURES as i, LOG_LEVELS as l, PROTOCOL_SCHEMA_FILE as m, CAPABILITIES as n, FRAME_EDGE as o, METHODS as p, CONTROL_END_REASONS as r, FRAME_FPS as s, ACTIONS as t, LOG_SOURCES as u, TOUCH_PHASES as v, protocolJsonSchema as w, VIDEO_HEADER_VERSION as x, VIDEO_CODECS as y };
@@ -5,13 +5,13 @@ declare const PROTOCOL_SCHEMA_FILE = "protocol.schema.json";
5
5
  /** `read` serves state. `control` also runs {@link ACTIONS}; only the Mac grants it. */
6
6
  declare const CAPABILITIES: readonly ["read", "control"];
7
7
  type Capability = (typeof CAPABILITIES)[number];
8
- declare const METHODS: readonly ["hello", "status.subscribe", "logs.query", "logs.subscribe", "stats.get", "settings.get", "frames.subscribe", "frames.keyframe", "build.plan", "machine.get", "machine.history", "unsubscribe", "action"];
8
+ declare const METHODS: readonly ["hello", "status.subscribe", "logs.query", "logs.subscribe", "stats.get", "settings.get", "frames.subscribe", "frames.keyframe", "build.plan", "machine.get", "machine.history", "unsubscribe", "action", "control.begin", "control.end", "input.touch", "input.text", "input.button", "input.rotate", "input.posture"];
9
9
  type Method = (typeof METHODS)[number];
10
10
  /**
11
11
  * `unauthorized`, `pairing-expired` and `protocol-unsupported` refuse the client until it pairs again or
12
12
  * updates; clients retry the others.
13
13
  */
14
- declare const ERROR_CODES: readonly ["unauthorized", "pairing-expired", "protocol-unsupported", "identity-unavailable", "bad-request", "unknown-method", "already-authenticated", "unknown-subscription", "unknown-workspace", "limit-exceeded", "slow-client", "status-failed", "logs-failed", "stim-failed", "frames-failed", "forbidden", "unknown-action", "action-busy", "action-failed"];
14
+ declare const ERROR_CODES: readonly ["unauthorized", "pairing-expired", "protocol-unsupported", "identity-unavailable", "bad-request", "unknown-method", "already-authenticated", "unknown-subscription", "unknown-workspace", "limit-exceeded", "slow-client", "status-failed", "logs-failed", "stim-failed", "frames-failed", "forbidden", "unknown-action", "action-busy", "action-failed", "device-busy", "unknown-session"];
15
15
  type ErrorCode = (typeof ERROR_CODES)[number];
16
16
  type RequestId = number | string;
17
17
  /** Spends a single-use pairing token; the result carries the new device token. */
@@ -93,6 +93,9 @@ type StatsResult = Record<string, unknown>;
93
93
  /** `stim settings --json`; the CLI masks sensitive values. */
94
94
  type SettingsResult = Record<string, unknown>;
95
95
  type Platform = "ios" | "android";
96
+ /** `reload` also reaches the workspace's Stim-owned Chrome page. */
97
+ declare const RELOAD_PLATFORMS: readonly ["ios", "android", "web"];
98
+ type ReloadPlatform = (typeof RELOAD_PLATFORMS)[number];
96
99
  declare const FRAME_FPS: {
97
100
  readonly default: 5;
98
101
  readonly max: 30;
@@ -122,7 +125,7 @@ declare const VIDEO_CODECS: readonly ["h264"];
122
125
  type VideoCodec = (typeof VIDEO_CODECS)[number];
123
126
  /**
124
127
  * With `video`, frames arrive as binary WebSocket messages, one H.264 access unit each; see {@link VideoPacket}.
125
- * A subscription whose device cannot be encoded, such as an iPhone Duo, still sends JSON `frame` events.
128
+ * A subscription whose device falls back to screenshots still sends JSON `frame` events.
126
129
  */
127
130
  interface FramesSubscribeResult extends SubscribeResult {
128
131
  video?: VideoCodec;
@@ -134,9 +137,12 @@ interface KeyframeParams {
134
137
  declare const VIDEO_HEADER_VERSION = 1;
135
138
  /** The keyframe bit of a {@link VideoPacket}'s flags. */
136
139
  declare const VIDEO_KEYFRAME = 1;
140
+ /** The flag bits of a {@link VideoPacket} that carry an iPhone Duo's posture, as `posture` on a `frame` event. */
141
+ declare const VIDEO_FOLDED = 2;
142
+ declare const VIDEO_UNFOLDED = 4;
137
143
  /**
138
144
  * The layout of a binary video message, big-endian: u8 version ({@link VIDEO_HEADER_VERSION}), u8 flags
139
- * ({@link VIDEO_KEYFRAME}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
145
+ * ({@link VIDEO_KEYFRAME}, and on an iPhone Duo {@link VIDEO_FOLDED} or {@link VIDEO_UNFOLDED}), u16 header length, u32 sequence number of the messages sent on this subscription, f64 capture time in milliseconds since the
140
146
  * epoch on the Mac's clock, u16 width, u16 height, u8 subscription id length N, N bytes of ASCII subscription
141
147
  * id. After the header comes one Annex-B H.264 access unit; a keyframe carries its SPS and PPS. The stream has
142
148
  * no B-frames, so each access unit is shown as it arrives.
@@ -148,19 +154,20 @@ interface VideoPacket {
148
154
  capturedAt: number;
149
155
  width: number;
150
156
  height: number;
157
+ posture?: "folded" | "unfolded";
151
158
  accessUnit: Uint8Array;
152
159
  }
153
160
  /** Each action runs one fixed `stim` command in the workspace. */
154
161
  declare const ACTIONS: readonly ["reload", "stop"];
155
162
  type ActionName = (typeof ACTIONS)[number];
156
163
  /**
157
- * `reload` runs `stim reload --json`, with `platform` when both platforms are live; `stop` runs
164
+ * `reload` runs `stim reload --json`, with `platform` when more than one platform is live; `stop` runs
158
165
  * `stim stop --json`. `workspace` is an environment `path` from a status payload. Needs `control`.
159
166
  */
160
167
  type ActionParams = {
161
168
  action: "reload";
162
169
  workspace: string;
163
- platform?: Platform;
170
+ platform?: ReloadPlatform;
164
171
  } | {
165
172
  action: "stop";
166
173
  workspace: string;
@@ -171,6 +178,72 @@ interface ActionResult {
171
178
  workspace: string;
172
179
  output: Record<string, unknown>;
173
180
  }
181
+ /**
182
+ * Starts a control session on the device `stim status` lists as owned by `workspace` in `slot`. Needs
183
+ * `control`. Refused with `device-busy` while an agent, a device lock or another client drives the device,
184
+ * unless `takeOver` is true.
185
+ */
186
+ interface ControlBeginParams {
187
+ workspace: string;
188
+ platform: Platform;
189
+ slot?: string;
190
+ takeOver?: boolean;
191
+ }
192
+ /**
193
+ * `lease` is the `stim device lock` lease the server holds for the session, or null when it holds none, such
194
+ * as after taking over a device another workspace leases. `postures` lists what `input.posture` accepts for
195
+ * the device: `folded` and `unfolded` for an iPhone Duo, all three for an emulator with a hinge, and none
196
+ * otherwise.
197
+ */
198
+ interface ControlBeginResult {
199
+ session: string;
200
+ platform: Platform;
201
+ lease: {
202
+ grantedAt: string | null;
203
+ expiresAt: string;
204
+ } | null;
205
+ postures: DevicePosture[];
206
+ }
207
+ interface ControlEndParams {
208
+ session: string;
209
+ }
210
+ declare const TOUCH_PHASES: readonly ["down", "move", "up"];
211
+ type TouchPhase = (typeof TOUCH_PHASES)[number];
212
+ /** `x` and `y` are fractions of the upright screen, origin top-left. `display` is 0 for the main display. */
213
+ interface InputTouchParams {
214
+ session: string;
215
+ phase: TouchPhase;
216
+ x: number;
217
+ y: number;
218
+ display?: number;
219
+ }
220
+ declare const MAX_INPUT_TEXT = 256;
221
+ /** Printable ASCII, where `\n` presses Return, `\t` Tab and `\b` Delete. */
222
+ interface InputTextParams {
223
+ session: string;
224
+ text: string;
225
+ }
226
+ /** `home` and `lock` on both platforms; `back` and `app-switch` on Android only. */
227
+ declare const INPUT_BUTTONS: readonly ["home", "lock", "back", "app-switch"];
228
+ type InputButton = (typeof INPUT_BUTTONS)[number];
229
+ interface InputButtonParams {
230
+ session: string;
231
+ button: InputButton;
232
+ }
233
+ /** `left` turns the device a quarter turn counterclockwise, `right` clockwise. */
234
+ declare const ROTATE_DIRECTIONS: readonly ["left", "right"];
235
+ type RotateDirection = (typeof ROTATE_DIRECTIONS)[number];
236
+ interface InputRotateParams {
237
+ session: string;
238
+ direction: RotateDirection;
239
+ }
240
+ declare const DEVICE_POSTURES: readonly ["folded", "half-open", "unfolded"];
241
+ type DevicePosture = (typeof DEVICE_POSTURES)[number];
242
+ /** Moves the hinge of a device whose `control.begin` result lists `posture`. */
243
+ interface InputPostureParams {
244
+ session: string;
245
+ posture: DevicePosture;
246
+ }
174
247
  /** Predicts the next `ios` or `android` build of `workspace` in `slot` (`default` when absent). */
175
248
  interface BuildPlanParams {
176
249
  workspace: string;
@@ -292,6 +365,34 @@ interface Methods {
292
365
  params: ActionParams;
293
366
  result: ActionResult;
294
367
  };
368
+ "control.begin": {
369
+ params: ControlBeginParams;
370
+ result: ControlBeginResult;
371
+ };
372
+ "control.end": {
373
+ params: ControlEndParams;
374
+ result: Record<string, never>;
375
+ };
376
+ "input.touch": {
377
+ params: InputTouchParams;
378
+ result: Record<string, never>;
379
+ };
380
+ "input.text": {
381
+ params: InputTextParams;
382
+ result: Record<string, never>;
383
+ };
384
+ "input.button": {
385
+ params: InputButtonParams;
386
+ result: Record<string, never>;
387
+ };
388
+ "input.rotate": {
389
+ params: InputRotateParams;
390
+ result: Record<string, never>;
391
+ };
392
+ "input.posture": {
393
+ params: InputPostureParams;
394
+ result: Record<string, never>;
395
+ };
295
396
  }
296
397
  type ClientRequest = { [M in Method]: {
297
398
  id: RequestId;
@@ -357,10 +458,21 @@ interface FrameDelayedEvent {
357
458
  subscription: string;
358
459
  delayed: boolean;
359
460
  }
360
- type ServerEvent = StatusEvent | LogsEvent | FrameEvent | FrameDelayedEvent | ErrorEvent;
461
+ declare const CONTROL_END_REASONS: readonly ["idle", "taken-over", "device-gone", "forbidden", "failed"];
462
+ /**
463
+ * The server ended a control session: no input for 5 minutes, another client took the device over, the device
464
+ * stopped or changed owner, the device lost `control`, or input could not reach the device.
465
+ */
466
+ interface ControlEndedEvent {
467
+ event: "control-ended";
468
+ session: string;
469
+ reason: (typeof CONTROL_END_REASONS)[number];
470
+ message: string;
471
+ }
472
+ type ServerEvent = StatusEvent | LogsEvent | FrameEvent | FrameDelayedEvent | ErrorEvent | ControlEndedEvent;
361
473
  type ServerMessage = ServerResponse | ServerEvent;
362
474
  type JsonSchema = Record<string, unknown>;
363
475
  /** The JSON Schema of every message on the wire, written to `dist/protocol.schema.json` at build time. */
364
476
  declare function protocolJsonSchema(): JsonSchema;
365
477
  //#endregion
366
- export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, Capability, ClientRequest, DeviceAuth, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, RequestId, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
478
+ export { ACTIONS, ActionName, ActionParams, ActionResult, BuildPlanParams, BuildPlanResult, CAPABILITIES, CONTROL_END_REASONS, Capability, ClientRequest, ControlBeginParams, ControlBeginResult, ControlEndParams, ControlEndedEvent, DEVICE_POSTURES, DeviceAuth, DevicePosture, ERROR_CODES, ErrorCode, ErrorEvent, FRAME_EDGE, FRAME_FPS, FrameDelayedEvent, FrameEvent, FrameTarget, FramesSubscribeResult, HelloParams, HelloResult, INPUT_BUTTONS, InputButton, InputButtonParams, InputPostureParams, InputRotateParams, InputTextParams, InputTouchParams, KeyframeParams, LOG_LEVELS, LOG_SOURCES, LogFilter, LogLevel, LogRecord, LogSource, LogsEvent, LogsQueryResult, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, MachineHistory, MachineHistoryParams, MachineUsage, MachineVolume, MemoryPressure, Method, Methods, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, PairingAuth, Platform, ProtocolError, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, ReloadPlatform, RequestId, RotateDirection, ServerEvent, ServerMessage, ServerResponse, SettingsResult, StatsResult, StatusEvent, SubscribeResult, TOUCH_PHASES, TouchPhase, UnsubscribeParams, UsageSample, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, VideoCodec, VideoPacket, WorkspaceParams, protocolJsonSchema };
package/dist/protocol.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as FRAME_FPS, c as MAX_LOG_TAIL, d as PROTOCOL_VERSION, f as VIDEO_CODECS, h as protocolJsonSchema, i as FRAME_EDGE, l as METHODS, m as VIDEO_KEYFRAME, n as CAPABILITIES, o as LOG_LEVELS, p as VIDEO_HEADER_VERSION, r as ERROR_CODES, s as LOG_SOURCES, t as ACTIONS, u as PROTOCOL_SCHEMA_FILE } from "./protocol-CmlG7X7_.mjs";
2
- export { ACTIONS, CAPABILITIES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, LOG_LEVELS, LOG_SOURCES, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, VIDEO_CODECS, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, protocolJsonSchema };
1
+ import { C as VIDEO_UNFOLDED, S as VIDEO_KEYFRAME, _ as ROTATE_DIRECTIONS, a as ERROR_CODES, b as VIDEO_FOLDED, c as INPUT_BUTTONS, d as MAX_INPUT_TEXT, f as MAX_LOG_TAIL, g as RELOAD_PLATFORMS, h as PROTOCOL_VERSION, i as DEVICE_POSTURES, l as LOG_LEVELS, m as PROTOCOL_SCHEMA_FILE, n as CAPABILITIES, o as FRAME_EDGE, p as METHODS, r as CONTROL_END_REASONS, s as FRAME_FPS, t as ACTIONS, u as LOG_SOURCES, v as TOUCH_PHASES, w as protocolJsonSchema, x as VIDEO_HEADER_VERSION, y as VIDEO_CODECS } from "./protocol-rC-5KT1O.mjs";
2
+ export { ACTIONS, CAPABILITIES, CONTROL_END_REASONS, DEVICE_POSTURES, ERROR_CODES, FRAME_EDGE, FRAME_FPS, INPUT_BUTTONS, LOG_LEVELS, LOG_SOURCES, MAX_INPUT_TEXT, MAX_LOG_TAIL, METHODS, PROTOCOL_SCHEMA_FILE, PROTOCOL_VERSION, RELOAD_PLATFORMS, ROTATE_DIRECTIONS, TOUCH_PHASES, VIDEO_CODECS, VIDEO_FOLDED, VIDEO_HEADER_VERSION, VIDEO_KEYFRAME, VIDEO_UNFOLDED, protocolJsonSchema };