@ycodium-ai/plugin-devices 0.2.3817 → 0.2.3904

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/dist/index.js CHANGED
@@ -135,6 +135,7 @@ var parseDeviceAction = (platform2, input) => {
135
135
  "setPermission",
136
136
  "openUrl",
137
137
  "launchApp",
138
+ "relaunchApp",
138
139
  "terminateApp",
139
140
  "shake",
140
141
  "sendPush"
@@ -178,6 +179,7 @@ var parseDeviceAction = (platform2, input) => {
178
179
  case "openUrl":
179
180
  return { type, url: url(input["url"]) };
180
181
  case "launchApp":
182
+ case "relaunchApp":
181
183
  case "terminateApp":
182
184
  return { type, appId: appId(platform2, input["appId"]) };
183
185
  case "sendPush":
@@ -199,6 +201,7 @@ var IOS_ACTIONS = /* @__PURE__ */ new Set([
199
201
  "setPermission",
200
202
  "openUrl",
201
203
  "launchApp",
204
+ "relaunchApp",
202
205
  "terminateApp",
203
206
  "sendPush"
204
207
  ]);
@@ -208,10 +211,10 @@ var ANDROID_ACTIONS = /* @__PURE__ */ new Set([
208
211
  "setToggle",
209
212
  "setOrientation",
210
213
  "setLocation",
211
- "clearLocation",
212
214
  "setPermission",
213
215
  "openUrl",
214
216
  "launchApp",
217
+ "relaunchApp",
215
218
  "terminateApp"
216
219
  ]);
217
220
  var IOS_TOGGLES = /* @__PURE__ */ new Set([
@@ -279,6 +282,7 @@ var actionPayload = {
279
282
  ),
280
283
  branch("openUrl", { url: { type: "string", minLength: 1, maxLength: 2048 } }, ["url"]),
281
284
  branch("launchApp", { appId: appId2 }, ["appId"]),
285
+ branch("relaunchApp", { appId: appId2 }, ["appId"]),
282
286
  branch("terminateApp", { appId: appId2 }, ["appId"]),
283
287
  branch("shake"),
284
288
  branch("sendPush", { appId: appId2, payload: { type: ["string", "object"] } }, ["appId", "payload"])
@@ -559,9 +563,16 @@ var registerCommands = (ctx, service) => {
559
563
  import * as NodeFSP3 from "node:fs/promises";
560
564
  import * as NodePath3 from "node:path";
561
565
 
566
+ // src/environmentTypes.ts
567
+ var ANDROID_VERSIONS = [
568
+ { api: "35", name: "Android 15" },
569
+ { api: "30", name: "Android 11" }
570
+ ];
571
+ var BASE_ANDROID_API = ANDROID_VERSIONS[0].api;
572
+
562
573
  // src/androidPackages.ts
563
574
  var repo = "https://dl.google.com/android/repository/";
564
- var archive = (name, file, sha1, bytes, destination = "sdk") => ({ name, url: `${repo}${file}`, sha1, bytes, destination });
575
+ var archive = (name, file, sha1, bytes, destination = "") => ({ name, url: `${repo}${file}`, sha1, bytes, destination });
565
576
  var platformTools = {
566
577
  linux: archive(
567
578
  "Platform-Tools 37.0.1",
@@ -608,19 +619,38 @@ var emulators = {
608
619
  455342868
609
620
  )
610
621
  };
611
- var androidArchives = (platform2, arch2) => {
622
+ var images = {
623
+ "35": {
624
+ name: "Android 15 (API 35), AOSP",
625
+ x64: ["x86_64-35_r02.zip", "2d857d170c0d1b827149565da34b3383e5306f7f", 782404023],
626
+ arm64: ["arm64-v8a-35_r02.zip", "2026a06409db630b56711afdbffb457c1dbaed49", 769099654]
627
+ },
628
+ "30": {
629
+ name: "Android 11 (API 30), AOSP",
630
+ x64: ["x86_64-30_r11.zip", "5e4de3946d46f88856c35efcc4d797b381456347", 676379777],
631
+ arm64: ["arm64-v8a-30_r02.zip", "e96298145a5e0bfd6da4816f51b06c520d8dba72", 548363615]
632
+ }
633
+ };
634
+ var image = (api, arch2) => {
635
+ const [file, sha1, bytes] = images[api][arch2];
636
+ return archive(
637
+ images[api].name,
638
+ `sys-img/android/${file}`,
639
+ sha1,
640
+ bytes,
641
+ `system-images/android-${api}/default`
642
+ );
643
+ };
644
+ var androidArchives = (platform2, arch2, api = BASE_ANDROID_API) => {
612
645
  const emulator = emulators[`${platform2}-${arch2}`];
613
646
  const tools = platformTools[platform2];
614
647
  if (!emulator || !tools) return null;
615
- const arm = arch2 === "arm64";
616
- const image = archive(
617
- "Android 15 (API 35), AOSP revision 2",
618
- `sys-img/android/${arm ? "arm64-v8a" : "x86_64"}-35_r02.zip`,
619
- arm ? "2026a06409db630b56711afdbffb457c1dbaed49" : "2d857d170c0d1b827149565da34b3383e5306f7f",
620
- arm ? 769099654 : 782404023,
621
- "image"
622
- );
623
- return [tools, emulator, image];
648
+ const imageArch = arch2 === "arm64" ? "arm64" : "x64";
649
+ return [
650
+ tools,
651
+ emulator,
652
+ ...[.../* @__PURE__ */ new Set([BASE_ANDROID_API, api])].map((item) => image(item, imageArch))
653
+ ];
624
654
  };
625
655
 
626
656
  // src/environmentArchive.ts
@@ -826,7 +856,9 @@ var throttledEnvironmentProgress = (publish, now = Date.now) => {
826
856
  import * as NodeFSP2 from "node:fs/promises";
827
857
  import * as NodePath2 from "node:path";
828
858
  var ENVIRONMENT_MARKER = ".ycodium-devices-environment.json";
829
- var ANDROID_ENVIRONMENT_VERSION = "Android 15 / API 35 \xB7 Emulator 37.2.12";
859
+ var androidVersionLabel = (api) => `${ANDROID_VERSIONS.find((item) => item.api === api)?.name} / API ${api} \xB7 Emulator 37.2.12`;
860
+ var ANDROID_ENVIRONMENT_VERSION = androidVersionLabel(BASE_ANDROID_API);
861
+ var androidAvdName = (api) => `Ycodium_API_${api}`;
830
862
  var environmentPaths = (root) => ({
831
863
  root,
832
864
  sdk: NodePath2.join(root, "sdk"),
@@ -939,27 +971,28 @@ var selectedPrivateSdk = async (dataDir) => {
939
971
  if (!root) return null;
940
972
  return await privateEnvironmentReady(root) ? NodePath2.join(root, "sdk") : null;
941
973
  };
942
- var privateEnvironmentReady = async (root) => {
974
+ var privateEnvironmentReady = async (root, api = BASE_ANDROID_API) => {
943
975
  try {
944
976
  await assertPrivateEnvironmentPaths(root);
945
977
  const paths = environmentPaths(root);
946
978
  if (await NodeFSP2.readFile(NodePath2.join(root, ".prepared"), "utf8") !== ANDROID_ENVIRONMENT_VERSION || await NodeFSP2.readFile(NodePath2.join(paths.sdk, ".ycodium-components"), "utf8") !== ANDROID_ENVIRONMENT_VERSION)
947
979
  return false;
948
- const avd = NodePath2.join(paths.avd, "Ycodium_API_35.avd");
949
- await assertPrivatePath(root, NodePath2.join(avd, "config.ini"));
950
- await assertPrivatePath(root, NodePath2.join(paths.avd, "Ycodium_API_35.ini"));
951
- const config = await NodeFSP2.readFile(NodePath2.join(avd, "config.ini"), "utf8");
952
- const image = /^image\.sysdir\.1\s*=\s*(system-images\/android-35\/default\/(?:x86_64|arm64-v8a)\/)\s*$/m.exec(
953
- config
954
- )?.[1];
955
- if (!image) return false;
956
- await assertPrivatePath(root, NodePath2.join(paths.sdk, image, "system.img"));
957
- await NodeFSP2.access(NodePath2.join(paths.sdk, image, "system.img"));
958
- if (!(await NodeFSP2.readFile(NodePath2.join(paths.avd, "Ycodium_API_35.ini"), "utf8")).includes(
959
- `path=${avd}
960
- `
961
- ))
962
- return false;
980
+ for (const item of /* @__PURE__ */ new Set([BASE_ANDROID_API, api])) {
981
+ const avd = NodePath2.join(paths.avd, `${androidAvdName(item)}.avd`);
982
+ const ini = NodePath2.join(paths.avd, `${androidAvdName(item)}.ini`);
983
+ await assertPrivatePath(root, NodePath2.join(avd, "config.ini"));
984
+ await assertPrivatePath(root, ini);
985
+ const config = await NodeFSP2.readFile(NodePath2.join(avd, "config.ini"), "utf8");
986
+ const image2 = new RegExp(
987
+ `^image\\.sysdir\\.1\\s*=\\s*(system-images/android-${item}/default/(?:x86_64|arm64-v8a)/)\\s*$`,
988
+ "m"
989
+ ).exec(config)?.[1];
990
+ if (!image2) return false;
991
+ await assertPrivatePath(root, NodePath2.join(paths.sdk, image2, "system.img"));
992
+ await NodeFSP2.access(NodePath2.join(paths.sdk, image2, "system.img"));
993
+ if (!(await NodeFSP2.readFile(ini, "utf8")).includes(`path=${avd}
994
+ `)) return false;
995
+ }
963
996
  for (const suffix of ["", ".exe"]) {
964
997
  try {
965
998
  await NodeFSP2.access(NodePath2.join(paths.sdk, "platform-tools", `adb${suffix}`));
@@ -1003,6 +1036,18 @@ var cleanEnvironment = async (root, category) => {
1003
1036
  var GIB = 1024 ** 3;
1004
1037
  var androidGuide = "https://developer.android.com/studio/run/emulator-acceleration";
1005
1038
  var appleGuide = "https://developer.apple.com/documentation/xcode/downloading-and-installing-additional-xcode-components";
1039
+ var androidApi = (version) => {
1040
+ const requested = version?.trim() || BASE_ANDROID_API;
1041
+ const selected = ANDROID_VERSIONS.find((item) => item.api === requested);
1042
+ if (!selected) throw new DeviceError("invalid-input", `Unsupported Android API: ${requested}`);
1043
+ return selected.api;
1044
+ };
1045
+ var pendingArchives = async (archives, sdk) => {
1046
+ const present = await Promise.all(
1047
+ archives.map((item) => NodeFSP3.lstat(NodePath3.join(sdk, item.destination)).catch(() => null))
1048
+ );
1049
+ return archives.filter((_item, index) => present[index] === null);
1050
+ };
1006
1051
  var DeviceEnvironments = class {
1007
1052
  busy = false;
1008
1053
  abort = new AbortController();
@@ -1115,12 +1160,12 @@ var DeviceEnvironments = class {
1115
1160
  deviceDataBytes
1116
1161
  };
1117
1162
  }
1118
- const archives = androidArchives(probe.platform, probe.arch);
1163
+ const api = androidApi(input.version);
1164
+ const archives = androidArchives(probe.platform, probe.arch, api);
1119
1165
  const sdk = await this.options.externalSdk();
1120
- const prepared = await privateEnvironmentReady(root);
1121
- const installed = await NodeFSP3.lstat(paths.sdk).catch(() => null) !== null;
1122
- const missing = installed ? [] : await Promise.all(
1123
- (archives ?? []).map(async (item) => {
1166
+ const prepared = await privateEnvironmentReady(root, api);
1167
+ const missing = await Promise.all(
1168
+ (await pendingArchives(archives ?? [], paths.sdk)).map(async (item) => {
1124
1169
  const cached = await NodeFSP3.lstat(archivePath(item, paths.cache)).catch(() => null);
1125
1170
  return cached?.size === item.bytes ? 0 : item.bytes;
1126
1171
  })
@@ -1131,7 +1176,7 @@ var DeviceEnvironments = class {
1131
1176
  source: sdk.available && sdk.root !== paths.sdk ? "external" : "private",
1132
1177
  ...sdk.available && sdk.root ? { existingSdkRoot: sdk.root } : {},
1133
1178
  root,
1134
- version: ANDROID_ENVIRONMENT_VERSION,
1179
+ version: androidVersionLabel(api),
1135
1180
  downloadBytes: archives ? missing.reduce((sum, bytes) => sum + bytes, 0) : null,
1136
1181
  requiredFreeBytes: 12 * GIB,
1137
1182
  freeBytes: free,
@@ -1199,7 +1244,7 @@ var DeviceEnvironments = class {
1199
1244
  ])
1200
1245
  await NodeFSP3.mkdir(directory, { recursive: true });
1201
1246
  if (input.platform === "ios") await this.prepareIos(plan, progress, signal);
1202
- else await this.prepareAndroid(plan, progress, signal);
1247
+ else await this.prepareAndroid(plan, androidApi(input.version), progress, signal);
1203
1248
  } finally {
1204
1249
  await release?.();
1205
1250
  }
@@ -1207,57 +1252,68 @@ var DeviceEnvironments = class {
1207
1252
  return this.plan(input);
1208
1253
  });
1209
1254
  }
1210
- async prepareAndroid(plan, progress, signal) {
1255
+ async prepareAndroid(plan, api, progress, signal) {
1211
1256
  const probe = await this.probe();
1212
- const archives = androidArchives(probe.platform, probe.arch);
1257
+ const archives = androidArchives(probe.platform, probe.arch, api);
1213
1258
  if (!archives) throw new Error("Android host target is unsupported");
1214
1259
  const paths = environmentPaths(plan.root);
1215
1260
  const staging = await NodeFSP3.mkdtemp(NodePath3.join(paths.temp, "prepare-"));
1216
1261
  try {
1217
1262
  const existing2 = await NodeFSP3.lstat(paths.sdk).catch(() => null);
1263
+ if (existing2 && (existing2.isSymbolicLink() || !existing2.isDirectory() || await NodeFSP3.readFile(NodePath3.join(paths.sdk, ".ycodium-components"), "utf8").catch(
1264
+ () => ""
1265
+ ) !== ANDROID_ENVIRONMENT_VERSION))
1266
+ throw new Error(
1267
+ "An unrecognized private SDK already exists; its installation will not be overwritten"
1268
+ );
1269
+ const pending = await pendingArchives(archives, paths.sdk);
1270
+ const totalBytes = pending.reduce((sum, item) => sum + item.bytes, 0);
1271
+ let finished = 0;
1272
+ for (const item of pending) {
1273
+ const received = (bytes) => progress({
1274
+ phase: "download",
1275
+ component: item.name,
1276
+ receivedBytes: finished + bytes,
1277
+ totalBytes
1278
+ });
1279
+ received(0);
1280
+ signal.throwIfAborted();
1281
+ const file = await downloadArchive(
1282
+ item,
1283
+ paths.cache,
1284
+ (url2, init) => this.options.fetch(url2, { ...init, signal }),
1285
+ received
1286
+ );
1287
+ finished += item.bytes;
1288
+ progress({
1289
+ phase: "extract",
1290
+ component: item.name,
1291
+ receivedBytes: finished,
1292
+ totalBytes
1293
+ });
1294
+ await extractEnvironmentZip(file, NodePath3.join(staging, item.destination), signal);
1295
+ }
1218
1296
  if (existing2) {
1219
- if (existing2.isSymbolicLink() || !existing2.isDirectory() || await NodeFSP3.readFile(NodePath3.join(paths.sdk, ".ycodium-components"), "utf8").catch(
1220
- () => ""
1221
- ) !== plan.version)
1222
- throw new Error(
1223
- "An unrecognized private SDK already exists; its installation will not be overwritten"
1224
- );
1225
- } else {
1226
- const totalBytes = archives.reduce((sum, item) => sum + item.bytes, 0);
1227
- let finished = 0;
1228
- for (const item of archives) {
1229
- const received = (bytes) => progress({
1230
- phase: "download",
1231
- component: item.name,
1232
- receivedBytes: finished + bytes,
1233
- totalBytes
1234
- });
1235
- received(0);
1236
- signal.throwIfAborted();
1237
- const file = await downloadArchive(
1238
- item,
1239
- paths.cache,
1240
- (url2, init) => this.options.fetch(url2, { ...init, signal }),
1241
- received
1242
- );
1243
- finished += item.bytes;
1244
- progress({
1245
- phase: "extract",
1246
- component: item.name,
1247
- receivedBytes: finished,
1248
- totalBytes
1249
- });
1250
- await extractEnvironmentZip(
1251
- file,
1252
- item.destination === "sdk" ? staging : NodePath3.join(staging, "system-images", "android-35", "default"),
1253
- signal
1254
- );
1297
+ for (const item of pending) {
1298
+ const target2 = NodePath3.join(paths.sdk, item.destination);
1299
+ await assertPrivatePath(paths.root, target2);
1300
+ await NodeFSP3.mkdir(NodePath3.dirname(target2), { recursive: true });
1301
+ await NodeFSP3.rename(NodePath3.join(staging, item.destination), target2);
1255
1302
  }
1256
- await NodeFSP3.writeFile(NodePath3.join(staging, ".ycodium-components"), plan.version);
1303
+ } else {
1304
+ await NodeFSP3.writeFile(
1305
+ NodePath3.join(staging, ".ycodium-components"),
1306
+ ANDROID_ENVIRONMENT_VERSION
1307
+ );
1257
1308
  await NodeFSP3.rename(staging, paths.sdk);
1258
1309
  }
1259
1310
  progress({ phase: "device" });
1260
- await this.createPrivateAvd(paths.root, probe.arch === "arm64" ? "arm64-v8a" : "x86_64");
1311
+ for (const item of /* @__PURE__ */ new Set([BASE_ANDROID_API, api]))
1312
+ await this.createPrivateAvd(
1313
+ paths.root,
1314
+ probe.arch === "arm64" ? "arm64-v8a" : "x86_64",
1315
+ item
1316
+ );
1261
1317
  signal.throwIfAborted();
1262
1318
  progress({ phase: "validate" });
1263
1319
  const validated = await this.options.process.run({
@@ -1268,7 +1324,7 @@ var DeviceEnvironments = class {
1268
1324
  });
1269
1325
  if (validated.exitCode !== 0)
1270
1326
  throw new Error(`The private SDK could not be validated. ${validated.stderr.trim()}`);
1271
- await NodeFSP3.writeFile(NodePath3.join(plan.root, ".prepared"), plan.version);
1327
+ await NodeFSP3.writeFile(NodePath3.join(plan.root, ".prepared"), ANDROID_ENVIRONMENT_VERSION);
1272
1328
  await NodeFSP3.writeFile(
1273
1329
  NodePath3.join(this.options.dataDir, "android-environment.json"),
1274
1330
  JSON.stringify({ root: plan.root })
@@ -1277,9 +1333,9 @@ var DeviceEnvironments = class {
1277
1333
  await NodeFSP3.rm(staging, { recursive: true, force: true });
1278
1334
  }
1279
1335
  }
1280
- async createPrivateAvd(root, abi) {
1336
+ async createPrivateAvd(root, abi, api) {
1281
1337
  const paths = environmentPaths(root);
1282
- const name = "Ycodium_API_35";
1338
+ const name = androidAvdName(api);
1283
1339
  const avd = NodePath3.join(paths.avd, `${name}.avd`);
1284
1340
  await assertPrivatePath(root, avd);
1285
1341
  await assertPrivatePath(root, NodePath3.join(paths.avd, `${name}.ini`));
@@ -1294,7 +1350,7 @@ var DeviceEnvironments = class {
1294
1350
  await NodeFSP3.mkdir(avd, { recursive: true });
1295
1351
  const config = {
1296
1352
  AvdId: name,
1297
- "avd.ini.displayname": "Ycodium Android 15",
1353
+ "avd.ini.displayname": `Ycodium ${ANDROID_VERSIONS.find((item) => item.api === api)?.name}`,
1298
1354
  "abi.type": abi,
1299
1355
  "hw.cpu.arch": abi === "arm64-v8a" ? "arm64" : "x86_64",
1300
1356
  "hw.cpu.ncore": "2",
@@ -1305,8 +1361,10 @@ var DeviceEnvironments = class {
1305
1361
  "hw.gpu.enabled": "yes",
1306
1362
  "hw.gpu.mode": "auto",
1307
1363
  "hw.keyboard": "yes",
1364
+ // 不写时 API 30 按实体按键处理,屏幕上没有导航栏。
1365
+ "hw.mainKeys": "no",
1308
1366
  "disk.dataPartition.size": "4G",
1309
- "image.sysdir.1": `system-images/android-35/default/${abi}/`,
1367
+ "image.sysdir.1": `system-images/android-${api}/default/${abi}/`,
1310
1368
  "tag.id": "default",
1311
1369
  "tag.display": "Default Android System Image",
1312
1370
  "PlayStore.enabled": "false"
@@ -1320,7 +1378,7 @@ var DeviceEnvironments = class {
1320
1378
  NodePath3.join(paths.avd, `${name}.ini`),
1321
1379
  `avd.ini.encoding=UTF-8
1322
1380
  path=${avd}
1323
- target=android-35
1381
+ target=android-${api}
1324
1382
  `,
1325
1383
  { flag: "wx" }
1326
1384
  );
@@ -2189,7 +2247,7 @@ import * as NodePath7 from "node:path";
2189
2247
  import * as NodeFSP6 from "node:fs/promises";
2190
2248
  import * as NodePath6 from "node:path";
2191
2249
  import * as NodeOS from "node:os";
2192
- var DEVICE_HUB_VERSION = "0.12.0";
2250
+ var DEVICE_HUB_VERSION = "0.19.1";
2193
2251
  var AGENT_DEVICE_VERSION = "0.21.12";
2194
2252
  var runtimeTarget = `${NodeOS.platform()}-${NodeOS.arch()}`;
2195
2253
  var HUB_SPEC = {
@@ -2696,6 +2754,10 @@ var runIos = async (tools, udid, action) => {
2696
2754
  case "launchApp":
2697
2755
  await simctl(tools, udid, ["launch", action.appId], "launch");
2698
2756
  return;
2757
+ case "relaunchApp":
2758
+ await simctl(tools, udid, ["terminate", action.appId], "terminate").catch(() => void 0);
2759
+ await simctl(tools, udid, ["launch", action.appId], "launch");
2760
+ return;
2699
2761
  case "terminateApp":
2700
2762
  await simctl(tools, udid, ["terminate", action.appId], "terminate");
2701
2763
  return;
@@ -2712,6 +2774,7 @@ var runIos = async (tools, udid, action) => {
2712
2774
  var adb = (tools, serial, args, operation) => checked(tools.run("adb", ["-s", serial, ...args]), operation);
2713
2775
  var runAndroid = async (tools, serial, action) => {
2714
2776
  const shell = (args, operation) => adb(tools, serial, ["shell", ...args], operation);
2777
+ const launch = (appId3) => shell(["monkey", "-p", appId3, "-c", "android.intent.category.LAUNCHER", "1"], "launch");
2715
2778
  switch (action.type) {
2716
2779
  case "setAppearance":
2717
2780
  await shell(["cmd", "uimode", "night", action.value === "dark" ? "yes" : "no"], "appearance");
@@ -2768,13 +2831,21 @@ var runAndroid = async (tools, serial, action) => {
2768
2831
  "location"
2769
2832
  );
2770
2833
  return;
2771
- case "clearLocation":
2772
- return;
2773
2834
  case "setPermission": {
2774
2835
  const permissions = ANDROID_PERMISSIONS[action.permission];
2775
2836
  if (permissions === void 0) throw unsupported("permission", "android");
2776
2837
  const verb = action.decision === "grant" ? "grant" : "revoke";
2777
- for (const permission of permissions) {
2838
+ const declared = new Set(
2839
+ (await shell(["dumpsys", "package", action.appId], "permission")).split(/[\s:,]+/u)
2840
+ );
2841
+ const requested = permissions.filter((permission) => declared.has(permission));
2842
+ if (requested.length === 0) {
2843
+ throw new DeviceError(
2844
+ "command-failed",
2845
+ `${action.appId} does not declare this permission, so Android cannot change it.`
2846
+ );
2847
+ }
2848
+ for (const permission of requested) {
2778
2849
  await shell(["pm", verb, action.appId, permission], "permission").catch(() => void 0);
2779
2850
  }
2780
2851
  return;
@@ -2786,14 +2857,16 @@ var runAndroid = async (tools, serial, action) => {
2786
2857
  );
2787
2858
  return;
2788
2859
  case "launchApp":
2789
- await shell(
2790
- ["monkey", "-p", action.appId, "-c", "android.intent.category.LAUNCHER", "1"],
2791
- "launch"
2792
- );
2860
+ await launch(action.appId);
2861
+ return;
2862
+ case "relaunchApp":
2863
+ await shell(["am", "force-stop", action.appId], "terminate");
2864
+ await launch(action.appId);
2793
2865
  return;
2794
2866
  case "terminateApp":
2795
2867
  await shell(["am", "force-stop", action.appId], "terminate");
2796
2868
  return;
2869
+ case "clearLocation":
2797
2870
  case "setLiquidGlass":
2798
2871
  case "setColorFilter":
2799
2872
  case "shake":
@@ -2859,22 +2932,32 @@ var readIos = async (tools, udid) => {
2859
2932
  };
2860
2933
  return { settings, foregroundApp: null };
2861
2934
  };
2935
+ var ANDROID_DETAIL_SEPARATOR = "--ycodium-detail--";
2936
+ var ANDROID_DETAIL_READS = [
2937
+ "cmd uimode night",
2938
+ "settings get system font_scale",
2939
+ "settings get global animator_duration_scale",
2940
+ "settings get global wifi_on",
2941
+ // `dumpsys window windows` 在 API 36 起不再打印焦点;不带过滤的整份 dump 还打。
2942
+ "dumpsys window"
2943
+ ];
2862
2944
  var readAndroid = async (tools, serial) => {
2863
- const shell = (args) => quiet(async () => (await adb(tools, serial, ["shell", ...args], args[0] ?? "shell")).trim());
2864
- const [night, fontScale, animator, wifi, focus] = await Promise.all([
2865
- shell(["cmd", "uimode", "night"]),
2866
- shell(["settings", "get", "system", "font_scale"]),
2867
- shell(["settings", "get", "global", "animator_duration_scale"]),
2868
- shell(["settings", "get", "global", "wifi_on"]),
2869
- // `dumpsys window windows` 在 API 36 起不再打印焦点;不带过滤的整份 dump 还打。
2870
- shell(["dumpsys", "window"])
2871
- ]);
2945
+ const output = await quiet(
2946
+ () => adb(
2947
+ tools,
2948
+ serial,
2949
+ ["shell", ANDROID_DETAIL_READS.join(` ; echo ${ANDROID_DETAIL_SEPARATOR} ; `)],
2950
+ "detail"
2951
+ )
2952
+ );
2953
+ const [night, fontScale, animator, wifi, focus] = (output ?? "").split(ANDROID_DETAIL_SEPARATOR).map((part) => part.trim() || void 0);
2872
2954
  const scale = fontScale && fontScale !== "null" ? Number(fontScale) : Number.NaN;
2873
2955
  const focused = focus?.match(/m(?:CurrentFocus|FocusedApp)=\w+\{[^ ]+ u\d+ ([^/ ]+)\//);
2874
2956
  const settings = {
2875
2957
  ...night?.includes("yes") ? { appearance: "dark" } : night?.includes("no") ? { appearance: "light" } : {},
2876
2958
  ...Number.isFinite(scale) ? { textSize: textSizeFromAndroid(scale) } : {},
2877
- ...animator !== void 0 && animator !== "null" ? { reduceMotion: Number(animator) === 0 } : {},
2959
+ // 没动过这项设置时系统读出 "null",含义是默认动画,不是未知。
2960
+ ...animator === void 0 ? {} : { reduceMotion: animator !== "null" && Number(animator) === 0 },
2878
2961
  ...wifi === "1" || wifi === "0" ? { networkEnabled: wifi === "1" } : {}
2879
2962
  };
2880
2963
  const foregroundApp = focused?.[1] === void 0 ? null : { id: focused[1] };
@@ -3048,6 +3131,8 @@ var classifyBootFailure = (error) => {
3048
3131
  };
3049
3132
 
3050
3133
  // src/service.ts
3134
+ var ANDROID_BOOT_POLL_MS = 2e3;
3135
+ var ANDROID_BOOT_POLLS = 60;
3051
3136
  var sameDevice = (a, hostId, deviceId) => a.hostId === hostId && a.id === deviceId;
3052
3137
  var DeviceService = class {
3053
3138
  environmentMutating = false;
@@ -3161,7 +3246,9 @@ var DeviceService = class {
3161
3246
  }
3162
3247
  }
3163
3248
  }
3164
- return list.errors.length > 0 ? { devices, detail: list.errors.join("\n") } : { devices };
3249
+ const iosAvailable = (await this.host.platformAvailability("ios")).available;
3250
+ const errors = iosAvailable ? list.errors : list.errors.filter((error) => !error.startsWith("[apple-utils]"));
3251
+ return errors.length > 0 ? { devices, detail: errors.join("\n") } : { devices };
3165
3252
  }
3166
3253
  async refresh(ready) {
3167
3254
  const generation = this.environmentGeneration;
@@ -3213,6 +3300,22 @@ var DeviceService = class {
3213
3300
  this.pendingOpens--;
3214
3301
  }
3215
3302
  }
3303
+ /** hub 在模拟器连上 adb 时就返回,这时系统还没开机完,画面出不来;等不到也照常打开。 */
3304
+ async waitForAndroidBoot(serial) {
3305
+ for (let poll = 0; poll < ANDROID_BOOT_POLLS; poll += 1) {
3306
+ const result = await this.host.runSdkCommand("adb", [
3307
+ "-s",
3308
+ serial,
3309
+ "shell",
3310
+ "getprop",
3311
+ "sys.boot_completed"
3312
+ ]);
3313
+ if (result.code === 0 && result.stdout.trim() === "1") return;
3314
+ await new Promise(
3315
+ (resolve3) => setTimeout(resolve3, this.options.androidBootPollMs ?? ANDROID_BOOT_POLL_MS)
3316
+ );
3317
+ }
3318
+ }
3216
3319
  async openDevice(input) {
3217
3320
  this.resolveHost(input.hostId);
3218
3321
  const availability = await this.host.platformAvailability(input.platform);
@@ -3254,6 +3357,7 @@ var DeviceService = class {
3254
3357
  }
3255
3358
  if (device.platform === "ios") await hub.attachIosStream(device.id);
3256
3359
  bootedId = result.serial ?? result.id ?? device.id;
3360
+ if (device.platform === "android") await this.waitForAndroidBoot(bootedId);
3257
3361
  } finally {
3258
3362
  this.publish((state) => ({
3259
3363
  ...state,
@@ -3652,7 +3756,7 @@ var deviceToolkit = (service) => ({
3652
3756
  var HUB_SERVICE = "hub";
3653
3757
  var index_default = definePlugin({
3654
3758
  id: "devices",
3655
- apiVersion: 60,
3759
+ apiVersion: 66,
3656
3760
  activate: async (ctx) => {
3657
3761
  const direct = (input, init) => ctx.proxies.fetch(PLUGIN_PROXY_DIRECT).then((fetchDirect) => fetchDirect(input, init));
3658
3762
  const host = new LocalDeviceHost({
package/locales/en.json CHANGED
@@ -9,6 +9,8 @@
9
9
  "directory": "Storage directory on this machine",
10
10
  "defaultDirectory": "Ycodium private data directory",
11
11
  "directoryHint": "Choose an absolute folder on the desired disk. A dedicated ycodium-devices folder is created inside it. This path belongs to the connected machine.",
12
+ "androidVersion": "Android version",
13
+ "androidVersionOption": "{{name}} (API {{api}})",
12
14
  "iosVersion": "iOS runtime version",
13
15
  "iosVersionPlaceholder": "For example, 18.0",
14
16
  "inspect": "Check prerequisites and storage",
@@ -29,7 +31,7 @@
29
31
  "external": "Existing SDKs are reused by default. Preparing a private environment selects it unless an SDK location is explicitly set in Settings. Normal use of existing virtual devices writes to their data. Private preparation does not modify the external SDK.",
30
32
  "guide": "Official setup guidance",
31
33
  "appleOwnership": "Apple installs this runtime in its shared simulator environment. Only its download archive is stored privately. No existing runtime is upgraded or removed. Xcode may require you to complete permissions or initialization yourself.",
32
- "privateOwnership": "Android 15 AOSP, Platform-Tools and Emulator are downloaded from Google at pinned revisions. SDK files, virtual device data, configuration and temporary files stay in private storage. Applications installed in a virtual device are user data.",
34
+ "privateOwnership": "The chosen Android AOSP image, Platform-Tools and Emulator are downloaded from Google at pinned revisions. SDK files, virtual device data, configuration and temporary files stay in private storage. Applications installed in a virtual device are user data.",
33
35
  "appleLicense": "Apple software license agreements",
34
36
  "androidLicense": "Android SDK License Agreement",
35
37
  "acceptLicense": "I accept the applicable component licenses",
@@ -244,6 +246,7 @@
244
246
  "presetPlaceholder": "Preset…",
245
247
  "set": "Set",
246
248
  "clear": "Clear",
249
+ "current": "Set to {{latitude}}, {{longitude}}",
247
250
  "sanFrancisco": "San Francisco",
248
251
  "newYork": "New York",
249
252
  "london": "London",
@@ -294,6 +297,7 @@
294
297
  "updateTo": "Verify bundled {{version}}",
295
298
  "pendingPolicy": "Device helpers are included with Ycodium. Missing or incomplete components require repairing or updating Ycodium on this machine.",
296
299
  "checkHint": "Checking reads the installed tools without installing or starting anything.",
300
+ "checked": "Checked. The versions above are current.",
297
301
  "check": "Check versions",
298
302
  "checkFailed": "Could not read the tool versions. Try again.",
299
303
  "updateFailed": "Bundled components are missing or incomplete. Repair or update Ycodium on this machine."
@@ -9,6 +9,8 @@
9
9
  "directory": "这台机器上的存储目录",
10
10
  "defaultDirectory": "Ycodium 私有数据目录",
11
11
  "directoryHint": "填写目标磁盘上的绝对路径,组件将放进其中专用的 ycodium-devices 文件夹。路径属于当前连接的机器。",
12
+ "androidVersion": "Android 版本",
13
+ "androidVersionOption": "{{name}}(API {{api}})",
12
14
  "iosVersion": "iOS 运行时版本",
13
15
  "iosVersionPlaceholder": "例如 18.0",
14
16
  "inspect": "检查条件与存储空间",
@@ -29,7 +31,7 @@
29
31
  "external": "默认复用已有 SDK。准备私有环境后会接入它,但设置中明确指定的 SDK 位置优先。正常使用已有虚拟设备会写入其数据;私有准备不会修改外部 SDK。",
30
32
  "guide": "官方设置指引",
31
33
  "appleOwnership": "Apple 会将运行时安装到共享模拟器环境,只有下载归档存放在私有目录。不会升级或删除已有运行时。Xcode 如需权限或首次初始化,须由你自行完成。",
32
- "privateOwnership": "从 Google 下载固定版本的 Android 15 AOSP、Platform-Tools 和 Emulator。SDK、虚拟设备数据、配置和临时文件保存在私有目录。虚拟设备中安装的应用属于用户数据。",
34
+ "privateOwnership": "从 Google 下载固定版本的所选 Android AOSP 镜像、Platform-Tools 和 Emulator。SDK、虚拟设备数据、配置和临时文件保存在私有目录。虚拟设备中安装的应用属于用户数据。",
33
35
  "appleLicense": "Apple 软件许可协议",
34
36
  "androidLicense": "Android SDK 许可协议",
35
37
  "acceptLicense": "我接受适用的组件许可协议",
@@ -244,6 +246,7 @@
244
246
  "presetPlaceholder": "预设…",
245
247
  "set": "设置",
246
248
  "clear": "清除",
249
+ "current": "已设为 {{latitude}}, {{longitude}}",
247
250
  "sanFrancisco": "旧金山",
248
251
  "newYork": "纽约",
249
252
  "london": "伦敦",
@@ -294,6 +297,7 @@
294
297
  "updateTo": "校验随包版本 {{version}}",
295
298
  "pendingPolicy": "设备辅助工具随 Ycodium 提供。组件缺失或不完整时,请修复或更新这台机器上的 Ycodium。",
296
299
  "checkHint": "检查只读取已装的工具,不安装也不启动任何东西。",
300
+ "checked": "检查完成,上面的版本是最新读取的结果。",
297
301
  "check": "检查版本",
298
302
  "checkFailed": "读不到工具版本,请重试。",
299
303
  "updateFailed": "随包组件缺失或不完整,请修复或更新这台机器上的 Ycodium。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-devices",
3
- "version": "0.2.3817",
3
+ "version": "0.2.3904",
4
4
  "description": "Lets agents drive iOS Simulators and Android Emulators on this machine.",
5
5
  "keywords": [
6
6
  "android",
@@ -16,6 +16,7 @@
16
16
  "dist",
17
17
  "locales",
18
18
  "assets",
19
+ "skills",
19
20
  "README.md"
20
21
  ],
21
22
  "type": "module",
@@ -33,7 +34,7 @@
33
34
  "tailwindcss": "^4.0.0",
34
35
  "typescript": "~6.0.3",
35
36
  "vite-plus": "0.2.2",
36
- "@ycodium-ai/plugin-api": "0.2.3817",
37
+ "@ycodium-ai/plugin-api": "0.2.3904",
37
38
  "ycodium-fetch-ports": "0.0.0",
38
39
  "ycodium-sse-stream": "0.0.0"
39
40
  },
@@ -44,6 +45,11 @@
44
45
  "description": "Lets agents drive iOS Simulators and Android Emulators on this machine.",
45
46
  "descriptionKey": "presentation.description",
46
47
  "icon": "./assets/icon.svg",
48
+ "skills": [
49
+ {
50
+ "dir": "./skills/mobile-devices"
51
+ }
52
+ ],
47
53
  "ui": {
48
54
  "assets": "./assets",
49
55
  "entry": "ui.js",