@ycodium-ai/plugin-devices 0.2.3510 → 0.2.3566

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.
@@ -0,0 +1,239 @@
1
+ // 独立 host:node 助手,只处理指定 SDK 或 Apple 官方工具,不修改系统设置。
2
+ import * as NodeFS from "node:fs";
3
+ import * as NodePath from "node:path";
4
+ import * as NodeChildProcess from "node:child_process";
5
+ import * as NodeNet from "node:net";
6
+ import * as NodeModule from "node:module";
7
+ // oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程助手,没有宿主 Effect 上下文。
8
+ const hostPlatform = process.platform;
9
+
10
+ /** 以交付文件和实际装载结果判定;原生助手可由对应平台的构建阶段提供,不能在用户机器现编。 */
11
+ export function probeIosTools(runtimeDir) {
12
+ if (hostPlatform !== "darwin" || !runtimeDir) return { available: false };
13
+ const dist = NodePath.join(
14
+ runtimeDir,
15
+ "node_modules",
16
+ "expo-device-hub",
17
+ "vendor",
18
+ "serve-sim",
19
+ "dist",
20
+ );
21
+ try {
22
+ for (const file of [
23
+ "serve-sim.js",
24
+ "bin/LiveKitWebRTC.framework/LiveKitWebRTC",
25
+ "simax/serve-sim-ax-settings",
26
+ "simcam/serve-sim-camera-helper",
27
+ "simcam/libSimCameraInjector.dylib",
28
+ "capability-loader/libServeSimCapabilityLoader.dylib",
29
+ ])
30
+ NodeFS.accessSync(NodePath.join(dist, file), NodeFS.constants.R_OK);
31
+ NodeModule.createRequire(import.meta.url)(
32
+ NodePath.join(dist, "native", "serve-sim-native.node"),
33
+ );
34
+ return { available: true };
35
+ } catch {
36
+ return { available: false };
37
+ }
38
+ }
39
+
40
+ export function privateAndroidEnvironment(root, sdk, env = process.env) {
41
+ const user = NodePath.join(root, "user");
42
+ return {
43
+ ...env,
44
+ ANDROID_HOME: sdk,
45
+ ANDROID_SDK_ROOT: sdk,
46
+ ANDROID_USER_HOME: user,
47
+ ANDROID_EMULATOR_HOME: user,
48
+ ANDROID_AVD_HOME: NodePath.join(root, "avd"),
49
+ ANDROID_SDK_HOME: root,
50
+ XDG_CONFIG_HOME: NodePath.join(root, "config"),
51
+ XDG_CACHE_HOME: NodePath.join(root, "cache"),
52
+ TMPDIR: NodePath.join(root, "temp"),
53
+ TMP: NodePath.join(root, "temp"),
54
+ TEMP: NodePath.join(root, "temp"),
55
+ };
56
+ }
57
+
58
+ /** adb 自身会结束协议版本不符的服务器;在调用客户端之前只读握手,发现冲突就让用户处理。 */
59
+ export async function guardAdbServer(
60
+ sdk,
61
+ env = process.env,
62
+ versionProbe = () =>
63
+ run(NodePath.join(sdk, "platform-tools", hostPlatform === "win32" ? "adb.exe" : "adb"), [
64
+ "version",
65
+ ]),
66
+ ) {
67
+ const version = versionProbe();
68
+ const protocol = /Android Debug Bridge version \d+\.\d+\.(\d+)/.exec(version.stdout)?.[1];
69
+ if (version.code !== 0 || !protocol) throw new Error("The selected Android SDK adb cannot run");
70
+ const specified = env.ADB_SERVER_SOCKET;
71
+ const endpoint = specified ? /^tcp:([^:]+):(\d+)$/.exec(specified) : null;
72
+ if (specified && !endpoint)
73
+ throw new Error(
74
+ "The configured ADB server socket cannot be checked safely; use a TCP server socket",
75
+ );
76
+ const host = endpoint?.[1] ?? "127.0.0.1";
77
+ const port = Number(endpoint?.[2] ?? env.ANDROID_ADB_SERVER_PORT ?? "5037");
78
+ await new Promise((resolve, reject) => {
79
+ const socket = NodeNet.createConnection({ host, port });
80
+ let received = Buffer.alloc(0);
81
+ const finish = (error) => {
82
+ socket.destroy();
83
+ if (error) reject(error);
84
+ else resolve();
85
+ };
86
+ socket.setTimeout(3000, () =>
87
+ finish(new Error("The existing ADB server did not answer; Ycodium will not restart it")),
88
+ );
89
+ socket.on("error", (error) =>
90
+ error.code === "ECONNREFUSED"
91
+ ? finish()
92
+ : finish(new Error("The configured ADB server could not be checked")),
93
+ );
94
+ socket.on("connect", () => socket.write("000chost:version"));
95
+ socket.on("data", (data) => {
96
+ received = Buffer.concat([received, data]);
97
+ if (received.length < 8) return;
98
+ const length = Number.parseInt(received.subarray(4, 8).toString(), 16);
99
+ if (received.subarray(0, 4).toString() !== "OKAY" || !Number.isFinite(length) || length > 64)
100
+ return finish(new Error("The existing ADB server returned an incompatible protocol"));
101
+ if (received.length < 8 + length) return;
102
+ const server = Number.parseInt(received.subarray(8, 8 + length).toString(), 16);
103
+ finish(
104
+ server === Number(protocol)
105
+ ? undefined
106
+ : new Error(
107
+ "An incompatible ADB server is already running. Ycodium will not stop or restart it; select the matching SDK or manage that server yourself",
108
+ ),
109
+ );
110
+ });
111
+ socket.on("end", () => {
112
+ if (received.length < 12)
113
+ finish(new Error("The existing ADB server closed before reporting its version"));
114
+ });
115
+ });
116
+ }
117
+
118
+ /** 仅私有 SDK 的属主标记允许重定向 AVD;外部 SDK 保留用户自己的 AVD 查找路径。 */
119
+ export function applyPrivateAndroidEnvironment(sdk, env = process.env) {
120
+ if (!sdk) return false;
121
+ const root = NodePath.dirname(sdk);
122
+ try {
123
+ const record = JSON.parse(
124
+ NodeFS.readFileSync(NodePath.join(root, ".ycodium-devices-environment.json"), "utf8"),
125
+ );
126
+ if (
127
+ record.owner !== "ycodium-devices" ||
128
+ NodePath.resolve(record.sdk) !== NodePath.resolve(sdk)
129
+ )
130
+ return false;
131
+ Object.assign(env, privateAndroidEnvironment(root, sdk, env));
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function run(command, args, timeout = 15000) {
139
+ const result = NodeChildProcess.spawnSync(command, args, {
140
+ encoding: "utf8",
141
+ timeout,
142
+ windowsHide: true,
143
+ });
144
+ return {
145
+ code: result.status ?? 127,
146
+ stdout: result.stdout ?? "",
147
+ stderr: result.stderr ?? result.error?.message ?? "",
148
+ };
149
+ }
150
+ export function probeXcode() {
151
+ if (hostPlatform !== "darwin")
152
+ return { ready: false, reasonCode: "ios-needs-macos", runtimes: [] };
153
+ const selected = run("xcode-select", ["--print-path"]);
154
+ const directory = selected.stdout.trim();
155
+ if (selected.code !== 0 || !directory.includes(".app/Contents/Developer"))
156
+ return { ready: false, reasonCode: "xcode-missing", runtimes: [] };
157
+ if (run("xcodebuild", ["-checkFirstLaunchStatus"]).code !== 0)
158
+ return { ready: false, reasonCode: "xcode-incomplete", runtimes: [] };
159
+ const result = run("xcrun", ["simctl", "list", "runtimes", "--json"]);
160
+ try {
161
+ const runtimes = JSON.parse(result.stdout).runtimes.filter(
162
+ (item) => item.isAvailable && item.identifier?.includes("iOS"),
163
+ );
164
+ return {
165
+ ready: true,
166
+ directory,
167
+ runtimes,
168
+ reasonCode: runtimes.length ? undefined : "ios-runtime-missing",
169
+ };
170
+ } catch {
171
+ return { ready: false, reasonCode: "xcode-incomplete", runtimes: [] };
172
+ }
173
+ }
174
+ export function checkAcceleration(sdk) {
175
+ const command = NodePath.join(
176
+ sdk,
177
+ "emulator",
178
+ hostPlatform === "win32" ? "emulator.exe" : "emulator",
179
+ );
180
+ return run(command, ["-accel-check"]);
181
+ }
182
+
183
+ /** HypervisorPresent 说明当前 hypervisor 是否运行,不代表 CPU 或固件的虚拟化开关。 */
184
+ export function windowsAccelerationStatus(facts) {
185
+ if (!facts || typeof facts !== "object")
186
+ return { available: false, reasonCode: "virtualization-probe-failed" };
187
+ const bool = (value) => (typeof value === "boolean" ? value : null);
188
+ const state = {
189
+ hardwareSupported: bool(facts.hardwareSupported),
190
+ firmwareEnabled: bool(facts.firmwareEnabled),
191
+ slatSupported: bool(facts.slatSupported),
192
+ hypervisorPresent: bool(facts.hypervisorPresent),
193
+ whpxEnabled:
194
+ facts.whpxInstallState === 1
195
+ ? true
196
+ : facts.whpxInstallState === 2 || facts.whpxInstallState === 3
197
+ ? false
198
+ : null,
199
+ };
200
+ if (state.hypervisorPresent === true && state.whpxEnabled === true)
201
+ return { ...state, available: true };
202
+ if (state.hypervisorPresent === false && state.firmwareEnabled === false)
203
+ return { ...state, available: false, reasonCode: "virtualization-firmware-disabled" };
204
+ if (state.whpxEnabled === false)
205
+ return { ...state, available: false, reasonCode: "windows-hypervisor-platform-disabled" };
206
+ if (state.whpxEnabled === true && state.hypervisorPresent === false)
207
+ return { ...state, available: false, reasonCode: "windows-hypervisor-not-running" };
208
+ return { ...state, available: false, reasonCode: "virtualization-probe-failed" };
209
+ }
210
+
211
+ export function probeWindowsAcceleration(execute = run) {
212
+ const result = execute("powershell.exe", [
213
+ "-NoProfile",
214
+ "-NonInteractive",
215
+ "-Command",
216
+ "$ErrorActionPreference = 'Stop'; $cpu = @(Get-CimInstance Win32_Processor)[0]; $machine = Get-CimInstance Win32_ComputerSystem; $feature = Get-CimInstance Win32_OptionalFeature -Filter \"Name='HypervisorPlatform'\"; @{ hardwareSupported = $cpu.VMMonitorModeExtensions; firmwareEnabled = $cpu.VirtualizationFirmwareEnabled; slatSupported = $cpu.SecondLevelAddressTranslationExtensions; hypervisorPresent = $machine.HypervisorPresent; whpxInstallState = $feature.InstallState } | ConvertTo-Json -Compress",
217
+ ]);
218
+ if (result.code !== 0) return windowsAccelerationStatus(null);
219
+ try {
220
+ return windowsAccelerationStatus(JSON.parse(result.stdout));
221
+ } catch {
222
+ return windowsAccelerationStatus(null);
223
+ }
224
+ }
225
+
226
+ export function probeVirtualization() {
227
+ if (hostPlatform === "darwin")
228
+ return { available: run("sysctl", ["-n", "kern.hv_support"]).stdout.trim() === "1" };
229
+ if (hostPlatform === "linux") {
230
+ try {
231
+ NodeFS.accessSync("/dev/kvm", NodeFS.constants.R_OK | NodeFS.constants.W_OK);
232
+ return { available: true };
233
+ } catch {
234
+ return { available: false };
235
+ }
236
+ }
237
+ if (hostPlatform === "win32") return probeWindowsAcceleration();
238
+ return { available: false };
239
+ }
package/dist/helper.mjs CHANGED
@@ -2,8 +2,7 @@
2
2
  // 跑在服务器自己的 Node 上(桌面版是带 ELECTRON_RUN_AS_NODE 的 Electron)。
3
3
  //
4
4
  // 宿主不许插件给子进程改 PATH,而 hub 在 Windows 上只靠 PATH 找模拟器、agent-device 的守护进程
5
- // 也要 adb,npm 的安装脚本要在 PATH 上找 node。所以这几类子进程都经这里:在这个进程里补好 PATH,
6
- // 再在同一进程里装载目标脚本(`node`、`npm` 模式)或起目标程序(`run` 模式)。插件停用时宿主杀的
5
+ // 也要 adb。这几类子进程经这里补好 PATH,再装载随包入口(`node` 模式)或起目标程序(`run` 模式)。插件停用时宿主杀的
7
6
  // 是这个进程,装载进来的目标脚本也就跟着没了。
8
7
  //
9
8
  // 只用 node 内置模块;输出给插件解析的一律是单行 JSON。
@@ -21,23 +20,33 @@ import * as NodeDiagnosticsChannel from "node:diagnostics_channel";
21
20
  import * as NodeFS from "node:fs";
22
21
  import * as NodePath from "node:path";
23
22
  import * as NodeURL from "node:url";
24
- import * as NodeZlib from "node:zlib";
23
+
24
+ import {
25
+ applyPrivateAndroidEnvironment,
26
+ probeXcode,
27
+ probeVirtualization,
28
+ checkAcceleration,
29
+ guardAdbServer,
30
+ probeIosTools,
31
+ } from "./environment-helper.mjs";
25
32
 
26
33
  // oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程脚本,没有 Effect 运行时可注入。
27
34
  const hostPlatform = process.platform;
35
+ // oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程助手,没有宿主 Effect 上下文。
36
+ const hostArch = process.arch;
28
37
  const isWindows = hostPlatform === "win32";
29
38
  const exe = (name) => (isWindows ? `${name}.exe` : name);
30
39
 
31
40
  /** `--sdk x --shim y -- rest…`:选项在 `--` 之前,其余原样交给目标。其它环境变量由插件经 spawn 的 env 给。 */
32
41
  function parseArgs(argv) {
33
- const options = { sdk: null, shim: null, rest: [] };
42
+ const options = { sdk: null, shim: null, tools: null, rest: [] };
34
43
  for (let index = 0; index < argv.length; index += 1) {
35
44
  const value = argv[index];
36
45
  if (value === "--") {
37
46
  options.rest = argv.slice(index + 1);
38
47
  break;
39
48
  }
40
- if (value === "--sdk" || value === "--shim") {
49
+ if (value === "--sdk" || value === "--shim" || value === "--tools") {
41
50
  options[value.slice(2)] = argv[index + 1] || null;
42
51
  index += 1;
43
52
  }
@@ -145,103 +154,6 @@ function observeEmulatorLaunches(shim) {
145
154
  });
146
155
  }
147
156
 
148
- /**
149
- * 在 `directory` 里放一个叫 node 的垫片,指向跑这个助手的可执行文件。npm 的安装脚本
150
- * (比如原生依赖的预编译下载器)按名字在 PATH 上找 node,而这台机器的 PATH 上可能根本没有。
151
- * ELECTRON_RUN_AS_NODE 已经在本进程环境里(宿主起 host:node 时补的),垫片起的进程照样继承。
152
- */
153
- function writeNodeShim(directory) {
154
- NodeFS.mkdirSync(directory, { recursive: true });
155
- if (isWindows) {
156
- NodeFS.writeFileSync(NodePath.join(directory, "node.cmd"), `@"${process.execPath}" %*\r\n`);
157
- } else {
158
- const file = NodePath.join(directory, "node");
159
- NodeFS.writeFileSync(
160
- file,
161
- `#!/bin/sh\nexec '${process.execPath.replaceAll("'", `'"'"'`)}' "$@"\n`,
162
- );
163
- NodeFS.chmodSync(file, 0o755);
164
- }
165
- }
166
-
167
- /** 读 tar 头里的一个八进制数字段。 */
168
- function octal(block, start, length) {
169
- const text = block
170
- .subarray(start, start + length)
171
- .toString("utf8")
172
- .replaceAll("\0", "")
173
- .trim();
174
- return text.length === 0 ? 0 : Number.parseInt(text, 8);
175
- }
176
-
177
- function text(block, start, length) {
178
- const raw = block.subarray(start, start + length);
179
- const end = raw.indexOf(0);
180
- return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
181
- }
182
-
183
- /** PAX 扩展头里的 `path=`;其余键这里用不到。 */
184
- function paxPath(body) {
185
- let offset = 0;
186
- while (offset < body.length) {
187
- const space = body.indexOf(0x20, offset);
188
- if (space === -1) break;
189
- const length = Number.parseInt(body.subarray(offset, space).toString("utf8"), 10);
190
- if (!Number.isFinite(length) || length <= 0) break;
191
- const record = body.subarray(space + 1, offset + length - 1).toString("utf8");
192
- if (record.startsWith("path=")) return record.slice("path=".length);
193
- offset += length;
194
- }
195
- return null;
196
- }
197
-
198
- /**
199
- * 解一个 npm 包的 .tgz 到 `destination`,去掉包内统一的第一层目录(npm 包是 `package/`)。
200
- * 只认普通文件和目录;条目路径解析后必须仍在 `destination` 内。
201
- */
202
- function extractTarball(file, destination) {
203
- const archive = NodeZlib.gunzipSync(NodeFS.readFileSync(file));
204
- const root = NodePath.resolve(destination);
205
- let offset = 0;
206
- let pendingName = null;
207
- let count = 0;
208
- while (offset + 512 <= archive.length) {
209
- const block = archive.subarray(offset, offset + 512);
210
- if (block.every((byte) => byte === 0)) break;
211
- const size = octal(block, 124, 12);
212
- const type = String.fromCharCode(block[156] || 0x30);
213
- const bodyStart = offset + 512;
214
- const body = archive.subarray(bodyStart, bodyStart + size);
215
- offset = bodyStart + Math.ceil(size / 512) * 512;
216
- if (type === "x") {
217
- pendingName = paxPath(body);
218
- continue;
219
- }
220
- if (type === "L") {
221
- pendingName = text(body, 0, body.length);
222
- continue;
223
- }
224
- if (type === "g") continue;
225
- const prefix = text(block, 345, 155);
226
- const name = pendingName ?? (prefix ? `${prefix}/${text(block, 0, 100)}` : text(block, 0, 100));
227
- pendingName = null;
228
- const relative = name.split("/").slice(1).join("/");
229
- if (relative.length === 0) continue;
230
- const target = NodePath.resolve(root, relative);
231
- if (target !== root && !target.startsWith(root + NodePath.sep)) {
232
- throw new Error(`tar entry escapes the destination: ${name}`);
233
- }
234
- if (type === "5") {
235
- NodeFS.mkdirSync(target, { recursive: true });
236
- } else if (type === "0" || type === "\0" || type === "7") {
237
- NodeFS.mkdirSync(NodePath.dirname(target), { recursive: true });
238
- NodeFS.writeFileSync(target, body, { mode: octal(block, 100, 8) & 0o777 || 0o644 });
239
- count += 1;
240
- }
241
- }
242
- return count;
243
- }
244
-
245
157
  function exists(file) {
246
158
  try {
247
159
  NodeFS.accessSync(file);
@@ -294,20 +206,13 @@ function probeAndroidSdk(override) {
294
206
  return { root: null, adb: false, emulator: false, avdmanager: false, legacyAvdmanager: false };
295
207
  }
296
208
 
297
- function hasXcrun() {
298
- if (hostPlatform !== "darwin") return false;
299
- const result = NodeChildProcess.spawnSync("xcrun", ["--version"], {
300
- stdio: "ignore",
301
- timeout: 10_000,
302
- });
303
- return result.status === 0;
304
- }
305
-
306
209
  /** 把 SDK 的两个目录排到 PATH 最前,并设好 ANDROID_HOME。 */
307
210
  function applySdkEnvironment(sdk) {
308
211
  if (!sdk) return;
309
212
  prependPath([NodePath.join(sdk, "platform-tools"), NodePath.join(sdk, "emulator")]);
310
213
  process.env.ANDROID_HOME = sdk;
214
+ process.env.ANDROID_SDK_ROOT = sdk;
215
+ applyPrivateAndroidEnvironment(sdk);
311
216
  }
312
217
 
313
218
  /** 在本进程里装载一个 Node 入口脚本,就像 `node entry …args` 起的一样。 */
@@ -319,23 +224,75 @@ async function loadEntry(entry, args) {
319
224
  async function main() {
320
225
  const [mode, ...rest] = process.argv.slice(2);
321
226
  const options = parseArgs(rest);
227
+ if (mode === "environment-probe") {
228
+ process.stdout.write(
229
+ `${JSON.stringify({ platform: hostPlatform, arch: hostArch, iosToolsAvailable: probeIosTools(options.tools).available, virtualization: probeVirtualization(), xcode: probeXcode(), ...(options.sdk ? { acceleration: checkAcceleration(options.sdk) } : {}) })}\n`,
230
+ );
231
+ return;
232
+ }
322
233
  if (mode === "probe") {
234
+ const android = probeAndroidSdk(options.sdk);
235
+ applySdkEnvironment(android.root);
236
+ const androidValidated =
237
+ android.adb &&
238
+ android.emulator &&
239
+ [
240
+ [NodePath.join(android.root, "platform-tools", exe("adb")), ["version"]],
241
+ [NodePath.join(android.root, "emulator", exe("emulator")), ["-version"]],
242
+ ].every(
243
+ ([program, argv]) =>
244
+ NodeChildProcess.spawnSync(program, argv, {
245
+ stdio: "ignore",
246
+ timeout: 10000,
247
+ windowsHide: true,
248
+ }).status === 0,
249
+ );
250
+ const xcode = probeXcode();
323
251
  process.stdout.write(
324
252
  `${JSON.stringify({
325
253
  node: process.version,
326
254
  execPath: process.execPath,
327
255
  electron: process.versions.electron ?? null,
328
256
  platform: hostPlatform,
329
- android: probeAndroidSdk(options.sdk),
330
- xcrun: hasXcrun(),
257
+ arch: hostArch,
258
+ android,
259
+ androidValidated,
260
+ xcrun: xcode.ready,
261
+ iosRuntimeAvailable: xcode.runtimes.length > 0,
262
+ iosToolsAvailable: probeIosTools(options.tools).available,
331
263
  })}\n`,
332
264
  );
333
265
  return;
334
266
  }
267
+ if (mode === "validate-android") {
268
+ if (!options.sdk) throw new Error("SDK directory is required");
269
+ applySdkEnvironment(options.sdk);
270
+ const commands = [
271
+ [NodePath.join(options.sdk, "platform-tools", exe("adb")), ["version"]],
272
+ [NodePath.join(options.sdk, "emulator", exe("emulator")), ["-version"]],
273
+ [NodePath.join(options.sdk, "emulator", exe("emulator")), ["-list-avds"]],
274
+ [NodePath.join(options.sdk, "emulator", exe("emulator")), ["-accel-check"]],
275
+ ];
276
+ for (const [program, argv] of commands) {
277
+ const result = NodeChildProcess.spawnSync(program, argv, {
278
+ encoding: "utf8",
279
+ timeout: 15000,
280
+ windowsHide: true,
281
+ });
282
+ if (
283
+ result.status !== 0 ||
284
+ (argv[0] === "-list-avds" && !result.stdout.includes("Ycodium_API_35"))
285
+ )
286
+ throw new Error(`Private SDK validation failed: ${argv[0]}`);
287
+ }
288
+ process.stdout.write(`${JSON.stringify({ validated: true })}\n`);
289
+ return;
290
+ }
335
291
  if (mode === "node") {
336
292
  const [entry, ...args] = options.rest;
337
293
  if (!entry) throw new Error("helper node: missing entry script");
338
294
  applySdkEnvironment(options.sdk);
295
+ if (options.sdk) await guardAdbServer(options.sdk);
339
296
  const shim = prepareBreakawayShim({
340
297
  platform: hostPlatform,
341
298
  directory: options.shim,
@@ -345,24 +302,12 @@ async function main() {
345
302
  await loadEntry(entry, args);
346
303
  return;
347
304
  }
348
- if (mode === "npm") {
349
- const [npmCli, ...args] = options.rest;
350
- if (!npmCli || !options.shim) throw new Error("helper npm: missing npm-cli.js or --shim");
351
- writeNodeShim(options.shim);
352
- prependPath([options.shim]);
353
- await loadEntry(npmCli, args);
354
- return;
355
- }
356
- if (mode === "extract") {
357
- const [file, destination] = options.rest;
358
- if (!file || !destination) throw new Error("helper extract: missing archive or destination");
359
- process.stdout.write(`${JSON.stringify({ files: extractTarball(file, destination) })}\n`);
360
- return;
361
- }
362
305
  if (mode === "run") {
363
306
  const [command, ...args] = options.rest;
364
307
  if (!command) throw new Error("helper run: missing command");
365
308
  applySdkEnvironment(options.sdk);
309
+ if (options.sdk && command === "adb" && args[0] !== "version")
310
+ await guardAdbServer(options.sdk);
366
311
  const resolved =
367
312
  command === "emulator" && options.sdk
368
313
  ? NodePath.join(options.sdk, "emulator", exe("emulator"))
@@ -390,7 +335,11 @@ async function main() {
390
335
  throw new Error(`helper: unknown mode ${String(mode)}`);
391
336
  }
392
337
 
393
- if (process.argv[1] && NodePath.resolve(process.argv[1]) === NodeURL.fileURLToPath(import.meta.url))
338
+ if (
339
+ process.argv[1] &&
340
+ NodeFS.realpathSync(process.argv[1]) ===
341
+ NodeFS.realpathSync(NodeURL.fileURLToPath(import.meta.url))
342
+ )
394
343
  main().catch((error) => {
395
344
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
396
345
  process.exitCode = 1;