@stim-cli/server 1.17.0 → 1.17.2
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 +45 -27
- 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-CJIlV28Y.mjs} +359 -108
- package/dist/stim-server.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ The design is in
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
stim-server [--port <n>]
|
|
16
|
+
stim-server [--port <n>] [--loopback-only]
|
|
17
|
+
# serve paired clients, port 7787 by default
|
|
17
18
|
stim-server pair [--port <n>] [--control]
|
|
18
19
|
# print a single-use pairing payload
|
|
19
20
|
stim-server devices [list] # list pairings, approvals, requests and hosted sessions
|
|
@@ -28,6 +29,13 @@ stim-server service install|status|update|rollback|uninstall
|
|
|
28
29
|
# run stim-server as a macOS LaunchAgent, see below
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
`--loopback-only` makes the server listen on `127.0.0.1` only and refuse any request
|
|
33
|
+
that names a remote peer (`X-Forwarded-For`, as `tailscale serve --https` adds), so
|
|
34
|
+
phones and other Macs cannot connect; `/health` then reports `loopbackOnly: true`.
|
|
35
|
+
Without it, the server also listens on this Mac's Tailscale addresses while Tailscale
|
|
36
|
+
runs (see [Tailscale](#tailscale)). Stim Desktop starts its own server with the flag
|
|
37
|
+
unless it serves phones.
|
|
38
|
+
|
|
31
39
|
`--env KEY=VALUE` and `--path-prepend <dir>` (each repeatable) apply to the
|
|
32
40
|
serving command: see [Run as a service](#run-as-a-service).
|
|
33
41
|
|
|
@@ -57,7 +65,7 @@ app (checked at most every 5 seconds), or `null` when the check fails. Stim
|
|
|
57
65
|
Desktop uses it to find a running server and show its route. A request through `tailscale serve` or on a Tailscale address
|
|
58
66
|
without an `Origin` or `Sec-Fetch-Site` header, which a web page's request
|
|
59
67
|
carries, gets only `{ "server": "stim-server", "version", "protocol" }`, which Stim
|
|
60
|
-
Desktop's
|
|
68
|
+
Desktop's Remote Macs list uses to find stim-server on the other Macs of the
|
|
61
69
|
tailnet. A request from this Mac with a `Host` other than `127.0.0.1` or
|
|
62
70
|
`localhost` gets HTTP 426, like any other plain HTTP request.
|
|
63
71
|
|
|
@@ -91,7 +99,7 @@ A person on the worker runs the command generated for their client Mac:
|
|
|
91
99
|
```bash
|
|
92
100
|
stim-server setup --client <node-id> --ticket <43-base64url-characters> --expires <ISO-time> \
|
|
93
101
|
--build --device-host [--port <n>] [--label <name>] \
|
|
94
|
-
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
|
|
102
|
+
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
|
|
95
103
|
```
|
|
96
104
|
|
|
97
105
|
If the server is not installed globally, use `npx --yes --package @stim-cli/server@<version> stim-server`
|
|
@@ -144,8 +152,17 @@ unreadable routes refuse with a remedy. Tool checks print fixes for Xcode,
|
|
|
144
152
|
simulator runtimes, CocoaPods, JDK, Android SDK and the Stim build; they install
|
|
145
153
|
nothing. Android and CocoaPods checks are not needed for hosting alone.
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
In a terminal, setup prints a banner, then one line per step with a green check,
|
|
156
|
+
a yellow arrow while a step runs, or a red cross; `NO_COLOR`, `TERM=dumb` and a
|
|
157
|
+
non-terminal give plain `[ok]`, `[failed]` and `[pending]` marks, no colors and no
|
|
158
|
+
in-place line. The y/N questions stay, one line each. The run ends with a
|
|
159
|
+
headline such as "janics-mac-mini is ready to build for Janic's MacBook Pro", one
|
|
160
|
+
line per approved capability, a `Fix` line for each skipped permission or missing
|
|
161
|
+
tool, and one `To undo` line. `--verbose` prints every step with its detail, the
|
|
162
|
+
install, service and route notes, and the long summary with the undo commands, as
|
|
163
|
+
earlier releases did. Plain progress goes to stdout; errors use
|
|
164
|
+
`stim-server: <message>` on stderr (when stdout is a terminal, the refusal appears on stdout only).
|
|
165
|
+
`--json` sends the long (verbose) progress to stderr, without the banner, and prints one final payload with `ok`,
|
|
149
166
|
`label`, `port`, `route` (state, DNS name, HTTPS port), `server` (version and
|
|
150
167
|
Stim build), `managed`, separate `granted` request ids and client identities,
|
|
151
168
|
`permissions`, `tools` and `warnings`. Final permission values are `granted`,
|
|
@@ -172,7 +189,7 @@ with detail `interrupted`, completes the journal, releases the setup claim,
|
|
|
172
189
|
and exits 1. A typed N also exits 1; a question timeout follows ticket expiry
|
|
173
190
|
and exits 2.
|
|
174
191
|
|
|
175
|
-
The
|
|
192
|
+
The `To undo` line (or the `--verbose` summary) lists the undo commands:
|
|
176
193
|
|
|
177
194
|
```bash
|
|
178
195
|
stim-server devices revoke <build-request-id>
|
|
@@ -199,8 +216,9 @@ same tailnet or with the Mac shared to the phone's user.
|
|
|
199
216
|
addresses, never on every interface. It re-reads the Tailscale state in the
|
|
200
217
|
background, so a server that started before Tailscale was up, or while it did
|
|
201
218
|
not answer, starts listening on the Tailscale addresses once it runs, and stops
|
|
202
|
-
when it goes away. In Stim Desktop,
|
|
203
|
-
|
|
219
|
+
when it goes away. In Stim Desktop, the Pair a Phone wizard (Settings > Phones) creates and
|
|
220
|
+
verifies the private route without a terminal command; with the Phone app flag
|
|
221
|
+
off, **Set up connection** on Settings > Server does. A
|
|
204
222
|
missing or unreadable route keeps Desktop pairing unavailable; HTTPS setup may
|
|
205
223
|
require Tailscale browser approval. Existing routes stay unchanged.
|
|
206
224
|
|
|
@@ -284,7 +302,7 @@ registration check. `hello` reports
|
|
|
284
302
|
the capabilities and actions of the connection's device when it connects; a
|
|
285
303
|
connection sees a new grant after it reconnects. Under Stim Host, an approved
|
|
286
304
|
`hello` also carries the same `host` grants as `/health`, which `stim doctor`
|
|
287
|
-
reports for
|
|
305
|
+
reports for remote Macs.
|
|
288
306
|
|
|
289
307
|
Every method other than `hello` needs `read`. A device with only `build`
|
|
290
308
|
gets `forbidden` for all of them.
|
|
@@ -335,7 +353,7 @@ bundle, Android APK or prebuilt macOS app, stream the iOS simulator, Android emu
|
|
|
335
353
|
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
354
|
|
|
337
355
|
The Stim client can name expected hosts with
|
|
338
|
-
`stim settings set
|
|
356
|
+
`stim settings set remote.machines '["<mac>"]'` and request access with
|
|
339
357
|
`stim doctor --fix`. It stores a separate private credential in
|
|
340
358
|
`$STIM_HOME/device-host-machines.json`, pins the worker's tailnet node, and
|
|
341
359
|
reports approval under `deviceHosts` in doctor JSON. Plain doctor makes no
|
|
@@ -824,8 +842,8 @@ manifest requests are not launch proof; development remains `unverified` until
|
|
|
824
842
|
the workspace observes the app's own bundle delivery.
|
|
825
843
|
Approved clients use `stim ios|android --remote auto` for automatic device placement.
|
|
826
844
|
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 `
|
|
845
|
+
pressure, no budget refusal and load below `server.maxLoadPerCore`. Otherwise
|
|
846
|
+
they rank compatible offers from `remote.machines` and select a host, or wait
|
|
829
847
|
locally if none admits. A reserve refusal after an offer fails with
|
|
830
848
|
`STIM_HOSTING_REFUSED` and asks to retry; it does not re-place the built app.
|
|
831
849
|
Automatic native macOS placement remains separate work.
|
|
@@ -938,7 +956,7 @@ before replacement. Automatic placement remains in
|
|
|
938
956
|
## Run as a service
|
|
939
957
|
|
|
940
958
|
`stim-server service install` runs stim-server as a per-user LaunchAgent on
|
|
941
|
-
macOS, so a
|
|
959
|
+
macOS, so a remote Mac or a phone-serving Mac keeps it running without a
|
|
942
960
|
terminal:
|
|
943
961
|
|
|
944
962
|
```bash
|
|
@@ -1079,18 +1097,18 @@ and uninstall of one label run one at a time.
|
|
|
1079
1097
|
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
1098
|
second service beside the first, for example with another `STIM_HOME`.
|
|
1081
1099
|
|
|
1082
|
-
To set up a
|
|
1100
|
+
To set up a remote Mac: install Stim and stim-server on the Mac, run
|
|
1083
1101
|
`stim-server service install --serve` (with the pins its toolchain needs), set
|
|
1084
|
-
`
|
|
1085
|
-
`stim settings set
|
|
1086
|
-
Approve the request on the
|
|
1102
|
+
`server.workerRoot` there, and on each client run
|
|
1103
|
+
`stim settings set remote.machines '["<mac>"]'` and `stim doctor --fix`.
|
|
1104
|
+
Approve the request on the remote Mac with
|
|
1087
1105
|
`stim-server devices grant <id> --build`.
|
|
1088
1106
|
|
|
1089
1107
|
## Remote update
|
|
1090
1108
|
|
|
1091
1109
|
A Mac this one approved for builds (`--build`) or device hosting
|
|
1092
1110
|
(`--device-host`) can update this Mac's `stim-server service` over its tailnet
|
|
1093
|
-
connection, so the
|
|
1111
|
+
connection, so the remote Mac keeps up with the client without ssh. A
|
|
1094
1112
|
server that offers it lists `server-update` in its `hello` features. A paired
|
|
1095
1113
|
phone, a `read` or `control` device and a connection from this Mac cannot. The
|
|
1096
1114
|
methods need that approval, not `read`:
|
|
@@ -1126,8 +1144,8 @@ of an existing hosted session still answers during the drain.
|
|
|
1126
1144
|
|
|
1127
1145
|
Stim Desktop starts these updates through its own local stim-server with two
|
|
1128
1146
|
methods that only the local Desktop control connection may call:
|
|
1129
|
-
`machines.update.start` takes `{ "machine" }`, an approved `
|
|
1130
|
-
|
|
1147
|
+
`machines.update.start` takes `{ "machine" }`, an approved `remote.machines`
|
|
1148
|
+
entry. It connects to that machine's pinned node with
|
|
1131
1149
|
the stored credential and asks it for this server's npm release. When this
|
|
1132
1150
|
server runs from a Stim checkout, it instead packs the checkout's workspace
|
|
1133
1151
|
packages as `npm pack` would and sends them. `machines.update.status` takes
|
|
@@ -1160,7 +1178,7 @@ closes its connections and cancels its builds.
|
|
|
1160
1178
|
average per CPU), `builds` (this Mac's own Stim runs in prebuild, pods or
|
|
1161
1179
|
compile that it did not offload, plus the offloaded builds it runs), `maxBuilds` (its
|
|
1162
1180
|
`concurrency.maxBuilds`, 0 when unlimited), `maxLoadPerCore` (its
|
|
1163
|
-
`
|
|
1181
|
+
`server.maxLoadPerCore`, default 2), and `declined`: why it would refuse a
|
|
1164
1182
|
build now, or null. It declines while it runs its limit of offloaded builds,
|
|
1165
1183
|
while the worker root's volume has less than 10 GiB free, while `builds`
|
|
1166
1184
|
reaches a non-zero `maxBuilds`, or while `loadPerCore` is at or above
|
|
@@ -1197,7 +1215,7 @@ closes its connections and cancels its builds.
|
|
|
1197
1215
|
`assemble<variant>` for `abi` with ccache under the area's Stim home. A
|
|
1198
1216
|
Gradle daemon leaves the build's process group, so no claim or slot tracks
|
|
1199
1217
|
it. An Android build keeps the client's daemon warm, with Gradle's idle
|
|
1200
|
-
timeout set to `
|
|
1218
|
+
timeout set to `server.gradleDaemonIdleMinutes` (default 30; 0 stops it
|
|
1201
1219
|
when the build ends), and stops it when the build gets SIGTERM. Gradle
|
|
1202
1220
|
fixes a daemon's idle timeout when the daemon starts, so a new value
|
|
1203
1221
|
applies to the next daemon. Once no build of that client runs, stim-server
|
|
@@ -1269,7 +1287,7 @@ closes its connections and cancels its builds.
|
|
|
1269
1287
|
is cancelled; one left by a server that crashed stays under
|
|
1270
1288
|
`repos/<repo>/out/` until you delete it.
|
|
1271
1289
|
|
|
1272
|
-
The worker root is `
|
|
1290
|
+
The worker root is `server.workerRoot` in this Mac's Stim settings, or
|
|
1273
1291
|
`$STIM_HOME/build-worker`. Each client gets `<root>/<device id>/`, with its
|
|
1274
1292
|
blobs, caches (`CP_HOME_DIR`, `CP_CACHE_DIR`, the pnpm store and
|
|
1275
1293
|
`GRADLE_USER_HOME` and SwiftPM) and one area
|
|
@@ -1712,10 +1730,10 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1712
1730
|
one result for 60 seconds, shared by every connection: a request while the
|
|
1713
1731
|
commands run waits for them, and `measuredAt` says when they started. The
|
|
1714
1732
|
commands fail after 150 seconds. Servers that predate it answer
|
|
1715
|
-
`unknown-method`. It also carries `buildMachines`, the `
|
|
1733
|
+
`unknown-method`. It also carries `buildMachines`, the `remoteMachines` list
|
|
1716
1734
|
of `stim doctor --json --platform ios` (never with `--fix`), so a phone can
|
|
1717
|
-
show whether each
|
|
1718
|
-
when `
|
|
1735
|
+
show whether each remote Mac takes builds. The server runs doctor only
|
|
1736
|
+
when `remote.machines` names a machine, else the list is empty; it runs it
|
|
1719
1737
|
in the registered workspace that ran doctor for iOS most recently, because
|
|
1720
1738
|
doctor judges a machine against an app. Like a doctor run from a terminal,
|
|
1721
1739
|
that records the run for the workspace. `buildMachines` is null with
|
|
@@ -1727,7 +1745,7 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1727
1745
|
at a time, and sets `buildMachinesPending` while that background run has not
|
|
1728
1746
|
settled a result yet, or the settled one is 60 seconds old or older. A client
|
|
1729
1747
|
that wants the refreshed result asks `machine.details` again. It also
|
|
1730
|
-
carries `buildClients`, the builds this Mac ran
|
|
1748
|
+
carries `buildClients`, the builds this Mac ran for other Macs, one entry
|
|
1731
1749
|
per client Mac read from the `build` records of the audit log, with `id`,
|
|
1732
1750
|
`name`, `builds`, `failed`, `buildMs`, `lastAt` and the same three counts
|
|
1733
1751
|
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
|
});
|
|
@@ -845,15 +848,27 @@ function answersAs(health, expected) {
|
|
|
845
848
|
return !health.stimBuild || !expected.stimBuild || health.stimBuild === expected.stimBuild;
|
|
846
849
|
}
|
|
847
850
|
const packageNames = (entries) => entries.map((entry) => isJsonObject(entry) ? `${String(entry.name)}@${String(entry.version)}` : "?").join(", ");
|
|
848
|
-
|
|
849
|
-
|
|
851
|
+
const lastLines = (text) => text.trim().split("\n").slice(-8).join("\n");
|
|
852
|
+
/**
|
|
853
|
+
* Why `npm audit signatures --json` output does not vouch for every installed package, or null when it does. When npm
|
|
854
|
+
* fails it prints `{"error":{"summary","detail"}}` on stdout, or only text on stderr; the message carries that error.
|
|
855
|
+
*/
|
|
856
|
+
function signatureProblem(output, stderr = "") {
|
|
850
857
|
let report;
|
|
851
858
|
try {
|
|
852
859
|
report = JSON.parse(output);
|
|
853
860
|
} catch {
|
|
854
|
-
|
|
861
|
+
report = null;
|
|
862
|
+
}
|
|
863
|
+
if (isJsonObject(report) && isJsonObject(report.error)) {
|
|
864
|
+
const { summary, detail } = report.error;
|
|
865
|
+
const text = [summary, detail].filter((part) => typeof part === "string" && part.trim() !== "");
|
|
866
|
+
if (text.length) return `npm audit signatures failed: ${text.map((part) => part.trim()).join(" ")}`;
|
|
867
|
+
}
|
|
868
|
+
if (!isJsonObject(report) || !Array.isArray(report.invalid) || !Array.isArray(report.missing)) {
|
|
869
|
+
const error = lastLines(stderr);
|
|
870
|
+
return `npm audit signatures did not print a JSON report${error ? `:\n${error}` : ""}`;
|
|
855
871
|
}
|
|
856
|
-
if (!isJsonObject(report) || !Array.isArray(report.invalid) || !Array.isArray(report.missing)) return "npm audit signatures did not print a JSON report";
|
|
857
872
|
if (report.invalid.length) return `npm found invalid registry signatures or attestations: ${packageNames(report.invalid)}`;
|
|
858
873
|
if (report.missing.length) return `npm found packages without registry signatures: ${packageNames(report.missing)}`;
|
|
859
874
|
return null;
|
|
@@ -1104,6 +1119,10 @@ function findTailscale(env) {
|
|
|
1104
1119
|
for (const dir of (env.PATH ?? "").split(delimiter)) if (dir && executable(join(dir, "tailscale"))) return join(dir, "tailscale");
|
|
1105
1120
|
return executable(MAC_APP_BINARY) ? MAC_APP_BINARY : null;
|
|
1106
1121
|
}
|
|
1122
|
+
function tailscaleInstall(env, isExecutable = executable) {
|
|
1123
|
+
if (isExecutable(MAC_APP_BINARY)) return "app";
|
|
1124
|
+
return (env.PATH ?? "").split(delimiter).some((dir) => dir && isExecutable(join(dir, "tailscale"))) ? "cli" : "none";
|
|
1125
|
+
}
|
|
1107
1126
|
function parseTailscaleStatus(value) {
|
|
1108
1127
|
if (!isJsonObject(value) || typeof value.BackendState !== "string") return {
|
|
1109
1128
|
state: "unavailable",
|
|
@@ -1897,7 +1916,6 @@ function serverBuild(script) {
|
|
|
1897
1916
|
}
|
|
1898
1917
|
}
|
|
1899
1918
|
const describeBuild = (build) => `stim-server ${build.version}${build.stimBuild ? ` (Stim build ${build.stimBuild})` : ""}`;
|
|
1900
|
-
const lastLines = (text) => text.trim().split("\n").slice(-8).join("\n");
|
|
1901
1919
|
function sameFile(a, b) {
|
|
1902
1920
|
try {
|
|
1903
1921
|
return realpathSync(a) === realpathSync(b);
|
|
@@ -1928,6 +1946,32 @@ function installedIntegrity(staging) {
|
|
|
1928
1946
|
return null;
|
|
1929
1947
|
}
|
|
1930
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* The uploaded package files by package name, as `file:` specs. npm `audit signatures` skips a node only when its
|
|
1951
|
+
* dependent's spec is a file spec (npm lib/utils/verify-signatures.js), so the staging project lists every uploaded
|
|
1952
|
+
* package as a direct dependency and overrides every edge to it with `$name`.
|
|
1953
|
+
*/
|
|
1954
|
+
async function localPackageSpecs(dir, names) {
|
|
1955
|
+
const specs = {};
|
|
1956
|
+
for (const file of names) {
|
|
1957
|
+
const path = join(dir, file);
|
|
1958
|
+
const manifest = await run$2("/usr/bin/tar", [
|
|
1959
|
+
"-xzOf",
|
|
1960
|
+
path,
|
|
1961
|
+
"package/package.json"
|
|
1962
|
+
], process.env, 6e4);
|
|
1963
|
+
let name;
|
|
1964
|
+
try {
|
|
1965
|
+
name = JSON.parse(manifest.stdout).name;
|
|
1966
|
+
} catch {
|
|
1967
|
+
name = null;
|
|
1968
|
+
}
|
|
1969
|
+
if (!manifest.ok || typeof name !== "string" || !name) throw new ServiceError(`${path} is not an npm package file.${manifest.ok ? "" : `\n${lastLines(manifest.stderr)}`}`);
|
|
1970
|
+
if (specs[name]) throw new ServiceError(`${dir} has more than one package file for ${name}.`);
|
|
1971
|
+
specs[name] = `file:${path}`;
|
|
1972
|
+
}
|
|
1973
|
+
return specs;
|
|
1974
|
+
}
|
|
1931
1975
|
async function installServer(versions, source, node, log) {
|
|
1932
1976
|
mkdirSync(versions, { recursive: true });
|
|
1933
1977
|
for (const entry of readdirSync(versions)) if (entry.startsWith(".install-")) rmSync(join(versions, entry), {
|
|
@@ -1941,6 +1985,7 @@ async function installServer(versions, source, node, log) {
|
|
|
1941
1985
|
const registry = [`--registry=${REGISTRY}`, `--@stim-cli:registry=${REGISTRY}`];
|
|
1942
1986
|
let files = [];
|
|
1943
1987
|
let specs;
|
|
1988
|
+
let localPackages = null;
|
|
1944
1989
|
if ("release" in source) specs = [`${SERVER_PACKAGE}@${source.release}`];
|
|
1945
1990
|
else {
|
|
1946
1991
|
const names = readdirSync(source.from).filter((name) => name.endsWith(".tgz"));
|
|
@@ -1949,9 +1994,16 @@ async function installServer(versions, source, node, log) {
|
|
|
1949
1994
|
name,
|
|
1950
1995
|
sha256: createHash("sha256").update(readFileSync(join(source.from, name))).digest("hex")
|
|
1951
1996
|
}));
|
|
1952
|
-
specs =
|
|
1953
|
-
|
|
1954
|
-
|
|
1997
|
+
specs = [];
|
|
1998
|
+
localPackages = await localPackageSpecs(source.from, names);
|
|
1999
|
+
}
|
|
2000
|
+
const overrides = localPackages && Object.fromEntries(Object.keys(localPackages).map((name) => [name, `$${name}`]));
|
|
2001
|
+
const project = localPackages ? {
|
|
2002
|
+
private: true,
|
|
2003
|
+
dependencies: localPackages,
|
|
2004
|
+
overrides
|
|
2005
|
+
} : { private: true };
|
|
2006
|
+
writeFileSync(join(staging, "package.json"), `${JSON.stringify(project, null, 2)}\n`);
|
|
1955
2007
|
log(`Installing ${"release" in source ? `${SERVER_PACKAGE}@${source.release} from ${REGISTRY}` : `${files.length} package file(s) from ${source.from}`}.`);
|
|
1956
2008
|
const installed = await run$2(npm, [
|
|
1957
2009
|
"install",
|
|
@@ -1966,14 +2018,15 @@ async function installServer(versions, source, node, log) {
|
|
|
1966
2018
|
...specs
|
|
1967
2019
|
], env, NPM_TIMEOUT_MS);
|
|
1968
2020
|
if (!installed.ok) throw new ServiceError(`npm install failed:\n${lastLines(installed.stderr)}`);
|
|
1969
|
-
const
|
|
2021
|
+
const audit = await run$2(npm, [
|
|
1970
2022
|
"audit",
|
|
1971
2023
|
"signatures",
|
|
1972
2024
|
"--json",
|
|
1973
2025
|
"--prefix",
|
|
1974
2026
|
staging,
|
|
1975
2027
|
...registry
|
|
1976
|
-
], env, NPM_TIMEOUT_MS)
|
|
2028
|
+
], env, NPM_TIMEOUT_MS);
|
|
2029
|
+
const problem = signatureProblem(audit.stdout, audit.stderr);
|
|
1977
2030
|
if (problem) throw new ServiceError(`${problem}. Not switching to it.`);
|
|
1978
2031
|
const script = join(staging, ...SERVER_SCRIPT);
|
|
1979
2032
|
const build = existsSync(script) ? serverBuild(script) : null;
|
|
@@ -2534,8 +2587,8 @@ function nonEmptyDir(path) {
|
|
|
2534
2587
|
}
|
|
2535
2588
|
}
|
|
2536
2589
|
function gradleDaemonIdleMs() {
|
|
2537
|
-
const definition = settingDefinition("
|
|
2538
|
-
const value = loadConfig()?.
|
|
2590
|
+
const definition = settingDefinition("server.gradleDaemonIdleMinutes");
|
|
2591
|
+
const value = loadConfig()?.server?.gradleDaemonIdleMinutes;
|
|
2539
2592
|
return (typeof value === "number" && settingValueError(definition, value) === null ? value : definition.default) * 6e4;
|
|
2540
2593
|
}
|
|
2541
2594
|
const GRADLE_DAEMON = "org.gradle.launcher.daemon.bootstrap.GradleDaemon";
|
|
@@ -3258,6 +3311,106 @@ var BuildSession = class {
|
|
|
3258
3311
|
}
|
|
3259
3312
|
};
|
|
3260
3313
|
//#endregion
|
|
3314
|
+
//#region src/setup-output.ts
|
|
3315
|
+
function setupDisplayFor(stdoutIsTty, env, raw) {
|
|
3316
|
+
return {
|
|
3317
|
+
tty: stdoutIsTty,
|
|
3318
|
+
color: stdoutIsTty && !env.NO_COLOR && env.TERM !== "dumb",
|
|
3319
|
+
raw
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
const BANNER = [
|
|
3323
|
+
" _ _",
|
|
3324
|
+
" ___| |_(_)_ __ ___ ___ ___ _ ____ _____ _ __",
|
|
3325
|
+
" / __| __| | '_ ` _ \\ ____ / __|/ _ \\ '__\\ \\ / / _ \\ '__|",
|
|
3326
|
+
" \\__ \\ |_| | | | | | |____|\\__ \\ __/ | \\ V / __/ |",
|
|
3327
|
+
" |___/\\__|_|_| |_| |_| |___/\\___|_| \\_/ \\___|_|"
|
|
3328
|
+
];
|
|
3329
|
+
const CHECK = String.fromCharCode(10003);
|
|
3330
|
+
const CROSS = String.fromCharCode(10007);
|
|
3331
|
+
const ARROW = String.fromCharCode(8594);
|
|
3332
|
+
const CODES = {
|
|
3333
|
+
green: 32,
|
|
3334
|
+
yellow: 33,
|
|
3335
|
+
red: 31,
|
|
3336
|
+
dim: 2,
|
|
3337
|
+
bold: 1
|
|
3338
|
+
};
|
|
3339
|
+
const WAITING = /* @__PURE__ */ new Set([
|
|
3340
|
+
"approve",
|
|
3341
|
+
"permissions.screenRecording",
|
|
3342
|
+
"permissions.deviceControl"
|
|
3343
|
+
]);
|
|
3344
|
+
var SetupPrinter = class {
|
|
3345
|
+
transient = false;
|
|
3346
|
+
write;
|
|
3347
|
+
display;
|
|
3348
|
+
verbose;
|
|
3349
|
+
constructor(write, display, verbose) {
|
|
3350
|
+
this.write = write;
|
|
3351
|
+
this.display = display;
|
|
3352
|
+
this.verbose = verbose;
|
|
3353
|
+
}
|
|
3354
|
+
paint(code, text) {
|
|
3355
|
+
return this.display.color ? `\u001b[${CODES[code]}m${text}\u001b[0m` : text;
|
|
3356
|
+
}
|
|
3357
|
+
mark(state) {
|
|
3358
|
+
const { color } = this.display;
|
|
3359
|
+
if (state === "ok") return color ? this.paint("green", CHECK) : "[ok]";
|
|
3360
|
+
if (state === "failed") return color ? this.paint("red", CROSS) : "[failed]";
|
|
3361
|
+
if (state === "running") return color ? this.paint("yellow", ARROW) : "[..]";
|
|
3362
|
+
return color ? this.paint("yellow", "!") : `[${state}]`;
|
|
3363
|
+
}
|
|
3364
|
+
clear() {
|
|
3365
|
+
if (!this.transient) return;
|
|
3366
|
+
this.display.raw("\r\x1B[2K");
|
|
3367
|
+
this.transient = false;
|
|
3368
|
+
}
|
|
3369
|
+
line(text) {
|
|
3370
|
+
this.clear();
|
|
3371
|
+
this.write(text);
|
|
3372
|
+
}
|
|
3373
|
+
banner() {
|
|
3374
|
+
if (!this.display.tty) return;
|
|
3375
|
+
this.write(this.paint("bold", BANNER.join("\n")));
|
|
3376
|
+
this.write("");
|
|
3377
|
+
}
|
|
3378
|
+
step(step) {
|
|
3379
|
+
if (this.verbose) {
|
|
3380
|
+
const { state, title, detail, fix } = step;
|
|
3381
|
+
this.line(`[${state}] ${title}${detail ? `: ${detail}` : ""}${fix ? `\nFix: ${fix}` : ""}`);
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
if (step.state === "running") {
|
|
3385
|
+
if (!step.running) return;
|
|
3386
|
+
if (this.display.tty && this.display.color) {
|
|
3387
|
+
this.clear();
|
|
3388
|
+
this.display.raw(`${this.mark("running")} ${step.running}`);
|
|
3389
|
+
this.transient = true;
|
|
3390
|
+
} else if (WAITING.has(step.id) || this.display.tty) this.line(`${this.mark("running")} ${step.running}`);
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
if (step.hidden) {
|
|
3394
|
+
this.clear();
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
const text = step.text ?? `${step.title}${step.detail ? `: ${step.detail}` : ""}`;
|
|
3398
|
+
this.line(`${this.mark(step.state)} ${text}`);
|
|
3399
|
+
}
|
|
3400
|
+
result(lines) {
|
|
3401
|
+
this.clear();
|
|
3402
|
+
for (const { state, text } of lines) this.write(`${this.mark(state)} ${text}`);
|
|
3403
|
+
}
|
|
3404
|
+
headline(state, text) {
|
|
3405
|
+
this.clear();
|
|
3406
|
+
this.write("");
|
|
3407
|
+
this.write(`${this.mark(state)} ${this.paint(state === "ok" ? "green" : state === "failed" ? "red" : "yellow", text)}`);
|
|
3408
|
+
}
|
|
3409
|
+
dim(text) {
|
|
3410
|
+
this.line(this.paint("dim", text));
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
//#endregion
|
|
3261
3414
|
//#region src/setup-journal.ts
|
|
3262
3415
|
function writeSetupJournal(hash, journal) {
|
|
3263
3416
|
const file = setupJournalFile(hash);
|
|
@@ -3324,7 +3477,8 @@ function parseSetupArgs(args, now) {
|
|
|
3324
3477
|
multiple: true
|
|
3325
3478
|
},
|
|
3326
3479
|
yes: { type: "boolean" },
|
|
3327
|
-
json: { type: "boolean" }
|
|
3480
|
+
json: { type: "boolean" },
|
|
3481
|
+
verbose: { type: "boolean" }
|
|
3328
3482
|
}
|
|
3329
3483
|
});
|
|
3330
3484
|
if (!values.client?.trim()) throw new SetupRefusal("--client takes a tailnet node id.");
|
|
@@ -3353,7 +3507,8 @@ function parseSetupArgs(args, now) {
|
|
|
3353
3507
|
env,
|
|
3354
3508
|
pathPrepend,
|
|
3355
3509
|
yes: values.yes === true,
|
|
3356
|
-
json: values.json === true
|
|
3510
|
+
json: values.json === true,
|
|
3511
|
+
verbose: values.verbose === true
|
|
3357
3512
|
};
|
|
3358
3513
|
}
|
|
3359
3514
|
function selectGrants(records, match) {
|
|
@@ -3394,6 +3549,11 @@ function setupVersionDecision(current, desired, managed) {
|
|
|
3394
3549
|
if (managed && compareVersions(current, desired) < 0) return "update";
|
|
3395
3550
|
return compareVersions(current.replace(/-.*/, ""), SETUP_MIN_VERSION) < 0 ? "too-old" : "reuse";
|
|
3396
3551
|
}
|
|
3552
|
+
function tailscaleRefusal(install) {
|
|
3553
|
+
if (install === "none") return "Install Tailscale on this Mac and sign in to the same tailnet: https://tailscale.com/download/mac";
|
|
3554
|
+
if (install === "app") return "Turn on Tailscale from its menu bar icon on this Mac, then run this again.";
|
|
3555
|
+
return "Run tailscale up on this Mac, then run this again.";
|
|
3556
|
+
}
|
|
3397
3557
|
function setupExitCode(journal) {
|
|
3398
3558
|
if (journal.steps.some((s) => s.state === "failed")) return 1;
|
|
3399
3559
|
if (journal.granted.length === 0) return 2;
|
|
@@ -3456,6 +3616,7 @@ function defaultSetupDeps() {
|
|
|
3456
3616
|
now: Date.now,
|
|
3457
3617
|
sleep,
|
|
3458
3618
|
tty: process.stdin.isTTY === true,
|
|
3619
|
+
display: setupDisplayFor(process.stdout.isTTY === true, process.env, (text) => void process.stdout.write(text)),
|
|
3459
3620
|
stdout: (line) => console.log(line),
|
|
3460
3621
|
stderr: (line) => console.error(line),
|
|
3461
3622
|
confirm: (question, timeoutMs) => confirmSetup(createInterface({
|
|
@@ -3488,7 +3649,7 @@ function defaultSetupDeps() {
|
|
|
3488
3649
|
if (process.platform !== "darwin") throw new SetupRefusal("setup runs only on macOS.");
|
|
3489
3650
|
if (compareVersions(process.versions.node, "22.12.0") < 0) throw new SetupRefusal("setup needs Node 22.12 or later.");
|
|
3490
3651
|
const status = await readRawTailscaleStatus(binary(), process.env);
|
|
3491
|
-
if (!isJsonObject(status) || !isJsonObject(status.Self) || typeof status.Self.DNSName !== "string" || !status.Self.DNSName) throw new SetupRefusal(
|
|
3652
|
+
if (!isJsonObject(status) || !isJsonObject(status.Self) || typeof status.Self.DNSName !== "string" || !status.Self.DNSName) throw new SetupRefusal(tailscaleRefusal(tailscaleInstall(process.env)));
|
|
3492
3653
|
if (!isJsonObject(status.Peer) || !Object.values(status.Peer).some((p) => isJsonObject(p) && p.ID === options.nodeId)) throw new SetupRefusal("That Mac is not on this tailnet");
|
|
3493
3654
|
if (!(await run$2("launchctl", ["print", `gui/${process.getuid?.()}`])).ok) throw new SetupRefusal("Log in to a GUI session on this Mac before running setup.");
|
|
3494
3655
|
return { dnsName: status.Self.DNSName.replace(/\.$/, "") };
|
|
@@ -3518,7 +3679,6 @@ function defaultSetupDeps() {
|
|
|
3518
3679
|
records: (now) => [...readBuildClients(now), ...readDeviceHostClients(now)],
|
|
3519
3680
|
grant: grantDevice,
|
|
3520
3681
|
panes: hostPermissionPanes,
|
|
3521
|
-
permissions: readHostPermissions,
|
|
3522
3682
|
requestPermissions: requestHostPermissions,
|
|
3523
3683
|
async openPane(pane) {
|
|
3524
3684
|
const result = await run$2("/usr/bin/open", [`x-apple.systempreferences:com.apple.preference.security?${pane}`]);
|
|
@@ -3552,28 +3712,30 @@ function requireApprovalMode(deps, options) {
|
|
|
3552
3712
|
async function checkPermissions(deps, options, host, desktop, output, report) {
|
|
3553
3713
|
const wait = (work) => interruptible(work, deps.signal);
|
|
3554
3714
|
const panes = await wait(deps.panes());
|
|
3555
|
-
for (const [key, field, title, pane, feature] of [[
|
|
3715
|
+
for (const [key, field, title, pane, feature, label] of [[
|
|
3556
3716
|
"screenRecording",
|
|
3557
3717
|
"screenRecording",
|
|
3558
3718
|
panes.screen,
|
|
3559
3719
|
"Privacy_ScreenCapture",
|
|
3560
|
-
"viewing hosted simulators"
|
|
3720
|
+
"viewing hosted simulators",
|
|
3721
|
+
"Screen recording"
|
|
3561
3722
|
], [
|
|
3562
3723
|
"deviceControl",
|
|
3563
3724
|
"accessibility",
|
|
3564
3725
|
panes.control,
|
|
3565
3726
|
"Privacy_Accessibility",
|
|
3566
|
-
"controlling hosted simulators"
|
|
3727
|
+
"controlling hosted simulators",
|
|
3728
|
+
"Device control"
|
|
3567
3729
|
]]) {
|
|
3568
3730
|
const id = `permissions.${key}`;
|
|
3569
|
-
const read = async () =>
|
|
3731
|
+
const read = async () => (await wait(deps.health(options.port)))?.host ?? null;
|
|
3570
3732
|
let permissions = await read();
|
|
3571
3733
|
if (permissions?.[field]) {
|
|
3572
3734
|
output.permissions[key] = "granted";
|
|
3573
|
-
report(id, "ok", title);
|
|
3735
|
+
report(id, "ok", title, "Already granted.", void 0, { text: `${label} permission already granted` });
|
|
3574
3736
|
continue;
|
|
3575
3737
|
}
|
|
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
|
|
3738
|
+
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
3739
|
if (!desktop) await wait(deps.requestPermissions(host.app));
|
|
3578
3740
|
const start = deps.now();
|
|
3579
3741
|
let opened = false, skipped = false;
|
|
@@ -3589,10 +3751,87 @@ async function checkPermissions(deps, options, host, desktop, output, report) {
|
|
|
3589
3751
|
permissions = await read();
|
|
3590
3752
|
}
|
|
3591
3753
|
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}`);
|
|
3754
|
+
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
3755
|
}
|
|
3594
3756
|
}
|
|
3595
|
-
function
|
|
3757
|
+
function toolChecks(tools, stimBuild) {
|
|
3758
|
+
return [
|
|
3759
|
+
[
|
|
3760
|
+
"Xcode",
|
|
3761
|
+
!!tools.xcode && !!tools.simulatorSdk,
|
|
3762
|
+
tools.xcode ?? "Missing Xcode or iOS SDK",
|
|
3763
|
+
"Install Xcode from the App Store; sudo xcodebuild -runFirstLaunch"
|
|
3764
|
+
],
|
|
3765
|
+
[
|
|
3766
|
+
"iOS runtime",
|
|
3767
|
+
tools.runtimes.length > 0,
|
|
3768
|
+
tools.runtimes.join(", ") || "No iOS simulator runtime",
|
|
3769
|
+
"xcodebuild -downloadPlatform iOS"
|
|
3770
|
+
],
|
|
3771
|
+
[
|
|
3772
|
+
"CocoaPods",
|
|
3773
|
+
!!tools.cocoapods,
|
|
3774
|
+
tools.cocoapods ?? "Missing CocoaPods",
|
|
3775
|
+
"brew install cocoapods; or gem install bundler"
|
|
3776
|
+
],
|
|
3777
|
+
[
|
|
3778
|
+
"JDK",
|
|
3779
|
+
!!tools.jdk,
|
|
3780
|
+
tools.jdk ?? "Missing JDK",
|
|
3781
|
+
"brew install --cask zulu@17"
|
|
3782
|
+
],
|
|
3783
|
+
[
|
|
3784
|
+
"Android SDK",
|
|
3785
|
+
!!tools.androidSdk?.platforms.length && !!tools.androidSdk.buildTools.length && !!tools.androidSdk.ndk.length,
|
|
3786
|
+
tools.androidSdk ? JSON.stringify(tools.androidSdk) : "Missing Android SDK",
|
|
3787
|
+
"Install Android Studio or set ANDROID_HOME"
|
|
3788
|
+
],
|
|
3789
|
+
[
|
|
3790
|
+
"Stim build",
|
|
3791
|
+
!!stimBuild && stimBuild === tools.stimBuild,
|
|
3792
|
+
`${stimBuild ?? "unknown"} (setup: ${tools.stimBuild ?? "unknown"})`,
|
|
3793
|
+
"Install This Mac's Build from Stim Desktop > Settings > Remote Macs"
|
|
3794
|
+
]
|
|
3795
|
+
];
|
|
3796
|
+
}
|
|
3797
|
+
const capabilityNoun = (capability) => capability === "build" ? "builds" : "hosted simulators";
|
|
3798
|
+
function approvalQuestion(concise, name, capability, nodeId) {
|
|
3799
|
+
if (!concise) {
|
|
3800
|
+
const verb = capability === "build" ? "build on this Mac" : "host simulators on this Mac";
|
|
3801
|
+
return `${name} (node ${nodeId.slice(0, 4)}...) asks to ${verb}. Approve? [y/N] `;
|
|
3802
|
+
}
|
|
3803
|
+
return `Approve ${name} to ${capability === "build" ? "build here (runs its project code" : "host simulators here (runs its app code"} on this Mac)? [y/N] `;
|
|
3804
|
+
}
|
|
3805
|
+
function serverText(desktop, decision, version) {
|
|
3806
|
+
if (desktop) return `using the app's stim-server ${version}`;
|
|
3807
|
+
return `stim-server ${version} ${decision === "reuse" ? "already installed" : "installed"}`;
|
|
3808
|
+
}
|
|
3809
|
+
function approvalView(complete) {
|
|
3810
|
+
return complete ? { hidden: true } : { text: "no approval before the ticket expired: builds or hosted simulators will not work" };
|
|
3811
|
+
}
|
|
3812
|
+
function printSuccess(output, options, concise) {
|
|
3813
|
+
const { printer, gaps, exit } = concise;
|
|
3814
|
+
const machine = output.route.dnsName?.split(".")[0] ?? output.label;
|
|
3815
|
+
const client = output.granted[0]?.client.name ?? options.nodeId;
|
|
3816
|
+
const names = {
|
|
3817
|
+
build: "build",
|
|
3818
|
+
"device-host": "host simulators"
|
|
3819
|
+
};
|
|
3820
|
+
const what = options.capabilities.map((c) => names[c]).join(" and ");
|
|
3821
|
+
if (exit === 0) printer.headline("ok", `${machine} is ready to ${what} for ${client}`);
|
|
3822
|
+
else if (exit === 1) printer.headline("failed", "Setup did not finish");
|
|
3823
|
+
else if (exit === 2) printer.headline("pending", "No request was approved before the ticket expired");
|
|
3824
|
+
else printer.headline("pending", `${machine} is set up, with gaps`);
|
|
3825
|
+
printer.result(output.granted.map((g) => ({
|
|
3826
|
+
state: "ok",
|
|
3827
|
+
text: `${capabilityNoun(g.capability)}: approved for ${g.client.name} (request ${g.id})`
|
|
3828
|
+
})));
|
|
3829
|
+
for (const gap of gaps) printer.line(` Fix (${gap.subject}): ${gap.fix}`);
|
|
3830
|
+
const undo = [...output.granted.map((g) => `stim-server devices revoke ${g.id}`), ...output.managed && output.granted.length ? [`stim-server service uninstall --label ${output.label}`] : []];
|
|
3831
|
+
if (undo.length) printer.dim(`To undo: ${undo.join("; ")}`);
|
|
3832
|
+
}
|
|
3833
|
+
function summarizeSetup(output, options, log, concise) {
|
|
3834
|
+
if (concise) return printSuccess(output, options, concise);
|
|
3596
3835
|
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
3836
|
if (options.capabilities.includes("device-host")) {
|
|
3598
3837
|
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 +3883,17 @@ async function runSetup(args, version, deps) {
|
|
|
3644
3883
|
warnings: []
|
|
3645
3884
|
};
|
|
3646
3885
|
const log = (line) => (json ? deps.stderr : deps.stdout)(line);
|
|
3886
|
+
const verbose = args.includes("--verbose");
|
|
3887
|
+
const display = deps.display ?? {
|
|
3888
|
+
tty: false,
|
|
3889
|
+
color: false,
|
|
3890
|
+
raw: () => {}
|
|
3891
|
+
};
|
|
3892
|
+
const printer = new SetupPrinter(log, display, json || verbose);
|
|
3893
|
+
const concise = !json && !verbose;
|
|
3894
|
+
const chatter = concise ? () => {} : log;
|
|
3895
|
+
const gaps = [];
|
|
3896
|
+
if (!json) printer.banner();
|
|
3647
3897
|
const wait = (work) => interruptible(work, deps.signal);
|
|
3648
3898
|
let release;
|
|
3649
3899
|
let active = "args";
|
|
@@ -3656,7 +3906,7 @@ async function runSetup(args, version, deps) {
|
|
|
3656
3906
|
throw error;
|
|
3657
3907
|
}
|
|
3658
3908
|
};
|
|
3659
|
-
const step = (id, state, title, detail, fix) => {
|
|
3909
|
+
const step = (id, state, title, detail, fix, view = {}) => {
|
|
3660
3910
|
if (deps.signal?.aborted && state !== "failed") throw new SetupRefusal("interrupted");
|
|
3661
3911
|
active = id;
|
|
3662
3912
|
const value = {
|
|
@@ -3670,7 +3920,14 @@ async function runSetup(args, version, deps) {
|
|
|
3670
3920
|
if (index < 0) journal.steps.push(value);
|
|
3671
3921
|
else journal.steps[index] = value;
|
|
3672
3922
|
save();
|
|
3673
|
-
|
|
3923
|
+
if (fix && state !== "ok" && state !== "running") gaps.push({
|
|
3924
|
+
subject: title,
|
|
3925
|
+
fix
|
|
3926
|
+
});
|
|
3927
|
+
printer.step({
|
|
3928
|
+
...value,
|
|
3929
|
+
...view
|
|
3930
|
+
});
|
|
3674
3931
|
};
|
|
3675
3932
|
try {
|
|
3676
3933
|
options = parseSetupArgs(args, deps.now());
|
|
@@ -3693,14 +3950,14 @@ async function runSetup(args, version, deps) {
|
|
|
3693
3950
|
requireApprovalMode(deps, options);
|
|
3694
3951
|
release = deps.claim();
|
|
3695
3952
|
deps.prune(deps.now());
|
|
3696
|
-
step("preflight", "ok", "Preflight");
|
|
3953
|
+
step("preflight", "ok", "Preflight", void 0, void 0, { hidden: true });
|
|
3697
3954
|
const opts = options;
|
|
3698
3955
|
let installed = null;
|
|
3699
3956
|
let host;
|
|
3700
3957
|
let script = "";
|
|
3701
3958
|
let desktop = false;
|
|
3702
3959
|
await deps.withInstallClaim(opts.label, async () => {
|
|
3703
|
-
step("server", "running", "stim-server");
|
|
3960
|
+
step("server", "running", "stim-server", void 0, void 0, { running: "Checking stim-server" });
|
|
3704
3961
|
installed = await wait(deps.installed(opts.label));
|
|
3705
3962
|
if (installed && installed.port !== opts.port) throw new SetupRefusal(`${opts.label} uses port ${installed.port}; rerun with that --port or another --label.`);
|
|
3706
3963
|
const health = await wait(deps.health(opts.port));
|
|
@@ -3718,7 +3975,7 @@ async function runSetup(args, version, deps) {
|
|
|
3718
3975
|
script: installed.script,
|
|
3719
3976
|
node: installed.node,
|
|
3720
3977
|
port: installed.port
|
|
3721
|
-
}, { release: version },
|
|
3978
|
+
}, { release: version }, chatter))) chatter(note);
|
|
3722
3979
|
installed = await wait(deps.installed(opts.label));
|
|
3723
3980
|
const updated = await wait(deps.health(opts.port));
|
|
3724
3981
|
if (!updated) throw new SetupRefusal("The updated server is not answering.");
|
|
@@ -3728,7 +3985,7 @@ async function runSetup(args, version, deps) {
|
|
|
3728
3985
|
};
|
|
3729
3986
|
script = installed?.script ?? "";
|
|
3730
3987
|
} else if (decision === "install") {
|
|
3731
|
-
const target = await wait(deps.install(deps.versions(opts.label), { release: version }, deps.node,
|
|
3988
|
+
const target = await wait(deps.install(deps.versions(opts.label), { release: version }, deps.node, chatter));
|
|
3732
3989
|
script = target.script;
|
|
3733
3990
|
output.server = target.build;
|
|
3734
3991
|
} else {
|
|
@@ -3738,10 +3995,10 @@ async function runSetup(args, version, deps) {
|
|
|
3738
3995
|
};
|
|
3739
3996
|
script = installed?.script ?? "";
|
|
3740
3997
|
}
|
|
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");
|
|
3998
|
+
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) });
|
|
3999
|
+
step("host", "running", "Stim Host", void 0, void 0, { running: "Installing Stim Host" });
|
|
3743
4000
|
host = await wait(deps.installHost());
|
|
3744
|
-
step("host", "ok", "Stim Host", host.app);
|
|
4001
|
+
step("host", "ok", "Stim Host", host.app, void 0, { text: "Stim Host installed" });
|
|
3745
4002
|
if (installed?.host) {
|
|
3746
4003
|
const app = dirname(dirname(dirname(installed.host)));
|
|
3747
4004
|
host = {
|
|
@@ -3751,7 +4008,7 @@ async function runSetup(args, version, deps) {
|
|
|
3751
4008
|
name: basename(app, ".app")
|
|
3752
4009
|
};
|
|
3753
4010
|
}
|
|
3754
|
-
step("service", "running", "Service");
|
|
4011
|
+
step("service", "running", "Service", void 0, void 0, { running: "Starting the service" });
|
|
3755
4012
|
if (!desktop) {
|
|
3756
4013
|
for (const note of await wait(deps.installJob({
|
|
3757
4014
|
...opts,
|
|
@@ -3760,14 +4017,14 @@ async function runSetup(args, version, deps) {
|
|
|
3760
4017
|
script,
|
|
3761
4018
|
host,
|
|
3762
4019
|
requestPermissions: false
|
|
3763
|
-
})))
|
|
4020
|
+
}))) chatter(note);
|
|
3764
4021
|
const ready = await wait(deps.health(opts.port));
|
|
3765
4022
|
if (!ready || ready.startup && ready.startup.state !== "ready") throw new SetupRefusal("The installed server is not ready; check service status.");
|
|
3766
4023
|
}
|
|
3767
|
-
step("service", "ok", "Service", desktop ? "Reusing app server; no LaunchAgent installed." : `${opts.label} running
|
|
4024
|
+
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
4025
|
});
|
|
3769
4026
|
const previous = desktop ? null : await wait(deps.installed(opts.label));
|
|
3770
|
-
step("route", "running", "Tailnet route");
|
|
4027
|
+
step("route", "running", "Tailnet route", void 0, void 0, { running: "Checking the tailnet route" });
|
|
3771
4028
|
const route = await wait(deps.route(opts.port));
|
|
3772
4029
|
output.route.state = route.state;
|
|
3773
4030
|
output.route.port = route.port;
|
|
@@ -3800,8 +4057,8 @@ async function runSetup(args, version, deps) {
|
|
|
3800
4057
|
}
|
|
3801
4058
|
throw error;
|
|
3802
4059
|
}
|
|
3803
|
-
step("route", "ok", "Tailnet route", `${preflight.dnsName}: https port ${verified.port}`);
|
|
3804
|
-
step("approve", "running", "Access approval", `Waiting until ${opts.expiresAt}
|
|
4060
|
+
step("route", "ok", "Tailnet route", `${preflight.dnsName}: https port ${verified.port}`, void 0, { text: `tailnet route https ${verified.port} (no Funnel)` });
|
|
4061
|
+
step("approve", "running", "Access approval", `Waiting until ${opts.expiresAt}.`, void 0, { running: `Waiting for approval until ${opts.expiresAt}` });
|
|
3805
4062
|
const declined = /* @__PURE__ */ new Set();
|
|
3806
4063
|
requireApprovalMode(deps, opts);
|
|
3807
4064
|
while (deps.now() < Date.parse(opts.expiresAt)) {
|
|
@@ -3813,15 +4070,17 @@ async function runSetup(args, version, deps) {
|
|
|
3813
4070
|
now: deps.now()
|
|
3814
4071
|
});
|
|
3815
4072
|
for (const { capability, record, approved } of grants) {
|
|
3816
|
-
const verb = capability === "build" ? "build on this Mac" : "host simulators on this Mac";
|
|
3817
4073
|
if (!approved && !opts.yes && !deps.tty) throw new SetupRefusal("To approve requests, rerun with --yes or in a terminal.");
|
|
3818
4074
|
if (!approved && !opts.yes) {
|
|
3819
|
-
|
|
3820
|
-
|
|
4075
|
+
printer.clear();
|
|
4076
|
+
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.");
|
|
4077
|
+
const question = approvalQuestion(concise, record.name, capability, opts.nodeId);
|
|
4078
|
+
const yes = await wait(deps.confirm(question, Math.max(1, Date.parse(opts.expiresAt) - deps.now())));
|
|
3821
4079
|
if (deps.now() >= Date.parse(opts.expiresAt)) break;
|
|
3822
4080
|
if (!yes) {
|
|
3823
4081
|
declined.add(capability);
|
|
3824
|
-
|
|
4082
|
+
printer.clear();
|
|
4083
|
+
if (!(concise && display.tty)) deps.stderr(`stim-server: ${capability} approval refused.`);
|
|
3825
4084
|
step(`approve.${capability}`, "failed", `${capability} refused`, "No approval given.");
|
|
3826
4085
|
continue;
|
|
3827
4086
|
}
|
|
@@ -3840,55 +4099,18 @@ async function runSetup(args, version, deps) {
|
|
|
3840
4099
|
name: record.name
|
|
3841
4100
|
}
|
|
3842
4101
|
});
|
|
3843
|
-
step(`approve.${capability}`, "ok", `${approved ? "Already approved" : "Approved"} ${record.name} (${opts.nodeId}) for ${capability === "build" ? "builds" : "hosted simulators"}`, `request ${record.id}`);
|
|
4102
|
+
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
4103
|
}
|
|
3845
4104
|
if (opts.capabilities.every((c) => declined.has(c) || journal.granted.some((g) => g.capability === c))) break;
|
|
3846
4105
|
await wait(deps.sleep(Math.min(1e3, Math.max(0, Date.parse(opts.expiresAt) - deps.now()))));
|
|
3847
4106
|
}
|
|
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.");
|
|
4107
|
+
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
4108
|
if (journal.granted.length) {
|
|
3850
4109
|
if (opts.capabilities.includes("device-host")) await checkPermissions(deps, opts, host, desktop, output, step);
|
|
3851
|
-
step("tools", "running", "Tools");
|
|
4110
|
+
step("tools", "running", "Tools", void 0, void 0, { running: "Checking build tools" });
|
|
3852
4111
|
const tools = await wait(deps.toolchain(opts));
|
|
3853
4112
|
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
|
-
];
|
|
4113
|
+
const checks = toolChecks(tools, output.server.stimBuild);
|
|
3892
4114
|
for (const [tool, present, detail, fix] of checks) {
|
|
3893
4115
|
const needed = opts.capabilities.includes("build") || tool === "Xcode" || tool === "iOS runtime" || tool === "Stim build";
|
|
3894
4116
|
output.tools.push({
|
|
@@ -3897,9 +4119,12 @@ async function runSetup(args, version, deps) {
|
|
|
3897
4119
|
detail,
|
|
3898
4120
|
fix
|
|
3899
4121
|
});
|
|
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);
|
|
4122
|
+
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
4123
|
}
|
|
3902
|
-
step("tools", "ok", "Tools", "Checked; installs nothing."
|
|
4124
|
+
step("tools", "ok", "Tools", "Checked; installs nothing.", void 0, {
|
|
4125
|
+
text: "build tools ready",
|
|
4126
|
+
hidden: output.tools.some((t) => t.state === "missing")
|
|
4127
|
+
});
|
|
3903
4128
|
}
|
|
3904
4129
|
} catch (error) {
|
|
3905
4130
|
const message = deps.signal?.aborted ? "interrupted" : error instanceof Error ? error.message : String(error);
|
|
@@ -3908,11 +4133,13 @@ async function runSetup(args, version, deps) {
|
|
|
3908
4133
|
step(active, expired ? "pending" : "failed", expired ? "Ticket expired" : "Setup refused", message, error instanceof SetupRefusal ? error.fix : void 0);
|
|
3909
4134
|
} catch (writeError) {
|
|
3910
4135
|
const detail = `Could not write the setup journal: ${writeError.message}`;
|
|
4136
|
+
printer.clear();
|
|
3911
4137
|
deps.stderr(`stim-server: ${detail}`);
|
|
3912
4138
|
output.warnings.push(detail);
|
|
3913
4139
|
}
|
|
3914
4140
|
const code = error.code;
|
|
3915
|
-
|
|
4141
|
+
printer.clear();
|
|
4142
|
+
if (!(concise && display.tty)) deps.stderr(`stim-server: ${code ? `${code}: ` : ""}${message}`);
|
|
3916
4143
|
output.warnings.push(message);
|
|
3917
4144
|
} finally {
|
|
3918
4145
|
try {
|
|
@@ -3935,11 +4162,16 @@ async function runSetup(args, version, deps) {
|
|
|
3935
4162
|
detail: message
|
|
3936
4163
|
});
|
|
3937
4164
|
journal.exit = setupExitCode(journal);
|
|
4165
|
+
printer.clear();
|
|
3938
4166
|
deps.stderr(`stim-server: ${message}`);
|
|
3939
4167
|
output.warnings.push(message);
|
|
3940
4168
|
}
|
|
3941
4169
|
output.ok = journal.exit === 0;
|
|
3942
|
-
if (options) summarizeSetup(output, options, log
|
|
4170
|
+
if (options) summarizeSetup(output, options, log, concise ? {
|
|
4171
|
+
printer,
|
|
4172
|
+
gaps,
|
|
4173
|
+
exit: journal.exit
|
|
4174
|
+
} : void 0);
|
|
3943
4175
|
if (json) deps.stdout(JSON.stringify(output));
|
|
3944
4176
|
} finally {
|
|
3945
4177
|
release?.();
|
|
@@ -5179,11 +5411,14 @@ var AgentDeviceDriver = class {
|
|
|
5179
5411
|
listening(proxy) {
|
|
5180
5412
|
return new Promise((resolve, reject) => {
|
|
5181
5413
|
let output = "";
|
|
5414
|
+
let errors = "";
|
|
5182
5415
|
const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("The agent-device proxy did not report a listening address.")), this.options.startTimeoutMs);
|
|
5183
5416
|
const finish = (result) => {
|
|
5184
5417
|
clearTimeout(timer);
|
|
5185
5418
|
proxy.stdout?.off("data", onData);
|
|
5186
|
-
proxy.off("
|
|
5419
|
+
proxy.stderr?.off("data", onError);
|
|
5420
|
+
proxy.stderr?.resume();
|
|
5421
|
+
proxy.off("close", onExit);
|
|
5187
5422
|
proxy.off("error", finish);
|
|
5188
5423
|
if (result instanceof Error) reject(result);
|
|
5189
5424
|
else resolve(result);
|
|
@@ -5193,10 +5428,13 @@ var AgentDeviceDriver = class {
|
|
|
5193
5428
|
const match = LISTENING.exec(output);
|
|
5194
5429
|
if (match) finish(new URL(match[1]));
|
|
5195
5430
|
};
|
|
5196
|
-
const onExit = (code) => finish(/* @__PURE__ */ new Error(`The agent-device proxy exited with ${String(code)} before it was ready
|
|
5431
|
+
const onExit = (code) => finish(/* @__PURE__ */ new Error(`The agent-device proxy exited with ${String(code)} before it was ready.${errors.trim() ? ` ${errors.trim()}` : ""}`));
|
|
5432
|
+
const onError = (chunk) => {
|
|
5433
|
+
errors = (errors + chunk.toString()).slice(-400);
|
|
5434
|
+
};
|
|
5197
5435
|
proxy.stdout?.on("data", onData);
|
|
5198
|
-
proxy.stderr?.
|
|
5199
|
-
proxy.once("
|
|
5436
|
+
proxy.stderr?.on("data", onError);
|
|
5437
|
+
proxy.once("close", onExit);
|
|
5200
5438
|
proxy.once("error", finish);
|
|
5201
5439
|
});
|
|
5202
5440
|
}
|
|
@@ -11000,12 +11238,12 @@ function part(outcome, label) {
|
|
|
11000
11238
|
}
|
|
11001
11239
|
const iosDoctorRanAt = ([, record]) => Date.parse(record.doctorRuns?.ios?.at ?? "") || 0;
|
|
11002
11240
|
/**
|
|
11003
|
-
* Where `machine.details` runs `stim doctor` for the
|
|
11241
|
+
* Where `machine.details` runs `stim doctor` for the remote Macs: null when `remote.machines` names none, else
|
|
11004
11242
|
* the registered workspace that still exists and ran doctor for iOS most recently, or the first one; `cwd` is null
|
|
11005
11243
|
* when no registered workspace exists. Doctor refuses outside a project, and judges a machine against that app.
|
|
11006
11244
|
*/
|
|
11007
11245
|
function doctorWorkspace(config, exists) {
|
|
11008
|
-
const machines = config?.
|
|
11246
|
+
const machines = config?.remote?.machines;
|
|
11009
11247
|
if (!Array.isArray(machines) || machines.length === 0) return null;
|
|
11010
11248
|
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
11249
|
}
|
|
@@ -11015,10 +11253,10 @@ async function runDoctor(run, cwd) {
|
|
|
11015
11253
|
buildMachines: null,
|
|
11016
11254
|
buildMachinesError: error
|
|
11017
11255
|
};
|
|
11018
|
-
const machines = payload.
|
|
11256
|
+
const machines = payload.remoteMachines;
|
|
11019
11257
|
if (!Array.isArray(machines)) return {
|
|
11020
11258
|
buildMachines: null,
|
|
11021
|
-
buildMachinesError: "This stim does not report
|
|
11259
|
+
buildMachinesError: "This stim does not report remote Macs; update it."
|
|
11022
11260
|
};
|
|
11023
11261
|
return { buildMachines: machines.filter((entry) => isObject(entry) && typeof entry.machine === "string" && typeof entry.state === "string") };
|
|
11024
11262
|
}
|
|
@@ -11029,7 +11267,7 @@ const isObject = (value) => value !== null && typeof value === "object" && !Arra
|
|
|
11029
11267
|
* and the rest of `machine.details` has nothing to do with it. `snapshot` never awaits doctor. It returns the last
|
|
11030
11268
|
* settled result immediately (with `buildMachinesPending: true` alongside it once that result is `ttlMs` old or
|
|
11031
11269
|
* older), starting at most one background run to refresh it; while no result has ever settled, it returns
|
|
11032
|
-
* `buildMachines: null` with `buildMachinesPending: true`. `
|
|
11270
|
+
* `buildMachines: null` with `buildMachinesPending: true`. `remote.machines` naming no machine, or `doctor` being
|
|
11033
11271
|
* an error or an unresolved workspace, answers synchronously and never starts a run.
|
|
11034
11272
|
*/
|
|
11035
11273
|
var BuildMachinesCache = class {
|
|
@@ -11127,7 +11365,7 @@ async function loadMachineDetails(run, audit, readStats) {
|
|
|
11127
11365
|
/**
|
|
11128
11366
|
* The gc/stats part of one `machine.details` result shared by every connection: a request while a load runs joins
|
|
11129
11367
|
* it, and a finished load answers for `ttlMs` after it settled, so phones cannot make the Mac run `stim gc` back
|
|
11130
|
-
* to back.
|
|
11368
|
+
* to back. Remote Macs are cached and refreshed separately by `BuildMachinesCache`, never joined into this
|
|
11131
11369
|
* promise, so a re-poll observes its progress instead of the gc/stats snapshot from whenever this last loaded.
|
|
11132
11370
|
*/
|
|
11133
11371
|
var MachineDetailsCache = class {
|
|
@@ -12425,7 +12663,7 @@ const failed = (code, message) => ({ error: {
|
|
|
12425
12663
|
message
|
|
12426
12664
|
} });
|
|
12427
12665
|
/**
|
|
12428
|
-
* Asks an approved
|
|
12666
|
+
* Asks an approved remote Mac or device host to update its stim-server to what this Mac runs: the same npm
|
|
12429
12667
|
* release, or this checkout's own packed build. It connects with the credential `stim doctor --fix` stored for that
|
|
12430
12668
|
* machine and sends the token only to the node it pinned.
|
|
12431
12669
|
*/
|
|
@@ -14756,6 +14994,10 @@ async function startServer(options) {
|
|
|
14756
14994
|
return;
|
|
14757
14995
|
}
|
|
14758
14996
|
const peer = peerAddress(request);
|
|
14997
|
+
if (options.loopbackOnly && peer !== null) {
|
|
14998
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
14999
|
+
return;
|
|
15000
|
+
}
|
|
14759
15001
|
if (limiter.blocked(peer ?? "local")) {
|
|
14760
15002
|
socket.end("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
14761
15003
|
return;
|
|
@@ -14797,7 +15039,8 @@ async function startServer(options) {
|
|
|
14797
15039
|
},
|
|
14798
15040
|
tailscale: healthTailscale(tailscale),
|
|
14799
15041
|
route,
|
|
14800
|
-
nativeViewerOpened
|
|
15042
|
+
nativeViewerOpened,
|
|
15043
|
+
...options.loopbackOnly ? { loopbackOnly: true } : {}
|
|
14801
15044
|
};
|
|
14802
15045
|
response.writeHead(startup.state === "ready" ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
14803
15046
|
};
|
|
@@ -14844,6 +15087,10 @@ async function startServer(options) {
|
|
|
14844
15087
|
};
|
|
14845
15088
|
async function listenOn(host) {
|
|
14846
15089
|
const server = createServer$1((request, response) => {
|
|
15090
|
+
if (options.loopbackOnly && peerAddress(request) !== null) {
|
|
15091
|
+
response.writeHead(403, { "content-type": "text/plain" }).end("This stim-server serves this Mac only.\n");
|
|
15092
|
+
return;
|
|
15093
|
+
}
|
|
14847
15094
|
if (localHealthRequest(request)) {
|
|
14848
15095
|
answerHealth(response);
|
|
14849
15096
|
return;
|
|
@@ -14912,7 +15159,7 @@ async function startServer(options) {
|
|
|
14912
15159
|
const monitor = options.tailscaleMonitor;
|
|
14913
15160
|
const listenFailures = /* @__PURE__ */ new Set();
|
|
14914
15161
|
const reconcile = async (snapshot) => {
|
|
14915
|
-
const tailnet = snapshot.state.state === "running" ? snapshot.state.ips : [];
|
|
15162
|
+
const tailnet = snapshot.state.state === "running" && !options.loopbackOnly ? snapshot.state.ips : [];
|
|
14916
15163
|
const wanted = /* @__PURE__ */ new Set([...options.hosts, ...tailnet]);
|
|
14917
15164
|
for (const [host, listener] of servers) {
|
|
14918
15165
|
if (wanted.has(host)) continue;
|
|
@@ -15064,12 +15311,13 @@ function watchTailscale(options) {
|
|
|
15064
15311
|
//#endregion
|
|
15065
15312
|
//#region bin/stim-server.ts
|
|
15066
15313
|
const USAGE = `Usage:
|
|
15067
|
-
stim-server [--port <n>] [--env KEY=VALUE]... [--path-prepend <dir>]...
|
|
15314
|
+
stim-server [--port <n>] [--loopback-only] [--env KEY=VALUE]... [--path-prepend <dir>]...
|
|
15068
15315
|
serve paired clients (default port ${DEFAULT_PORT});
|
|
15316
|
+
--loopback-only serves this Mac only (127.0.0.1, no remote peers);
|
|
15069
15317
|
--env and --path-prepend apply after the login shell's environment
|
|
15070
15318
|
stim-server setup --client <node-id> --ticket <t> --expires <iso>
|
|
15071
15319
|
[--build] [--device-host] [--port <n>] [--label <name>]
|
|
15072
|
-
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
|
|
15320
|
+
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
|
|
15073
15321
|
worker-side approval for this node and ticket until expiry
|
|
15074
15322
|
stim-server service install [--port <n>] [--label <name>] [--serve]
|
|
15075
15323
|
[--env KEY=VALUE]... [--path-prepend <dir>]...
|
|
@@ -15115,7 +15363,7 @@ function routeNote(route, port) {
|
|
|
15115
15363
|
function macName(tailscale) {
|
|
15116
15364
|
return tailscale.state === "running" && tailscale.hostName || hostname();
|
|
15117
15365
|
}
|
|
15118
|
-
async function serve(port, extraEnv, pathPrepend) {
|
|
15366
|
+
async function serve(port, extraEnv, pathPrepend, loopbackOnly) {
|
|
15119
15367
|
const captureHost = hostFromExecutable(process.env.STIM_HOST_EXECUTABLE);
|
|
15120
15368
|
const login = loginShellEnvironment();
|
|
15121
15369
|
if (!login) console.error("stim-server: could not read the login shell environment; using this process environment.");
|
|
@@ -15141,6 +15389,7 @@ async function serve(port, extraEnv, pathPrepend) {
|
|
|
15141
15389
|
name: macName(tailscale),
|
|
15142
15390
|
host: captureHost,
|
|
15143
15391
|
hosts: ["127.0.0.1"],
|
|
15392
|
+
...loopbackOnly ? { loopbackOnly: true } : {},
|
|
15144
15393
|
port,
|
|
15145
15394
|
stimCli: stim.cli,
|
|
15146
15395
|
stimVersion: stim.version,
|
|
@@ -15171,13 +15420,13 @@ async function serve(port, extraEnv, pathPrepend) {
|
|
|
15171
15420
|
}
|
|
15172
15421
|
if (note) console.error(note);
|
|
15173
15422
|
};
|
|
15174
|
-
|
|
15175
|
-
monitor.onChange((snapshot, previous) => {
|
|
15423
|
+
if (!loopbackOnly) monitor.onChange((snapshot, previous) => {
|
|
15176
15424
|
const { state } = snapshot;
|
|
15177
15425
|
if (state.state === "running") console.log("Tailscale is running.");
|
|
15178
15426
|
else if (previous.state.state === "running") console.error("Tailscale stopped.");
|
|
15179
15427
|
if (state.state === "running" || previous.state.state === "running") announce(snapshot.binary, state);
|
|
15180
15428
|
});
|
|
15429
|
+
if (!loopbackOnly) await announce(tailscaleBinary, tailscale);
|
|
15181
15430
|
const shutdown = () => {
|
|
15182
15431
|
monitor.stop();
|
|
15183
15432
|
server.close().catch((error) => {
|
|
@@ -15308,6 +15557,7 @@ async function main() {
|
|
|
15308
15557
|
allowPositionals: true,
|
|
15309
15558
|
options: {
|
|
15310
15559
|
port: { type: "string" },
|
|
15560
|
+
"loopback-only": { type: "boolean" },
|
|
15311
15561
|
json: { type: "boolean" },
|
|
15312
15562
|
control: { type: "boolean" },
|
|
15313
15563
|
read: { type: "boolean" },
|
|
@@ -15350,7 +15600,8 @@ async function main() {
|
|
|
15350
15600
|
}
|
|
15351
15601
|
if ((values.label !== void 0 || values.serve) && !service) fail(`--label and --serve apply only to \`service\`.\n${USAGE}`);
|
|
15352
15602
|
if ((values.release !== void 0 || values.from !== void 0) && !service) fail(`--release and --from apply only to \`service update\`.\n${USAGE}`);
|
|
15353
|
-
if (command
|
|
15603
|
+
if (values["loopback-only"] && command !== void 0) fail(`--loopback-only applies only to serving.\n${USAGE}`);
|
|
15604
|
+
if (command === void 0) return serve(port, extraEnv, pathPrepend, values["loopback-only"] === true);
|
|
15354
15605
|
const grant = command === "devices" && sub === "grant";
|
|
15355
15606
|
if (values.read && !grant) fail(`--read applies only to \`devices grant\`.\n${USAGE}`);
|
|
15356
15607
|
if (values.build && !grant) fail(`--build applies only to \`devices grant\`.\n${USAGE}`);
|
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-CJIlV28Y.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.2",
|
|
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.2",
|
|
32
|
+
"stim": "1.17.2",
|
|
33
33
|
"ws": "^8.21.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|