@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 +33 -24
- package/dist/{protocol-wBQmYsnp.mjs → protocol-Btu-PwQS.mjs} +2 -2
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +2 -2
- package/dist/{stim-server-CP50F1Y_.mjs → stim-server-CGqz0EFG.mjs} +273 -90
- package/dist/stim-server.mjs +1 -1
- package/package.json +3 -3
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
|
|
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
|
-
|
|
148
|
-
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
828
|
-
they rank compatible offers from `
|
|
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
|
|
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
|
|
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
|
-
`
|
|
1085
|
-
`stim settings set
|
|
1086
|
-
Approve the request on the
|
|
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
|
|
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 `
|
|
1130
|
-
|
|
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
|
-
`
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
1718
|
-
when `
|
|
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
|
|
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: "`
|
|
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
|
|
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
|
@@ -2119,7 +2119,7 @@
|
|
|
2119
2119
|
"array",
|
|
2120
2120
|
"null"
|
|
2121
2121
|
],
|
|
2122
|
-
"description": "`
|
|
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
|
|
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-
|
|
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("
|
|
2538
|
-
const value = loadConfig()?.
|
|
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 () =>
|
|
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
|
|
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
|
-
|
|
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 },
|
|
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,
|
|
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
|
-
})))
|
|
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
|
-
|
|
3820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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?.
|
|
5199
|
-
proxy.once("
|
|
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
|
|
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?.
|
|
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.
|
|
11201
|
+
const machines = payload.remoteMachines;
|
|
11019
11202
|
if (!Array.isArray(machines)) return {
|
|
11020
11203
|
buildMachines: null,
|
|
11021
|
-
buildMachinesError: "This stim does not report
|
|
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`. `
|
|
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.
|
|
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
|
|
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>]...
|
package/dist/stim-server.mjs
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
32
|
-
"stim": "1.17.
|
|
31
|
+
"@stim-cli/core": "^1.17.1",
|
|
32
|
+
"stim": "1.17.1",
|
|
33
33
|
"ws": "^8.21.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|