@stim-cli/server 1.17.0 → 1.17.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.
package/README.md CHANGED
@@ -57,7 +57,7 @@ app (checked at most every 5 seconds), or `null` when the check fails. Stim
57
57
  Desktop uses it to find a running server and show its route. A request through `tailscale serve` or on a Tailscale address
58
58
  without an `Origin` or `Sec-Fetch-Site` header, which a web page's request
59
59
  carries, gets only `{ "server": "stim-server", "version", "protocol" }`, which Stim
60
- Desktop's Build machines list uses to find stim-server on the other Macs of the
60
+ Desktop's Remote Macs list uses to find stim-server on the other Macs of the
61
61
  tailnet. A request from this Mac with a `Host` other than `127.0.0.1` or
62
62
  `localhost` gets HTTP 426, like any other plain HTTP request.
63
63
 
@@ -91,7 +91,7 @@ A person on the worker runs the command generated for their client Mac:
91
91
  ```bash
92
92
  stim-server setup --client <node-id> --ticket <43-base64url-characters> --expires <ISO-time> \
93
93
  --build --device-host [--port <n>] [--label <name>] \
94
- [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
94
+ [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
95
95
  ```
96
96
 
97
97
  If the server is not installed globally, use `npx --yes --package @stim-cli/server@<version> stim-server`
@@ -144,8 +144,17 @@ unreadable routes refuse with a remedy. Tool checks print fixes for Xcode,
144
144
  simulator runtimes, CocoaPods, JDK, Android SDK and the Stim build; they install
145
145
  nothing. Android and CocoaPods checks are not needed for hosting alone.
146
146
 
147
- Plain progress goes to stdout; errors use `stim-server: <message>` on stderr.
148
- `--json` sends progress to stderr and prints one final payload with `ok`,
147
+ In a terminal, setup prints a banner, then one line per step with a green check,
148
+ a yellow arrow while a step runs, or a red cross; `NO_COLOR`, `TERM=dumb` and a
149
+ non-terminal give plain `[ok]`, `[failed]` and `[pending]` marks, no colors and no
150
+ in-place line. The y/N questions stay, one line each. The run ends with a
151
+ headline such as "janics-mac-mini is ready to build for Janic's MacBook Pro", one
152
+ line per approved capability, a `Fix` line for each skipped permission or missing
153
+ tool, and one `To undo` line. `--verbose` prints every step with its detail, the
154
+ install, service and route notes, and the long summary with the undo commands, as
155
+ earlier releases did. Plain progress goes to stdout; errors use
156
+ `stim-server: <message>` on stderr (when stdout is a terminal, the refusal appears on stdout only).
157
+ `--json` sends the long (verbose) progress to stderr, without the banner, and prints one final payload with `ok`,
149
158
  `label`, `port`, `route` (state, DNS name, HTTPS port), `server` (version and
150
159
  Stim build), `managed`, separate `granted` request ids and client identities,
151
160
  `permissions`, `tools` and `warnings`. Final permission values are `granted`,
@@ -172,7 +181,7 @@ with detail `interrupted`, completes the journal, releases the setup claim,
172
181
  and exits 1. A typed N also exits 1; a question timeout follows ticket expiry
173
182
  and exits 2.
174
183
 
175
- The summary lists what is ready and the undo commands:
184
+ The `To undo` line (or the `--verbose` summary) lists the undo commands:
176
185
 
177
186
  ```bash
178
187
  stim-server devices revoke <build-request-id>
@@ -284,7 +293,7 @@ registration check. `hello` reports
284
293
  the capabilities and actions of the connection's device when it connects; a
285
294
  connection sees a new grant after it reconnects. Under Stim Host, an approved
286
295
  `hello` also carries the same `host` grants as `/health`, which `stim doctor`
287
- reports for hosting machines.
296
+ reports for remote Macs.
288
297
 
289
298
  Every method other than `hello` needs `read`. A device with only `build`
290
299
  gets `forbidden` for all of them.
@@ -335,7 +344,7 @@ bundle, Android APK or prebuilt macOS app, stream the iOS simulator, Android emu
335
344
  iOS and Android apps connect back to Metro on the client Mac. Automatic placement remains in [#2266](https://github.com/appandflow/stim/issues/2266).
336
345
 
337
346
  The Stim client can name expected hosts with
338
- `stim settings set hosting.machines '["<mac>"]'` and request access with
347
+ `stim settings set remote.machines '["<mac>"]'` and request access with
339
348
  `stim doctor --fix`. It stores a separate private credential in
340
349
  `$STIM_HOME/device-host-machines.json`, pins the worker's tailnet node, and
341
350
  reports approval under `deviceHosts` in doctor JSON. Plain doctor makes no
@@ -824,8 +833,8 @@ manifest requests are not launch proof; development remains `unverified` until
824
833
  the workspace observes the app's own bundle delivery.
825
834
  Approved clients use `stim ios|android --remote auto` for automatic device placement.
826
835
  They stay local while there is a free slot, no queued device run, normal memory
827
- pressure, no budget refusal and load below `offload.maxLoadPerCore`. Otherwise
828
- they rank compatible offers from `hosting.machines` and select a host, or wait
836
+ pressure, no budget refusal and load below `server.maxLoadPerCore`. Otherwise
837
+ they rank compatible offers from `remote.machines` and select a host, or wait
829
838
  locally if none admits. A reserve refusal after an offer fails with
830
839
  `STIM_HOSTING_REFUSED` and asks to retry; it does not re-place the built app.
831
840
  Automatic native macOS placement remains separate work.
@@ -938,7 +947,7 @@ before replacement. Automatic placement remains in
938
947
  ## Run as a service
939
948
 
940
949
  `stim-server service install` runs stim-server as a per-user LaunchAgent on
941
- macOS, so a build machine or a phone-serving Mac keeps it running without a
950
+ macOS, so a remote Mac or a phone-serving Mac keeps it running without a
942
951
  terminal:
943
952
 
944
953
  ```bash
@@ -1079,18 +1088,18 @@ and uninstall of one label run one at a time.
1079
1088
  installed and, when `install` created it, the serve route. Logs, pairings and the host app stay; other labels may use the app. Use `--label` and `--port` to run a
1080
1089
  second service beside the first, for example with another `STIM_HOME`.
1081
1090
 
1082
- To set up a build machine: install Stim and stim-server on the Mac, run
1091
+ To set up a remote Mac: install Stim and stim-server on the Mac, run
1083
1092
  `stim-server service install --serve` (with the pins its toolchain needs), set
1084
- `offload.workerRoot` there, and on each client run
1085
- `stim settings set offload.machines '["<mac>"]'` and `stim doctor --fix`.
1086
- Approve the request on the build machine with
1093
+ `server.workerRoot` there, and on each client run
1094
+ `stim settings set remote.machines '["<mac>"]'` and `stim doctor --fix`.
1095
+ Approve the request on the remote Mac with
1087
1096
  `stim-server devices grant <id> --build`.
1088
1097
 
1089
1098
  ## Remote update
1090
1099
 
1091
1100
  A Mac this one approved for builds (`--build`) or device hosting
1092
1101
  (`--device-host`) can update this Mac's `stim-server service` over its tailnet
1093
- connection, so the build machine keeps up with the client without ssh. A
1102
+ connection, so the remote Mac keeps up with the client without ssh. A
1094
1103
  server that offers it lists `server-update` in its `hello` features. A paired
1095
1104
  phone, a `read` or `control` device and a connection from this Mac cannot. The
1096
1105
  methods need that approval, not `read`:
@@ -1126,8 +1135,8 @@ of an existing hosted session still answers during the drain.
1126
1135
 
1127
1136
  Stim Desktop starts these updates through its own local stim-server with two
1128
1137
  methods that only the local Desktop control connection may call:
1129
- `machines.update.start` takes `{ "machine" }`, an approved `offload.machines`
1130
- or `hosting.machines` entry. It connects to that machine's pinned node with
1138
+ `machines.update.start` takes `{ "machine" }`, an approved `remote.machines`
1139
+ entry. It connects to that machine's pinned node with
1131
1140
  the stored credential and asks it for this server's npm release. When this
1132
1141
  server runs from a Stim checkout, it instead packs the checkout's workspace
1133
1142
  packages as `npm pack` would and sends them. `machines.update.status` takes
@@ -1160,7 +1169,7 @@ closes its connections and cancels its builds.
1160
1169
  average per CPU), `builds` (this Mac's own Stim runs in prebuild, pods or
1161
1170
  compile that it did not offload, plus the offloaded builds it runs), `maxBuilds` (its
1162
1171
  `concurrency.maxBuilds`, 0 when unlimited), `maxLoadPerCore` (its
1163
- `offload.maxLoadPerCore`, default 2), and `declined`: why it would refuse a
1172
+ `server.maxLoadPerCore`, default 2), and `declined`: why it would refuse a
1164
1173
  build now, or null. It declines while it runs its limit of offloaded builds,
1165
1174
  while the worker root's volume has less than 10 GiB free, while `builds`
1166
1175
  reaches a non-zero `maxBuilds`, or while `loadPerCore` is at or above
@@ -1197,7 +1206,7 @@ closes its connections and cancels its builds.
1197
1206
  `assemble<variant>` for `abi` with ccache under the area's Stim home. A
1198
1207
  Gradle daemon leaves the build's process group, so no claim or slot tracks
1199
1208
  it. An Android build keeps the client's daemon warm, with Gradle's idle
1200
- timeout set to `offload.gradleDaemonIdleMinutes` (default 30; 0 stops it
1209
+ timeout set to `server.gradleDaemonIdleMinutes` (default 30; 0 stops it
1201
1210
  when the build ends), and stops it when the build gets SIGTERM. Gradle
1202
1211
  fixes a daemon's idle timeout when the daemon starts, so a new value
1203
1212
  applies to the next daemon. Once no build of that client runs, stim-server
@@ -1269,7 +1278,7 @@ closes its connections and cancels its builds.
1269
1278
  is cancelled; one left by a server that crashed stays under
1270
1279
  `repos/<repo>/out/` until you delete it.
1271
1280
 
1272
- The worker root is `offload.workerRoot` in this Mac's Stim settings, or
1281
+ The worker root is `server.workerRoot` in this Mac's Stim settings, or
1273
1282
  `$STIM_HOME/build-worker`. Each client gets `<root>/<device id>/`, with its
1274
1283
  blobs, caches (`CP_HOME_DIR`, `CP_CACHE_DIR`, the pnpm store and
1275
1284
  `GRADLE_USER_HOME` and SwiftPM) and one area
@@ -1712,10 +1721,10 @@ Events are `{ "event", "subscription", ... }`.
1712
1721
  one result for 60 seconds, shared by every connection: a request while the
1713
1722
  commands run waits for them, and `measuredAt` says when they started. The
1714
1723
  commands fail after 150 seconds. Servers that predate it answer
1715
- `unknown-method`. It also carries `buildMachines`, the `buildMachines` list
1724
+ `unknown-method`. It also carries `buildMachines`, the `remoteMachines` list
1716
1725
  of `stim doctor --json --platform ios` (never with `--fix`), so a phone can
1717
- show whether each build machine takes builds. The server runs doctor only
1718
- when `offload.machines` names a machine, else the list is empty; it runs it
1726
+ show whether each remote Mac takes builds. The server runs doctor only
1727
+ when `remote.machines` names a machine, else the list is empty; it runs it
1719
1728
  in the registered workspace that ran doctor for iOS most recently, because
1720
1729
  doctor judges a machine against an app. Like a doctor run from a terminal,
1721
1730
  that records the run for the workspace. `buildMachines` is null with
@@ -1727,7 +1736,7 @@ Events are `{ "event", "subscription", ... }`.
1727
1736
  at a time, and sets `buildMachinesPending` while that background run has not
1728
1737
  settled a result yet, or the settled one is 60 seconds old or older. A client
1729
1738
  that wants the refreshed result asks `machine.details` again. It also
1730
- carries `buildClients`, the builds this Mac ran as a build machine, one entry
1739
+ carries `buildClients`, the builds this Mac ran for other Macs, one entry
1731
1740
  per client Mac read from the `build` records of the audit log, with `id`,
1732
1741
  `name`, `builds`, `failed`, `buildMs`, `lastAt` and the same three counts
1733
1742
  for `today`, most recent client first, with `today` on this Mac's local
@@ -1398,7 +1398,7 @@ function protocolJsonSchema() {
1398
1398
  statsError: { type: "string" },
1399
1399
  buildMachines: {
1400
1400
  type: ["array", "null"],
1401
- description: "`buildMachines` from `stim doctor --json --platform ios`.",
1401
+ description: "`remoteMachines` from `stim doctor --json --platform ios`.",
1402
1402
  items: { type: "object" }
1403
1403
  },
1404
1404
  buildMachinesError: { type: "string" },
@@ -1406,7 +1406,7 @@ function protocolJsonSchema() {
1406
1406
  buildMachinesPending: { type: "boolean" },
1407
1407
  buildClients: {
1408
1408
  type: "array",
1409
- description: "The builds this Mac ran for each client as a build machine, from the audit log.",
1409
+ description: "The builds this Mac ran for each client, from the audit log.",
1410
1410
  items: {
1411
1411
  type: "object",
1412
1412
  required: [
package/dist/protocol.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as protocolJsonSchema } from "./protocol-wBQmYsnp.mjs";
1
+ import { t as protocolJsonSchema } from "./protocol-Btu-PwQS.mjs";
2
2
  export * from "@stim-cli/core/protocol";
3
3
  export { protocolJsonSchema };
@@ -2119,7 +2119,7 @@
2119
2119
  "array",
2120
2120
  "null"
2121
2121
  ],
2122
- "description": "`buildMachines` from `stim doctor --json --platform ios`.",
2122
+ "description": "`remoteMachines` from `stim doctor --json --platform ios`.",
2123
2123
  "items": {
2124
2124
  "type": "object"
2125
2125
  }
@@ -2135,7 +2135,7 @@
2135
2135
  },
2136
2136
  "buildClients": {
2137
2137
  "type": "array",
2138
- "description": "The builds this Mac ran for each client as a build machine, from the audit log.",
2138
+ "description": "The builds this Mac ran for each client, from the audit log.",
2139
2139
  "items": {
2140
2140
  "type": "object",
2141
2141
  "required": [
@@ -1,4 +1,4 @@
1
- import { n as protocol_exports } from "./protocol-wBQmYsnp.mjs";
1
+ import { n as protocol_exports } from "./protocol-Btu-PwQS.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
4
4
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -33,6 +33,8 @@ function parseEnvironment(output) {
33
33
  * The login shell's environment, as Stim Desktop captures it: a process started by launchd lacks the
34
34
  * PATH entries and variables such as ANDROID_HOME that the shell profile sets. The shell writes to a
35
35
  * file because a background process started by a profile can keep a pipe open after the shell exits.
36
+ * It runs in its own session: an interactive shell on the caller's terminal takes over the foreground process group,
37
+ * and the caller is then stopped with SIGTTOU at its next terminal write.
36
38
  */
37
39
  function loginShellEnvironment() {
38
40
  const dir = mkdtempSync(join(tmpdir(), "stim-server-env-"));
@@ -45,6 +47,7 @@ function loginShellEnvironment() {
45
47
  file
46
48
  ], {
47
49
  stdio: "ignore",
50
+ detached: true,
48
51
  timeout: 15e3,
49
52
  killSignal: "SIGKILL"
50
53
  });
@@ -2534,8 +2537,8 @@ function nonEmptyDir(path) {
2534
2537
  }
2535
2538
  }
2536
2539
  function gradleDaemonIdleMs() {
2537
- const definition = settingDefinition("offload.gradleDaemonIdleMinutes");
2538
- const value = loadConfig()?.offload?.gradleDaemonIdleMinutes;
2540
+ const definition = settingDefinition("server.gradleDaemonIdleMinutes");
2541
+ const value = loadConfig()?.server?.gradleDaemonIdleMinutes;
2539
2542
  return (typeof value === "number" && settingValueError(definition, value) === null ? value : definition.default) * 6e4;
2540
2543
  }
2541
2544
  const GRADLE_DAEMON = "org.gradle.launcher.daemon.bootstrap.GradleDaemon";
@@ -3258,6 +3261,106 @@ var BuildSession = class {
3258
3261
  }
3259
3262
  };
3260
3263
  //#endregion
3264
+ //#region src/setup-output.ts
3265
+ function setupDisplayFor(stdoutIsTty, env, raw) {
3266
+ return {
3267
+ tty: stdoutIsTty,
3268
+ color: stdoutIsTty && !env.NO_COLOR && env.TERM !== "dumb",
3269
+ raw
3270
+ };
3271
+ }
3272
+ const BANNER = [
3273
+ " _ _",
3274
+ " ___| |_(_)_ __ ___ ___ ___ _ ____ _____ _ __",
3275
+ " / __| __| | '_ ` _ \\ ____ / __|/ _ \\ '__\\ \\ / / _ \\ '__|",
3276
+ " \\__ \\ |_| | | | | | |____|\\__ \\ __/ | \\ V / __/ |",
3277
+ " |___/\\__|_|_| |_| |_| |___/\\___|_| \\_/ \\___|_|"
3278
+ ];
3279
+ const CHECK = String.fromCharCode(10003);
3280
+ const CROSS = String.fromCharCode(10007);
3281
+ const ARROW = String.fromCharCode(8594);
3282
+ const CODES = {
3283
+ green: 32,
3284
+ yellow: 33,
3285
+ red: 31,
3286
+ dim: 2,
3287
+ bold: 1
3288
+ };
3289
+ const WAITING = /* @__PURE__ */ new Set([
3290
+ "approve",
3291
+ "permissions.screenRecording",
3292
+ "permissions.deviceControl"
3293
+ ]);
3294
+ var SetupPrinter = class {
3295
+ transient = false;
3296
+ write;
3297
+ display;
3298
+ verbose;
3299
+ constructor(write, display, verbose) {
3300
+ this.write = write;
3301
+ this.display = display;
3302
+ this.verbose = verbose;
3303
+ }
3304
+ paint(code, text) {
3305
+ return this.display.color ? `\u001b[${CODES[code]}m${text}\u001b[0m` : text;
3306
+ }
3307
+ mark(state) {
3308
+ const { color } = this.display;
3309
+ if (state === "ok") return color ? this.paint("green", CHECK) : "[ok]";
3310
+ if (state === "failed") return color ? this.paint("red", CROSS) : "[failed]";
3311
+ if (state === "running") return color ? this.paint("yellow", ARROW) : "[..]";
3312
+ return color ? this.paint("yellow", "!") : `[${state}]`;
3313
+ }
3314
+ clear() {
3315
+ if (!this.transient) return;
3316
+ this.display.raw("\r\x1B[2K");
3317
+ this.transient = false;
3318
+ }
3319
+ line(text) {
3320
+ this.clear();
3321
+ this.write(text);
3322
+ }
3323
+ banner() {
3324
+ if (!this.display.tty) return;
3325
+ this.write(this.paint("bold", BANNER.join("\n")));
3326
+ this.write("");
3327
+ }
3328
+ step(step) {
3329
+ if (this.verbose) {
3330
+ const { state, title, detail, fix } = step;
3331
+ this.line(`[${state}] ${title}${detail ? `: ${detail}` : ""}${fix ? `\nFix: ${fix}` : ""}`);
3332
+ return;
3333
+ }
3334
+ if (step.state === "running") {
3335
+ if (!step.running) return;
3336
+ if (this.display.tty && this.display.color) {
3337
+ this.clear();
3338
+ this.display.raw(`${this.mark("running")} ${step.running}`);
3339
+ this.transient = true;
3340
+ } else if (WAITING.has(step.id) || this.display.tty) this.line(`${this.mark("running")} ${step.running}`);
3341
+ return;
3342
+ }
3343
+ if (step.hidden) {
3344
+ this.clear();
3345
+ return;
3346
+ }
3347
+ const text = step.text ?? `${step.title}${step.detail ? `: ${step.detail}` : ""}`;
3348
+ this.line(`${this.mark(step.state)} ${text}`);
3349
+ }
3350
+ result(lines) {
3351
+ this.clear();
3352
+ for (const { state, text } of lines) this.write(`${this.mark(state)} ${text}`);
3353
+ }
3354
+ headline(state, text) {
3355
+ this.clear();
3356
+ this.write("");
3357
+ this.write(`${this.mark(state)} ${this.paint(state === "ok" ? "green" : state === "failed" ? "red" : "yellow", text)}`);
3358
+ }
3359
+ dim(text) {
3360
+ this.line(this.paint("dim", text));
3361
+ }
3362
+ };
3363
+ //#endregion
3261
3364
  //#region src/setup-journal.ts
3262
3365
  function writeSetupJournal(hash, journal) {
3263
3366
  const file = setupJournalFile(hash);
@@ -3324,7 +3427,8 @@ function parseSetupArgs(args, now) {
3324
3427
  multiple: true
3325
3428
  },
3326
3429
  yes: { type: "boolean" },
3327
- json: { type: "boolean" }
3430
+ json: { type: "boolean" },
3431
+ verbose: { type: "boolean" }
3328
3432
  }
3329
3433
  });
3330
3434
  if (!values.client?.trim()) throw new SetupRefusal("--client takes a tailnet node id.");
@@ -3353,7 +3457,8 @@ function parseSetupArgs(args, now) {
3353
3457
  env,
3354
3458
  pathPrepend,
3355
3459
  yes: values.yes === true,
3356
- json: values.json === true
3460
+ json: values.json === true,
3461
+ verbose: values.verbose === true
3357
3462
  };
3358
3463
  }
3359
3464
  function selectGrants(records, match) {
@@ -3456,6 +3561,7 @@ function defaultSetupDeps() {
3456
3561
  now: Date.now,
3457
3562
  sleep,
3458
3563
  tty: process.stdin.isTTY === true,
3564
+ display: setupDisplayFor(process.stdout.isTTY === true, process.env, (text) => void process.stdout.write(text)),
3459
3565
  stdout: (line) => console.log(line),
3460
3566
  stderr: (line) => console.error(line),
3461
3567
  confirm: (question, timeoutMs) => confirmSetup(createInterface({
@@ -3518,7 +3624,6 @@ function defaultSetupDeps() {
3518
3624
  records: (now) => [...readBuildClients(now), ...readDeviceHostClients(now)],
3519
3625
  grant: grantDevice,
3520
3626
  panes: hostPermissionPanes,
3521
- permissions: readHostPermissions,
3522
3627
  requestPermissions: requestHostPermissions,
3523
3628
  async openPane(pane) {
3524
3629
  const result = await run$2("/usr/bin/open", [`x-apple.systempreferences:com.apple.preference.security?${pane}`]);
@@ -3552,28 +3657,30 @@ function requireApprovalMode(deps, options) {
3552
3657
  async function checkPermissions(deps, options, host, desktop, output, report) {
3553
3658
  const wait = (work) => interruptible(work, deps.signal);
3554
3659
  const panes = await wait(deps.panes());
3555
- for (const [key, field, title, pane, feature] of [[
3660
+ for (const [key, field, title, pane, feature, label] of [[
3556
3661
  "screenRecording",
3557
3662
  "screenRecording",
3558
3663
  panes.screen,
3559
3664
  "Privacy_ScreenCapture",
3560
- "viewing hosted simulators"
3665
+ "viewing hosted simulators",
3666
+ "Screen recording"
3561
3667
  ], [
3562
3668
  "deviceControl",
3563
3669
  "accessibility",
3564
3670
  panes.control,
3565
3671
  "Privacy_Accessibility",
3566
- "controlling hosted simulators"
3672
+ "controlling hosted simulators",
3673
+ "Device control"
3567
3674
  ]]) {
3568
3675
  const id = `permissions.${key}`;
3569
- const read = async () => desktop ? (await wait(deps.health(options.port)))?.host ?? null : wait(deps.permissions(host.executable));
3676
+ const read = async () => (await wait(deps.health(options.port)))?.host ?? null;
3570
3677
  let permissions = await read();
3571
3678
  if (permissions?.[field]) {
3572
3679
  output.permissions[key] = "granted";
3573
- report(id, "ok", title);
3680
+ report(id, "ok", title, "Already granted.", void 0, { text: `${label} permission already granted` });
3574
3681
  continue;
3575
3682
  }
3576
- report(id, "running", title, `Click Allow for ${desktop ? "the app serving this port" : host.name}. Prompts appear on this Mac's screen. Press s in a terminal to skip.`);
3683
+ report(id, "running", title, `Click Allow for ${desktop ? "the app serving this port" : host.name}. Prompts appear on this Mac's screen. Press s in a terminal to skip.`, void 0, { running: `${label} permission: click Allow for ${desktop ? "the app serving this port" : host.name} on this Mac's screen (press s to skip)` });
3577
3684
  if (!desktop) await wait(deps.requestPermissions(host.app));
3578
3685
  const start = deps.now();
3579
3686
  let opened = false, skipped = false;
@@ -3589,10 +3696,87 @@ async function checkPermissions(deps, options, host, desktop, output, report) {
3589
3696
  permissions = await read();
3590
3697
  }
3591
3698
  output.permissions[key] = permissions?.[field] ? "granted" : skipped ? "skipped" : "denied";
3592
- report(id, permissions?.[field] ? "ok" : skipped ? "skipped" : "pending", title, permissions?.[field] ? "Granted." : `${skipped ? "skipped" : "pending"}: ${feature} will not work.`, `System Settings > Privacy & Security > ${title}`);
3699
+ report(id, permissions?.[field] ? "ok" : skipped ? "skipped" : "pending", title, permissions?.[field] ? "Granted." : `${skipped ? "skipped" : "pending"}: ${feature} will not work.`, `System Settings > Privacy & Security > ${title}`, { text: permissions?.[field] ? `${label} permission granted` : `${label} permission ${skipped ? "skipped" : "not granted"}: ${feature} will not work` });
3593
3700
  }
3594
3701
  }
3595
- function summarizeSetup(output, options, log) {
3702
+ function toolChecks(tools, stimBuild) {
3703
+ return [
3704
+ [
3705
+ "Xcode",
3706
+ !!tools.xcode && !!tools.simulatorSdk,
3707
+ tools.xcode ?? "Missing Xcode or iOS SDK",
3708
+ "Install Xcode from the App Store; sudo xcodebuild -runFirstLaunch"
3709
+ ],
3710
+ [
3711
+ "iOS runtime",
3712
+ tools.runtimes.length > 0,
3713
+ tools.runtimes.join(", ") || "No iOS simulator runtime",
3714
+ "xcodebuild -downloadPlatform iOS"
3715
+ ],
3716
+ [
3717
+ "CocoaPods",
3718
+ !!tools.cocoapods,
3719
+ tools.cocoapods ?? "Missing CocoaPods",
3720
+ "brew install cocoapods; or gem install bundler"
3721
+ ],
3722
+ [
3723
+ "JDK",
3724
+ !!tools.jdk,
3725
+ tools.jdk ?? "Missing JDK",
3726
+ "brew install --cask zulu@17"
3727
+ ],
3728
+ [
3729
+ "Android SDK",
3730
+ !!tools.androidSdk?.platforms.length && !!tools.androidSdk.buildTools.length && !!tools.androidSdk.ndk.length,
3731
+ tools.androidSdk ? JSON.stringify(tools.androidSdk) : "Missing Android SDK",
3732
+ "Install Android Studio or set ANDROID_HOME"
3733
+ ],
3734
+ [
3735
+ "Stim build",
3736
+ !!stimBuild && stimBuild === tools.stimBuild,
3737
+ `${stimBuild ?? "unknown"} (setup: ${tools.stimBuild ?? "unknown"})`,
3738
+ "Install This Mac's Build from Stim Desktop > Settings > Remote Macs"
3739
+ ]
3740
+ ];
3741
+ }
3742
+ const capabilityNoun = (capability) => capability === "build" ? "builds" : "hosted simulators";
3743
+ function approvalQuestion(concise, name, capability, nodeId) {
3744
+ if (!concise) {
3745
+ const verb = capability === "build" ? "build on this Mac" : "host simulators on this Mac";
3746
+ return `${name} (node ${nodeId.slice(0, 4)}...) asks to ${verb}. Approve? [y/N] `;
3747
+ }
3748
+ return `Approve ${name} to ${capability === "build" ? "build here (runs its project code" : "host simulators here (runs its app code"} on this Mac)? [y/N] `;
3749
+ }
3750
+ function serverText(desktop, decision, version) {
3751
+ if (desktop) return `using the app's stim-server ${version}`;
3752
+ return `stim-server ${version} ${decision === "reuse" ? "already installed" : "installed"}`;
3753
+ }
3754
+ function approvalView(complete) {
3755
+ return complete ? { hidden: true } : { text: "no approval before the ticket expired: builds or hosted simulators will not work" };
3756
+ }
3757
+ function printSuccess(output, options, concise) {
3758
+ const { printer, gaps, exit } = concise;
3759
+ const machine = output.route.dnsName?.split(".")[0] ?? output.label;
3760
+ const client = output.granted[0]?.client.name ?? options.nodeId;
3761
+ const names = {
3762
+ build: "build",
3763
+ "device-host": "host simulators"
3764
+ };
3765
+ const what = options.capabilities.map((c) => names[c]).join(" and ");
3766
+ if (exit === 0) printer.headline("ok", `${machine} is ready to ${what} for ${client}`);
3767
+ else if (exit === 1) printer.headline("failed", "Setup did not finish");
3768
+ else if (exit === 2) printer.headline("pending", "No request was approved before the ticket expired");
3769
+ else printer.headline("pending", `${machine} is set up, with gaps`);
3770
+ printer.result(output.granted.map((g) => ({
3771
+ state: "ok",
3772
+ text: `${capabilityNoun(g.capability)}: approved for ${g.client.name} (request ${g.id})`
3773
+ })));
3774
+ for (const gap of gaps) printer.line(` Fix (${gap.subject}): ${gap.fix}`);
3775
+ const undo = [...output.granted.map((g) => `stim-server devices revoke ${g.id}`), ...output.managed && output.granted.length ? [`stim-server service uninstall --label ${output.label}`] : []];
3776
+ if (undo.length) printer.dim(`To undo: ${undo.join("; ")}`);
3777
+ }
3778
+ function summarizeSetup(output, options, log, concise) {
3779
+ if (concise) return printSuccess(output, options, concise);
3596
3780
  for (const capability of options.capabilities) if (!output.granted.some((g) => g.capability === capability)) output.warnings.push(`${capability === "build" ? "Builds" : "Hosted simulators"} have no approval.`);
3597
3781
  if (options.capabilities.includes("device-host")) {
3598
3782
  for (const [key, feature] of [["screenRecording", "Viewing hosted simulators"], ["deviceControl", "Controlling hosted simulators"]]) if (output.permissions[key] !== "granted") output.warnings.push(`${feature} requires a macOS permission grant.`);
@@ -3644,6 +3828,17 @@ async function runSetup(args, version, deps) {
3644
3828
  warnings: []
3645
3829
  };
3646
3830
  const log = (line) => (json ? deps.stderr : deps.stdout)(line);
3831
+ const verbose = args.includes("--verbose");
3832
+ const display = deps.display ?? {
3833
+ tty: false,
3834
+ color: false,
3835
+ raw: () => {}
3836
+ };
3837
+ const printer = new SetupPrinter(log, display, json || verbose);
3838
+ const concise = !json && !verbose;
3839
+ const chatter = concise ? () => {} : log;
3840
+ const gaps = [];
3841
+ if (!json) printer.banner();
3647
3842
  const wait = (work) => interruptible(work, deps.signal);
3648
3843
  let release;
3649
3844
  let active = "args";
@@ -3656,7 +3851,7 @@ async function runSetup(args, version, deps) {
3656
3851
  throw error;
3657
3852
  }
3658
3853
  };
3659
- const step = (id, state, title, detail, fix) => {
3854
+ const step = (id, state, title, detail, fix, view = {}) => {
3660
3855
  if (deps.signal?.aborted && state !== "failed") throw new SetupRefusal("interrupted");
3661
3856
  active = id;
3662
3857
  const value = {
@@ -3670,7 +3865,14 @@ async function runSetup(args, version, deps) {
3670
3865
  if (index < 0) journal.steps.push(value);
3671
3866
  else journal.steps[index] = value;
3672
3867
  save();
3673
- log(`${state === "ok" ? "[ok]" : state === "failed" ? "[failed]" : `[${state}]`} ${title}${detail ? `: ${detail}` : ""}${fix ? `\nFix: ${fix}` : ""}`);
3868
+ if (fix && state !== "ok" && state !== "running") gaps.push({
3869
+ subject: title,
3870
+ fix
3871
+ });
3872
+ printer.step({
3873
+ ...value,
3874
+ ...view
3875
+ });
3674
3876
  };
3675
3877
  try {
3676
3878
  options = parseSetupArgs(args, deps.now());
@@ -3693,14 +3895,14 @@ async function runSetup(args, version, deps) {
3693
3895
  requireApprovalMode(deps, options);
3694
3896
  release = deps.claim();
3695
3897
  deps.prune(deps.now());
3696
- step("preflight", "ok", "Preflight");
3898
+ step("preflight", "ok", "Preflight", void 0, void 0, { hidden: true });
3697
3899
  const opts = options;
3698
3900
  let installed = null;
3699
3901
  let host;
3700
3902
  let script = "";
3701
3903
  let desktop = false;
3702
3904
  await deps.withInstallClaim(opts.label, async () => {
3703
- step("server", "running", "stim-server");
3905
+ step("server", "running", "stim-server", void 0, void 0, { running: "Checking stim-server" });
3704
3906
  installed = await wait(deps.installed(opts.label));
3705
3907
  if (installed && installed.port !== opts.port) throw new SetupRefusal(`${opts.label} uses port ${installed.port}; rerun with that --port or another --label.`);
3706
3908
  const health = await wait(deps.health(opts.port));
@@ -3718,7 +3920,7 @@ async function runSetup(args, version, deps) {
3718
3920
  script: installed.script,
3719
3921
  node: installed.node,
3720
3922
  port: installed.port
3721
- }, { release: version }, log))) log(note);
3923
+ }, { release: version }, chatter))) chatter(note);
3722
3924
  installed = await wait(deps.installed(opts.label));
3723
3925
  const updated = await wait(deps.health(opts.port));
3724
3926
  if (!updated) throw new SetupRefusal("The updated server is not answering.");
@@ -3728,7 +3930,7 @@ async function runSetup(args, version, deps) {
3728
3930
  };
3729
3931
  script = installed?.script ?? "";
3730
3932
  } else if (decision === "install") {
3731
- const target = await wait(deps.install(deps.versions(opts.label), { release: version }, deps.node, log));
3933
+ const target = await wait(deps.install(deps.versions(opts.label), { release: version }, deps.node, chatter));
3732
3934
  script = target.script;
3733
3935
  output.server = target.build;
3734
3936
  } else {
@@ -3738,10 +3940,10 @@ async function runSetup(args, version, deps) {
3738
3940
  };
3739
3941
  script = installed?.script ?? "";
3740
3942
  }
3741
- step("server", "ok", `stim-server ${output.server.version}`, desktop ? "Using the existing server; permissions are that app's." : decision === "reuse" ? "Already installed; never downgrades." : "Installed exact release.");
3742
- step("host", "running", "Stim Host");
3943
+ step("server", "ok", `stim-server ${output.server.version}`, desktop ? "Using the existing server; permissions are that app's." : decision === "reuse" ? "Already installed; never downgrades." : "Installed exact release.", void 0, { text: serverText(desktop, decision, output.server.version) });
3944
+ step("host", "running", "Stim Host", void 0, void 0, { running: "Installing Stim Host" });
3743
3945
  host = await wait(deps.installHost());
3744
- step("host", "ok", "Stim Host", host.app);
3946
+ step("host", "ok", "Stim Host", host.app, void 0, { text: "Stim Host installed" });
3745
3947
  if (installed?.host) {
3746
3948
  const app = dirname(dirname(dirname(installed.host)));
3747
3949
  host = {
@@ -3751,7 +3953,7 @@ async function runSetup(args, version, deps) {
3751
3953
  name: basename(app, ".app")
3752
3954
  };
3753
3955
  }
3754
- step("service", "running", "Service");
3956
+ step("service", "running", "Service", void 0, void 0, { running: "Starting the service" });
3755
3957
  if (!desktop) {
3756
3958
  for (const note of await wait(deps.installJob({
3757
3959
  ...opts,
@@ -3760,14 +3962,14 @@ async function runSetup(args, version, deps) {
3760
3962
  script,
3761
3963
  host,
3762
3964
  requestPermissions: false
3763
- }))) log(note);
3965
+ }))) chatter(note);
3764
3966
  const ready = await wait(deps.health(opts.port));
3765
3967
  if (!ready || ready.startup && ready.startup.state !== "ready") throw new SetupRefusal("The installed server is not ready; check service status.");
3766
3968
  }
3767
- step("service", "ok", "Service", desktop ? "Reusing app server; no LaunchAgent installed." : `${opts.label} running.`);
3969
+ step("service", "ok", "Service", desktop ? "Reusing app server; no LaunchAgent installed." : `${opts.label} running.`, void 0, { text: desktop ? "reusing the app server (no LaunchAgent)" : `LaunchAgent ${opts.label} running` });
3768
3970
  });
3769
3971
  const previous = desktop ? null : await wait(deps.installed(opts.label));
3770
- step("route", "running", "Tailnet route");
3972
+ step("route", "running", "Tailnet route", void 0, void 0, { running: "Checking the tailnet route" });
3771
3973
  const route = await wait(deps.route(opts.port));
3772
3974
  output.route.state = route.state;
3773
3975
  output.route.port = route.port;
@@ -3800,8 +4002,8 @@ async function runSetup(args, version, deps) {
3800
4002
  }
3801
4003
  throw error;
3802
4004
  }
3803
- step("route", "ok", "Tailnet route", `${preflight.dnsName}: https port ${verified.port}`);
3804
- step("approve", "running", "Access approval", `Waiting until ${opts.expiresAt}.`);
4005
+ step("route", "ok", "Tailnet route", `${preflight.dnsName}: https port ${verified.port}`, void 0, { text: `tailnet route https ${verified.port} (no Funnel)` });
4006
+ step("approve", "running", "Access approval", `Waiting until ${opts.expiresAt}.`, void 0, { running: `Waiting for approval until ${opts.expiresAt}` });
3805
4007
  const declined = /* @__PURE__ */ new Set();
3806
4008
  requireApprovalMode(deps, opts);
3807
4009
  while (deps.now() < Date.parse(opts.expiresAt)) {
@@ -3813,15 +4015,17 @@ async function runSetup(args, version, deps) {
3813
4015
  now: deps.now()
3814
4016
  });
3815
4017
  for (const { capability, record, approved } of grants) {
3816
- const verb = capability === "build" ? "build on this Mac" : "host simulators on this Mac";
3817
4018
  if (!approved && !opts.yes && !deps.tty) throw new SetupRefusal("To approve requests, rerun with --yes or in a terminal.");
3818
4019
  if (!approved && !opts.yes) {
3819
- log(capability === "build" ? "This runs its project code on this Mac to build." : "This runs its native app code in session-owned simulators on this Mac.");
3820
- const yes = await wait(deps.confirm(`${record.name} (node ${opts.nodeId.slice(0, 4)}...) asks to ${verb}. Approve? [y/N] `, Math.max(1, Date.parse(opts.expiresAt) - deps.now())));
4020
+ printer.clear();
4021
+ if (!concise) log(capability === "build" ? "This runs its project code on this Mac to build." : "This runs its native app code in session-owned simulators on this Mac.");
4022
+ const question = approvalQuestion(concise, record.name, capability, opts.nodeId);
4023
+ const yes = await wait(deps.confirm(question, Math.max(1, Date.parse(opts.expiresAt) - deps.now())));
3821
4024
  if (deps.now() >= Date.parse(opts.expiresAt)) break;
3822
4025
  if (!yes) {
3823
4026
  declined.add(capability);
3824
- deps.stderr(`stim-server: ${capability} approval refused.`);
4027
+ printer.clear();
4028
+ if (!(concise && display.tty)) deps.stderr(`stim-server: ${capability} approval refused.`);
3825
4029
  step(`approve.${capability}`, "failed", `${capability} refused`, "No approval given.");
3826
4030
  continue;
3827
4031
  }
@@ -3840,55 +4044,18 @@ async function runSetup(args, version, deps) {
3840
4044
  name: record.name
3841
4045
  }
3842
4046
  });
3843
- step(`approve.${capability}`, "ok", `${approved ? "Already approved" : "Approved"} ${record.name} (${opts.nodeId}) for ${capability === "build" ? "builds" : "hosted simulators"}`, `request ${record.id}`);
4047
+ step(`approve.${capability}`, "ok", `${approved ? "Already approved" : "Approved"} ${record.name} (${opts.nodeId}) for ${capability === "build" ? "builds" : "hosted simulators"}`, `request ${record.id}`, void 0, { text: `${approved ? "already approved" : "approved"} ${record.name} for ${capabilityNoun(capability)}` });
3844
4048
  }
3845
4049
  if (opts.capabilities.every((c) => declined.has(c) || journal.granted.some((g) => g.capability === c))) break;
3846
4050
  await wait(deps.sleep(Math.min(1e3, Math.max(0, Date.parse(opts.expiresAt) - deps.now()))));
3847
4051
  }
3848
- step("approve", journal.granted.length === opts.capabilities.length ? "ok" : "pending", "Access approval", journal.granted.length === opts.capabilities.length ? "Chosen capabilities approved." : "Missing approval: builds or hosted simulators will not work. Generate a new command after expiry.");
4052
+ step("approve", journal.granted.length === opts.capabilities.length ? "ok" : "pending", "Access approval", journal.granted.length === opts.capabilities.length ? "Chosen capabilities approved." : "Missing approval: builds or hosted simulators will not work. Generate a new command after expiry.", void 0, approvalView(journal.granted.length === opts.capabilities.length));
3849
4053
  if (journal.granted.length) {
3850
4054
  if (opts.capabilities.includes("device-host")) await checkPermissions(deps, opts, host, desktop, output, step);
3851
- step("tools", "running", "Tools");
4055
+ step("tools", "running", "Tools", void 0, void 0, { running: "Checking build tools" });
3852
4056
  const tools = await wait(deps.toolchain(opts));
3853
4057
  if (!tools) throw new SetupRefusal("Could not read the local build toolchain.");
3854
- const checks = [
3855
- [
3856
- "Xcode",
3857
- !!tools.xcode && !!tools.simulatorSdk,
3858
- tools.xcode ?? "Missing Xcode or iOS SDK",
3859
- "Install Xcode from the App Store; sudo xcodebuild -runFirstLaunch"
3860
- ],
3861
- [
3862
- "iOS runtime",
3863
- tools.runtimes.length > 0,
3864
- tools.runtimes.join(", ") || "No iOS simulator runtime",
3865
- "xcodebuild -downloadPlatform iOS"
3866
- ],
3867
- [
3868
- "CocoaPods",
3869
- !!tools.cocoapods,
3870
- tools.cocoapods ?? "Missing CocoaPods",
3871
- "brew install cocoapods; or gem install bundler"
3872
- ],
3873
- [
3874
- "JDK",
3875
- !!tools.jdk,
3876
- tools.jdk ?? "Missing JDK",
3877
- "brew install --cask zulu@17"
3878
- ],
3879
- [
3880
- "Android SDK",
3881
- !!tools.androidSdk?.platforms.length && !!tools.androidSdk.buildTools.length && !!tools.androidSdk.ndk.length,
3882
- tools.androidSdk ? JSON.stringify(tools.androidSdk) : "Missing Android SDK",
3883
- "Install Android Studio or set ANDROID_HOME"
3884
- ],
3885
- [
3886
- "Stim build",
3887
- !!output.server.stimBuild && output.server.stimBuild === tools.stimBuild,
3888
- `${output.server.stimBuild ?? "unknown"} (setup: ${tools.stimBuild ?? "unknown"})`,
3889
- "Install This Mac's Build from Stim Desktop > Settings > Build machines"
3890
- ]
3891
- ];
4058
+ const checks = toolChecks(tools, output.server.stimBuild);
3892
4059
  for (const [tool, present, detail, fix] of checks) {
3893
4060
  const needed = opts.capabilities.includes("build") || tool === "Xcode" || tool === "iOS runtime" || tool === "Stim build";
3894
4061
  output.tools.push({
@@ -3897,9 +4064,12 @@ async function runSetup(args, version, deps) {
3897
4064
  detail,
3898
4065
  fix
3899
4066
  });
3900
- step(`tools.${tool}`, present || !needed ? "ok" : "pending", tool, !needed ? "Not needed for hosted simulators." : present ? detail : `${detail}; ${opts.capabilities.includes("build") ? "builds requiring this tool" : "hosted simulators"} will not work.`, present || !needed ? void 0 : fix);
4067
+ step(`tools.${tool}`, present || !needed ? "ok" : "pending", tool, !needed ? "Not needed for hosted simulators." : present ? detail : `${detail}; ${opts.capabilities.includes("build") ? "builds requiring this tool" : "hosted simulators"} will not work.`, present || !needed ? void 0 : fix, { hidden: present || !needed });
3901
4068
  }
3902
- step("tools", "ok", "Tools", "Checked; installs nothing.");
4069
+ step("tools", "ok", "Tools", "Checked; installs nothing.", void 0, {
4070
+ text: "build tools ready",
4071
+ hidden: output.tools.some((t) => t.state === "missing")
4072
+ });
3903
4073
  }
3904
4074
  } catch (error) {
3905
4075
  const message = deps.signal?.aborted ? "interrupted" : error instanceof Error ? error.message : String(error);
@@ -3908,11 +4078,13 @@ async function runSetup(args, version, deps) {
3908
4078
  step(active, expired ? "pending" : "failed", expired ? "Ticket expired" : "Setup refused", message, error instanceof SetupRefusal ? error.fix : void 0);
3909
4079
  } catch (writeError) {
3910
4080
  const detail = `Could not write the setup journal: ${writeError.message}`;
4081
+ printer.clear();
3911
4082
  deps.stderr(`stim-server: ${detail}`);
3912
4083
  output.warnings.push(detail);
3913
4084
  }
3914
4085
  const code = error.code;
3915
- deps.stderr(`stim-server: ${code ? `${code}: ` : ""}${message}`);
4086
+ printer.clear();
4087
+ if (!(concise && display.tty)) deps.stderr(`stim-server: ${code ? `${code}: ` : ""}${message}`);
3916
4088
  output.warnings.push(message);
3917
4089
  } finally {
3918
4090
  try {
@@ -3935,11 +4107,16 @@ async function runSetup(args, version, deps) {
3935
4107
  detail: message
3936
4108
  });
3937
4109
  journal.exit = setupExitCode(journal);
4110
+ printer.clear();
3938
4111
  deps.stderr(`stim-server: ${message}`);
3939
4112
  output.warnings.push(message);
3940
4113
  }
3941
4114
  output.ok = journal.exit === 0;
3942
- if (options) summarizeSetup(output, options, log);
4115
+ if (options) summarizeSetup(output, options, log, concise ? {
4116
+ printer,
4117
+ gaps,
4118
+ exit: journal.exit
4119
+ } : void 0);
3943
4120
  if (json) deps.stdout(JSON.stringify(output));
3944
4121
  } finally {
3945
4122
  release?.();
@@ -5179,11 +5356,14 @@ var AgentDeviceDriver = class {
5179
5356
  listening(proxy) {
5180
5357
  return new Promise((resolve, reject) => {
5181
5358
  let output = "";
5359
+ let errors = "";
5182
5360
  const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("The agent-device proxy did not report a listening address.")), this.options.startTimeoutMs);
5183
5361
  const finish = (result) => {
5184
5362
  clearTimeout(timer);
5185
5363
  proxy.stdout?.off("data", onData);
5186
- proxy.off("exit", onExit);
5364
+ proxy.stderr?.off("data", onError);
5365
+ proxy.stderr?.resume();
5366
+ proxy.off("close", onExit);
5187
5367
  proxy.off("error", finish);
5188
5368
  if (result instanceof Error) reject(result);
5189
5369
  else resolve(result);
@@ -5193,10 +5373,13 @@ var AgentDeviceDriver = class {
5193
5373
  const match = LISTENING.exec(output);
5194
5374
  if (match) finish(new URL(match[1]));
5195
5375
  };
5196
- const onExit = (code) => finish(/* @__PURE__ */ new Error(`The agent-device proxy exited with ${String(code)} before it was ready.`));
5376
+ const onExit = (code) => finish(/* @__PURE__ */ new Error(`The agent-device proxy exited with ${String(code)} before it was ready.${errors.trim() ? ` ${errors.trim()}` : ""}`));
5377
+ const onError = (chunk) => {
5378
+ errors = (errors + chunk.toString()).slice(-400);
5379
+ };
5197
5380
  proxy.stdout?.on("data", onData);
5198
- proxy.stderr?.resume();
5199
- proxy.once("exit", onExit);
5381
+ proxy.stderr?.on("data", onError);
5382
+ proxy.once("close", onExit);
5200
5383
  proxy.once("error", finish);
5201
5384
  });
5202
5385
  }
@@ -11000,12 +11183,12 @@ function part(outcome, label) {
11000
11183
  }
11001
11184
  const iosDoctorRanAt = ([, record]) => Date.parse(record.doctorRuns?.ios?.at ?? "") || 0;
11002
11185
  /**
11003
- * Where `machine.details` runs `stim doctor` for the build machines: null when `offload.machines` names none, else
11186
+ * Where `machine.details` runs `stim doctor` for the remote Macs: null when `remote.machines` names none, else
11004
11187
  * the registered workspace that still exists and ran doctor for iOS most recently, or the first one; `cwd` is null
11005
11188
  * when no registered workspace exists. Doctor refuses outside a project, and judges a machine against that app.
11006
11189
  */
11007
11190
  function doctorWorkspace(config, exists) {
11008
- const machines = config?.offload?.machines;
11191
+ const machines = config?.remote?.machines;
11009
11192
  if (!Array.isArray(machines) || machines.length === 0) return null;
11010
11193
  return { cwd: Object.entries(config?.projects ?? {}).filter(([path]) => exists(path)).reduce((best, entry) => best === null || iosDoctorRanAt(entry) > iosDoctorRanAt(best) ? entry : best, null)?.[0] ?? null };
11011
11194
  }
@@ -11015,10 +11198,10 @@ async function runDoctor(run, cwd) {
11015
11198
  buildMachines: null,
11016
11199
  buildMachinesError: error
11017
11200
  };
11018
- const machines = payload.buildMachines;
11201
+ const machines = payload.remoteMachines;
11019
11202
  if (!Array.isArray(machines)) return {
11020
11203
  buildMachines: null,
11021
- buildMachinesError: "This stim does not report build machines; update it."
11204
+ buildMachinesError: "This stim does not report remote Macs; update it."
11022
11205
  };
11023
11206
  return { buildMachines: machines.filter((entry) => isObject(entry) && typeof entry.machine === "string" && typeof entry.state === "string") };
11024
11207
  }
@@ -11029,7 +11212,7 @@ const isObject = (value) => value !== null && typeof value === "object" && !Arra
11029
11212
  * and the rest of `machine.details` has nothing to do with it. `snapshot` never awaits doctor. It returns the last
11030
11213
  * settled result immediately (with `buildMachinesPending: true` alongside it once that result is `ttlMs` old or
11031
11214
  * older), starting at most one background run to refresh it; while no result has ever settled, it returns
11032
- * `buildMachines: null` with `buildMachinesPending: true`. `offload.machines` naming no machine, or `doctor` being
11215
+ * `buildMachines: null` with `buildMachinesPending: true`. `remote.machines` naming no machine, or `doctor` being
11033
11216
  * an error or an unresolved workspace, answers synchronously and never starts a run.
11034
11217
  */
11035
11218
  var BuildMachinesCache = class {
@@ -11127,7 +11310,7 @@ async function loadMachineDetails(run, audit, readStats) {
11127
11310
  /**
11128
11311
  * The gc/stats part of one `machine.details` result shared by every connection: a request while a load runs joins
11129
11312
  * it, and a finished load answers for `ttlMs` after it settled, so phones cannot make the Mac run `stim gc` back
11130
- * to back. Build machines are cached and refreshed separately by `BuildMachinesCache`, never joined into this
11313
+ * to back. Remote Macs are cached and refreshed separately by `BuildMachinesCache`, never joined into this
11131
11314
  * promise, so a re-poll observes its progress instead of the gc/stats snapshot from whenever this last loaded.
11132
11315
  */
11133
11316
  var MachineDetailsCache = class {
@@ -12425,7 +12608,7 @@ const failed = (code, message) => ({ error: {
12425
12608
  message
12426
12609
  } });
12427
12610
  /**
12428
- * Asks an approved build machine or device host to update its stim-server to what this Mac runs: the same npm
12611
+ * Asks an approved remote Mac or device host to update its stim-server to what this Mac runs: the same npm
12429
12612
  * release, or this checkout's own packed build. It connects with the credential `stim doctor --fix` stored for that
12430
12613
  * machine and sends the token only to the node it pinned.
12431
12614
  */
@@ -15069,7 +15252,7 @@ const USAGE = `Usage:
15069
15252
  --env and --path-prepend apply after the login shell's environment
15070
15253
  stim-server setup --client <node-id> --ticket <t> --expires <iso>
15071
15254
  [--build] [--device-host] [--port <n>] [--label <name>]
15072
- [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
15255
+ [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
15073
15256
  worker-side approval for this node and ticket until expiry
15074
15257
  stim-server service install [--port <n>] [--label <name>] [--serve]
15075
15258
  [--env KEY=VALUE]... [--path-prepend <dir>]...
@@ -4,6 +4,6 @@ const encode = (version) => version.split(".", 3).reduce((sum, part) => sum * 1e
4
4
  if (encode(process.versions.node) < encode("22.12.0")) {
5
5
  process.stderr.write(`STIM_NODE_UNSUPPORTED: stim-server needs Node 22.12.0 or later; this is Node ${process.versions.node} at ${process.execPath}. To run stim-server where a project pins an older Node, see https://stim.appandflow.com/docs/requirements#older-node-pins\n`);
6
6
  process.exitCode = 1;
7
- } else import("./stim-server-CP50F1Y_.mjs");
7
+ } else import("./stim-server-CGqz0EFG.mjs");
8
8
  //#endregion
9
9
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stim-cli/server",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "Serves Stim status to paired clients over Tailscale, and runs reload and stop for clients granted control.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,8 +28,8 @@
28
28
  "./protocol.schema.json": "./dist/protocol.schema.json"
29
29
  },
30
30
  "dependencies": {
31
- "@stim-cli/core": "^1.17.0",
32
- "stim": "1.17.0",
31
+ "@stim-cli/core": "^1.17.1",
32
+ "stim": "1.17.1",
33
33
  "ws": "^8.21.3"
34
34
  },
35
35
  "devDependencies": {