stim 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -10
- package/dist/android-CC5bUer7.mjs +204 -0
- package/dist/android-DnZlT3aU.mjs +2279 -0
- package/dist/android-cas-DGlTStWu.mjs +1078 -0
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/android-xmyGBNy5.mjs +665 -0
- package/dist/app-install-oZIQRbdD.mjs +1141 -0
- package/dist/build-lock-vE45ZzJZ.mjs +645 -0
- package/dist/build-slots-CgacA9zQ.mjs +152 -0
- package/dist/{cache-manifest-4tH64LQ9.mjs → cache-manifest-oeH18ERr.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/cli.mjs +24 -27166
- package/dist/collector-run.mjs +6 -4
- package/dist/command-output-8wHMEde0.mjs +289 -0
- package/dist/{config-7kmtuhO2.mjs → config-D7uu8Gbq.mjs} +43 -12
- package/dist/deps-64wVnpZc.mjs +435 -0
- package/dist/dev-client-Bvky1hya.mjs +557 -0
- package/dist/device-DJKv5Qej.mjs +250 -0
- package/dist/device-lease-D-G3SgVe.mjs +335 -0
- package/dist/device-pool-BlHXnYYs.mjs +408 -0
- package/dist/device-remote-CCgkG82J.mjs +1209 -0
- package/dist/doctor-BRF9aAJ3.mjs +530 -0
- package/dist/doctor-DlVxTtMq.mjs +1137 -0
- package/dist/error-diagnostics-B5IJwv3-.mjs +636 -0
- package/dist/gc-DhXdgSdt.mjs +1651 -0
- package/dist/guide-DrhrDBo5.mjs +4319 -0
- package/dist/ios-BZxKPuqP.mjs +382 -0
- package/dist/ios-CK1Vrk6Q.mjs +2721 -0
- package/dist/ios-device-CYbuRNXR.mjs +81 -0
- package/dist/ios-device-CdSm0GDw.mjs +413 -0
- package/dist/logs-Dxoy16Qv.mjs +176 -0
- package/dist/logs-query-oqn47Ffx.mjs +273 -0
- package/dist/metro-B01H3EuD.mjs +184 -0
- package/dist/metro-store-CDDZHtPl.mjs +92 -0
- package/dist/ownership-B9QIdQOw.mjs +475 -0
- package/dist/ownership-D8HmSv2E.mjs +60 -0
- package/dist/ownership-claim-CeJLEpjD.mjs +569 -0
- package/dist/{project-Dit1CckW.mjs → project-COj6nzzh.mjs} +12 -3
- package/dist/reclaim-BPvN2HlP.mjs +347 -0
- package/dist/reload-DYZEBbZy.mjs +332 -0
- package/dist/remote-cache-DOi5-zLj.mjs +934 -0
- package/dist/{server-bare-BmTVVmBB.mjs → server-bare-BYHqEeyw.mjs} +3 -2
- package/dist/{server-expo-CHJe1r6j.mjs → server-expo-BEMr_m6p.mjs} +5 -4
- package/dist/server-expo-rb0k-0w7.mjs +2 -0
- package/dist/settings-BSwrY4FZ.mjs +687 -0
- package/dist/spawn-entry-nCE6m770.mjs +14 -0
- package/dist/start-ByCylYtB.mjs +822 -0
- package/dist/{state-2nush3MK.mjs → state-IYEY-zi8.mjs} +21 -22
- package/dist/stats-DZ4Q5J-7.mjs +62 -0
- package/dist/stats-wqxPm7Tv.mjs +307 -0
- package/dist/status-Dia0XTqG.mjs +363 -0
- package/dist/stop-BApyaF21.mjs +591 -0
- package/dist/stop-D6o5zAdm.mjs +3 -0
- package/dist/supervisor-run.d.mts +0 -1
- package/dist/supervisor-run.mjs +4 -4
- package/dist/workspace-process-lock-CxOO9-pf.mjs +47 -0
- package/dist/worktree-Dx3ZC5lS.mjs +539 -0
- package/dist/worktree-DxEmUI_n.mjs +1322 -0
- package/dist/xcode-CDB6fytw.mjs +1705 -0
- package/package.json +5 -5
- package/dist/android-jc8MnUyL.mjs +0 -514
- package/dist/metro-store-COLl1pOk.mjs +0 -1249
- package/dist/server-expo-BZJm--wA.mjs +0 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
//#region src/collector/ios-device.ts
|
|
3
|
+
const CONSOLE_ENV = { OS_ACTIVITY_DT_MODE: "enable" };
|
|
4
|
+
const DEV_MENU_LAUNCH_ARGS = [
|
|
5
|
+
"-EXDevMenuShowsAtLaunch",
|
|
6
|
+
"0",
|
|
7
|
+
"-EXDevMenuShowFloatingActionButton",
|
|
8
|
+
"0"
|
|
9
|
+
];
|
|
10
|
+
const FATAL_MARKERS = [
|
|
11
|
+
/^\*\*\* Terminating app due to uncaught exception/,
|
|
12
|
+
/^libc\+\+abi: terminating/,
|
|
13
|
+
/^\*\*\* Assertion failure in/,
|
|
14
|
+
/^(?:\S+:\d+: )?Fatal error: /
|
|
15
|
+
];
|
|
16
|
+
const MIRROR_LINE = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+[+-]\d{4}) (.+?)\[(\d+):(\d+)\] ?(?:\[([^\]\s]+)\] )?([\s\S]*)$/;
|
|
17
|
+
function deviceConsoleLevel(message) {
|
|
18
|
+
const text = typeof message === "string" ? message : "";
|
|
19
|
+
return FATAL_MARKERS.some((marker) => marker.test(text)) ? "fatal" : "info";
|
|
20
|
+
}
|
|
21
|
+
function parseDeviceConsoleLine(line, { now = Date.now } = {}) {
|
|
22
|
+
if (typeof line !== "string") return null;
|
|
23
|
+
const text = line.replace(/\r$/, "").trimEnd();
|
|
24
|
+
if (!text.trim()) return null;
|
|
25
|
+
const match = MIRROR_LINE.exec(text);
|
|
26
|
+
const record = {
|
|
27
|
+
ts: now(),
|
|
28
|
+
src: "device",
|
|
29
|
+
level: "info",
|
|
30
|
+
msg: text,
|
|
31
|
+
raw: true
|
|
32
|
+
};
|
|
33
|
+
if (!match) {
|
|
34
|
+
record.level = text.startsWith("ERROR: ") ? "error" : deviceConsoleLevel(text);
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
37
|
+
const [, timestamp, proc, pid, , category, message] = match;
|
|
38
|
+
if (message === void 0 || !message.trim()) return null;
|
|
39
|
+
const parsed = Date.parse(timestamp);
|
|
40
|
+
if (Number.isFinite(parsed)) record.ts = parsed;
|
|
41
|
+
record.msg = message;
|
|
42
|
+
record.level = deviceConsoleLevel(message);
|
|
43
|
+
record.proc = `${proc}(${pid})`;
|
|
44
|
+
if (category) record.category = category;
|
|
45
|
+
return record;
|
|
46
|
+
}
|
|
47
|
+
function deviceConsoleArgs({ udid, bundleId, payloadUrl = null }) {
|
|
48
|
+
const args = [
|
|
49
|
+
"devicectl",
|
|
50
|
+
"device",
|
|
51
|
+
"process",
|
|
52
|
+
"launch",
|
|
53
|
+
"--quiet",
|
|
54
|
+
"--device",
|
|
55
|
+
udid,
|
|
56
|
+
"--console",
|
|
57
|
+
"--terminate-existing",
|
|
58
|
+
"--environment-variables",
|
|
59
|
+
JSON.stringify(CONSOLE_ENV)
|
|
60
|
+
];
|
|
61
|
+
if (payloadUrl) args.push("--payload-url", payloadUrl);
|
|
62
|
+
args.push(bundleId);
|
|
63
|
+
if (payloadUrl) args.push("--", ...DEV_MENU_LAUNCH_ARGS);
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
function startIosDeviceConsole({ udid, bundleId, payloadUrl = null, spawnFn = null }) {
|
|
67
|
+
return (spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts)))("xcrun", deviceConsoleArgs({
|
|
68
|
+
udid,
|
|
69
|
+
bundleId,
|
|
70
|
+
payloadUrl
|
|
71
|
+
}), {
|
|
72
|
+
stdio: [
|
|
73
|
+
"ignore",
|
|
74
|
+
"pipe",
|
|
75
|
+
"pipe"
|
|
76
|
+
],
|
|
77
|
+
detached: false
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { startIosDeviceConsole as i, deviceConsoleLevel as n, parseDeviceConsoleLine as r, DEV_MENU_LAUNCH_ARGS as t };
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
|
|
2
|
+
import { r as INSTALL_ERROR } from "./app-install-oZIQRbdD.mjs";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
//#region src/engine/ios-device.ts
|
|
7
|
+
const DEVICECTL_TIMEOUT_MS = 3e4;
|
|
8
|
+
function text(value) {
|
|
9
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
13
|
+
}
|
|
14
|
+
function parseDevicectlDevices(payload) {
|
|
15
|
+
let data = payload;
|
|
16
|
+
if (typeof payload === "string") try {
|
|
17
|
+
data = JSON.parse(payload);
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const devices = record(record(data).result).devices;
|
|
22
|
+
if (!Array.isArray(devices)) return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const raw of devices) {
|
|
25
|
+
const entry = record(raw);
|
|
26
|
+
const hardware = record(entry.hardwareProperties);
|
|
27
|
+
const properties = record(entry.deviceProperties);
|
|
28
|
+
const connection = record(entry.connectionProperties);
|
|
29
|
+
const udid = text(hardware.udid);
|
|
30
|
+
if (!udid) continue;
|
|
31
|
+
out.push({
|
|
32
|
+
udid,
|
|
33
|
+
name: text(properties.name) ?? udid,
|
|
34
|
+
bootState: text(properties.bootState),
|
|
35
|
+
developerModeStatus: text(properties.developerModeStatus),
|
|
36
|
+
pairingState: text(connection.pairingState),
|
|
37
|
+
transportType: text(connection.transportType)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function listIosDevices({ exec = null } = {}) {
|
|
43
|
+
const executor = exec || getExecutor();
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "stim-devicectl-"));
|
|
45
|
+
const out = join(dir, "devices.json");
|
|
46
|
+
try {
|
|
47
|
+
executor.runFile("xcrun", [
|
|
48
|
+
"devicectl",
|
|
49
|
+
"list",
|
|
50
|
+
"devices",
|
|
51
|
+
"-j",
|
|
52
|
+
out
|
|
53
|
+
], { timeoutMs: DEVICECTL_TIMEOUT_MS });
|
|
54
|
+
return parseDevicectlDevices(readFileSync(out, "utf-8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(dir, {
|
|
59
|
+
recursive: true,
|
|
60
|
+
force: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const DEVELOPER_MODE_REMEDY = "Turn on Settings > Privacy & Security > Developer Mode on the phone, restart it, then reconnect.";
|
|
65
|
+
const PAIRING_REMEDY = "Unlock the phone, tap Trust on the pairing prompt, then reconnect the cable.";
|
|
66
|
+
const CABLE_REMEDY = "Connect the phone with a cable and check `xcrun devicectl list devices`, then retry.";
|
|
67
|
+
const WIRED_TRANSPORTS = /* @__PURE__ */ new Set(["wired", "usb"]);
|
|
68
|
+
function isWired(device) {
|
|
69
|
+
return device.transportType === null || WIRED_TRANSPORTS.has(device.transportType.toLowerCase());
|
|
70
|
+
}
|
|
71
|
+
function describe(device) {
|
|
72
|
+
return `${device.udid} (${device.name})`;
|
|
73
|
+
}
|
|
74
|
+
function wirelessRefusal(device) {
|
|
75
|
+
return {
|
|
76
|
+
error: `${describe(device)} is paired over ${device.transportType}, not a cable, and Stim installs over the cable only.`,
|
|
77
|
+
remedy: CABLE_REMEDY
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function unhealthy(device) {
|
|
81
|
+
if (device.pairingState !== null && device.pairingState.toLowerCase() !== "paired") return {
|
|
82
|
+
error: `${describe(device)} is connected but ${device.pairingState}, so devicectl cannot drive it.`,
|
|
83
|
+
remedy: PAIRING_REMEDY
|
|
84
|
+
};
|
|
85
|
+
if (device.developerModeStatus !== null && device.developerModeStatus.toLowerCase() !== "enabled") return {
|
|
86
|
+
error: `${describe(device)} has Developer Mode ${device.developerModeStatus}, so it will not run a development build.`,
|
|
87
|
+
remedy: DEVELOPER_MODE_REMEDY
|
|
88
|
+
};
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function iosPoolCandidates(devices) {
|
|
92
|
+
return (Array.isArray(devices) ? devices : []).filter((device) => isWired(device) && unhealthy(device) === null);
|
|
93
|
+
}
|
|
94
|
+
function iosPoolNoCandidatesRefusal(devices) {
|
|
95
|
+
const listed = Array.isArray(devices) ? devices : [];
|
|
96
|
+
const cabled = listed.filter(isWired);
|
|
97
|
+
const problems = cabled.map((device) => unhealthy(device));
|
|
98
|
+
if (cabled.length > 0 && problems.every((problem) => problem !== null)) {
|
|
99
|
+
const reasons = problems;
|
|
100
|
+
return {
|
|
101
|
+
error: reasons.map((reason) => reason.error).join(" "),
|
|
102
|
+
remedy: [...new Set(reasons.map((reason) => reason.remedy))].join(" ")
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return resolveIosPhysicalDevice(null, listed);
|
|
106
|
+
}
|
|
107
|
+
function resolveIosPhysicalDevice(requested, devices) {
|
|
108
|
+
const listed = Array.isArray(devices) ? devices : [];
|
|
109
|
+
const cabled = listed.filter(isWired);
|
|
110
|
+
const wireless = listed.filter((d) => !isWired(d));
|
|
111
|
+
if (requested) {
|
|
112
|
+
const match = cabled.find((d) => d.udid.toLowerCase() === requested.toLowerCase());
|
|
113
|
+
if (match) return unhealthy(match) ?? {
|
|
114
|
+
udid: match.udid,
|
|
115
|
+
name: match.name
|
|
116
|
+
};
|
|
117
|
+
const overNetwork = wireless.find((d) => d.udid.toLowerCase() === requested.toLowerCase());
|
|
118
|
+
if (overNetwork) return wirelessRefusal(overNetwork);
|
|
119
|
+
return {
|
|
120
|
+
error: cabled.length ? `${requested} is not connected. devicectl reports these cabled devices: ${cabled.map(describe).join(", ")}.` : `${requested} is not connected by cable, and devicectl reports no cabled device at all.`,
|
|
121
|
+
remedy: "Check the cable and `xcrun devicectl list devices`, then retry with a UDID it lists."
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (cabled.length === 1) {
|
|
125
|
+
const only = cabled[0];
|
|
126
|
+
return unhealthy(only) ?? {
|
|
127
|
+
udid: only.udid,
|
|
128
|
+
name: only.name
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (cabled.length > 1) return {
|
|
132
|
+
error: `Several devices are connected: ${cabled.map(describe).join(", ")}.`,
|
|
133
|
+
remedy: "Name the one to build for with `stim ios --device <udid>`."
|
|
134
|
+
};
|
|
135
|
+
const onlyWireless = wireless[0];
|
|
136
|
+
if (onlyWireless) return wirelessRefusal(onlyWireless);
|
|
137
|
+
return {
|
|
138
|
+
error: "No physical iOS device is connected.",
|
|
139
|
+
remedy: "Plug the phone in, unlock it, tap Trust, turn on Settings > Privacy & Security > Developer Mode, then check `xcrun devicectl list devices`."
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const DEVICECTL_INSTALL_TIMEOUT_MS = 3e5;
|
|
143
|
+
const LOCKED_REMEDY = "Unlock the phone and keep it awake, then run the command again.";
|
|
144
|
+
function errorText(error) {
|
|
145
|
+
const withOutput = error;
|
|
146
|
+
const stderr = String(withOutput?.stderr ?? "").trim();
|
|
147
|
+
const stdout = String(withOutput?.stdout ?? "").trim();
|
|
148
|
+
return [
|
|
149
|
+
String(withOutput?.message ?? error).trim(),
|
|
150
|
+
stderr,
|
|
151
|
+
stdout
|
|
152
|
+
].filter(Boolean).join("\n");
|
|
153
|
+
}
|
|
154
|
+
function iosInstallFailureKind(output) {
|
|
155
|
+
const out = String(output ?? "");
|
|
156
|
+
if (/MismatchedApplicationIdentifierEntitlement|application-identifier entitlement[^\n]*does not match/i.test(out)) return "signer";
|
|
157
|
+
if (/device is locked|is locked\b|passcode/i.test(out)) return "locked";
|
|
158
|
+
if (/not paired|pairing|trust this computer|untrusted host/i.test(out)) return "untrusted-host";
|
|
159
|
+
if (/developer mode/i.test(out)) return "developer-mode";
|
|
160
|
+
if (/not enough (?:free )?(?:disk )?space|insufficient (?:disk )?space|storage is full/i.test(out)) return "storage";
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function iosInstallRemedy(kind, { udid, bundleId }) {
|
|
164
|
+
switch (kind) {
|
|
165
|
+
case "signer": return `${bundleId ?? "The app"} is already installed on ${udid} under a different team, and Stim could not remove it. Delete the app on the phone (its data goes with it), then run the command again.`;
|
|
166
|
+
case "locked": return LOCKED_REMEDY;
|
|
167
|
+
case "untrusted-host": return "Unlock the phone, tap Trust on the pairing prompt, then run the command again.";
|
|
168
|
+
case "developer-mode": return "Turn on Settings > Privacy & Security > Developer Mode on the phone, restart it, then run the command again.";
|
|
169
|
+
case "storage": return "Free space on the phone (Settings > General > iPhone Storage), then run the command again.";
|
|
170
|
+
default: return `Check that ${udid} is still connected and unlocked (\`xcrun devicectl list devices\`), and that the app was built for the iphoneos SDK.`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function installIosDeviceApp({ udid, appPath, bundleId = null }, { exec = null } = {}) {
|
|
174
|
+
const e = exec || getExecutor();
|
|
175
|
+
const install = () => {
|
|
176
|
+
e.runFile("xcrun", [
|
|
177
|
+
"devicectl",
|
|
178
|
+
"device",
|
|
179
|
+
"install",
|
|
180
|
+
"app",
|
|
181
|
+
"--device",
|
|
182
|
+
udid,
|
|
183
|
+
appPath
|
|
184
|
+
], { timeoutMs: DEVICECTL_INSTALL_TIMEOUT_MS });
|
|
185
|
+
};
|
|
186
|
+
try {
|
|
187
|
+
install();
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
appPath
|
|
191
|
+
};
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const failure = errorText(err);
|
|
194
|
+
const kind = iosInstallFailureKind(failure);
|
|
195
|
+
if (kind !== "signer" || !bundleId) return {
|
|
196
|
+
failed: true,
|
|
197
|
+
code: INSTALL_ERROR,
|
|
198
|
+
reason: `devicectl could not install ${appPath} on ${udid}: ${failure}`,
|
|
199
|
+
remedy: iosInstallRemedy(kind, {
|
|
200
|
+
udid,
|
|
201
|
+
bundleId
|
|
202
|
+
})
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
e.runFile("xcrun", [
|
|
206
|
+
"devicectl",
|
|
207
|
+
"device",
|
|
208
|
+
"uninstall",
|
|
209
|
+
"app",
|
|
210
|
+
"--device",
|
|
211
|
+
udid,
|
|
212
|
+
bundleId
|
|
213
|
+
], { timeoutMs: DEVICECTL_INSTALL_TIMEOUT_MS });
|
|
214
|
+
} catch (uninstallErr) {
|
|
215
|
+
return {
|
|
216
|
+
failed: true,
|
|
217
|
+
code: INSTALL_ERROR,
|
|
218
|
+
reason: `devicectl refused to install ${appPath} over the copy of ${bundleId} already on ${udid}, which a different team signed, and the uninstall failed too: ${errorText(uninstallErr)}`,
|
|
219
|
+
remedy: iosInstallRemedy("signer", {
|
|
220
|
+
udid,
|
|
221
|
+
bundleId
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
install();
|
|
227
|
+
} catch (retryErr) {
|
|
228
|
+
return {
|
|
229
|
+
failed: true,
|
|
230
|
+
code: INSTALL_ERROR,
|
|
231
|
+
reason: `devicectl could not install ${appPath} on ${udid} even after uninstalling ${bundleId}: ${errorText(retryErr)}`,
|
|
232
|
+
remedy: iosInstallRemedy(null, {
|
|
233
|
+
udid,
|
|
234
|
+
bundleId
|
|
235
|
+
})
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
appPath,
|
|
241
|
+
uninstalled: true,
|
|
242
|
+
note: `${bundleId} was already installed on ${udid} under a different team, so it was uninstalled (its data went with it) before this app could be installed. An uninstall also clears the phone's developer trust and its Local Network permission, so the launch below may need Settings > General > VPN & Device Management again`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function parseDeviceProcesses(payload) {
|
|
247
|
+
let data = payload;
|
|
248
|
+
if (typeof payload === "string") try {
|
|
249
|
+
data = JSON.parse(payload);
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const running = record(record(data).result).runningProcesses;
|
|
254
|
+
if (!Array.isArray(running)) return [];
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const raw of running) {
|
|
257
|
+
const entry = record(raw);
|
|
258
|
+
const pid = Number(entry.processIdentifier);
|
|
259
|
+
const executable = text(entry.executable);
|
|
260
|
+
if (!Number.isInteger(pid) || pid <= 0 || !executable) continue;
|
|
261
|
+
out.push({
|
|
262
|
+
pid,
|
|
263
|
+
executable
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
function deviceProcessPid(processes, appName) {
|
|
269
|
+
const marker = `/${appName}.app/`;
|
|
270
|
+
for (const entry of processes) if (entry.executable.includes(marker)) return entry.pid;
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function iosDeviceProcess({ udid, appName }, { exec = null } = {}) {
|
|
274
|
+
const executor = exec || getExecutor();
|
|
275
|
+
const dir = mkdtempSync(join(tmpdir(), "stim-devicectl-"));
|
|
276
|
+
const out = join(dir, "processes.json");
|
|
277
|
+
try {
|
|
278
|
+
executor.runFile("xcrun", [
|
|
279
|
+
"devicectl",
|
|
280
|
+
"device",
|
|
281
|
+
"info",
|
|
282
|
+
"processes",
|
|
283
|
+
"--device",
|
|
284
|
+
udid,
|
|
285
|
+
"--quiet",
|
|
286
|
+
"--json-output",
|
|
287
|
+
out
|
|
288
|
+
], { timeoutMs: DEVICECTL_TIMEOUT_MS });
|
|
289
|
+
return deviceProcessPid(parseDeviceProcesses(readFileSync(out, "utf-8")), appName);
|
|
290
|
+
} catch {
|
|
291
|
+
return;
|
|
292
|
+
} finally {
|
|
293
|
+
rmSync(dir, {
|
|
294
|
+
recursive: true,
|
|
295
|
+
force: true
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const LAUNCH_REFUSALS = [
|
|
300
|
+
["untrusted-developer", /profile has not been explicitly trusted by the user/i],
|
|
301
|
+
["untrusted-developer", /FBSOpenApplicationErrorDomain error 3\b/],
|
|
302
|
+
["untrusted-developer", /denied by service delegate[^\n]*for reason: Security/],
|
|
303
|
+
["locked", /device is locked|is locked\b|passcode/i],
|
|
304
|
+
["not-installed", /(?:application|app)[^\n]*\b(?:is )?(?:unknown to FrontBoard|not installed)/i]
|
|
305
|
+
];
|
|
306
|
+
function iosLaunchRefusalKind(output) {
|
|
307
|
+
for (const line of String(output ?? "").split(/\r?\n/)) for (const [kind, pattern] of LAUNCH_REFUSALS) if (pattern.test(line)) return kind;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function iosLaunchRemedy(kind, { udid, bundleId }) {
|
|
311
|
+
switch (kind) {
|
|
312
|
+
case "untrusted-developer": return "The phone has not trusted this build's developer certificate, which it refuses to do without a human. On the phone open Settings > General > VPN & Device Management, tap the developer profile under DEVELOPER APP, tap Trust, then run the command again.";
|
|
313
|
+
case "locked": return LOCKED_REMEDY;
|
|
314
|
+
case "not-installed": return `${bundleId} is not installed on ${udid} any more. Run the command again to install it.`;
|
|
315
|
+
default: return `Run \`xcrun devicectl device process launch --console --device ${udid} ${bundleId}\` to see what the phone reports.`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const LAUNCH_PROBE_TIMEOUT_MS = 45e3;
|
|
319
|
+
const LAUNCH_PROBE_POLL_MS = 1e3;
|
|
320
|
+
const COLLECTOR_ENDED_EVENTS = /* @__PURE__ */ new Set(["collector_failed", "collector_stopped"]);
|
|
321
|
+
function launchEvidence(records) {
|
|
322
|
+
const lines = [];
|
|
323
|
+
for (const entry of records) {
|
|
324
|
+
const msg = typeof entry.msg === "string" ? entry.msg.trim() : "";
|
|
325
|
+
if (!msg) continue;
|
|
326
|
+
if (entry.event === "collector_started" || entry.event === "collector_empty") continue;
|
|
327
|
+
lines.push(msg);
|
|
328
|
+
}
|
|
329
|
+
return lines.slice(-6);
|
|
330
|
+
}
|
|
331
|
+
function collectorRecordsFor(records, collectorPid) {
|
|
332
|
+
if (!collectorPid) return [];
|
|
333
|
+
const marker = new RegExp(`\\bcollector pid ${collectorPid}\\b`);
|
|
334
|
+
const start = records.findIndex((entry) => entry.event === "collector_started" && typeof entry.msg === "string" && marker.test(entry.msg));
|
|
335
|
+
return start < 0 ? [] : records.slice(start);
|
|
336
|
+
}
|
|
337
|
+
async function awaitIosDeviceLaunch({ udid, bundleId, appName, collectorPid = null, readRecords, probe, timeoutMs = LAUNCH_PROBE_TIMEOUT_MS, pollMs = LAUNCH_PROBE_POLL_MS, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
338
|
+
const probeProcess = probe ?? (() => iosDeviceProcess({
|
|
339
|
+
udid,
|
|
340
|
+
appName
|
|
341
|
+
}));
|
|
342
|
+
const deadline = now() + Math.max(0, timeoutMs);
|
|
343
|
+
for (;;) {
|
|
344
|
+
const records = collectorRecordsFor(readRecords(), collectorPid);
|
|
345
|
+
const ended = records.find((entry) => typeof entry.event === "string" && COLLECTOR_ENDED_EVENTS.has(entry.event));
|
|
346
|
+
const pid = probeProcess();
|
|
347
|
+
if (typeof pid === "number") return { pid };
|
|
348
|
+
if (ended) {
|
|
349
|
+
const lines = launchEvidence(records);
|
|
350
|
+
const kind = iosLaunchRefusalKind(lines.join("\n"));
|
|
351
|
+
return {
|
|
352
|
+
failed: true,
|
|
353
|
+
reason: `devicectl could not keep ${bundleId} running on ${udid}: the console ended before the app appeared in the device's process list.`,
|
|
354
|
+
remedy: iosLaunchRemedy(kind, {
|
|
355
|
+
udid,
|
|
356
|
+
bundleId
|
|
357
|
+
}),
|
|
358
|
+
lines
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (now() >= deadline) {
|
|
362
|
+
const lines = launchEvidence(records);
|
|
363
|
+
return {
|
|
364
|
+
failed: true,
|
|
365
|
+
reason: `${bundleId} did not appear in ${udid}'s process list within ${Math.round(timeoutMs / 1e3)}s of the launch.`,
|
|
366
|
+
remedy: iosLaunchRemedy(iosLaunchRefusalKind(lines.join("\n")), {
|
|
367
|
+
udid,
|
|
368
|
+
bundleId
|
|
369
|
+
}),
|
|
370
|
+
lines
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - now())));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function verifyIosDeviceReleaseLaunch({ udid, appName, waitMs = 3e3, probe, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
377
|
+
const probeProcess = probe ?? (() => iosDeviceProcess({
|
|
378
|
+
udid,
|
|
379
|
+
appName
|
|
380
|
+
}));
|
|
381
|
+
const startedAt = now();
|
|
382
|
+
await sleep(Math.max(0, waitMs));
|
|
383
|
+
const pid = probeProcess();
|
|
384
|
+
const waitedMs = now() - startedAt;
|
|
385
|
+
if (pid === void 0) return {
|
|
386
|
+
verified: false,
|
|
387
|
+
reason: "probe-failed",
|
|
388
|
+
waitedMs
|
|
389
|
+
};
|
|
390
|
+
return pid === null ? {
|
|
391
|
+
verified: false,
|
|
392
|
+
reason: "exited",
|
|
393
|
+
waitedMs,
|
|
394
|
+
pid: null
|
|
395
|
+
} : {
|
|
396
|
+
verified: true,
|
|
397
|
+
waitedMs,
|
|
398
|
+
pid
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const LOCAL_NETWORK_PROHIBITED = /_NSURLErrorNWPathKey=unsatisfied \(Local network prohibited\)/;
|
|
402
|
+
function localNetworkPending(records, { since = 0, pid = null, lanOrigin }) {
|
|
403
|
+
if (!lanOrigin) return false;
|
|
404
|
+
const fromApp = pid ? new RegExp(`\\(${pid}\\)$`) : null;
|
|
405
|
+
for (const entry of records) {
|
|
406
|
+
if (Number(entry.ts) < since) continue;
|
|
407
|
+
if (fromApp && !(typeof entry.proc === "string" && fromApp.test(entry.proc))) continue;
|
|
408
|
+
if (LOCAL_NETWORK_PROHIBITED.test(typeof entry.msg === "string" ? entry.msg : "")) return true;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
export { iosDeviceProcess as a, listIosDevices as c, verifyIosDeviceReleaseLaunch as d, installIosDeviceApp as i, localNetworkPending as l, LAUNCH_PROBE_TIMEOUT_MS as n, iosPoolCandidates as o, awaitIosDeviceLaunch as r, iosPoolNoCandidatesRefusal as s, DEVICECTL_INSTALL_TIMEOUT_MS as t, resolveIosPhysicalDevice as u };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { n as SOURCES, t as LEVELS } from "./ndjson-DcAtEx_K.mjs";
|
|
2
|
+
import { D as workspaceLogsDir, p as readWorkspaceState } from "./state-IYEY-zi8.mjs";
|
|
3
|
+
import { a as findProjectRoot } from "./project-COj6nzzh.mjs";
|
|
4
|
+
import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
|
|
5
|
+
import { i as captureWorkspaceCrashes, t as errorDiagnostics } from "./error-diagnostics-B5IJwv3-.mjs";
|
|
6
|
+
import { a as followLogs, i as fileSizes, n as buildCriteria, o as parseSince, r as compileGrep, s as queryLogs } from "./logs-query-oqn47Ffx.mjs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
//#region src/commands/logs.ts
|
|
9
|
+
const LEVEL_WIDTH = 5;
|
|
10
|
+
const SRC_WIDTH = 6;
|
|
11
|
+
const INDENT = " ";
|
|
12
|
+
function padTimePart(value, width = 2) {
|
|
13
|
+
return String(value).padStart(width, "0");
|
|
14
|
+
}
|
|
15
|
+
function formatTime(ts) {
|
|
16
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) return "--:--:--.---";
|
|
17
|
+
const d = new Date(ts);
|
|
18
|
+
return `${padTimePart(d.getHours())}:${padTimePart(d.getMinutes())}:${padTimePart(d.getSeconds())}.${padTimePart(d.getMilliseconds(), 3)}`;
|
|
19
|
+
}
|
|
20
|
+
function formatStackFrame(frame) {
|
|
21
|
+
if (!frame || typeof frame !== "object") return null;
|
|
22
|
+
const where = [
|
|
23
|
+
frame.file,
|
|
24
|
+
frame.line,
|
|
25
|
+
frame.column
|
|
26
|
+
].filter((p) => p !== void 0 && p !== null && p !== "").join(":");
|
|
27
|
+
if (frame.fn && where) return `at ${frame.fn} (${where})`;
|
|
28
|
+
if (frame.fn) return `at ${frame.fn}`;
|
|
29
|
+
if (where) return `at ${where}`;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function formatRecord(record, { paint } = {}) {
|
|
33
|
+
const colour = paint || ((t) => t);
|
|
34
|
+
const level = String(record?.level ?? "").padEnd(LEVEL_WIDTH);
|
|
35
|
+
const src = String(record?.src ?? "").padEnd(SRC_WIDTH);
|
|
36
|
+
const [first, ...rest] = (record?.msg === void 0 || record?.msg === null ? "" : String(record.msg)).split("\n");
|
|
37
|
+
const lines = [`${formatTime(record?.ts)} ${colour(level)} ${src} ${first}`];
|
|
38
|
+
for (const line of rest) lines.push(`${INDENT}${line}`);
|
|
39
|
+
for (const frame of Array.isArray(record?.stack) ? record.stack : []) {
|
|
40
|
+
const rendered = formatStackFrame(frame);
|
|
41
|
+
if (rendered) lines.push(`${INDENT}${rendered}`);
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
function parseTail(value) {
|
|
46
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return { n: value };
|
|
47
|
+
if (typeof value !== "string" || !/^\d+$/.test(value.trim())) return { error: `Invalid --tail value ${JSON.stringify(value)}. Use a non-negative whole number, e.g. --tail 50.` };
|
|
48
|
+
return { n: parseInt(value.trim(), 10) };
|
|
49
|
+
}
|
|
50
|
+
function validateSources(sources) {
|
|
51
|
+
if (sources === void 0 || sources === null) return { sources: void 0 };
|
|
52
|
+
const list = Array.isArray(sources) ? sources : [sources];
|
|
53
|
+
if (list.includes("all")) return { sources: [...SOURCES] };
|
|
54
|
+
const unknown = list.filter((s) => !SOURCES.includes(s));
|
|
55
|
+
if (unknown.length > 0) return { error: `Unknown --source value(s): ${unknown.join(", ")}. Use one or more of: ${SOURCES.join(", ")}, or all.` };
|
|
56
|
+
return { sources: list };
|
|
57
|
+
}
|
|
58
|
+
function validateLevel(level) {
|
|
59
|
+
if (LEVELS.includes(level)) return { level };
|
|
60
|
+
return { error: `Invalid --level value ${JSON.stringify(level)}. Use one of: ${LEVELS.join(", ")}.` };
|
|
61
|
+
}
|
|
62
|
+
const ERRORS_PRINT_CAP = 20;
|
|
63
|
+
const LEVEL_COLOURS = {
|
|
64
|
+
debug: chalk.dim,
|
|
65
|
+
info: chalk.reset,
|
|
66
|
+
warn: chalk.yellow,
|
|
67
|
+
error: chalk.red,
|
|
68
|
+
fatal: chalk.bgRed
|
|
69
|
+
};
|
|
70
|
+
function logsCommand(program) {
|
|
71
|
+
program.command("logs").description("Query this workspace's merged NDJSON log timeline (bundler, client, device, build). Prints and exits; nothing matching is a successful, empty result. Use --follow to stream.").option("--source <s...>", "Only these sources: metro, client, device, build, or all").option("--level <l>", `Minimum level: ${LEVELS.join(", ")}`).option("--since <d>", "Only records newer than this, e.g. 30s, 5m, 2h").option("--grep <re>", "Only records whose message matches this regular expression").option("--tail <n>", "Only the last n matching records").option("--errors", "Errors and fatals since the last marker, from metro, client and build, plus confirmed native app-crash reports. Add --source device or --source all for general device errors.").option("--follow", "Keep streaming new records until interrupted").option("--json", "Emit the raw records, one per line (valid NDJSON; zero matches is zero bytes, exit 0)").action(async (opts) => {
|
|
72
|
+
const root = findProjectRoot(process.cwd());
|
|
73
|
+
if (!root) {
|
|
74
|
+
console.error(chalk.red("Not in a React Native project (no package.json found)."));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const dir = workspaceLogsDir(root);
|
|
78
|
+
let sources;
|
|
79
|
+
if (opts.source !== void 0) {
|
|
80
|
+
const checked = validateSources(opts.source);
|
|
81
|
+
if (checked.error) fail(checked.error);
|
|
82
|
+
sources = checked.sources;
|
|
83
|
+
}
|
|
84
|
+
let minLevel;
|
|
85
|
+
if (opts.level !== void 0) {
|
|
86
|
+
const checked = validateLevel(opts.level);
|
|
87
|
+
if (checked.error) fail(checked.error);
|
|
88
|
+
minLevel = checked.level;
|
|
89
|
+
}
|
|
90
|
+
if (opts.since !== void 0) {
|
|
91
|
+
const parsed = parseSince(opts.since);
|
|
92
|
+
if (parsed.error) fail(parsed.error);
|
|
93
|
+
}
|
|
94
|
+
let tail;
|
|
95
|
+
if (opts.tail !== void 0) {
|
|
96
|
+
const parsed = parseTail(opts.tail);
|
|
97
|
+
if (parsed.error) fail(parsed.error);
|
|
98
|
+
tail = parsed.n;
|
|
99
|
+
}
|
|
100
|
+
if (opts.grep !== void 0) {
|
|
101
|
+
const compiled = compileGrep(opts.grep);
|
|
102
|
+
if (compiled.error) fail(compiled.error);
|
|
103
|
+
}
|
|
104
|
+
const query = {
|
|
105
|
+
dir,
|
|
106
|
+
sources,
|
|
107
|
+
minLevel,
|
|
108
|
+
since: opts.since,
|
|
109
|
+
grep: opts.grep,
|
|
110
|
+
tail,
|
|
111
|
+
errorsOnly: Boolean(opts.errors),
|
|
112
|
+
errorContext: Boolean(opts.errors && !opts.json && !opts.follow)
|
|
113
|
+
};
|
|
114
|
+
const emit = (record) => {
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
console.log(JSON.stringify(record));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const rendered = {
|
|
120
|
+
...record,
|
|
121
|
+
msg: launchErrorPreview([record], root, { full: true }).join("\n"),
|
|
122
|
+
stack: void 0
|
|
123
|
+
};
|
|
124
|
+
console.log(formatRecord(rendered, { paint: record?.level ? LEVEL_COLOURS[record.level] : void 0 }));
|
|
125
|
+
};
|
|
126
|
+
const offsets = opts.follow ? fileSizes(dir) : null;
|
|
127
|
+
captureWorkspaceCrashes(root, dir);
|
|
128
|
+
const rawRecords = queryLogs(query);
|
|
129
|
+
const supervisorPort = readWorkspaceState(root)?.supervisor?.port;
|
|
130
|
+
const records = opts.json ? rawRecords : await errorDiagnostics(rawRecords, {
|
|
131
|
+
root,
|
|
132
|
+
logsDir: dir,
|
|
133
|
+
port: typeof supervisorPort === "number" ? supervisorPort : null,
|
|
134
|
+
allowRequest: true
|
|
135
|
+
});
|
|
136
|
+
const errorCount = records.filter((record) => record.errorContext !== true).length;
|
|
137
|
+
let seenErrors = 0;
|
|
138
|
+
const capped = opts.errors && !opts.json && tail === void 0 && errorCount > 20 ? records.filter((record) => {
|
|
139
|
+
if (record.errorContext === true) return seenErrors <= 20;
|
|
140
|
+
seenErrors += 1;
|
|
141
|
+
return seenErrors <= 20;
|
|
142
|
+
}) : records;
|
|
143
|
+
for (const record of capped) emit(record);
|
|
144
|
+
const hidden = errorCount - capped.filter((record) => record.errorContext !== true).length;
|
|
145
|
+
if (hidden > 0) console.log(chalk.dim(`Showing 20 of ${errorCount} matching error records; ${hidden} not shown. Add --tail ${rawRecords.length} to include all captured records before grouping, or --json for raw records. Stacks are shown in full.`));
|
|
146
|
+
if (!opts.follow) {
|
|
147
|
+
if (records.length === 0 && !opts.json) console.error(chalk.dim(`No matching log records in ${dir}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const criteria = buildCriteria({
|
|
151
|
+
sources,
|
|
152
|
+
minLevel,
|
|
153
|
+
since: opts.since,
|
|
154
|
+
grep: opts.grep,
|
|
155
|
+
errorsOnly: Boolean(opts.errors)
|
|
156
|
+
});
|
|
157
|
+
const stop = followLogs({
|
|
158
|
+
dir,
|
|
159
|
+
offsets,
|
|
160
|
+
criteria,
|
|
161
|
+
onRecord: emit
|
|
162
|
+
});
|
|
163
|
+
const finish = () => {
|
|
164
|
+
stop();
|
|
165
|
+
process.exit(0);
|
|
166
|
+
};
|
|
167
|
+
process.on("SIGINT", finish);
|
|
168
|
+
process.on("SIGTERM", finish);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function fail(message) {
|
|
172
|
+
console.error(chalk.red(message));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
export { ERRORS_PRINT_CAP, logsCommand as default, formatRecord, formatStackFrame, formatTime, parseTail, validateLevel, validateSources };
|