lim 0.45.1 → 0.46.1

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 (229) hide show
  1. package/JSON_OUTPUT.md +123 -0
  2. package/README.md +34 -5
  3. package/bin/dev.js +6 -2
  4. package/bin/json-errors.cjs +35 -0
  5. package/bin/run.js +3 -0
  6. package/dist/base-command.d.ts +8 -2
  7. package/dist/base-command.d.ts.map +1 -1
  8. package/dist/base-command.js +77 -15
  9. package/dist/base-command.js.map +1 -1
  10. package/dist/base-command.snapshots.test.d.ts +2 -0
  11. package/dist/base-command.snapshots.test.d.ts.map +1 -0
  12. package/dist/base-command.snapshots.test.js +215 -0
  13. package/dist/base-command.snapshots.test.js.map +1 -0
  14. package/dist/commands/android/app-log.d.ts.map +1 -1
  15. package/dist/commands/android/app-log.js +10 -1
  16. package/dist/commands/android/app-log.js.map +1 -1
  17. package/dist/commands/android/camera.d.ts.map +1 -1
  18. package/dist/commands/android/camera.js +8 -2
  19. package/dist/commands/android/camera.js.map +1 -1
  20. package/dist/commands/android/connect.d.ts.map +1 -1
  21. package/dist/commands/android/connect.js +2 -2
  22. package/dist/commands/android/connect.js.map +1 -1
  23. package/dist/commands/android/create.d.ts.map +1 -1
  24. package/dist/commands/android/create.js +10 -1
  25. package/dist/commands/android/create.js.map +1 -1
  26. package/dist/commands/android/delete.js +1 -1
  27. package/dist/commands/android/delete.js.map +1 -1
  28. package/dist/commands/android/install-app.d.ts.map +1 -1
  29. package/dist/commands/android/install-app.js +4 -2
  30. package/dist/commands/android/install-app.js.map +1 -1
  31. package/dist/commands/android/launch-app.d.ts.map +1 -1
  32. package/dist/commands/android/launch-app.js +20 -5
  33. package/dist/commands/android/launch-app.js.map +1 -1
  34. package/dist/commands/android/logcat.js +1 -1
  35. package/dist/commands/android/logcat.js.map +1 -1
  36. package/dist/commands/android/open-url.d.ts.map +1 -1
  37. package/dist/commands/android/open-url.js +1 -1
  38. package/dist/commands/android/open-url.js.map +1 -1
  39. package/dist/commands/android/press-key.d.ts.map +1 -1
  40. package/dist/commands/android/press-key.js +1 -1
  41. package/dist/commands/android/press-key.js.map +1 -1
  42. package/dist/commands/android/record.d.ts.map +1 -1
  43. package/dist/commands/android/record.js +26 -5
  44. package/dist/commands/android/record.js.map +1 -1
  45. package/dist/commands/android/scroll.d.ts.map +1 -1
  46. package/dist/commands/android/scroll.js +7 -1
  47. package/dist/commands/android/scroll.js.map +1 -1
  48. package/dist/commands/android/set-wifi-bandwidth.d.ts.map +1 -1
  49. package/dist/commands/android/set-wifi-bandwidth.js +1 -1
  50. package/dist/commands/android/set-wifi-bandwidth.js.map +1 -1
  51. package/dist/commands/android/sync.d.ts.map +1 -1
  52. package/dist/commands/android/sync.js +10 -2
  53. package/dist/commands/android/sync.js.map +1 -1
  54. package/dist/commands/android/tap.d.ts.map +1 -1
  55. package/dist/commands/android/tap.js +1 -1
  56. package/dist/commands/android/tap.js.map +1 -1
  57. package/dist/commands/android/terminate-app.d.ts.map +1 -1
  58. package/dist/commands/android/terminate-app.js +1 -1
  59. package/dist/commands/android/terminate-app.js.map +1 -1
  60. package/dist/commands/android/tunnel/index.js +2 -2
  61. package/dist/commands/android/tunnel/index.js.map +1 -1
  62. package/dist/commands/android/type.d.ts.map +1 -1
  63. package/dist/commands/android/type.js +1 -1
  64. package/dist/commands/android/type.js.map +1 -1
  65. package/dist/commands/asset/delete.js +1 -1
  66. package/dist/commands/asset/delete.js.map +1 -1
  67. package/dist/commands/asset/pull.js +1 -1
  68. package/dist/commands/asset/pull.js.map +1 -1
  69. package/dist/commands/asset/push.d.ts.map +1 -1
  70. package/dist/commands/asset/push.js +4 -0
  71. package/dist/commands/asset/push.js.map +1 -1
  72. package/dist/commands/bazel/credentials/get.d.ts.map +1 -1
  73. package/dist/commands/bazel/credentials/get.js +6 -1
  74. package/dist/commands/bazel/credentials/get.js.map +1 -1
  75. package/dist/commands/gradle/build.d.ts +1 -0
  76. package/dist/commands/gradle/build.d.ts.map +1 -1
  77. package/dist/commands/gradle/build.js +21 -2
  78. package/dist/commands/gradle/build.js.map +1 -1
  79. package/dist/commands/gradle/delete.js +1 -1
  80. package/dist/commands/gradle/delete.js.map +1 -1
  81. package/dist/commands/gradle/sync.d.ts.map +1 -1
  82. package/dist/commands/gradle/sync.js +10 -2
  83. package/dist/commands/gradle/sync.js.map +1 -1
  84. package/dist/commands/ios/app-log.d.ts.map +1 -1
  85. package/dist/commands/ios/app-log.js +6 -3
  86. package/dist/commands/ios/app-log.js.map +1 -1
  87. package/dist/commands/ios/camera.d.ts.map +1 -1
  88. package/dist/commands/ios/camera.js +8 -2
  89. package/dist/commands/ios/camera.js.map +1 -1
  90. package/dist/commands/ios/create.d.ts +4 -0
  91. package/dist/commands/ios/create.d.ts.map +1 -1
  92. package/dist/commands/ios/create.js +17 -1
  93. package/dist/commands/ios/create.js.map +1 -1
  94. package/dist/commands/ios/delete.js +1 -1
  95. package/dist/commands/ios/delete.js.map +1 -1
  96. package/dist/commands/ios/keychain/generate-key.d.ts.map +1 -1
  97. package/dist/commands/ios/keychain/generate-key.js +2 -1
  98. package/dist/commands/ios/keychain/generate-key.js.map +1 -1
  99. package/dist/commands/ios/launch-app.d.ts.map +1 -1
  100. package/dist/commands/ios/launch-app.js +19 -4
  101. package/dist/commands/ios/launch-app.js.map +1 -1
  102. package/dist/commands/ios/maestro.d.ts.map +1 -1
  103. package/dist/commands/ios/maestro.js +4 -1
  104. package/dist/commands/ios/maestro.js.map +1 -1
  105. package/dist/commands/ios/open-url.d.ts.map +1 -1
  106. package/dist/commands/ios/open-url.js +1 -1
  107. package/dist/commands/ios/open-url.js.map +1 -1
  108. package/dist/commands/ios/press-key.d.ts.map +1 -1
  109. package/dist/commands/ios/press-key.js +1 -1
  110. package/dist/commands/ios/press-key.js.map +1 -1
  111. package/dist/commands/ios/record.d.ts.map +1 -1
  112. package/dist/commands/ios/record.js +28 -5
  113. package/dist/commands/ios/record.js.map +1 -1
  114. package/dist/commands/ios/scroll.d.ts.map +1 -1
  115. package/dist/commands/ios/scroll.js +9 -1
  116. package/dist/commands/ios/scroll.js.map +1 -1
  117. package/dist/commands/ios/swipe.d.ts.map +1 -1
  118. package/dist/commands/ios/swipe.js +1 -1
  119. package/dist/commands/ios/swipe.js.map +1 -1
  120. package/dist/commands/ios/sync.d.ts.map +1 -1
  121. package/dist/commands/ios/sync.js +12 -3
  122. package/dist/commands/ios/sync.js.map +1 -1
  123. package/dist/commands/ios/syslog.js +1 -1
  124. package/dist/commands/ios/syslog.js.map +1 -1
  125. package/dist/commands/ios/tap.d.ts.map +1 -1
  126. package/dist/commands/ios/tap.js +1 -1
  127. package/dist/commands/ios/tap.js.map +1 -1
  128. package/dist/commands/ios/terminate-app.d.ts.map +1 -1
  129. package/dist/commands/ios/terminate-app.js +1 -1
  130. package/dist/commands/ios/terminate-app.js.map +1 -1
  131. package/dist/commands/ios/toggle-keyboard.js +1 -1
  132. package/dist/commands/ios/toggle-keyboard.js.map +1 -1
  133. package/dist/commands/ios/tunnel/index.js +2 -2
  134. package/dist/commands/ios/tunnel/index.js.map +1 -1
  135. package/dist/commands/ios/type.d.ts.map +1 -1
  136. package/dist/commands/ios/type.js +7 -1
  137. package/dist/commands/ios/type.js.map +1 -1
  138. package/dist/commands/logout.js +1 -1
  139. package/dist/commands/logout.js.map +1 -1
  140. package/dist/commands/run.d.ts +4 -0
  141. package/dist/commands/run.d.ts.map +1 -1
  142. package/dist/commands/run.js +15 -1
  143. package/dist/commands/run.js.map +1 -1
  144. package/dist/commands/session/start.d.ts.map +1 -1
  145. package/dist/commands/session/start.js +2 -2
  146. package/dist/commands/session/start.js.map +1 -1
  147. package/dist/commands/session/status.js +2 -2
  148. package/dist/commands/session/status.js.map +1 -1
  149. package/dist/commands/session/stop.d.ts.map +1 -1
  150. package/dist/commands/session/stop.js +12 -5
  151. package/dist/commands/session/stop.js.map +1 -1
  152. package/dist/commands/tunnel/delete.d.ts.map +1 -1
  153. package/dist/commands/tunnel/delete.js +1 -1
  154. package/dist/commands/tunnel/delete.js.map +1 -1
  155. package/dist/commands/tunnel/run.d.ts.map +1 -1
  156. package/dist/commands/tunnel/run.js +6 -3
  157. package/dist/commands/tunnel/run.js.map +1 -1
  158. package/dist/commands/xcode/build.d.ts +1 -0
  159. package/dist/commands/xcode/build.d.ts.map +1 -1
  160. package/dist/commands/xcode/build.js +25 -7
  161. package/dist/commands/xcode/build.js.map +1 -1
  162. package/dist/commands/xcode/create.d.ts +1 -0
  163. package/dist/commands/xcode/create.d.ts.map +1 -1
  164. package/dist/commands/xcode/create.js +13 -4
  165. package/dist/commands/xcode/create.js.map +1 -1
  166. package/dist/commands/xcode/delete.js +1 -1
  167. package/dist/commands/xcode/delete.js.map +1 -1
  168. package/dist/commands/xcode/rbe/index.d.ts.map +1 -1
  169. package/dist/commands/xcode/rbe/index.js +6 -0
  170. package/dist/commands/xcode/rbe/index.js.map +1 -1
  171. package/dist/commands/xcode/sync.d.ts +4 -0
  172. package/dist/commands/xcode/sync.d.ts.map +1 -1
  173. package/dist/commands/xcode/sync.js +15 -2
  174. package/dist/commands/xcode/sync.js.map +1 -1
  175. package/dist/commands/xcode/test.d.ts +1 -0
  176. package/dist/commands/xcode/test.d.ts.map +1 -1
  177. package/dist/commands/xcode/test.js +1 -0
  178. package/dist/commands/xcode/test.js.map +1 -1
  179. package/dist/json-contract.test.d.ts +2 -0
  180. package/dist/json-contract.test.d.ts.map +1 -0
  181. package/dist/json-contract.test.js +703 -0
  182. package/dist/json-contract.test.js.map +1 -0
  183. package/dist/json-errors.test.d.ts +2 -0
  184. package/dist/json-errors.test.d.ts.map +1 -0
  185. package/dist/json-errors.test.js +95 -0
  186. package/dist/json-errors.test.js.map +1 -0
  187. package/dist/lib/build-command-helpers.d.ts +4 -1
  188. package/dist/lib/build-command-helpers.d.ts.map +1 -1
  189. package/dist/lib/build-command-helpers.js +22 -3
  190. package/dist/lib/build-command-helpers.js.map +1 -1
  191. package/dist/lib/build-run-command.d.ts.map +1 -1
  192. package/dist/lib/build-run-command.js +1 -1
  193. package/dist/lib/build-run-command.js.map +1 -1
  194. package/dist/lib/build-tools-command.d.ts.map +1 -1
  195. package/dist/lib/build-tools-command.js +1 -1
  196. package/dist/lib/build-tools-command.js.map +1 -1
  197. package/dist/lib/build-use-command.d.ts.map +1 -1
  198. package/dist/lib/build-use-command.js +9 -2
  199. package/dist/lib/build-use-command.js.map +1 -1
  200. package/dist/lib/build-use-command.test.js +11 -0
  201. package/dist/lib/build-use-command.test.js.map +1 -1
  202. package/dist/lib/cache-flags.d.ts +1 -0
  203. package/dist/lib/cache-flags.d.ts.map +1 -1
  204. package/dist/lib/cache-flags.js +5 -1
  205. package/dist/lib/cache-flags.js.map +1 -1
  206. package/dist/lib/cache-flags.test.js +8 -0
  207. package/dist/lib/cache-flags.test.js.map +1 -1
  208. package/dist/lib/cache.d.ts +1 -0
  209. package/dist/lib/cache.d.ts.map +1 -1
  210. package/dist/lib/cache.js +3 -0
  211. package/dist/lib/cache.js.map +1 -1
  212. package/dist/lib/daemon-client.d.ts +1 -1
  213. package/dist/lib/daemon-client.d.ts.map +1 -1
  214. package/dist/lib/daemon-client.js +2 -2
  215. package/dist/lib/daemon-client.js.map +1 -1
  216. package/dist/lib/instance-client-factory.d.ts.map +1 -1
  217. package/dist/lib/instance-client-factory.js +13 -3
  218. package/dist/lib/instance-client-factory.js.map +1 -1
  219. package/dist/lib/instance-client-factory.test.js +35 -0
  220. package/dist/lib/instance-client-factory.test.js.map +1 -1
  221. package/dist/lib/snapshot-project.d.ts +7 -0
  222. package/dist/lib/snapshot-project.d.ts.map +1 -0
  223. package/dist/lib/snapshot-project.js +194 -0
  224. package/dist/lib/snapshot-project.js.map +1 -0
  225. package/dist/lib/snapshot-project.test.d.ts +2 -0
  226. package/dist/lib/snapshot-project.test.d.ts.map +1 -0
  227. package/dist/lib/snapshot-project.test.js +156 -0
  228. package/dist/lib/snapshot-project.test.js.map +1 -0
  229. package/package.json +4 -3
package/JSON_OUTPUT.md ADDED
@@ -0,0 +1,123 @@
1
+ # CLI JSON contract
2
+
3
+ Use `--json` for automation on commands whose help lists that flag. JSON output is a public CLI interface. Preserve existing field names, types, and meanings when changing a command. Additive fields are allowed; consumers should ignore fields they do not use. Removing or renaming a field requires a documented compatibility change.
4
+
5
+ ## Transport and exit status
6
+
7
+ - Non-streaming commands write one JSON value to stdout. List commands return arrays, including `[]` for no results. Resource commands retain their API/SDK response fields, rather than rendering table cells as strings.
8
+ - Log streams, foreground builds and app launches, `sync --watch`, `tunnel run`, and `xcode test` write newline-delimited JSON. Each line is a complete JSON value. Newlines inside strings are escaped.
9
+ - Commands that hold a connection, such as `android connect`, device tunnels, reverse tunnels, and RBE, write their readiness result and stay running. Creation with `--rm` also stays running after its instance response. JSON changes output, not lifecycle.
10
+ - Human messages and SDK diagnostics never go to stdout in JSON mode. Diagnostics may appear on stderr. `--quiet --json` has the same JSON result as `--json`.
11
+ - Check the process exit status. Nonzero means failure. Execution commands can return a structured result with a nonzero `exitCode` or `code`; receiving JSON alone does not mean success. A failed mutation does not emit a success acknowledgement.
12
+ - CLI, validation, and API errors emit `{"error":{"code":"CLI_ERROR","message":"..."}}` on stderr. A specific error code replaces `CLI_ERROR` when available; HTTP errors also include numeric `status`. stderr may also contain diagnostics, so do not parse the whole stream as one JSON document. Explicit process exits after an execution result preserve the exit status without adding another result.
13
+ - Pass the CLI's `--json` before `--`. Flags after `--` belong to the invoked tool. Interactive commands that do not advertise JSON, including `login` and the onboarding `run`, reject it.
14
+
15
+ ## Instance and asset resources
16
+
17
+ | Commands | JSON result |
18
+ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `android create`, `ios create`, `xcode create`, `gradle create` | The created instance resource, including `metadata`, `spec`, and `status`. Existing attachment fields on iOS/Xcode creation are retained. |
20
+ | `android get`, `ios get`, `gradle get` | The instance resource. With no explicit or remembered instance, the existing fallback returns the corresponding list. |
21
+ | `xcode get` | The existing resolved Xcode target fields plus `simulator` status. |
22
+ | Instance `list` commands | Array of instance resources. |
23
+ | Instance `delete`, `asset delete`, `tunnel delete` | `{"ok":true,"id":"..."}`. The ID identifies the resource deleted. Instance deletion remains idempotent for an already-missing instance. |
24
+ | `asset push` | The uploaded asset resource, including `id`, `name`, and any API-provided expiry and URLs. |
25
+ | `asset list` | Array of asset resources. |
26
+ | `asset pull` | `{"localPath":"/absolute/path","bytes":123}`. |
27
+
28
+ `assets` aliases use the same contract as `asset`.
29
+
30
+ Android creation returns the same instance resource with or without `--connect`. The default still starts a background ADB tunnel. `--no-connect` skips it; JSON mode already suppresses browser opening. Foreground connection modes return the resource after starting the tunnel, before waiting for shutdown.
31
+
32
+ ```sh
33
+ lim android create --hard-timeout=10m --json > instance.json
34
+ jq -r '.metadata.id' instance.json
35
+
36
+ # Create without starting a local ADB tunnel.
37
+ lim android create --hard-timeout=10m --no-connect --json > instance.json
38
+ lim android connect --id "$(jq -r '.metadata.id' instance.json)"
39
+
40
+ lim asset push ./app.apk --json | jq -r .id
41
+ ```
42
+
43
+ Instance responses include access credentials. Store them with the same care as API responses.
44
+
45
+ ## Device actions and files
46
+
47
+ Existing JSON-enabled device commands keep their existing response shapes. In particular, element trees, selectors, device info, app lists, microphone operations, proxy operations, and file operations retain their API/SDK fields.
48
+
49
+ | Commands | JSON result |
50
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | iOS/Android `tap` | `{ok:true, instanceId, x, y}`; iOS adds `display` when explicitly selected. |
52
+ | iOS/Android `type` | `{ok:true, instanceId, textLength}`; iOS adds `enter` and `requireFocus`, Android adds `target` when selected. `textLength` counts UTF-16 code units; input text is not echoed. |
53
+ | iOS/Android `press-key` | `{ok:true, instanceId, key, modifiers}`. `modifiers` is an array, empty when none were requested. |
54
+ | iOS/Android `scroll` | `{ok:true, instanceId, direction, amount}`; iOS adds requested `momentum`, `coordinate: [x,y]`, and `display`. Android adds `target` when selected. |
55
+ | iOS/Android `open-url` | `{ok:true, instanceId, url}`. |
56
+ | iOS/Android `terminate-app` | `{ok:true, instanceId, bundleId}` on iOS; `packageName` on Android. |
57
+ | Detached iOS/Android `launch-app` | `{ok:true, instanceId, bundleId, mode}` on iOS; `packageName` on Android. `mode` is the effective launch mode, including defaults. iOS adds `runtime` when requested. Environment values are not echoed. |
58
+ | iOS `swipe` | `{ok:true, instanceId, from:[x,y], to:[x,y], durationMs}`; adds `display` when explicitly selected. |
59
+ | iOS `toggle-keyboard` | `{ok:true, instanceId}`. The API does not report the resulting keyboard visibility. |
60
+ | Android `install-app` | `{ok:true, instanceId, source}`. `source` is the supplied path or URL; local uploads also include `assetId`. Acknowledges sending the asset to the instance. |
61
+ | `ios install-app` | Existing installation result, including the bundle ID when supplied by the SDK. |
62
+ | `android set-wifi-bandwidth` | `{ok:true, instanceId, downKbps:0, upKbps:1000}`. Only requested directions are included; zero clears that limit. |
63
+ | iOS/Android `record start` | `{ok:true, instanceId, recording:true, quality, persist}`. Explicit retention adds `persistTtlSeconds`. iOS adds `touchIndicators` and, when requested, `display`. |
64
+ | iOS/Android `record stop` | `{ok:true, instanceId, recording:false, localPath, uploaded}`. `uploaded` is true after a requested presigned upload completes. |
65
+ | `ios keychain generate-key` | `{"key":"base64-encoded-key"}`. |
66
+ | `android connect` | `{"instanceId":"...","address":{"address":"127.0.0.1","port":12345}}` from the local tunnel. The command stays running. |
67
+
68
+ Camera `play` on both platforms returns `{ok:true, instanceId, action:"play", localPath, loop}`. Camera `clear` returns `{ok:true, instanceId, action:"clear"}`. Existing `ok` and `loop` fields are retained.
69
+
70
+ These action results identify the resolved instance, including when the CLI selected a remembered instance. Parameters describe the submitted action after it succeeds, not a separate observation of device state. Optional targeting fields are omitted when no override was supplied. Android `target` uses the SDK shape: `{x,y}` or `{selector:{resourceId, text, ...}}`.
71
+
72
+ Direct WebSocket connections and background session connections have the same command output contract.
73
+
74
+ ## Builds, execution, sync, and logs
75
+
76
+ | Commands | JSON result |
77
+ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
78
+ | Foreground `xcode build`, `gradle build` | NDJSON `{type:"stdout",line}` and `{type:"stderr",line}` events, followed by `{type:"result",instanceId,...SDK execution result}`. The result retains `execId`, `status`, `exitCode`, artifact/store fields, `timedOut`, and `incomplete`. |
79
+ | `xcode build --detach`, `gradle build --detach` | Existing `instanceId`, `execId`, `consoleUrl`, `logsCommand`, and optional `webhookUrl`. Acceptance does not mean completion. |
80
+ | Xcode/Gradle `run`, `tools`, `tools install`, mise `use` | SDK execution result plus captured `stdout` and `stderr` strings. Mixed `xcode use xcode@... node@...` adds the Xcode selection under `xcode`. Xcode-only selection keeps its existing selection response. |
81
+ | iOS `simctl`, `xcrun`, `xcodebuild`, `defaults`, `notify`; Android `adb-shell` | Existing tool execution response, with stdout, stderr, and the tool's `code` or `exitCode`. |
82
+ | `ios maestro` | `{"exitCode":0}`; Maestro output goes to stderr. |
83
+ | iOS/Android/Xcode/Gradle `sync` | SDK sync result, such as `bytesSent`, and optional installed app fields. No watcher function is serialized. |
84
+ | `sync --watch` | One SDK sync event per line, including `bytesSent`, `durationMs`, and optional installed app fields, for the initial sync and each update. |
85
+ | Android `app-log` without `--follow` | Shell result containing `stdout`, `stderr`, and `exitCode`. |
86
+ | iOS `app-log` without `--follow` | `{"stdout":"..."}`. |
87
+ | Android `app-log --follow` | One API app-log capture record per line. |
88
+ | iOS `app-log --follow`, Android `logcat`, iOS `syslog` | One `{"line":"..."}` record per line. |
89
+ | Foreground iOS/Android `launch-app` | The detached launch fields with `type:"launched"` instead of `ok`; iOS log events use `{type:"log",line}`. Exit events use `type:"exit"` and the SDK app-exit fields; Android includes its log tail in `logs`. |
90
+ | `xcode test` | Existing XCTest event records followed by the existing terminal `{exitCode, timedOut?, incomplete?}` record. |
91
+ | Xcode/Gradle `logs` | Existing collected execution-log response. |
92
+
93
+ Foreground build output events preserve the order received from the SDK and identify the original stream. Lines are not buffered until the build ends. Empty lines remain empty strings, and embedded newlines are escaped. Once execution returns, exactly one `result` event ends the stream, including for failed and timed-out executions. A CLI/API failure before an execution result is available emits an error on stderr instead. `--detach` retains its single acknowledgement object.
94
+
95
+ A `timedOut` execution result means the client stopped waiting. The remote execution may still be running. Inspect `incomplete` and follow its logs instead of assuming that retrying is harmless.
96
+
97
+ ```sh
98
+ lim gradle build . --json > build.ndjson 2> diagnostics.log
99
+ jq 'select(.type == "result")' build.ndjson
100
+ jq -r 'select(.type == "stdout" or .type == "stderr") | .line' build.ndjson
101
+ lim ios syslog --json | jq --unbuffered -r .line
102
+ lim xcode sync . --watch --json | jq --unbuffered .bytesSent
103
+ ```
104
+
105
+ ## Sessions and tunnels
106
+
107
+ | Commands | JSON result |
108
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `session start` | `{"instanceId":"...","running":true,"alreadyRunning":false}`. |
110
+ | `session stop` | `{"stopped":["instance-id"]}`. Empty if nothing was running; `--all` produces one array, not one response per session. Ambiguous selection fails. |
111
+ | `session status` | Existing array of session status objects. |
112
+ | Device tunnel start/status/stop, `ios reverse` | Existing readiness/status/stop responses. |
113
+ | `tunnel run` | One SDK connector event per line. |
114
+ | Private tunnel create/list/update/rotate | Existing API tunnel responses and token fields. |
115
+ | `xcode rbe` | Existing readiness result. An already-running tunnel returns its stored process fields plus `running: true`. |
116
+ | `xcode rbe --stop` | `{"stopped":false}` if none is running; otherwise `{stopped:true, forced, instanceId, pid}`. |
117
+ | `xcode rbe upload`, version selection, set-instance, and `skills install` | Existing structured results are unchanged. |
118
+
119
+ ## Maintaining the contract
120
+
121
+ Changes must keep JSON stdout free of human text on success, failure, empty results, daemon and direct connections, and foreground and detached paths. Use `outputJson` for finite results, `outputEvent` for streams, and `outputResult` for data with a human-readable alternative. Do not use plain logging or inherited child stdout for JSON results. Configure SDK clients so their informational logging cannot reach stdout.
122
+
123
+ The CLI tests exercise response shapes, stream framing, both device transports, default Android creation, foreground and detached behavior, failures, and the executable's error handling. Add a contract test when adding a new JSON branch or changing a response.
package/README.md CHANGED
@@ -498,16 +498,31 @@ lim xcode version list # Xcode versions the sandbox can build with
498
498
 
499
499
  ### Disk snapshots
500
500
 
501
- Restore and save build state across Xcode instances with a snapshot key:
501
+ New Xcode instances automatically restore and save a disk snapshot using the
502
+ Git origin host/repository and project subdirectory, for example
503
+ `github.com/acme/monorepo/apps/ios`. SSH and HTTPS origins produce the same key;
504
+ branches and commits share it. The key alone controls both restore and save.
505
+ Discovery reads repository-local Git metadata directly, including worktree and
506
+ submodule pointers, so it does not require the Git executable. Global Git
507
+ settings and URL rewrites are not used; config includes require an explicit key.
508
+
509
+ `build`, `test`, and `sync` use their project path; `create` uses the current
510
+ directory. `lim run` uses its detected project, and `lim ios create --xcode`
511
+ uses the current directory. Existing instances keep their original snapshot settings. Without
512
+ a usable Git origin, pass `--snapshot-key` explicitly. Use `--no-snapshot` to
513
+ create an uncached instance.
514
+
515
+ From the project directory:
502
516
 
503
517
  ```bash
504
- XCODE_ID=$(lim xcode create --snapshot-key myapp-main --quiet)
518
+ XCODE_ID=$(lim xcode create --quiet)
505
519
  lim xcode build . --id "$XCODE_ID" --scheme MyApp
506
520
  lim xcode delete "$XCODE_ID" --wait-snapshot
507
521
  ```
508
522
 
509
523
  The first build starts cold. Termination publishes the workspace, and the next
510
- instance restores it. Use `--snapshot-restore-keys "myapp-pr51,myapp-main"` at
524
+ instance restores it. Override with `--snapshot-key` when separate branches or
525
+ Xcode versions need independent saved state. Use `--snapshot-restore-keys "myapp-pr51,myapp-main"` at
511
526
  creation for ordered exact-key and literal-prefix fallbacks. Leave
512
527
  `--snapshot-paths` unset to save the whole workspace. On existing instances,
513
528
  restore keys and paths stay fixed; `build --snapshot-key` only binds a publication
@@ -994,7 +1009,7 @@ journey remains continuous. Set `LIM_DISABLE_TELEMETRY=1` to disable collection.
994
1009
 
995
1010
  ## JSON Output
996
1011
 
997
- All commands support `--json` for machine-readable output, making the CLI suitable for scripting and AI agent automation:
1012
+ Commands that advertise `--json` provide a machine-readable CLI contract. Finite commands return one JSON value; streams and watch modes use newline-delimited JSON. Human-readable diagnostics go to stderr. Always check the exit status. See [the JSON output contract](./JSON_OUTPUT.md) for response shapes, stream framing, and errors:
998
1013
 
999
1014
  ```bash
1000
1015
  # Get instance details as JSON
@@ -1213,7 +1228,21 @@ lim gradle run --env APP_ENV=staging -- npm run generate
1213
1228
  lim gradle build . --env APP_ENV=staging
1214
1229
  ```
1215
1230
 
1216
- `--additional-file localPath=remotePath` adds a file outside the source tree; its remote path is relative to the workspace.
1231
+ `lim gradle build`, `sync`, and `run` accept `--additional-file localPath=remotePath`
1232
+ to add files outside the source tree. Repeat the flag for multiple files. Remote
1233
+ paths starting with `~/` target the sandbox home used by builds and tool commands;
1234
+ other paths are relative to the workspace.
1235
+ For private npm dependencies, copy an existing registry configuration:
1236
+
1237
+ ```bash
1238
+ lim gradle build . \
1239
+ --additional-file "$HOME/.npmrc=~/.npmrc" \
1240
+ --additional-file "$HOME/.netrc=~/.netrc"
1241
+ ```
1242
+
1243
+ Pass the flag on each sync that needs the file; omitting it removes the previously
1244
+ synced file. Home files are shared by projects on the instance. If the configuration references
1245
+ an environment variable, pass it with `--env` as well.
1217
1246
 
1218
1247
  `run` syncs the current directory first. Add `--no-sync` to use the existing
1219
1248
  remote workspace. Its optional positional directory is relative to that
package/bin/dev.js CHANGED
@@ -9,8 +9,12 @@ require('ts-node').register({ project });
9
9
  const oclif = require('@oclif/core');
10
10
 
11
11
  oclif.settings.debug = true;
12
+ process.env.NODE_ENV = 'development';
13
+ const { handleJsonError } = require('./json-errors.cjs');
12
14
 
13
15
  oclif
14
- .execute({ development: true, dir: __dirname, args: process.argv.slice(2) })
16
+ .run(process.argv.slice(2), __dirname)
15
17
  .then(oclif.flush)
16
- .catch(oclif.Errors.handle);
18
+ .catch(async (error) => {
19
+ if (!(await handleJsonError(error))) return oclif.Errors.handle(error);
20
+ });
@@ -0,0 +1,35 @@
1
+ // Keep parser and command errors machine-readable, including failures before flags are parsed.
2
+ async function handleJsonError(error, argv = process.argv.slice(2)) {
3
+ const delimiter = argv.indexOf('--');
4
+ if (!argv.slice(0, delimiter < 0 ? argv.length : delimiter).includes('--json')) return false;
5
+ const exitCode = error?.oclif?.exit ?? error?.exitCode ?? 1;
6
+ process.exitCode = exitCode;
7
+ // Explicit exits follow an already-emitted execution result, or a successful early exit.
8
+ if (error?.code !== 'EEXIT') {
9
+ process.stderr.write(
10
+ JSON.stringify({
11
+ error: {
12
+ code: typeof error?.code === 'string' ? error.code : 'CLI_ERROR',
13
+ message: error instanceof Error ? error.message : String(error),
14
+ ...(typeof error?.status === 'number' ? { status: error.status } : {}),
15
+ },
16
+ }) + '\n',
17
+ );
18
+ }
19
+
20
+ // SDK sockets can outlive a failed command. Flush both streams before forcing exit,
21
+ // with the same ten-second flush budget as oclif for a stalled output consumer.
22
+ const deadline = setTimeout(() => process.exit(exitCode), 10_000);
23
+ deadline.unref();
24
+ try {
25
+ await Promise.all(
26
+ [process.stdout, process.stderr].map((stream) => new Promise((resolve) => stream.write('', resolve))),
27
+ );
28
+ } finally {
29
+ clearTimeout(deadline);
30
+ process.exit(exitCode);
31
+ }
32
+ return true;
33
+ }
34
+
35
+ module.exports = { handleJsonError };
package/bin/run.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const { handleJsonError } = require('./json-errors.cjs');
4
+
3
5
  const { flush } = require('@oclif/core');
4
6
  const { Config } = require('@oclif/core/config');
5
7
  const { handle } = require('@oclif/core/errors');
@@ -36,6 +38,7 @@ async function inferCommandArgv(argv) {
36
38
  }
37
39
 
38
40
  async function handleError(error) {
41
+ if (await handleJsonError(error)) return;
39
42
  if (!isUsageError(error)) {
40
43
  await handle(error);
41
44
  return;
@@ -39,6 +39,7 @@ export declare abstract class BaseCommand extends Command {
39
39
  private _client?;
40
40
  protected get client(): Limrun;
41
41
  private _parsedFlags?;
42
+ protected snapshotProjectPath?: string;
42
43
  private _lastResolvedInstanceId?;
43
44
  private _lastResolvedExpectedType?;
44
45
  private _xcodeReplacementIntent?;
@@ -57,7 +58,11 @@ export declare abstract class BaseCommand extends Command {
57
58
  /** ` in this workspace (<key>)`, or empty when resolved to the shared global slot. */
58
59
  protected scopeSuffix(): string;
59
60
  protected info(message?: string): void;
61
+ log(message?: string, ...args: any[]): void;
60
62
  protected output(message?: string): void;
63
+ protected outputResult(data: unknown, message: string): void;
64
+ /** Streams use one complete JSON value per line, including embedded newlines. */
65
+ protected outputEvent(data: unknown): void;
61
66
  /** Adapter passed to the shared tunnel command flows in lib/tunnel-command. */
62
67
  protected tunnelCommandIO(): TunnelCommandIO;
63
68
  protected withAuth<T>(fn: () => Promise<T>, options?: {
@@ -97,8 +102,9 @@ export declare abstract class BaseCommand extends Command {
97
102
  private createAndroidInstance;
98
103
  /** Whether `id` is a server-side instance THIS invocation auto-created. */
99
104
  protected wasCreatedThisRun(id: string | undefined): boolean;
100
- /** Build cache configuration from this command's flags, if it has any. */
105
+ /** Defaults apply only at creation; existing targets keep their snapshot configuration. */
101
106
  protected cacheConfigFromFlags(): XcodeCacheConfig | undefined;
107
+ protected buildSnapshotTip(instanceId: string): Promise<string>;
102
108
  /**
103
109
  * Blocks while a freshly created instance restores its cache, printing each phase as it
104
110
  * happens. A restore that fell back to a cold workspace just prints and returns, because the
@@ -149,7 +155,7 @@ export declare abstract class BaseCommand extends Command {
149
155
  status: Limrun.XcodeStatus;
150
156
  } | undefined>;
151
157
  /** Save the workspace preference and switch its existing sandbox when available. */
152
- protected setPreferredXcodeVersion(version: string, providedId?: string): Promise<void>;
158
+ protected setPreferredXcodeVersion(version: string, providedId?: string, renderJson?: (result: unknown) => void): Promise<void>;
153
159
  /** setXcode with the daemon's own refusal message instead of the transport's wrapping. */
154
160
  protected selectXcode(xcodeClient: XcodeClient, version: string): Promise<XcodeSelectResult>;
155
161
  private readXcodeSelection;
@@ -1 +1 @@
1
- {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"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"}
@@ -48,6 +48,7 @@ const api_2 = require("@limrun/api");
48
48
  const cache_1 = require("./lib/cache");
49
49
  const xcode_version_1 = require("./lib/xcode-version");
50
50
  const telemetry_1 = require("./lib/telemetry");
51
+ const snapshot_project_1 = require("./lib/snapshot-project");
51
52
  const VERSION = require('../package.json').version;
52
53
  // Full instance-id shape only: prefix_region_suffix with a long TypeID suffix.
53
54
  // TypeIDs are lowercase, so no /i flag; this avoids matching bare prefix words
@@ -79,7 +80,12 @@ class BaseCommand extends core_1.Command {
79
80
  // A management call with the placeholder gets a 403 marked
80
81
  // "unauthenticated:", which withAuth turns into login guidance or a
81
82
  // re-login.
82
- this._client = new api_1.default({ apiKey: apiKey || 'unauthenticated', baseURL });
83
+ this._client = new api_1.default({
84
+ apiKey: apiKey || 'unauthenticated',
85
+ baseURL,
86
+ userAgent: `lim/${VERSION}`,
87
+ ...(this.shouldSuppressInfo() ? { logLevel: 'warn' } : {}),
88
+ });
83
89
  }
84
90
  return this._client;
85
91
  }
@@ -133,8 +139,25 @@ class BaseCommand extends core_1.Command {
133
139
  super.log(message);
134
140
  }
135
141
  }
142
+ // Keep stdout machine-readable even if a human message bypasses info().
143
+ log(message = '', ...args) {
144
+ if (this.isJsonEnabled())
145
+ this.logToStderr(message, ...args);
146
+ else
147
+ super.log(message, ...args);
148
+ }
136
149
  output(message = '') {
137
- super.log(message);
150
+ this.log(message);
151
+ }
152
+ outputResult(data, message) {
153
+ if (this.isJsonEnabled())
154
+ this.outputJson(data);
155
+ else
156
+ this.output(message);
157
+ }
158
+ /** Streams use one complete JSON value per line, including embedded newlines. */
159
+ outputEvent(data) {
160
+ super.log(JSON.stringify(data));
138
161
  }
139
162
  /** Adapter passed to the shared tunnel command flows in lib/tunnel-command. */
140
163
  tunnelCommandIO() {
@@ -225,14 +248,14 @@ class BaseCommand extends core_1.Command {
225
248
  });
226
249
  return obj;
227
250
  });
228
- this.output(JSON.stringify(objects, null, 2));
251
+ this.outputJson(objects);
229
252
  }
230
253
  else {
231
254
  this.output((0, formatting_1.renderTable)(headers, rows));
232
255
  }
233
256
  }
234
257
  outputJson(data) {
235
- this.output(JSON.stringify(data, null, 2));
258
+ super.log(JSON.stringify(data, null, 2));
236
259
  }
237
260
  /**
238
261
  * Runs the sibling `list` command (e.g. `ios:list` for `ios get`), forwarding
@@ -268,7 +291,11 @@ class BaseCommand extends core_1.Command {
268
291
  async resolveXcodeClient(target) {
269
292
  if (target.apiUrl && target.token) {
270
293
  try {
271
- return await this.client.xcodeInstances.createClient({ apiUrl: target.apiUrl, token: target.token });
294
+ return await this.client.xcodeInstances.createClient({
295
+ apiUrl: target.apiUrl,
296
+ token: target.token,
297
+ logLevel: this.shouldSuppressInfo() ? 'warn' : 'info',
298
+ });
272
299
  }
273
300
  catch (err) {
274
301
  if (this.isCachedXcodeClientNotFound(err)) {
@@ -279,7 +306,10 @@ class BaseCommand extends core_1.Command {
279
306
  }
280
307
  const instance = await this.client.xcodeInstances.get(target.id);
281
308
  (0, config_1.registerCreatedInstance)(instance);
282
- return this.client.xcodeInstances.createClient({ instance });
309
+ return this.client.xcodeInstances.createClient({
310
+ instance,
311
+ logLevel: this.shouldSuppressInfo() ? 'warn' : 'info',
312
+ });
283
313
  }
284
314
  tryResolveAndroidInstance(providedId) {
285
315
  this._lastResolvedExpectedType = 'android';
@@ -617,17 +647,42 @@ class BaseCommand extends core_1.Command {
617
647
  wasCreatedThisRun(id) {
618
648
  return !!id && this._instancesCreatedThisRun.has(id);
619
649
  }
620
- /** Build cache configuration from this command's flags, if it has any. */
650
+ /** Defaults apply only at creation; existing targets keep their snapshot configuration. */
621
651
  cacheConfigFromFlags() {
622
652
  const flags = this.parsedFlags;
623
653
  if (!flags)
624
654
  return undefined;
625
655
  const asString = (name) => typeof flags[name] === 'string' ? flags[name] : undefined;
626
- return (0, cache_1.parseSnapshotConfig)({
656
+ const explicit = {
657
+ 'no-snapshot': flags['no-snapshot'] === true,
627
658
  'snapshot-key': asString('snapshot-key'),
628
659
  'snapshot-restore-keys': asString('snapshot-restore-keys'),
629
660
  'snapshot-paths': asString('snapshot-paths'),
630
- });
661
+ };
662
+ const config = (0, snapshot_project_1.projectSnapshotConfig)(explicit, this.snapshotProjectPath ?? process.cwd());
663
+ if (!(0, cache_1.parseSnapshotConfig)(explicit) && !explicit['no-snapshot']) {
664
+ this.info(config?.key ?
665
+ `Disk snapshot key: ${config.key}`
666
+ : 'Could not derive a disk snapshot key from Git origin. Use --snapshot-key to enable snapshots.');
667
+ }
668
+ return config;
669
+ }
670
+ async buildSnapshotTip(instanceId) {
671
+ if (this.isJsonEnabled() || this.parsedFlags?.['no-snapshot'])
672
+ return '';
673
+ try {
674
+ const snapshot = await this.client.xcodeInstances.getCache(instanceId, {
675
+ timeout: 2000,
676
+ maxRetries: 0,
677
+ });
678
+ if (snapshot.config)
679
+ return '';
680
+ }
681
+ catch {
682
+ // Unknown snapshot state must not change the build result or suggest a redundant key.
683
+ return '';
684
+ }
685
+ return '\nTip: Use --snapshot-key <project-key> for disk snapshots to build under 60 seconds!';
631
686
  }
632
687
  /**
633
688
  * Blocks while a freshly created instance restores its cache, printing each phase as it
@@ -808,7 +863,7 @@ class BaseCommand extends core_1.Command {
808
863
  return status ? { target, client, status } : undefined;
809
864
  }
810
865
  /** Save the workspace preference and switch its existing sandbox when available. */
811
- async setPreferredXcodeVersion(version, providedId) {
866
+ async setPreferredXcodeVersion(version, providedId, renderJson = (result) => this.outputJson(result)) {
812
867
  const previous = (0, config_1.loadXcodeVersionPreference)();
813
868
  // Keep the workspace preference on transient failures, but reject versions the node cannot provide.
814
869
  const record = () => {
@@ -840,13 +895,13 @@ class BaseCommand extends core_1.Command {
840
895
  record();
841
896
  if (!target || !result) {
842
897
  if (this.isJsonEnabled())
843
- this.outputJson({ preferred: version });
898
+ renderJson({ preferred: version });
844
899
  else
845
900
  this.output(`No sandbox instance found${this.scopeSuffix()}; the next one uses it.`);
846
901
  return;
847
902
  }
848
903
  if (this.isJsonEnabled()) {
849
- this.outputJson({ instanceId: target.id, preferred: version, ...result });
904
+ renderJson({ instanceId: target.id, preferred: version, ...result });
850
905
  return;
851
906
  }
852
907
  const verb = result.alreadyBound ? 'already uses' : 'now uses';
@@ -1092,11 +1147,18 @@ class BaseCommand extends core_1.Command {
1092
1147
  }
1093
1148
  async resolveGradleClient(target) {
1094
1149
  if (target.apiUrl && target.token) {
1095
- return this.client.gradleInstances.createClient({ apiUrl: target.apiUrl, token: target.token });
1150
+ return this.client.gradleInstances.createClient({
1151
+ apiUrl: target.apiUrl,
1152
+ token: target.token,
1153
+ logLevel: this.shouldSuppressInfo() ? 'warn' : 'info',
1154
+ });
1096
1155
  }
1097
1156
  const instance = await this.client.gradleInstances.get(target.id);
1098
1157
  (0, config_1.registerCreatedInstance)(instance);
1099
- return this.client.gradleInstances.createClient({ instance });
1158
+ return this.client.gradleInstances.createClient({
1159
+ instance,
1160
+ logLevel: this.shouldSuppressInfo() ? 'warn' : 'info',
1161
+ });
1100
1162
  }
1101
1163
  async resolveBuildToolClient(platform, id, mode = 'work') {
1102
1164
  if (platform === 'gradle') {
@@ -1132,7 +1194,7 @@ BaseCommand.baseFlags = {
1132
1194
  env: 'LIM_API_KEY',
1133
1195
  }),
1134
1196
  json: core_1.Flags.boolean({
1135
- description: 'Output structured JSON instead of human-readable tables or plain text when the command supports it.',
1197
+ description: 'Output JSON for automation. Streams and watch modes emit newline-delimited JSON. Human-readable diagnostics go to stderr.',
1136
1198
  default: false,
1137
1199
  }),
1138
1200
  quiet: core_1.Flags.boolean({