serve-sim 0.1.22 → 0.1.24
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 +54 -2
- package/Sources/SimCameraHelper/build.sh +32 -0
- package/Sources/SimCameraHelper/main.m +859 -0
- package/Sources/SimCameraInjector/SimCameraInjector.m +1160 -0
- package/Sources/SimCameraInjector/build.sh +33 -0
- package/Sources/SimCameraInjector/include/SimCamShared.h +55 -0
- package/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +24 -24
- package/dist/serve-sim.js +74 -30
- package/dist/simcam/libSimCameraInjector.dylib +0 -0
- package/dist/simcam/serve-sim-camera-helper +0 -0
- package/package.json +14 -3
- package/src/ax.ts +2 -2
- package/src/middleware.ts +93 -91
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serve-sim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Evan Bacon",
|
|
@@ -27,11 +27,18 @@
|
|
|
27
27
|
"dist/serve-sim.js",
|
|
28
28
|
"dist/middleware.js",
|
|
29
29
|
"dist/middleware.cjs",
|
|
30
|
+
"dist/simcam/libSimCameraInjector.dylib",
|
|
31
|
+
"dist/simcam/serve-sim-camera-helper",
|
|
30
32
|
"src/ax-shared.ts",
|
|
31
33
|
"src/ax.ts",
|
|
32
34
|
"src/middleware.ts",
|
|
33
35
|
"src/state.ts",
|
|
34
|
-
"bin/serve-sim-bin"
|
|
36
|
+
"bin/serve-sim-bin",
|
|
37
|
+
"Sources/SimCameraInjector/SimCameraInjector.m",
|
|
38
|
+
"Sources/SimCameraInjector/include/SimCamShared.h",
|
|
39
|
+
"Sources/SimCameraInjector/build.sh",
|
|
40
|
+
"Sources/SimCameraHelper/main.m",
|
|
41
|
+
"Sources/SimCameraHelper/build.sh"
|
|
35
42
|
],
|
|
36
43
|
"exports": {
|
|
37
44
|
"./middleware": {
|
|
@@ -51,10 +58,14 @@
|
|
|
51
58
|
},
|
|
52
59
|
"devDependencies": {
|
|
53
60
|
"@types/bun": "latest",
|
|
54
|
-
"
|
|
61
|
+
"@types/react": "^19.0.0",
|
|
62
|
+
"@types/react-dom": "^19.0.0",
|
|
63
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
55
64
|
"preact": "^10.29.1",
|
|
56
65
|
"react": "^19.0.0",
|
|
57
66
|
"react-dom": "^19.0.0",
|
|
67
|
+
"serve-sim-client": "workspace:*",
|
|
68
|
+
"tailwindcss": "^4.1.7",
|
|
58
69
|
"typescript": "^5.7.0"
|
|
59
70
|
},
|
|
60
71
|
"dependencies": {
|
package/src/ax.ts
CHANGED
|
@@ -62,12 +62,12 @@ function normalizeAxTree(roots: RawAxeNode[]): AxSnapshot {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
for (let index = 0; index < node.children.length && elements.length < MAX_ELEMENTS; index++) {
|
|
65
|
-
visit(node.children[index]
|
|
65
|
+
visit(node.children[index]!, `${path}.${index}`);
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
for (let index = 0; index < roots.length && elements.length < MAX_ELEMENTS; index++) {
|
|
70
|
-
visit(roots[index]
|
|
70
|
+
visit(roots[index]!, String(index));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
return {
|
package/src/middleware.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync, unlinkSync } from "fs";
|
|
2
|
-
import { execSync, spawn, exec, execFile, type ChildProcess } from "child_process";
|
|
2
|
+
import { execSync, spawn, exec, execFile, type ChildProcess, type ExecException } from "child_process";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { createServer as createNetServer } from "net";
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
6
7
|
import { createAxStreamerCache } from "./ax";
|
|
7
8
|
|
|
9
|
+
type SimReq = IncomingMessage;
|
|
10
|
+
type SimRes = ServerResponse;
|
|
11
|
+
type SimNext = (err?: unknown) => void;
|
|
12
|
+
|
|
8
13
|
// Injected at build time as a base64-encoded string via `define`
|
|
9
14
|
declare const __PREVIEW_HTML_B64__: string;
|
|
10
15
|
const STATE_DIR = join(tmpdir(), "serve-sim");
|
|
@@ -31,6 +36,41 @@ type WebKitBridge = {
|
|
|
31
36
|
releaseHighlight?(targetId?: string): void;
|
|
32
37
|
};
|
|
33
38
|
|
|
39
|
+
type InspectWebKitBridgeTarget = {
|
|
40
|
+
targetId: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
appName?: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
bundleId?: string;
|
|
46
|
+
inUseByOtherInspector?: boolean;
|
|
47
|
+
source?: { kind?: string; id?: string };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type CdpHttpListEntry = {
|
|
51
|
+
id: string;
|
|
52
|
+
title: string;
|
|
53
|
+
url: string;
|
|
54
|
+
type: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type CdpHttpVersion = { Browser?: string };
|
|
59
|
+
|
|
60
|
+
type SimctlBootedList = {
|
|
61
|
+
devices: Record<string, Array<{ udid: string; state: string }>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SimctlAllList = {
|
|
65
|
+
devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ShutdownRequestBody = { udid?: string };
|
|
69
|
+
type StartRequestBody = { udid?: string };
|
|
70
|
+
type ReleaseRequestBody = { targetId?: string };
|
|
71
|
+
type HighlightRequestBody = { targetId?: string; on?: boolean };
|
|
72
|
+
type ExecRequestBody = { command?: string };
|
|
73
|
+
|
|
34
74
|
export interface ServeSimState {
|
|
35
75
|
pid: number;
|
|
36
76
|
port: number;
|
|
@@ -123,53 +163,6 @@ export function matchInstalledAppByDisplayName(
|
|
|
123
163
|
return null;
|
|
124
164
|
}
|
|
125
165
|
|
|
126
|
-
function shellQuote(value: string): string {
|
|
127
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function installedAppsForDevice(udid: string): Promise<Record<string, InstalledApp>> {
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
exec(
|
|
133
|
-
`xcrun simctl listapps ${shellQuote(udid)} | plutil -convert json -o - -`,
|
|
134
|
-
{ timeout: 3_000 },
|
|
135
|
-
(err, stdout) => {
|
|
136
|
-
if (err || !stdout) return resolve({});
|
|
137
|
-
try {
|
|
138
|
-
resolve(JSON.parse(stdout) as Record<string, InstalledApp>);
|
|
139
|
-
} catch {
|
|
140
|
-
resolve({});
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function detectCurrentForegroundApp(
|
|
148
|
-
udid: string,
|
|
149
|
-
stateUrl: string,
|
|
150
|
-
): Promise<{ bundleId: string; isReactNative: boolean } | null> {
|
|
151
|
-
const controller = new AbortController();
|
|
152
|
-
const timer = setTimeout(() => controller.abort(), 2_000);
|
|
153
|
-
try {
|
|
154
|
-
const res = await fetch(`${stateUrl}/ax`, { signal: controller.signal });
|
|
155
|
-
if (!res.ok) return null;
|
|
156
|
-
const tree = await res.json() as Array<{ AXLabel?: unknown }>;
|
|
157
|
-
const displayName = tree?.[0]?.AXLabel;
|
|
158
|
-
if (typeof displayName !== "string") return null;
|
|
159
|
-
|
|
160
|
-
const bundleId = matchInstalledAppByDisplayName(
|
|
161
|
-
await installedAppsForDevice(udid),
|
|
162
|
-
displayName,
|
|
163
|
-
);
|
|
164
|
-
if (!bundleId || !isUserFacingBundle(bundleId)) return null;
|
|
165
|
-
return { bundleId, isReactNative: await detectReactNative(udid, bundleId) };
|
|
166
|
-
} catch {
|
|
167
|
-
return null;
|
|
168
|
-
} finally {
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
166
|
// Cache simctl's booted-device set briefly so per-request cost stays bounded.
|
|
174
167
|
// The middleware runs inside the user's dev server (Metro etc.) and
|
|
175
168
|
// readServeSimStates() is called on every /api and every page load.
|
|
@@ -185,9 +178,7 @@ function getBootedUdids(): Set<string> | null {
|
|
|
185
178
|
stdio: ["ignore", "pipe", "pipe"],
|
|
186
179
|
timeout: 3_000,
|
|
187
180
|
});
|
|
188
|
-
const data = JSON.parse(output) as
|
|
189
|
-
devices: Record<string, Array<{ udid: string; state: string }>>;
|
|
190
|
-
};
|
|
181
|
+
const data = JSON.parse(output) as SimctlBootedList;
|
|
191
182
|
const booted = new Set<string>();
|
|
192
183
|
for (const runtime of Object.values(data.devices)) {
|
|
193
184
|
for (const device of runtime) {
|
|
@@ -272,7 +263,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
|
|
|
272
263
|
try {
|
|
273
264
|
const versionRes = await fetch(`${cdpUrl}/json/version`);
|
|
274
265
|
if (!versionRes.ok) return null;
|
|
275
|
-
const version = await versionRes.json() as
|
|
266
|
+
const version = await versionRes.json() as CdpHttpVersion;
|
|
276
267
|
if (version.Browser !== "Safari/inspect-webkit") return null;
|
|
277
268
|
return {
|
|
278
269
|
port,
|
|
@@ -283,13 +274,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
|
|
|
283
274
|
// shape `sim:<udid>:<appId>:<pageId>` and the description string
|
|
284
275
|
// `<deviceLabel> (<bundleId>)` are all we have here.
|
|
285
276
|
const listRes = await fetch(`${cdpUrl}/json/list`);
|
|
286
|
-
const targets = await listRes.json() as
|
|
287
|
-
id: string;
|
|
288
|
-
title: string;
|
|
289
|
-
url: string;
|
|
290
|
-
type: string;
|
|
291
|
-
description?: string;
|
|
292
|
-
}>;
|
|
277
|
+
const targets = await listRes.json() as CdpHttpListEntry[];
|
|
293
278
|
return targets
|
|
294
279
|
.filter((target) => target.id.startsWith("sim:"))
|
|
295
280
|
.map((target) => {
|
|
@@ -335,29 +320,35 @@ async function ensureInspectWebKitBridge(): Promise<WebKitBridge> {
|
|
|
335
320
|
// (and what the DevTools frontend CSP whitelists). `localhost` resolves
|
|
336
321
|
// to ::1 first on some setups, which would leave the iframe's
|
|
337
322
|
// ws://127.0.0.1:9222 connection refused.
|
|
338
|
-
const server = await startCdpServer({ host: "127.0.0.1", port })
|
|
323
|
+
const server = await startCdpServer({ host: "127.0.0.1", port }) as Awaited<ReturnType<typeof startCdpServer>> & {
|
|
324
|
+
highlightTarget?(targetId: string, on: boolean): Promise<void>;
|
|
325
|
+
releaseHighlight?(targetId?: string): void;
|
|
326
|
+
};
|
|
339
327
|
return {
|
|
340
328
|
port,
|
|
341
329
|
cdpUrl: `http://127.0.0.1:${port}`,
|
|
342
330
|
async listTargets() {
|
|
343
|
-
return server.getTargets()
|
|
344
|
-
.filter((target
|
|
345
|
-
.map((target
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
331
|
+
return (server.getTargets() as InspectWebKitBridgeTarget[])
|
|
332
|
+
.filter((target) => target.source?.kind === "simulator")
|
|
333
|
+
.map((target) => {
|
|
334
|
+
const url = target.url ?? "";
|
|
335
|
+
return {
|
|
336
|
+
id: target.targetId,
|
|
337
|
+
title: target.title || target.appName || url || "Untitled",
|
|
338
|
+
url: /^https?:/i.test(url) ? url : "about:blank",
|
|
339
|
+
type: target.type || "page",
|
|
340
|
+
appName: target.appName,
|
|
341
|
+
bundleId: target.bundleId,
|
|
342
|
+
udid: target.source?.id,
|
|
343
|
+
inUseByOtherInspector: !!target.inUseByOtherInspector,
|
|
344
|
+
};
|
|
345
|
+
});
|
|
355
346
|
},
|
|
356
347
|
highlightTarget: server.highlightTarget?.bind(server),
|
|
357
348
|
releaseHighlight: server.releaseHighlight?.bind(server),
|
|
358
349
|
};
|
|
359
|
-
} catch (err
|
|
360
|
-
if (err?.code === "EADDRINUSE") {
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
|
|
361
352
|
const existing = await existingInspectWebKitBridge(port);
|
|
362
353
|
if (existing) return existing;
|
|
363
354
|
continue;
|
|
@@ -392,6 +383,19 @@ function bridgeWsHost(_reqHost: string | undefined, bridgePort: number): string
|
|
|
392
383
|
}
|
|
393
384
|
|
|
394
385
|
let _html: string | null = null;
|
|
386
|
+
/**
|
|
387
|
+
* Best-effort absolute path to the running serve-sim entry script. Used so
|
|
388
|
+
* the in-page Camera tool can `node <path> camera ...` regardless of PATH.
|
|
389
|
+
* Falls back to the literal `serve-sim` if we can't determine a usable path.
|
|
390
|
+
*/
|
|
391
|
+
function serveSimBinPath(): string {
|
|
392
|
+
try {
|
|
393
|
+
const argv = process.argv;
|
|
394
|
+
if (argv[1] && existsSync(argv[1])) return argv[1];
|
|
395
|
+
} catch {}
|
|
396
|
+
return "serve-sim";
|
|
397
|
+
}
|
|
398
|
+
|
|
395
399
|
function loadHtml(): string {
|
|
396
400
|
if (!_html) {
|
|
397
401
|
_html = Buffer.from(__PREVIEW_HTML_B64__, "base64").toString("utf-8");
|
|
@@ -414,9 +418,7 @@ function listAllSimulators(): SimctlDevice[] {
|
|
|
414
418
|
stdio: ["ignore", "pipe", "ignore"],
|
|
415
419
|
timeout: 3_000,
|
|
416
420
|
});
|
|
417
|
-
const data = JSON.parse(output) as
|
|
418
|
-
devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
|
|
419
|
-
};
|
|
421
|
+
const data = JSON.parse(output) as SimctlAllList;
|
|
420
422
|
const out: SimctlDevice[] = [];
|
|
421
423
|
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
422
424
|
// Only iOS (skip watchOS / tvOS / visionOS for the grid MVP — the helper
|
|
@@ -433,10 +435,6 @@ function listAllSimulators(): SimctlDevice[] {
|
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
|
|
436
|
-
function deviceNameFor(udid: string): string | null {
|
|
437
|
-
return listAllSimulators().find((d) => d.udid === udid)?.name ?? null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
438
|
// Default per-simulator footprint when we have no running sim to measure
|
|
441
439
|
// from — a fresh booted iOS sim with one app launched typically sits in
|
|
442
440
|
// the 1.2–1.8 GB range. Used as a fallback only.
|
|
@@ -513,7 +511,7 @@ function readSimulatorMemoryUsage(): { perUdid: Record<string, number>; totalByt
|
|
|
513
511
|
const rssKb = Number(line.split(/\s+/, 1)[0]);
|
|
514
512
|
if (!Number.isFinite(rssKb)) continue;
|
|
515
513
|
const bytes = rssKb * 1024;
|
|
516
|
-
const udid = m[1]
|
|
514
|
+
const udid = m[1]!.toUpperCase();
|
|
517
515
|
perUdid[udid] = (perUdid[udid] ?? 0) + bytes;
|
|
518
516
|
totalBytes += bytes;
|
|
519
517
|
}
|
|
@@ -598,7 +596,7 @@ export interface SimMiddlewareOptions {
|
|
|
598
596
|
export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
599
597
|
const base = (options?.basePath ?? "/.sim").replace(/\/+$/, "");
|
|
600
598
|
|
|
601
|
-
return (req:
|
|
599
|
+
return (req: SimReq, res: SimRes, next?: SimNext) => {
|
|
602
600
|
const rawUrl: string = req.url ?? "";
|
|
603
601
|
const qIndex = rawUrl.indexOf("?");
|
|
604
602
|
const url = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
@@ -657,6 +655,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
657
655
|
appStateEndpoint: endpoint(base, "/appstate", state.device),
|
|
658
656
|
axEndpoint: endpoint(base, "/ax", state.device),
|
|
659
657
|
devtoolsEndpoint: endpoint(base, "/devtools", state.device),
|
|
658
|
+
// Forward the absolute path of the running serve-sim entry script
|
|
659
|
+
// so the in-page Camera tool can shell out via `node <bin> camera`
|
|
660
|
+
// without depending on `serve-sim` being on PATH.
|
|
661
|
+
serveSimBin: serveSimBinPath(),
|
|
660
662
|
gridApiEndpoint: gridApiBase,
|
|
661
663
|
gridStartEndpoint: gridApiBase + "/start",
|
|
662
664
|
gridShutdownEndpoint: gridApiBase + "/shutdown",
|
|
@@ -743,7 +745,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
743
745
|
});
|
|
744
746
|
req.on("end", () => {
|
|
745
747
|
let udid = "";
|
|
746
|
-
try { udid = JSON.parse(body).udid ?? ""; } catch {}
|
|
748
|
+
try { udid = (JSON.parse(body) as ShutdownRequestBody).udid ?? ""; } catch {}
|
|
747
749
|
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
|
|
748
750
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
749
751
|
res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
|
|
@@ -776,7 +778,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
776
778
|
});
|
|
777
779
|
req.on("end", () => {
|
|
778
780
|
let udid = "";
|
|
779
|
-
try { udid = JSON.parse(body).udid ?? ""; } catch {}
|
|
781
|
+
try { udid = (JSON.parse(body) as StartRequestBody).udid ?? ""; } catch {}
|
|
780
782
|
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
|
|
781
783
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
782
784
|
res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
|
|
@@ -875,10 +877,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
875
877
|
// Optional body { targetId } releases just one; empty body releases all.
|
|
876
878
|
if (url === base + "/devtools/release" && req.method === "POST") {
|
|
877
879
|
let body = "";
|
|
878
|
-
req.on("data", (chunk) => (body += chunk));
|
|
880
|
+
req.on("data", (chunk: Buffer) => (body += chunk));
|
|
879
881
|
req.on("end", async () => {
|
|
880
882
|
try {
|
|
881
|
-
const parsed = body ? JSON.parse(body)
|
|
883
|
+
const parsed: ReleaseRequestBody = body ? JSON.parse(body) : {};
|
|
882
884
|
const bridge = await ensureInspectWebKitBridge();
|
|
883
885
|
bridge.releaseHighlight?.(parsed.targetId);
|
|
884
886
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -898,10 +900,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
898
900
|
// { targetId: string, on: boolean }.
|
|
899
901
|
if (url === base + "/devtools/highlight" && req.method === "POST") {
|
|
900
902
|
let body = "";
|
|
901
|
-
req.on("data", (chunk) => (body += chunk));
|
|
903
|
+
req.on("data", (chunk: Buffer) => (body += chunk));
|
|
902
904
|
req.on("end", async () => {
|
|
903
905
|
try {
|
|
904
|
-
const { targetId, on } = JSON.parse(body || "{}") as
|
|
906
|
+
const { targetId, on } = JSON.parse(body || "{}") as HighlightRequestBody;
|
|
905
907
|
if (!targetId) {
|
|
906
908
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
907
909
|
res.end(JSON.stringify({ error: "Missing targetId" }));
|
|
@@ -971,7 +973,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
971
973
|
req.on("end", () => {
|
|
972
974
|
let command = "";
|
|
973
975
|
try {
|
|
974
|
-
command = JSON.parse(body).command ?? "";
|
|
976
|
+
command = (JSON.parse(body) as ExecRequestBody).command ?? "";
|
|
975
977
|
} catch {}
|
|
976
978
|
if (!command) {
|
|
977
979
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -983,7 +985,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
983
985
|
res.end(JSON.stringify({
|
|
984
986
|
stdout: stdout.toString(),
|
|
985
987
|
stderr: stderr.toString(),
|
|
986
|
-
exitCode: err ? (err as
|
|
988
|
+
exitCode: err ? (err as ExecException).code ?? 1 : 0,
|
|
987
989
|
}));
|
|
988
990
|
});
|
|
989
991
|
});
|