@ycodium-ai/plugin-devices 0.2.3510 → 0.2.3576

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
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import * as NodePath6 from "node:path";
2
+ import * as NodePath9 from "node:path";
3
3
  import { definePlugin, PLUGIN_PROXY_DIRECT } from "@ycodium-ai/plugin-api";
4
4
 
5
5
  // src/types.ts
@@ -21,7 +21,7 @@ var errorDetail = (error) => {
21
21
 
22
22
  // src/deviceActionInput.ts
23
23
  var invalid = (field, rule) => new DeviceError("invalid-input", `Invalid ${field}: ${rule}.`);
24
- var unsupported = (operation, platform) => new DeviceError("action-unsupported", `Device ${operation} is not supported on ${platform}.`);
24
+ var unsupported = (operation, platform2) => new DeviceError("action-unsupported", `Device ${operation} is not supported on ${platform2}.`);
25
25
  var DEVICE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
26
26
  var IOS_BUNDLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/;
27
27
  var ANDROID_PACKAGE_PATTERN = /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*$/;
@@ -89,12 +89,12 @@ var assertDeviceId = (id) => {
89
89
  }
90
90
  return id;
91
91
  };
92
- var appId = (platform, input) => {
93
- const ok = typeof input === "string" && (platform === "ios" ? IOS_BUNDLE_ID_PATTERN.test(input) : input.length <= MAX_ANDROID_PACKAGE_LENGTH && ANDROID_PACKAGE_PATTERN.test(input));
92
+ var appId = (platform2, input) => {
93
+ const ok = typeof input === "string" && (platform2 === "ios" ? IOS_BUNDLE_ID_PATTERN.test(input) : input.length <= MAX_ANDROID_PACKAGE_LENGTH && ANDROID_PACKAGE_PATTERN.test(input));
94
94
  if (!ok) {
95
95
  throw invalid(
96
- platform === "ios" ? "bundle id" : "package name",
97
- platform === "ios" ? "expected letters, digits, dots, hyphens and underscores" : "expected a dotted Java package name"
96
+ platform2 === "ios" ? "bundle id" : "package name",
97
+ platform2 === "ios" ? "expected letters, digits, dots, hyphens and underscores" : "expected a dotted Java package name"
98
98
  );
99
99
  }
100
100
  return input;
@@ -120,7 +120,7 @@ var pushPayload = (input) => {
120
120
  }
121
121
  return encoded;
122
122
  };
123
- var parseDeviceAction = (platform, input) => {
123
+ var parseDeviceAction = (platform2, input) => {
124
124
  if (!isRecord(input)) throw invalid("action", "expected an object");
125
125
  const type = oneOf(
126
126
  [
@@ -171,7 +171,7 @@ var parseDeviceAction = (platform, input) => {
171
171
  case "setPermission":
172
172
  return {
173
173
  type,
174
- appId: appId(platform, input["appId"]),
174
+ appId: appId(platform2, input["appId"]),
175
175
  permission: oneOf(PERMISSIONS, input["permission"], "permission"),
176
176
  decision: oneOf(DECISIONS, input["decision"], "decision")
177
177
  };
@@ -179,11 +179,11 @@ var parseDeviceAction = (platform, input) => {
179
179
  return { type, url: url(input["url"]) };
180
180
  case "launchApp":
181
181
  case "terminateApp":
182
- return { type, appId: appId(platform, input["appId"]) };
182
+ return { type, appId: appId(platform2, input["appId"]) };
183
183
  case "sendPush":
184
184
  return {
185
185
  type,
186
- appId: appId(platform, input["appId"]),
186
+ appId: appId(platform2, input["appId"]),
187
187
  payload: input["payload"]
188
188
  };
189
189
  }
@@ -225,10 +225,10 @@ var ANDROID_TOGGLES = /* @__PURE__ */ new Set([
225
225
  "reduceMotion",
226
226
  "networkEnabled"
227
227
  ]);
228
- var supportsAction = (platform, action) => {
229
- if (!(platform === "ios" ? IOS_ACTIONS : ANDROID_ACTIONS).has(action.type)) return false;
228
+ var supportsAction = (platform2, action) => {
229
+ if (!(platform2 === "ios" ? IOS_ACTIONS : ANDROID_ACTIONS).has(action.type)) return false;
230
230
  if (action.type === "setToggle") {
231
- return (platform === "ios" ? IOS_TOGGLES : ANDROID_TOGGLES).has(action.setting);
231
+ return (platform2 === "ios" ? IOS_TOGGLES : ANDROID_TOGGLES).has(action.setting);
232
232
  }
233
233
  return true;
234
234
  };
@@ -239,11 +239,11 @@ var target = {
239
239
  deviceId: { type: "string", minLength: 1, maxLength: 128 }
240
240
  };
241
241
  var appId2 = { type: "string", minLength: 1, maxLength: 255 };
242
- var branch = (type, properties = {}, required = []) => ({
242
+ var branch = (type, properties2 = {}, required = []) => ({
243
243
  type: "object",
244
244
  additionalProperties: false,
245
245
  required: ["deviceId", "type", ...required],
246
- properties: { ...target, type: { const: type }, ...properties }
246
+ properties: { ...target, type: { const: type }, ...properties2 }
247
247
  });
248
248
  var detailPayload = {
249
249
  type: "object",
@@ -373,9 +373,9 @@ var registerCommands = (ctx, service) => {
373
373
  if (!signal.aborted) emit(state);
374
374
  });
375
375
  try {
376
- await new Promise((resolve) => {
377
- if (signal.aborted) resolve();
378
- else signal.addEventListener("abort", () => resolve(), { once: true });
376
+ await new Promise((resolve3) => {
377
+ if (signal.aborted) resolve3();
378
+ else signal.addEventListener("abort", () => resolve3(), { once: true });
379
379
  });
380
380
  } finally {
381
381
  unsubscribe();
@@ -462,7 +462,7 @@ var registerCommands = (ctx, service) => {
462
462
  });
463
463
  ctx.command({
464
464
  name: "updateTool",
465
- description: "Install this plugin's pinned version of one device tool without starting it.",
465
+ description: "Verify this plugin's bundled version of one device tool without downloading or starting it.",
466
466
  scope: "operate",
467
467
  payloadSchema: updatePayload,
468
468
  handle: (payload) => service.updateTool(payload["tool"] === "agent" ? "agent" : "hub")
@@ -494,7 +494,7 @@ var registerCommands = (ctx, service) => {
494
494
  });
495
495
  ctx.stream({
496
496
  name: "updateTools",
497
- description: "Settings card action: install the pinned device tool versions.",
497
+ description: "Settings card action: verify the bundled device tool versions.",
498
498
  scope: "operate",
499
499
  payloadSchema: emptyPayload,
500
500
  handle: async (_payload, emit) => {
@@ -537,41 +537,1029 @@ var registerCommands = (ctx, service) => {
537
537
  });
538
538
  };
539
539
 
540
+ // src/environment.ts
541
+ import * as NodeFSP3 from "node:fs/promises";
542
+ import * as NodePath3 from "node:path";
543
+
544
+ // src/androidPackages.ts
545
+ var repo = "https://dl.google.com/android/repository/";
546
+ var archive = (name, file, sha1, bytes, destination = "sdk") => ({ name, url: `${repo}${file}`, sha1, bytes, destination });
547
+ var platformTools = {
548
+ linux: archive(
549
+ "Platform-Tools 37.0.1",
550
+ "platform-tools_r37.0.1-linux.zip",
551
+ "477254aa5f903c15cf51001717bdf347fb6b53e0",
552
+ 9054187
553
+ ),
554
+ darwin: archive(
555
+ "Platform-Tools 37.0.1",
556
+ "platform-tools_r37.0.1-darwin.zip",
557
+ "6ae73f4de6452dc57e62ec02b68eed92a4c21661",
558
+ 16110554
559
+ ),
560
+ win32: archive(
561
+ "Platform-Tools 37.0.1",
562
+ "platform-tools_r37.0.1-win.zip",
563
+ "e03e78b1d80b396f1c3358e31251cb31740e1110",
564
+ 8044989
565
+ )
566
+ };
567
+ var emulators = {
568
+ "linux-x64": archive(
569
+ "Android Emulator 37.2.12",
570
+ "emulator-linux_x64-16428233.zip",
571
+ "cd7362ea55dfb86a418958138dc396e74165dd01",
572
+ 349654171
573
+ ),
574
+ "darwin-x64": archive(
575
+ "Android Emulator 37.2.12",
576
+ "emulator-darwin_x64-16428233.zip",
577
+ "0c08fc22f41f2e63976f5503d593f821ff11e30b",
578
+ 488731099
579
+ ),
580
+ "darwin-arm64": archive(
581
+ "Android Emulator 37.2.12",
582
+ "emulator-darwin_aarch64-16428233.zip",
583
+ "3af4fe44ce82b3d88ae5678a53735f27ad729c15",
584
+ 416112708
585
+ ),
586
+ "win32-x64": archive(
587
+ "Android Emulator 37.2.12",
588
+ "emulator-windows_x64-16428233.zip",
589
+ "488ed747e82de7e9bb5247becd1ac043c7e5e85d",
590
+ 455342868
591
+ )
592
+ };
593
+ var androidArchives = (platform2, arch2) => {
594
+ const emulator = emulators[`${platform2}-${arch2}`];
595
+ const tools = platformTools[platform2];
596
+ if (!emulator || !tools) return null;
597
+ const arm = arch2 === "arm64";
598
+ const image = archive(
599
+ "Android 15 (API 35), AOSP revision 2",
600
+ `sys-img/android/${arm ? "arm64-v8a" : "x86_64"}-35_r02.zip`,
601
+ arm ? "2026a06409db630b56711afdbffb457c1dbaed49" : "2d857d170c0d1b827149565da34b3383e5306f7f",
602
+ arm ? 769099654 : 782404023,
603
+ "image"
604
+ );
605
+ return [tools, emulator, image];
606
+ };
607
+
608
+ // src/environmentArchive.ts
609
+ import * as NodeCrypto from "node:crypto";
610
+ import * as NodeFS from "node:fs";
611
+ import * as NodeFSP from "node:fs/promises";
612
+ import * as NodePath from "node:path";
613
+ import * as NodeStream from "node:stream";
614
+ import * as NodeStreamPromises from "node:stream/promises";
615
+ import * as NodeZlib from "node:zlib";
616
+ var childPath = (root, name) => {
617
+ if (name.includes("\\") || name.includes("\0")) throw new Error("Invalid archive path");
618
+ const result = NodePath.resolve(root, name);
619
+ if (!result.startsWith(`${NodePath.resolve(root)}${NodePath.sep}`))
620
+ throw new Error("Archive path escapes its destination");
621
+ return result;
622
+ };
623
+ var downloadArchive = async (item, directory, fetch) => {
624
+ await NodeFSP.mkdir(directory, { recursive: true });
625
+ const target2 = NodePath.join(directory, item.url.split("/").at(-1));
626
+ if ((await NodeFSP.lstat(target2).catch(() => null))?.isSymbolicLink())
627
+ throw new Error("Download cache entry must not be linked");
628
+ const verify = async () => {
629
+ const hash = NodeCrypto.createHash("sha1");
630
+ let size = 0;
631
+ for await (const bytes of NodeFS.createReadStream(target2)) {
632
+ hash.update(bytes);
633
+ size += bytes.length;
634
+ }
635
+ return size === item.bytes && hash.digest("hex") === item.sha1;
636
+ };
637
+ try {
638
+ if (await verify()) return target2;
639
+ } catch {
640
+ }
641
+ const temporary = `${target2}.${NodeCrypto.randomUUID()}.partial`;
642
+ try {
643
+ const response = await fetch(item.url);
644
+ if (!response.ok || !response.body)
645
+ throw new Error(`Downloading ${item.name} failed: HTTP ${response.status}`);
646
+ let size = 0;
647
+ const hash = NodeCrypto.createHash("sha1");
648
+ const meter = new NodeStream.Transform({
649
+ transform(chunk, _encoding, callback) {
650
+ size += chunk.length;
651
+ if (size > item.bytes) {
652
+ callback(new Error("Archive size exceeds the pinned length"));
653
+ return;
654
+ }
655
+ hash.update(chunk);
656
+ callback(null, chunk);
657
+ }
658
+ });
659
+ const body = response.body;
660
+ const chunks = async function* () {
661
+ const reader = body.getReader();
662
+ try {
663
+ while (true) {
664
+ const item2 = await reader.read();
665
+ if (item2.done) break;
666
+ yield item2.value;
667
+ }
668
+ } finally {
669
+ await reader.cancel().catch(() => void 0);
670
+ reader.releaseLock();
671
+ }
672
+ };
673
+ await NodeStreamPromises.pipeline(
674
+ NodeStream.Readable.from(chunks()),
675
+ meter,
676
+ NodeFS.createWriteStream(temporary, { flags: "wx" })
677
+ );
678
+ if (size !== item.bytes || hash.digest("hex") !== item.sha1)
679
+ throw new Error(`Integrity check failed for ${item.name}`);
680
+ await NodeFSP.rename(temporary, target2);
681
+ return target2;
682
+ } finally {
683
+ await NodeFSP.rm(temporary, { force: true });
684
+ }
685
+ };
686
+ var extractEnvironmentZip = async (file, destination, signal) => {
687
+ const handle = await NodeFSP.open(file, "r");
688
+ try {
689
+ const size = (await handle.stat()).size;
690
+ const tail = Buffer.alloc(Math.min(size, 65557));
691
+ await handle.read(tail, 0, tail.length, size - tail.length);
692
+ let end = tail.length - 22;
693
+ while (end >= 0 && tail.readUInt32LE(end) !== 101010256) end--;
694
+ if (end < 0) throw new Error("ZIP end record missing");
695
+ const count = tail.readUInt16LE(end + 10);
696
+ const centralSize = tail.readUInt32LE(end + 12);
697
+ const centralOffset = tail.readUInt32LE(end + 16);
698
+ if (count === 65535 || centralSize > 32 * 1024 * 1024 || centralOffset + centralSize > size)
699
+ throw new Error("Unsupported ZIP directory");
700
+ const directory = Buffer.alloc(centralSize);
701
+ await handle.read(directory, 0, centralSize, centralOffset);
702
+ let offset = 0;
703
+ for (let index = 0; index < count; index++) {
704
+ signal?.throwIfAborted();
705
+ if (directory.readUInt32LE(offset) !== 33639248)
706
+ throw new Error("Invalid ZIP directory entry");
707
+ const flags = directory.readUInt16LE(offset + 8);
708
+ const method = directory.readUInt16LE(offset + 10);
709
+ const compressed = directory.readUInt32LE(offset + 20);
710
+ const unpacked = directory.readUInt32LE(offset + 24);
711
+ const nameLength = directory.readUInt16LE(offset + 28);
712
+ const extraLength = directory.readUInt16LE(offset + 30);
713
+ const commentLength = directory.readUInt16LE(offset + 32);
714
+ const mode = directory.readUInt32LE(offset + 38) >>> 16;
715
+ const localOffset = directory.readUInt32LE(offset + 42);
716
+ const name = directory.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
717
+ offset += 46 + nameLength + extraLength + commentLength;
718
+ if (flags & 1 || ![0, 8].includes(method) || localOffset >= centralOffset)
719
+ throw new Error("Unsupported ZIP entry");
720
+ const target2 = childPath(destination, name);
721
+ if (name.endsWith("/")) {
722
+ await NodeFSP.mkdir(target2, { recursive: true });
723
+ continue;
724
+ }
725
+ await NodeFSP.mkdir(NodePath.dirname(target2), { recursive: true });
726
+ const header = Buffer.alloc(30);
727
+ await handle.read(header, 0, 30, localOffset);
728
+ if (header.readUInt32LE(0) !== 67324752) throw new Error("Invalid ZIP local header");
729
+ const dataOffset = localOffset + 30 + header.readUInt16LE(26) + header.readUInt16LE(28);
730
+ if (dataOffset + compressed > centralOffset)
731
+ throw new Error("ZIP data exceeds archive bounds");
732
+ const source = compressed ? NodeFS.createReadStream(file, { start: dataOffset, end: dataOffset + compressed - 1 }) : NodeStream.Readable.from([]);
733
+ const transforms = method === 8 ? [NodeZlib.createInflateRaw()] : [];
734
+ if ((mode & 61440) === 40960) {
735
+ if (unpacked > 4096) throw new Error("Invalid ZIP symbolic link");
736
+ const chunks = [];
737
+ let length = 0;
738
+ const collect = new NodeStream.Writable({
739
+ write(chunk, _encoding, callback) {
740
+ length += chunk.length;
741
+ if (length > 4096) {
742
+ callback(new Error("Invalid ZIP symbolic link"));
743
+ return;
744
+ }
745
+ chunks.push(Buffer.from(chunk));
746
+ callback();
747
+ }
748
+ });
749
+ await NodeStreamPromises.pipeline([source, ...transforms, collect], { signal });
750
+ const link = Buffer.concat(chunks).toString("utf8");
751
+ childPath(
752
+ destination,
753
+ NodePath.relative(destination, NodePath.resolve(NodePath.dirname(target2), link)).split(NodePath.sep).join("/")
754
+ );
755
+ await NodeFSP.symlink(link, target2);
756
+ } else {
757
+ let written = 0;
758
+ const limit = new NodeStream.Transform({
759
+ transform(chunk, _encoding, callback) {
760
+ written += chunk.length;
761
+ callback(
762
+ written > unpacked ? new Error("ZIP expands beyond its declared size") : null,
763
+ chunk
764
+ );
765
+ }
766
+ });
767
+ await NodeStreamPromises.pipeline(
768
+ [
769
+ source,
770
+ ...transforms,
771
+ limit,
772
+ NodeFS.createWriteStream(target2, { flags: "wx", mode: mode & 511 || 420 })
773
+ ],
774
+ { signal }
775
+ );
776
+ if (written !== unpacked) throw new Error("ZIP uncompressed length mismatch");
777
+ }
778
+ }
779
+ } finally {
780
+ await handle.close();
781
+ }
782
+ };
783
+
784
+ // src/environmentStorage.ts
785
+ import * as NodeFSP2 from "node:fs/promises";
786
+ import * as NodePath2 from "node:path";
787
+ var ENVIRONMENT_MARKER = ".ycodium-devices-environment.json";
788
+ var ANDROID_ENVIRONMENT_VERSION = "Android 15 / API 35 \xB7 Emulator 37.2.12";
789
+ var environmentPaths = (root) => ({
790
+ root,
791
+ sdk: NodePath2.join(root, "sdk"),
792
+ user: NodePath2.join(root, "user"),
793
+ avd: NodePath2.join(root, "avd"),
794
+ temp: NodePath2.join(root, "temp"),
795
+ cache: NodePath2.join(root, "cache")
796
+ });
797
+ var environmentRoot = (dataDir, platform2, directory) => {
798
+ if (directory?.trim()) {
799
+ if (!NodePath2.isAbsolute(directory.trim()))
800
+ throw new Error("Choose an absolute directory on this machine");
801
+ return NodePath2.join(NodePath2.resolve(directory.trim()), `ycodium-devices-${platform2}`);
802
+ }
803
+ return NodePath2.join(dataDir, "environments", platform2);
804
+ };
805
+ var assertOwnedEnvironment = async (root) => {
806
+ const stat = await NodeFSP2.lstat(root);
807
+ if (!stat.isDirectory() || stat.isSymbolicLink())
808
+ throw new Error("Environment root is not a private directory");
809
+ if ((await NodeFSP2.lstat(NodePath2.join(root, ENVIRONMENT_MARKER))).isSymbolicLink())
810
+ throw new Error("Environment ownership record must not be linked");
811
+ const record = JSON.parse(
812
+ await NodeFSP2.readFile(NodePath2.join(root, ENVIRONMENT_MARKER), "utf8")
813
+ );
814
+ if (typeof record !== "object" || record === null || !("owner" in record) || record.owner !== "ycodium-devices" || !("sdk" in record) || record.sdk !== NodePath2.join(root, "sdk"))
815
+ throw new Error("Directory is not owned by the Devices plugin");
816
+ };
817
+ var assertPrivatePath = async (root, target2) => {
818
+ const relative3 = NodePath2.relative(NodePath2.resolve(root), NodePath2.resolve(target2));
819
+ if (!relative3 || relative3.startsWith(`..${NodePath2.sep}`) || relative3 === ".." || NodePath2.isAbsolute(relative3))
820
+ throw new Error("Private path escapes its environment");
821
+ let current = root;
822
+ for (const part of relative3.split(NodePath2.sep)) {
823
+ current = NodePath2.join(current, part);
824
+ const stat = await NodeFSP2.lstat(current).catch((error) => {
825
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
826
+ throw error;
827
+ });
828
+ if (!stat) break;
829
+ if (stat.isSymbolicLink()) throw new Error("Refusing a linked private environment path");
830
+ }
831
+ };
832
+ var assertPrivateEnvironmentPaths = async (root) => {
833
+ await assertOwnedEnvironment(root);
834
+ for (const name of ["sdk", "avd", "cache", "temp", "user", "home", "config", ".prepared"])
835
+ await assertPrivatePath(root, NodePath2.join(root, name));
836
+ };
837
+ var claimEnvironment = async (root) => {
838
+ let owned = false;
839
+ try {
840
+ await assertOwnedEnvironment(root);
841
+ owned = true;
842
+ } catch {
843
+ }
844
+ if (owned) {
845
+ await assertPrivateEnvironmentPaths(root);
846
+ return;
847
+ }
848
+ await NodeFSP2.mkdir(root, { recursive: true });
849
+ if ((await NodeFSP2.lstat(root)).isSymbolicLink() || (await NodeFSP2.readdir(root)).length !== 0)
850
+ throw new Error("Choose an empty directory; existing files will not be changed");
851
+ await NodeFSP2.writeFile(
852
+ NodePath2.join(root, ENVIRONMENT_MARKER),
853
+ JSON.stringify({ owner: "ycodium-devices", sdk: NodePath2.join(root, "sdk") }),
854
+ { flag: "wx" }
855
+ );
856
+ };
857
+ var directoryBytes = async (root) => {
858
+ try {
859
+ const stat = await NodeFSP2.lstat(root);
860
+ if (stat.isSymbolicLink()) return 0;
861
+ if (!stat.isDirectory()) return stat.size;
862
+ const children = await NodeFSP2.readdir(root);
863
+ let total = 0;
864
+ for (const child of children) total += await directoryBytes(NodePath2.join(root, child));
865
+ return total;
866
+ } catch {
867
+ return 0;
868
+ }
869
+ };
870
+ var freeBytes = async (root) => {
871
+ let probe = root;
872
+ while (true) {
873
+ try {
874
+ const stat = await NodeFSP2.statfs(probe);
875
+ return stat.bavail * stat.bsize;
876
+ } catch {
877
+ }
878
+ const parent = NodePath2.dirname(probe);
879
+ if (parent === probe) return null;
880
+ probe = parent;
881
+ }
882
+ };
883
+ var selectedPrivateRoot = async (dataDir) => {
884
+ try {
885
+ const record = JSON.parse(
886
+ await NodeFSP2.readFile(NodePath2.join(dataDir, "android-environment.json"), "utf8")
887
+ );
888
+ if (typeof record !== "object" || record === null || !("root" in record) || typeof record.root !== "string")
889
+ return null;
890
+ await assertOwnedEnvironment(record.root);
891
+ return record.root;
892
+ } catch {
893
+ return null;
894
+ }
895
+ };
896
+ var selectedPrivateSdk = async (dataDir) => {
897
+ const root = await selectedPrivateRoot(dataDir);
898
+ if (!root) return null;
899
+ return await privateEnvironmentReady(root) ? NodePath2.join(root, "sdk") : null;
900
+ };
901
+ var privateEnvironmentReady = async (root) => {
902
+ try {
903
+ await assertPrivateEnvironmentPaths(root);
904
+ const paths = environmentPaths(root);
905
+ 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)
906
+ return false;
907
+ const avd = NodePath2.join(paths.avd, "Ycodium_API_35.avd");
908
+ await assertPrivatePath(root, NodePath2.join(avd, "config.ini"));
909
+ await assertPrivatePath(root, NodePath2.join(paths.avd, "Ycodium_API_35.ini"));
910
+ const config = await NodeFSP2.readFile(NodePath2.join(avd, "config.ini"), "utf8");
911
+ const image = /^image\.sysdir\.1=(system-images\/android-35\/default\/(?:x86_64|arm64-v8a)\/)\r?$/m.exec(
912
+ config
913
+ )?.[1];
914
+ if (!image) return false;
915
+ await assertPrivatePath(root, NodePath2.join(paths.sdk, image, "system.img"));
916
+ await NodeFSP2.access(NodePath2.join(paths.sdk, image, "system.img"));
917
+ if (!(await NodeFSP2.readFile(NodePath2.join(paths.avd, "Ycodium_API_35.ini"), "utf8")).includes(
918
+ `path=${avd}
919
+ `
920
+ ))
921
+ return false;
922
+ for (const suffix of ["", ".exe"]) {
923
+ try {
924
+ await NodeFSP2.access(NodePath2.join(paths.sdk, "platform-tools", `adb${suffix}`));
925
+ await NodeFSP2.access(NodePath2.join(paths.sdk, "emulator", `emulator${suffix}`));
926
+ return true;
927
+ } catch {
928
+ }
929
+ }
930
+ return false;
931
+ } catch {
932
+ return false;
933
+ }
934
+ };
935
+ var cleanEnvironment = async (root, category) => {
936
+ await assertPrivateEnvironmentPaths(root);
937
+ const target2 = NodePath2.resolve(root, category === "cache" ? "cache" : "avd");
938
+ if (NodePath2.dirname(target2) !== NodePath2.resolve(root))
939
+ throw new Error("Cleanup escaped the private environment");
940
+ const bytes = await directoryBytes(target2);
941
+ if (category === "devices") {
942
+ const devices = await NodeFSP2.readdir(target2).catch(() => []);
943
+ for (const name of devices) {
944
+ if (name.endsWith(".lock"))
945
+ throw new Error("Power off private virtual devices before deleting their data");
946
+ if (!name.endsWith(".avd")) continue;
947
+ if ((await NodeFSP2.lstat(NodePath2.join(target2, name))).isSymbolicLink())
948
+ throw new Error("Cleanup refuses a linked virtual device");
949
+ const entries = await NodeFSP2.readdir(NodePath2.join(target2, name));
950
+ if (entries.some((entry) => entry.endsWith(".lock")))
951
+ throw new Error("Power off private virtual devices before deleting their data");
952
+ }
953
+ }
954
+ const stat = await NodeFSP2.lstat(target2).catch(() => null);
955
+ if (stat?.isSymbolicLink()) throw new Error("Cleanup refuses a linked directory");
956
+ await NodeFSP2.rm(target2, { recursive: true, force: true });
957
+ if (category === "devices") await NodeFSP2.rm(NodePath2.join(root, ".prepared"), { force: true });
958
+ return bytes;
959
+ };
960
+
961
+ // src/environment.ts
962
+ var GIB = 1024 ** 3;
963
+ var androidGuide = "https://developer.android.com/studio/run/emulator-acceleration";
964
+ var appleGuide = "https://developer.apple.com/documentation/xcode/downloading-and-installing-additional-xcode-components";
965
+ var DeviceEnvironments = class {
966
+ busy = false;
967
+ abort = new AbortController();
968
+ progress = { status: "idle" };
969
+ listeners = /* @__PURE__ */ new Set();
970
+ active = null;
971
+ nextJobId = 1;
972
+ options;
973
+ constructor(options) {
974
+ this.options = options;
975
+ }
976
+ subscribe(listener) {
977
+ this.listeners.add(listener);
978
+ listener(this.progress);
979
+ return () => {
980
+ this.listeners.delete(listener);
981
+ };
982
+ }
983
+ publish(progress) {
984
+ this.progress = progress;
985
+ for (const listener of this.listeners) listener(progress);
986
+ }
987
+ start(input) {
988
+ if (this.busy)
989
+ throw new DeviceError("invalid-input", "Another simulator preparation is already running");
990
+ const id = this.nextJobId++;
991
+ this.publish({ id, status: "working" });
992
+ this.active = this.prepare(
993
+ input,
994
+ (stage) => this.publish({ id, status: "working", stage })
995
+ ).then(
996
+ (plan) => this.publish({ id, status: "done", plan }),
997
+ (error) => {
998
+ this.options.log?.warn("Simulator preparation failed", {
999
+ detail: error instanceof Error ? error.message : String(error)
1000
+ });
1001
+ this.publish({ id, status: "failed" });
1002
+ }
1003
+ );
1004
+ return this.progress;
1005
+ }
1006
+ async dispose() {
1007
+ this.abort.abort();
1008
+ await this.active;
1009
+ this.listeners.clear();
1010
+ }
1011
+ async probe() {
1012
+ const result = await this.options.process.run({
1013
+ program: "host:node",
1014
+ args: [
1015
+ this.options.helperPath,
1016
+ "environment-probe",
1017
+ ...this.options.runtimeDir ? ["--tools", this.options.runtimeDir] : []
1018
+ ],
1019
+ cwd: this.options.dataDir,
1020
+ timeoutMs: 6e4
1021
+ });
1022
+ if (result.exitCode !== 0)
1023
+ throw new Error("Could not inspect the machine's simulator prerequisites");
1024
+ const probe = JSON.parse(result.stdout.trim().split(/\r?\n/).at(-1) ?? "");
1025
+ if (typeof probe !== "object" || probe === null || !("platform" in probe) || !("arch" in probe) || !("virtualization" in probe) || !("xcode" in probe))
1026
+ throw new Error("Invalid simulator prerequisite report");
1027
+ return probe;
1028
+ }
1029
+ async plan(input) {
1030
+ const probe = await this.probe();
1031
+ const selected = input.platform === "android" && !input.directory?.trim() ? await selectedPrivateRoot(this.options.dataDir) : null;
1032
+ const root = selected ?? environmentRoot(this.options.dataDir, input.platform, input.directory);
1033
+ const paths = environmentPaths(root);
1034
+ const [free, cacheBytes, deviceDataBytes] = await Promise.all([
1035
+ freeBytes(root),
1036
+ directoryBytes(paths.cache),
1037
+ directoryBytes(paths.avd)
1038
+ ]);
1039
+ if (input.platform === "ios") {
1040
+ const version = input.version?.trim() ?? "";
1041
+ const runtimePresent = probe.xcode.runtimes.some((runtime) => runtime.version === version);
1042
+ return {
1043
+ platform: "ios",
1044
+ supported: probe.platform === "darwin" && probe.iosToolsAvailable !== false && probe.xcode.ready,
1045
+ source: "apple",
1046
+ root,
1047
+ version,
1048
+ downloadBytes: null,
1049
+ requiredFreeBytes: 20 * GIB,
1050
+ freeBytes: free,
1051
+ ready: runtimePresent,
1052
+ ...probe.platform === "darwin" && probe.iosToolsAvailable === false ? { reasonCode: "ios-tools-missing" } : !probe.xcode.ready ? { reasonCode: probe.xcode.reasonCode ?? "xcode-incomplete" } : !version ? { reasonCode: "ios-version-required" } : {},
1053
+ licenses: [{ url: "https://www.apple.com/legal/sla/" }],
1054
+ guidanceUrl: appleGuide,
1055
+ cacheBytes,
1056
+ deviceDataBytes
1057
+ };
1058
+ }
1059
+ const archives = androidArchives(probe.platform, probe.arch);
1060
+ const sdk = await this.options.externalSdk();
1061
+ const prepared = await privateEnvironmentReady(root);
1062
+ return {
1063
+ platform: "android",
1064
+ supported: archives !== null && probe.virtualization.available,
1065
+ source: sdk.available && sdk.root !== paths.sdk ? "external" : "private",
1066
+ ...sdk.available && sdk.root ? { existingSdkRoot: sdk.root } : {},
1067
+ root,
1068
+ version: ANDROID_ENVIRONMENT_VERSION,
1069
+ downloadBytes: archives?.reduce((sum, item) => sum + item.bytes, 0) ?? null,
1070
+ requiredFreeBytes: 12 * GIB,
1071
+ freeBytes: free,
1072
+ ready: sdk.available && (sdk.root !== paths.sdk || prepared),
1073
+ ...!archives ? { reasonCode: "android-platform-unsupported" } : !probe.virtualization.available ? { reasonCode: probe.virtualization.reasonCode ?? "virtualization-unavailable" } : {},
1074
+ licenses: [
1075
+ {
1076
+ url: "https://developer.android.com/studio/terms"
1077
+ }
1078
+ ],
1079
+ guidanceUrl: androidGuide,
1080
+ cacheBytes,
1081
+ deviceDataBytes
1082
+ };
1083
+ }
1084
+ async exclusively(operation) {
1085
+ if (this.busy)
1086
+ throw new DeviceError(
1087
+ "invalid-input",
1088
+ "Another simulator preparation or cleanup is already running"
1089
+ );
1090
+ this.busy = true;
1091
+ try {
1092
+ return await operation();
1093
+ } finally {
1094
+ this.busy = false;
1095
+ }
1096
+ }
1097
+ async prepare(input, progress = () => void 0) {
1098
+ return this.exclusively(async () => {
1099
+ const plan = await this.plan(input);
1100
+ this.abort.signal.throwIfAborted();
1101
+ if (!input.acceptLicenses)
1102
+ throw new DeviceError(
1103
+ "invalid-input",
1104
+ "Accept the component licenses before preparing a simulator"
1105
+ );
1106
+ if (!plan.supported)
1107
+ throw new DeviceError(
1108
+ "platform-unavailable",
1109
+ `Simulator preparation is unavailable: ${plan.reasonCode ?? "unsupported platform"}. See ${plan.guidanceUrl}`
1110
+ );
1111
+ if (plan.freeBytes === null || plan.freeBytes < plan.requiredFreeBytes)
1112
+ throw new DeviceError(
1113
+ "boot-disk-space",
1114
+ "There is not enough free disk space to prepare this simulator"
1115
+ );
1116
+ if (input.platform === "ios" && (!input.confirmAppleInstall || !/^\d+\.\d+(?:\.\d+)?$/.test(plan.version)))
1117
+ throw new DeviceError(
1118
+ "invalid-input",
1119
+ "Choose an explicit iOS version and confirm installation into Apple's shared simulator environment"
1120
+ );
1121
+ const release = await this.options.beforeMutation();
1122
+ try {
1123
+ await claimEnvironment(plan.root);
1124
+ await assertPrivateEnvironmentPaths(plan.root);
1125
+ const paths = environmentPaths(plan.root);
1126
+ for (const directory of [
1127
+ paths.cache,
1128
+ paths.temp,
1129
+ paths.user,
1130
+ NodePath3.join(plan.root, "home"),
1131
+ NodePath3.join(plan.root, "config")
1132
+ ])
1133
+ await NodeFSP3.mkdir(directory, { recursive: true });
1134
+ if (input.platform === "ios") await this.prepareIos(plan, progress);
1135
+ else await this.prepareAndroid(plan, progress);
1136
+ } finally {
1137
+ await release?.();
1138
+ }
1139
+ await this.options.afterPrepared();
1140
+ return this.plan(input);
1141
+ });
1142
+ }
1143
+ async prepareAndroid(plan, progress) {
1144
+ const probe = await this.probe();
1145
+ const archives = androidArchives(probe.platform, probe.arch);
1146
+ if (!archives) throw new Error("Android host target is unsupported");
1147
+ const paths = environmentPaths(plan.root);
1148
+ const staging = await NodeFSP3.mkdtemp(NodePath3.join(paths.temp, "prepare-"));
1149
+ try {
1150
+ const existing2 = await NodeFSP3.lstat(paths.sdk).catch(() => null);
1151
+ if (existing2) {
1152
+ if (existing2.isSymbolicLink() || !existing2.isDirectory() || await NodeFSP3.readFile(NodePath3.join(paths.sdk, ".ycodium-components"), "utf8").catch(
1153
+ () => ""
1154
+ ) !== plan.version)
1155
+ throw new Error(
1156
+ "An unrecognized private SDK already exists; its installation will not be overwritten"
1157
+ );
1158
+ } else {
1159
+ for (const item of archives) {
1160
+ progress(item.name);
1161
+ this.abort.signal.throwIfAborted();
1162
+ const file = await downloadArchive(
1163
+ item,
1164
+ paths.cache,
1165
+ (url2, init) => this.options.fetch(url2, { ...init, signal: this.abort.signal })
1166
+ );
1167
+ await extractEnvironmentZip(
1168
+ file,
1169
+ item.destination === "sdk" ? staging : NodePath3.join(staging, "system-images", "android-35", "default"),
1170
+ this.abort.signal
1171
+ );
1172
+ }
1173
+ await NodeFSP3.writeFile(NodePath3.join(staging, ".ycodium-components"), plan.version);
1174
+ await NodeFSP3.rename(staging, paths.sdk);
1175
+ }
1176
+ await this.createPrivateAvd(paths.root, probe.arch === "arm64" ? "arm64-v8a" : "x86_64");
1177
+ this.abort.signal.throwIfAborted();
1178
+ progress("validate");
1179
+ const validated = await this.options.process.run({
1180
+ program: "host:node",
1181
+ args: [this.options.helperPath, "validate-android", "--sdk", paths.sdk],
1182
+ cwd: this.options.dataDir,
1183
+ timeoutMs: 6e4
1184
+ });
1185
+ if (validated.exitCode !== 0)
1186
+ throw new Error(`The private SDK could not be validated. ${validated.stderr.trim()}`);
1187
+ await NodeFSP3.writeFile(NodePath3.join(plan.root, ".prepared"), plan.version);
1188
+ await NodeFSP3.writeFile(
1189
+ NodePath3.join(this.options.dataDir, "android-environment.json"),
1190
+ JSON.stringify({ root: plan.root })
1191
+ );
1192
+ } finally {
1193
+ await NodeFSP3.rm(staging, { recursive: true, force: true });
1194
+ }
1195
+ }
1196
+ async createPrivateAvd(root, abi) {
1197
+ const paths = environmentPaths(root);
1198
+ const name = "Ycodium_API_35";
1199
+ const avd = NodePath3.join(paths.avd, `${name}.avd`);
1200
+ await assertPrivatePath(root, avd);
1201
+ await assertPrivatePath(root, NodePath3.join(paths.avd, `${name}.ini`));
1202
+ const existing2 = await NodeFSP3.lstat(avd).catch(() => null);
1203
+ if (existing2) {
1204
+ if (existing2.isSymbolicLink() || !existing2.isDirectory())
1205
+ throw new Error("Private virtual device is not a directory");
1206
+ await NodeFSP3.access(NodePath3.join(avd, "config.ini"));
1207
+ await NodeFSP3.access(NodePath3.join(paths.avd, `${name}.ini`));
1208
+ return;
1209
+ }
1210
+ await NodeFSP3.mkdir(avd, { recursive: true });
1211
+ const config = {
1212
+ AvdId: name,
1213
+ "avd.ini.displayname": "Ycodium Android 15",
1214
+ "abi.type": abi,
1215
+ "hw.cpu.arch": abi === "arm64-v8a" ? "arm64" : "x86_64",
1216
+ "hw.cpu.ncore": "2",
1217
+ "hw.ramSize": "2048",
1218
+ "hw.lcd.width": "1080",
1219
+ "hw.lcd.height": "1920",
1220
+ "hw.lcd.density": "420",
1221
+ "hw.gpu.enabled": "yes",
1222
+ "hw.gpu.mode": "auto",
1223
+ "hw.keyboard": "yes",
1224
+ "disk.dataPartition.size": "4G",
1225
+ "image.sysdir.1": `system-images/android-35/default/${abi}/`,
1226
+ "tag.id": "default",
1227
+ "tag.display": "Default Android System Image",
1228
+ "PlayStore.enabled": "false"
1229
+ };
1230
+ await NodeFSP3.writeFile(
1231
+ NodePath3.join(avd, "config.ini"),
1232
+ Object.entries(config).map(([key2, value]) => `${key2}=${value}`).join("\n") + "\n",
1233
+ { flag: "wx" }
1234
+ );
1235
+ await NodeFSP3.writeFile(
1236
+ NodePath3.join(paths.avd, `${name}.ini`),
1237
+ `avd.ini.encoding=UTF-8
1238
+ path=${avd}
1239
+ target=android-35
1240
+ `,
1241
+ { flag: "wx" }
1242
+ );
1243
+ }
1244
+ async prepareIos(plan, progress) {
1245
+ if (plan.ready) return;
1246
+ const cache = NodePath3.join(plan.root, "cache", `ios-${plan.version}`);
1247
+ await NodeFSP3.mkdir(cache, { recursive: true });
1248
+ progress(`iOS ${plan.version}`);
1249
+ if (await this.runApple([
1250
+ "-downloadPlatform",
1251
+ "iOS",
1252
+ "-exportPath",
1253
+ cache,
1254
+ "-buildVersion",
1255
+ plan.version
1256
+ ]) !== 0)
1257
+ throw new Error(
1258
+ "Apple could not download this iOS runtime; check the selected Xcode version, network and Apple account in Xcode"
1259
+ );
1260
+ const names = (await NodeFSP3.readdir(cache)).filter((name) => name.endsWith(".dmg"));
1261
+ if (names.length !== 1)
1262
+ throw new Error("Apple did not return a unique simulator runtime archive");
1263
+ progress("apple-install");
1264
+ if (await this.runApple(["-importPlatform", NodePath3.join(cache, names[0])]) !== 0)
1265
+ throw new Error(
1266
+ "Apple could not import the runtime. Complete any required permissions in Xcode; Ycodium will not elevate privileges"
1267
+ );
1268
+ if (!(await this.probe()).xcode.runtimes.some((runtime) => runtime.version === plan.version))
1269
+ throw new Error("The requested iOS runtime is still unavailable after Apple's import");
1270
+ }
1271
+ async runApple(args) {
1272
+ this.abort.signal.throwIfAborted();
1273
+ const handle = await this.options.process.spawn({
1274
+ program: "host:node",
1275
+ args: [this.options.helperPath, "run", "--", "xcodebuild", ...args],
1276
+ cwd: this.options.dataDir
1277
+ });
1278
+ const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(36e5)]);
1279
+ const terminate = () => {
1280
+ void handle.kill().catch(() => void 0);
1281
+ };
1282
+ signal.addEventListener("abort", terminate, { once: true });
1283
+ if (signal.aborted) terminate();
1284
+ const drain = async (stream) => {
1285
+ for await (const _chunk of stream) {
1286
+ }
1287
+ };
1288
+ try {
1289
+ const [code] = await Promise.all([handle.exited, drain(handle.stdout), drain(handle.stderr)]);
1290
+ signal.throwIfAborted();
1291
+ return code;
1292
+ } finally {
1293
+ signal.removeEventListener("abort", terminate);
1294
+ }
1295
+ }
1296
+ cleanup(input) {
1297
+ return this.exclusively(async () => {
1298
+ if (!input.confirm || input.platform === "ios" && input.category === "devices")
1299
+ throw new DeviceError(
1300
+ "invalid-input",
1301
+ "Confirm cleanup of private download cache or Android virtual device data; Apple runtimes are managed in Xcode"
1302
+ );
1303
+ const release = input.category === "devices" ? await this.options.beforeMutation() : void 0;
1304
+ try {
1305
+ const plan = await this.plan(input);
1306
+ const releasedBytes = await cleanEnvironment(plan.root, input.category);
1307
+ await this.options.afterPrepared();
1308
+ return { releasedBytes };
1309
+ } finally {
1310
+ await release?.();
1311
+ }
1312
+ });
1313
+ }
1314
+ };
1315
+
1316
+ // src/environmentCommands.ts
1317
+ var properties = {
1318
+ platform: { type: "string", enum: ["android", "ios"] },
1319
+ directory: { type: "string" },
1320
+ version: { type: "string" }
1321
+ };
1322
+ var schema = (extra = {}, required = []) => ({
1323
+ type: "object",
1324
+ additionalProperties: false,
1325
+ properties: { ...properties, ...extra },
1326
+ required: ["platform", ...required]
1327
+ });
1328
+ var request = (payload) => ({
1329
+ platform: payload["platform"] === "ios" ? "ios" : "android",
1330
+ ...typeof payload["directory"] === "string" ? { directory: payload["directory"] } : {},
1331
+ ...typeof payload["version"] === "string" ? { version: payload["version"] } : {}
1332
+ });
1333
+ var registerEnvironmentCommands = (ctx, environments) => {
1334
+ ctx.command({
1335
+ name: "environmentPlan",
1336
+ description: "Inspect simulator prerequisites, selected storage, download size and licenses without downloading or modifying the machine.",
1337
+ scope: "read",
1338
+ payloadSchema: schema(),
1339
+ handle: (payload) => environments.plan(request(payload))
1340
+ });
1341
+ ctx.command({
1342
+ name: "prepareEnvironment",
1343
+ description: "Start user-triggered simulator preparation. Progress is read from environmentProgress; command completion only acknowledges the request.",
1344
+ scope: "operate",
1345
+ payloadSchema: schema(
1346
+ { acceptLicenses: { type: "boolean" }, confirmAppleInstall: { type: "boolean" } },
1347
+ ["acceptLicenses", "confirmAppleInstall"]
1348
+ ),
1349
+ handle: (payload) => environments.start({
1350
+ ...request(payload),
1351
+ acceptLicenses: payload["acceptLicenses"] === true,
1352
+ confirmAppleInstall: payload["confirmAppleInstall"] === true
1353
+ })
1354
+ });
1355
+ ctx.stream({
1356
+ name: "environmentProgress",
1357
+ description: "Current user-triggered simulator preparation progress; subscribing does not start preparation.",
1358
+ scope: "read",
1359
+ payloadSchema: { type: "object", properties: {}, additionalProperties: false },
1360
+ handle: async (_payload, emit, signal) => {
1361
+ const unsubscribe = environments.subscribe((progress) => {
1362
+ if (!signal.aborted) emit(progress);
1363
+ });
1364
+ try {
1365
+ await new Promise((resolve3) => {
1366
+ if (signal.aborted) resolve3();
1367
+ else signal.addEventListener("abort", () => resolve3(), { once: true });
1368
+ });
1369
+ } finally {
1370
+ unsubscribe();
1371
+ }
1372
+ }
1373
+ });
1374
+ ctx.command({
1375
+ name: "cleanupEnvironment",
1376
+ description: "User-confirmed cleanup of private download cache or Android virtual device data. Never deletes external SDKs, external virtual devices or Apple runtimes.",
1377
+ scope: "operate",
1378
+ payloadSchema: schema(
1379
+ { category: { type: "string", enum: ["cache", "devices"] }, confirm: { type: "boolean" } },
1380
+ ["category", "confirm"]
1381
+ ),
1382
+ handle: (payload) => environments.cleanup({
1383
+ ...request(payload),
1384
+ category: payload["category"] === "devices" ? "devices" : "cache",
1385
+ confirm: payload["confirm"] === true
1386
+ })
1387
+ });
1388
+ };
1389
+
1390
+ // src/probe.ts
1391
+ var MIN_NODE_VERSION = [22, 12, 0];
1392
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1393
+ var parseMachineProbe = (stdout) => {
1394
+ const line = stdout.trim().split(/\r?\n/).at(-1) ?? "";
1395
+ let value;
1396
+ try {
1397
+ value = JSON.parse(line);
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ if (!isRecord2(value) || !isRecord2(value["android"])) return null;
1402
+ const android = value["android"];
1403
+ if (typeof value["node"] !== "string" || typeof value["execPath"] !== "string" || typeof value["platform"] !== "string") {
1404
+ return null;
1405
+ }
1406
+ return {
1407
+ node: value["node"],
1408
+ execPath: value["execPath"],
1409
+ electron: typeof value["electron"] === "string" ? value["electron"] : null,
1410
+ platform: value["platform"],
1411
+ ...typeof value["arch"] === "string" ? { arch: value["arch"] } : {},
1412
+ xcrun: value["xcrun"] === true,
1413
+ ...typeof value["androidValidated"] === "boolean" ? { androidValidated: value["androidValidated"] } : {},
1414
+ ...typeof value["iosRuntimeAvailable"] === "boolean" ? { iosRuntimeAvailable: value["iosRuntimeAvailable"] } : {},
1415
+ ...typeof value["iosToolsAvailable"] === "boolean" ? { iosToolsAvailable: value["iosToolsAvailable"] } : {},
1416
+ android: {
1417
+ root: typeof android["root"] === "string" ? android["root"] : null,
1418
+ adb: android["adb"] === true,
1419
+ emulator: android["emulator"] === true,
1420
+ avdmanager: android["avdmanager"] === true,
1421
+ legacyAvdmanager: android["legacyAvdmanager"] === true
1422
+ }
1423
+ };
1424
+ };
1425
+ var nodeVersionSupported = (version) => {
1426
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
1427
+ if (!match) return false;
1428
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
1429
+ for (let index = 0; index < MIN_NODE_VERSION.length; index += 1) {
1430
+ const actual = parts[index];
1431
+ const required = MIN_NODE_VERSION[index];
1432
+ if (actual !== required) return actual > required;
1433
+ }
1434
+ return true;
1435
+ };
1436
+ var unavailable = (platform2, reasonCode, reason, sdkRoot) => ({
1437
+ platform: platform2,
1438
+ available: false,
1439
+ reasonCode,
1440
+ reason,
1441
+ ...sdkRoot === void 0 ? {} : { sdkRoot }
1442
+ });
1443
+ var platformAvailability = (probe, platform2) => {
1444
+ if (platform2 === "ios") {
1445
+ if (probe.platform !== "darwin") {
1446
+ return unavailable("ios", "ios-needs-macos", "iOS Simulators need macOS with Xcode.");
1447
+ }
1448
+ if (probe.iosToolsAvailable === false)
1449
+ return unavailable(
1450
+ "ios",
1451
+ "ios-tools-missing",
1452
+ "The bundled iOS Simulator streaming helpers are missing or could not load. Repair or update Ycodium on this machine."
1453
+ );
1454
+ if (!probe.xcrun) {
1455
+ return unavailable("ios", "xcode-missing", "Xcode command line tools were not found.");
1456
+ }
1457
+ if (probe.iosRuntimeAvailable === false)
1458
+ return unavailable(
1459
+ "ios",
1460
+ "ios-runtime-missing",
1461
+ "An iOS Simulator runtime is missing. Choose an explicit version in simulator preparation or install one in Xcode Settings."
1462
+ );
1463
+ return { platform: platform2, available: true };
1464
+ }
1465
+ const sdk = probe.android;
1466
+ if (!sdk.root) {
1467
+ return unavailable(
1468
+ "android",
1469
+ "android-sdk-missing",
1470
+ "Android SDK was not found. Install it with Android Studio or set its location in the Devices settings."
1471
+ );
1472
+ }
1473
+ if (!sdk.adb) {
1474
+ return unavailable(
1475
+ "android",
1476
+ "android-platform-tools-missing",
1477
+ `Android SDK Platform-Tools are missing from ${sdk.root}. Install them in Android Studio's SDK Manager.`,
1478
+ sdk.root
1479
+ );
1480
+ }
1481
+ if (!sdk.emulator) {
1482
+ return unavailable(
1483
+ "android",
1484
+ "android-emulator-missing",
1485
+ `Android Emulator is missing from ${sdk.root}. Install it in Android Studio's SDK Manager.`,
1486
+ sdk.root
1487
+ );
1488
+ }
1489
+ if (probe.androidValidated === false)
1490
+ return unavailable(
1491
+ "android",
1492
+ "android-sdk-invalid",
1493
+ "The Android SDK tools could not run. Check the SDK installation or prepare a private environment.",
1494
+ sdk.root
1495
+ );
1496
+ return { platform: platform2, available: true, sdkRoot: sdk.root };
1497
+ };
1498
+ var probeMachine = async (input) => {
1499
+ const args = [input.helperPath, "probe"];
1500
+ if (input.runtimeDir) args.push("--tools", input.runtimeDir);
1501
+ if (input.sdkOverride.trim().length > 0) args.push("--sdk", input.sdkOverride.trim());
1502
+ const result = await input.process.run({ program: "host:node", args, cwd: input.cwd, timeoutMs: 3e4 }).catch((error) => {
1503
+ throw new DeviceError(
1504
+ "node-unavailable",
1505
+ `Could not run Node.js to check device support: ${errorDetail(error)}`,
1506
+ { cause: error }
1507
+ );
1508
+ });
1509
+ const probe = result.exitCode === 0 ? parseMachineProbe(result.stdout) : null;
1510
+ if (!probe) {
1511
+ throw new DeviceError(
1512
+ "node-unavailable",
1513
+ `Checking device support failed: ${result.stderr.trim() || "the helper returned no result"}.`
1514
+ );
1515
+ }
1516
+ if (!nodeVersionSupported(probe.node)) {
1517
+ throw new DeviceError(
1518
+ "node-unavailable",
1519
+ `Device support requires Node.js 22.12 or newer; this Ycodium runs on ${probe.node}. Update Ycodium on this machine.`
1520
+ );
1521
+ }
1522
+ return probe;
1523
+ };
1524
+
1525
+ // src/localHost.ts
1526
+ import * as NodePath8 from "node:path";
1527
+
540
1528
  // src/agentDevice.ts
541
- import * as NodeCrypto from "node:crypto";
542
- import * as NodeFSP from "node:fs/promises";
543
- import * as NodePath from "node:path";
1529
+ import * as NodeCrypto2 from "node:crypto";
1530
+ import * as NodeFSP4 from "node:fs/promises";
1531
+ import * as NodePath4 from "node:path";
544
1532
  var DAEMON_READY_TIMEOUT_MS = 3e4;
545
1533
  var DAEMON_POLL_MS = 100;
546
- var key = (value) => NodeCrypto.createHash("sha256").update(value).digest("hex").slice(0, 24);
547
- var agentDeviceConfigPath = (dataDir, hostId) => NodePath.join(dataDir, "hosts", `${key(hostId)}.json`);
1534
+ var key = (value) => NodeCrypto2.createHash("sha256").update(value).digest("hex").slice(0, 24);
1535
+ var agentDeviceConfigPath = (dataDir, hostId) => NodePath4.join(dataDir, "hosts", `${key(hostId)}.json`);
548
1536
  var agentDeviceSession = (threadId, hostId, deviceId) => `ycodium-${key(JSON.stringify([threadId, hostId, deviceId]))}`;
549
1537
  var agentDeviceTargetArgs = (device) => device.platform === "ios" ? ["--platform", "ios", "--udid", device.id] : ["--platform", "android", "--serial", device.id];
550
1538
  var writeAgentDeviceConfig = async (file, endpoint) => {
551
- await NodeFSP.mkdir(NodePath.dirname(file), { recursive: true });
1539
+ await NodeFSP4.mkdir(NodePath4.dirname(file), { recursive: true });
552
1540
  const content = JSON.stringify({
553
1541
  daemonBaseUrl: endpoint.baseUrl,
554
1542
  daemonAuthToken: endpoint.token
555
1543
  });
556
- const existing2 = await NodeFSP.readFile(file, "utf8").catch(() => "");
1544
+ const existing2 = await NodeFSP4.readFile(file, "utf8").catch(() => "");
557
1545
  if (existing2 === content) return;
558
- const temporary = NodePath.join(
559
- NodePath.dirname(file),
560
- `.endpoint-${NodeCrypto.randomUUID()}.json`
1546
+ const temporary = NodePath4.join(
1547
+ NodePath4.dirname(file),
1548
+ `.endpoint-${NodeCrypto2.randomUUID()}.json`
561
1549
  );
562
1550
  try {
563
- await NodeFSP.writeFile(temporary, content, { mode: 384 });
564
- await NodeFSP.rename(temporary, file);
1551
+ await NodeFSP4.writeFile(temporary, content, { mode: 384 });
1552
+ await NodeFSP4.rename(temporary, file);
565
1553
  } finally {
566
- await NodeFSP.rm(temporary, { force: true }).catch(() => void 0);
1554
+ await NodeFSP4.rm(temporary, { force: true }).catch(() => void 0);
567
1555
  }
568
1556
  };
569
1557
  var LAUNCHER_DIR = "bin";
570
1558
  var ensureAgentDeviceLauncher = async (input) => {
571
- const directory = NodePath.join(input.dataDir, LAUNCHER_DIR);
572
- await NodeFSP.mkdir(directory, { recursive: true });
573
- const launcherPath = NodePath.join(directory, "agent-device-launcher.mjs");
574
- await NodeFSP.writeFile(
1559
+ const directory = NodePath4.join(input.dataDir, LAUNCHER_DIR);
1560
+ await NodeFSP4.mkdir(directory, { recursive: true });
1561
+ const launcherPath = NodePath4.join(directory, "agent-device-launcher.mjs");
1562
+ await NodeFSP4.writeFile(
575
1563
  launcherPath,
576
1564
  `import { spawn } from "node:child_process";
577
1565
  const args = process.argv.slice(2);
@@ -589,15 +1577,16 @@ const env = { ...process.env };
589
1577
  delete env.AGENT_DEVICE_DAEMON_BASE_URL;
590
1578
  delete env.AGENT_DEVICE_DAEMON_AUTH_TOKEN;
591
1579
  delete env.AGENT_DEVICE_CONFIG;
592
- const child = spawn(${JSON.stringify(input.nodePath)}, [${JSON.stringify(input.entryPath)}, ...args], { stdio: "inherit", env, windowsHide: true });
1580
+ const prefix = ${JSON.stringify(input.helperPath ? [input.helperPath, "node", ...input.sdkRoot ? ["--sdk", input.sdkRoot] : [], "--", input.entryPath] : [input.entryPath])};
1581
+ const child = spawn(${JSON.stringify(input.nodePath)}, [...prefix, ...args], { stdio: "inherit", env, windowsHide: true });
593
1582
  child.on("error", error => { console.error(error.message); process.exitCode = 1; });
594
1583
  child.on("exit", code => { process.exitCode = code ?? 1; });
595
1584
  `
596
1585
  );
597
1586
  if (input.platform === "win32") {
598
- const command2 = NodePath.join(directory, "agent-device.cmd");
1587
+ const command2 = NodePath4.join(directory, "agent-device.cmd");
599
1588
  const electron2 = input.electron ? "set ELECTRON_RUN_AS_NODE=1\r\n" : "";
600
- await NodeFSP.writeFile(
1589
+ await NodeFSP4.writeFile(
601
1590
  command2,
602
1591
  `@echo off\r
603
1592
  ${electron2}"${input.nodePath}" "${launcherPath}" %*\r
@@ -606,15 +1595,15 @@ ${electron2}"${input.nodePath}" "${launcherPath}" %*\r
606
1595
  return command2;
607
1596
  }
608
1597
  const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
609
- const command = NodePath.join(directory, "agent-device");
1598
+ const command = NodePath4.join(directory, "agent-device");
610
1599
  const electron = input.electron ? "ELECTRON_RUN_AS_NODE=1 " : "";
611
- await NodeFSP.writeFile(
1600
+ await NodeFSP4.writeFile(
612
1601
  command,
613
1602
  `#!/bin/sh
614
1603
  ${electron}exec ${quote(input.nodePath)} ${quote(launcherPath)} "$@"
615
1604
  `
616
1605
  );
617
- await NodeFSP.chmod(command, 493);
1606
+ await NodeFSP4.chmod(command, 493);
618
1607
  return command;
619
1608
  };
620
1609
  var parseDaemonFile = (raw) => {
@@ -634,7 +1623,7 @@ var parseDaemonFile = (raw) => {
634
1623
  version: typeof record["version"] === "string" ? record["version"] : void 0
635
1624
  };
636
1625
  };
637
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1626
+ var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
638
1627
  var daemonEnvironment = (stateDir) => ({
639
1628
  AGENT_DEVICE_STATE_DIR: stateDir,
640
1629
  AGENT_DEVICE_DAEMON_SERVER_MODE: "http",
@@ -654,10 +1643,10 @@ var AgentDaemon = class {
654
1643
  this.sleep = options.sleep ?? defaultSleep;
655
1644
  }
656
1645
  get daemonFilePath() {
657
- return NodePath.join(this.options.stateDir, "daemon.json");
1646
+ return NodePath4.join(this.options.stateDir, "daemon.json");
658
1647
  }
659
1648
  async readDaemonFile() {
660
- const raw = await NodeFSP.readFile(this.daemonFilePath, "utf8").catch(() => null);
1649
+ const raw = await NodeFSP4.readFile(this.daemonFilePath, "utf8").catch(() => null);
661
1650
  return raw === null ? null : parseDaemonFile(raw);
662
1651
  }
663
1652
  async healthy(port) {
@@ -686,11 +1675,11 @@ var AgentDaemon = class {
686
1675
  */
687
1676
  async ensure(input) {
688
1677
  this.entryPath = input.entryPath;
689
- await NodeFSP.mkdir(this.options.stateDir, { recursive: true });
1678
+ await NodeFSP4.mkdir(this.options.stateDir, { recursive: true });
690
1679
  const existing2 = await this.readDaemonFile();
691
1680
  if (existing2 !== null) {
692
1681
  if (await this.healthy(existing2.httpPort)) return this.toEndpoint(existing2, input.entryPath);
693
- await NodeFSP.rm(this.daemonFilePath, { force: true }).catch(() => void 0);
1682
+ await NodeFSP4.rm(this.daemonFilePath, { force: true }).catch(() => void 0);
694
1683
  }
695
1684
  const args = [this.options.helperPath, "node"];
696
1685
  if (input.sdkRoot !== null) args.push("--sdk", input.sdkRoot);
@@ -752,8 +1741,8 @@ var agentDeviceQuickStart = (device, targetArgs, command) => {
752
1741
  };
753
1742
 
754
1743
  // src/hubProcess.ts
755
- import * as NodeFSP2 from "node:fs/promises";
756
- import * as NodePath2 from "node:path";
1744
+ import * as NodeFSP5 from "node:fs/promises";
1745
+ import * as NodePath5 from "node:path";
757
1746
 
758
1747
  // ../../packages/ycodium-fetch-ports/src/fetchPorts.ts
759
1748
  import * as NodeNet from "node:net";
@@ -844,12 +1833,12 @@ var FETCH_BLOCKED_PORTS = /* @__PURE__ */ new Set([
844
1833
  var isFetchSafePort = (port) => !FETCH_BLOCKED_PORTS.has(port);
845
1834
  var MAX_SAFE_PORT_ATTEMPTS = 50;
846
1835
  var systemAssigned = () => 0;
847
- var listenOnce = (server, host, port) => new Promise((resolve, reject) => {
1836
+ var listenOnce = (server, host, port) => new Promise((resolve3, reject) => {
848
1837
  server.once("error", reject);
849
1838
  server.listen(port, host, () => {
850
1839
  server.off("error", reject);
851
1840
  const address = server.address();
852
- resolve(typeof address === "object" && address !== null ? address.port : 0);
1841
+ resolve3(typeof address === "object" && address !== null ? address.port : 0);
853
1842
  });
854
1843
  });
855
1844
  var listenOnSafePort = async (server, options = {}) => {
@@ -858,7 +1847,7 @@ var listenOnSafePort = async (server, options = {}) => {
858
1847
  for (let attempt = 0; attempt < MAX_SAFE_PORT_ATTEMPTS; attempt++) {
859
1848
  const port = await listenOnce(server, host, portForAttempt(attempt));
860
1849
  if (isFetchSafePort(port)) return port;
861
- await new Promise((resolve) => server.close(() => resolve()));
1850
+ await new Promise((resolve3) => server.close(() => resolve3()));
862
1851
  }
863
1852
  throw new Error(`\u8FDE\u7EED ${String(MAX_SAFE_PORT_ATTEMPTS)} \u6B21\u5206\u5230 fetch \u7684\u574F\u7AEF\u53E3`);
864
1853
  };
@@ -866,7 +1855,7 @@ var reserveFetchSafePort = async (options = {}) => {
866
1855
  const probe = NodeNet.createServer();
867
1856
  try {
868
1857
  const port = await listenOnSafePort(probe, options);
869
- await new Promise((resolve) => probe.close(() => resolve()));
1858
+ await new Promise((resolve3) => probe.close(() => resolve3()));
870
1859
  if (port <= 0) throw new Error("\u65E0\u6CD5\u53D6\u5F97\u76D1\u542C\u7AEF\u53E3");
871
1860
  return port;
872
1861
  } catch (cause) {
@@ -900,7 +1889,7 @@ var nodeProcessProbe = {
900
1889
  }
901
1890
  }
902
1891
  };
903
- var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1892
+ var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
904
1893
  var parseBreakawayWarning = (line) => {
905
1894
  try {
906
1895
  const record = JSON.parse(line);
@@ -953,7 +1942,7 @@ var HubProcess = class {
953
1942
  this.sleep = options.sleep ?? defaultSleep2;
954
1943
  }
955
1944
  get statePath() {
956
- return NodePath2.join(this.options.stateDir, "hub.json");
1945
+ return NodePath5.join(this.options.stateDir, "hub.json");
957
1946
  }
958
1947
  get running() {
959
1948
  return this.current?.hub ?? null;
@@ -974,7 +1963,7 @@ var HubProcess = class {
974
1963
  let previous = null;
975
1964
  try {
976
1965
  const parsed = JSON.parse(
977
- await NodeFSP2.readFile(this.statePath, "utf8")
1966
+ await NodeFSP5.readFile(this.statePath, "utf8")
978
1967
  );
979
1968
  if (Number.isSafeInteger(parsed.pid) && Number.isSafeInteger(parsed.port) && typeof parsed.entryPath === "string") {
980
1969
  previous = parsed;
@@ -991,18 +1980,18 @@ var HubProcess = class {
991
1980
  this.probe.terminate(previous.pid);
992
1981
  }
993
1982
  }
994
- await NodeFSP2.rm(this.statePath, { force: true }).catch(() => void 0);
1983
+ await NodeFSP5.rm(this.statePath, { force: true }).catch(() => void 0);
995
1984
  }
996
1985
  async spawnOnce(launch) {
997
1986
  await this.reapStale();
998
- await NodeFSP2.mkdir(this.options.stateDir, { recursive: true });
1987
+ await NodeFSP5.mkdir(this.options.stateDir, { recursive: true });
999
1988
  const port = await (this.options.reservePort ?? (() => reserveFetchSafePort()))();
1000
1989
  const origin = `http://127.0.0.1:${String(port)}`;
1001
1990
  const args = [
1002
1991
  this.options.helperPath,
1003
1992
  "node",
1004
1993
  "--shim",
1005
- NodePath2.join(this.options.stateDir, "emulator-shim")
1994
+ NodePath5.join(this.options.stateDir, "emulator-shim")
1006
1995
  ];
1007
1996
  if (launch.sdkRoot !== null) args.push("--sdk", launch.sdkRoot);
1008
1997
  args.push(
@@ -1053,7 +2042,7 @@ ${tail}` : ""}`
1053
2042
  }
1054
2043
  const hub = { origin, port, pid: handle.pid, startedAt: this.now() };
1055
2044
  const record = { pid: handle.pid, port, entryPath: launch.entryPath };
1056
- await NodeFSP2.writeFile(this.statePath, JSON.stringify(record)).catch(() => void 0);
2045
+ await NodeFSP5.writeFile(this.statePath, JSON.stringify(record)).catch(() => void 0);
1057
2046
  this.current = { hub, handle };
1058
2047
  this.options.log.info("Device hub started", { pid: handle.pid, port });
1059
2048
  this.options.onStarted?.(hub);
@@ -1090,115 +2079,21 @@ ${tail}` : ""}`
1090
2079
  const current = this.current;
1091
2080
  this.current = null;
1092
2081
  if (current !== null) await current.handle.kill().catch(() => void 0);
1093
- await NodeFSP2.rm(this.statePath, { force: true }).catch(() => void 0);
2082
+ await NodeFSP5.rm(this.statePath, { force: true }).catch(() => void 0);
1094
2083
  }
1095
2084
  };
1096
2085
 
1097
- // src/toolchain.ts
1098
- import * as NodeFSP4 from "node:fs/promises";
1099
- import * as NodePath4 from "node:path";
1100
-
1101
- // src/npmRuntime.ts
1102
- import * as NodeCrypto2 from "node:crypto";
1103
- import * as NodeFSP3 from "node:fs/promises";
1104
- import * as NodePath3 from "node:path";
1105
- var MANAGED_NPM_VERSION = "11.21.0";
1106
- var MANAGED_NPM_INTEGRITY = "sha512-Zov8KhamNneiLdELtj5YALtNmJW4L4fCLTzjfpzXG2w6MSHcf0UxgdlK5uloCuksWT+7mGUU7wi79cO6RqivPg==";
1107
- var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1108
- var managedNpmPaths = (dataDir, version = MANAGED_NPM_VERSION) => {
1109
- const installDir = NodePath3.join(dataDir, "npm", version);
1110
- return {
1111
- installDir,
1112
- cliPath: NodePath3.join(installDir, "bin", "npm-cli.js"),
1113
- sentinelPath: NodePath3.join(installDir, ".install-complete"),
1114
- shimDir: NodePath3.join(dataDir, "npm", "node-shim")
1115
- };
1116
- };
1117
- var integrityMatches = (bytes, integrity) => {
1118
- const [algorithm, expected] = integrity.split("-", 2);
1119
- if (algorithm !== "sha512" || expected === void 0) return false;
1120
- return NodeCrypto2.createHash("sha512").update(bytes).digest("base64") === expected;
1121
- };
1122
- var fileExists = (file) => NodeFSP3.access(file).then(
1123
- () => true,
1124
- () => false
1125
- );
1126
- var failed = (step, detail) => new DeviceError(
1127
- "install-failed",
1128
- `Preparing npm ${MANAGED_NPM_VERSION} failed while ${step}.${detail ? ` ${detail}` : ""}`
1129
- );
1130
- var ensureManagedNpm = async (input) => {
1131
- const paths = managedNpmPaths(input.dataDir);
1132
- const sentinel = await NodeFSP3.readFile(paths.sentinelPath, "utf8").catch(() => null);
1133
- if (sentinel?.trim() === MANAGED_NPM_VERSION && await fileExists(paths.cliPath)) return paths;
1134
- const registry = (input.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
1135
- const url2 = `${registry}/npm/-/npm-${MANAGED_NPM_VERSION}.tgz`;
1136
- let bytes;
1137
- try {
1138
- const response = await input.fetch(url2, { signal: AbortSignal.timeout(5 * 6e4) });
1139
- if (!response.ok)
1140
- throw failed("downloading", `The registry answered ${String(response.status)}.`);
1141
- bytes = new Uint8Array(await response.arrayBuffer());
1142
- } catch (cause) {
1143
- if (cause instanceof DeviceError) throw cause;
1144
- throw failed("downloading", errorDetail(cause));
1145
- }
1146
- if (!integrityMatches(bytes, MANAGED_NPM_INTEGRITY)) {
1147
- throw failed("verifying the download", "The archive does not match the pinned checksum.");
1148
- }
1149
- const parent = NodePath3.dirname(paths.installDir);
1150
- await NodeFSP3.mkdir(parent, { recursive: true });
1151
- const staging = await NodeFSP3.mkdtemp(NodePath3.join(parent, ".staging-"));
1152
- const archive = NodePath3.join(
1153
- parent,
1154
- `.npm-${MANAGED_NPM_VERSION}-${NodeCrypto2.randomUUID()}.tgz`
1155
- );
1156
- try {
1157
- await NodeFSP3.writeFile(archive, bytes);
1158
- const result = await input.process.run({
1159
- program: "host:node",
1160
- args: [input.helperPath, "extract", "--", archive, staging],
1161
- cwd: input.dataDir,
1162
- timeoutMs: 12e4
1163
- });
1164
- if (result.exitCode !== 0) throw failed("unpacking", result.stderr.trim());
1165
- if (!await fileExists(NodePath3.join(staging, "bin", "npm-cli.js"))) {
1166
- throw failed("unpacking", "The archive has no bin/npm-cli.js.");
1167
- }
1168
- await NodeFSP3.writeFile(
1169
- NodePath3.join(staging, ".install-complete"),
1170
- `${MANAGED_NPM_VERSION}
1171
- `
1172
- );
1173
- await NodeFSP3.rm(paths.installDir, { recursive: true, force: true });
1174
- await NodeFSP3.rename(staging, paths.installDir);
1175
- return paths;
1176
- } finally {
1177
- await NodeFSP3.rm(staging, { recursive: true, force: true }).catch(() => void 0);
1178
- await NodeFSP3.rm(archive, { force: true }).catch(() => void 0);
1179
- }
1180
- };
1181
- var runManagedNpm = (input) => input.process.run({
1182
- program: "host:node",
1183
- args: [
1184
- input.helperPath,
1185
- "npm",
1186
- "--shim",
1187
- input.npm.shimDir,
1188
- "--",
1189
- input.npm.cliPath,
1190
- ...input.args
1191
- ],
1192
- cwd: input.cwd,
1193
- timeoutMs: input.timeoutMs,
1194
- maxOutputBytes: 256 * 1024
1195
- });
2086
+ // src/serveSimHelpers.ts
2087
+ import * as NodeFSP7 from "node:fs/promises";
2088
+ import * as NodePath7 from "node:path";
1196
2089
 
1197
2090
  // src/toolchain.ts
2091
+ import * as NodeFSP6 from "node:fs/promises";
2092
+ import * as NodePath6 from "node:path";
2093
+ import * as NodeOS from "node:os";
1198
2094
  var DEVICE_HUB_VERSION = "0.12.0";
1199
2095
  var AGENT_DEVICE_VERSION = "0.21.12";
1200
- var INSTALL_TIMEOUT_MS = 10 * 6e4;
1201
- var VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.-]+)?$/;
2096
+ var runtimeTarget = `${NodeOS.platform()}-${NodeOS.arch()}`;
1202
2097
  var HUB_SPEC = {
1203
2098
  tool: "hub",
1204
2099
  packageName: "expo-device-hub",
@@ -1212,358 +2107,57 @@ var AGENT_DEVICE_SPEC = {
1212
2107
  entry: ["bin", "agent-device.mjs"]
1213
2108
  };
1214
2109
  var toolSpec = (tool) => tool === "hub" ? HUB_SPEC : AGENT_DEVICE_SPEC;
1215
- var toolsRoot = (dataDir) => NodePath4.join(dataDir, "tools");
1216
- var toolPaths = (dataDir, spec, version = spec.version) => {
1217
- const installDir = NodePath4.join(toolsRoot(dataDir), spec.packageName, version);
2110
+ var bundledToolPaths = (pluginDir, spec, runtimeDir) => {
2111
+ const installDir = runtimeDir ?? NodePath6.join(pluginDir, "runtime", runtimeTarget);
1218
2112
  return {
1219
2113
  installDir,
1220
- entryPath: NodePath4.join(installDir, "node_modules", spec.packageName, ...spec.entry),
1221
- sentinelPath: NodePath4.join(installDir, ".install-complete")
2114
+ entryPath: NodePath6.join(installDir, "node_modules", spec.packageName, ...spec.entry),
2115
+ sentinelPath: NodePath6.join(installDir, "node_modules", spec.packageName, "package.json")
1222
2116
  };
1223
2117
  };
1224
- var agentDeviceStateDir = (dataDir) => NodePath4.join(dataDir, "agent-device");
1225
- var fileExists2 = async (file) => {
2118
+ var agentDeviceStateDir = (dataDir) => NodePath6.join(dataDir, "agent-device");
2119
+ var isToolInstalled = async (pluginDir, spec, runtimeDir) => {
2120
+ const paths = bundledToolPaths(pluginDir, spec, runtimeDir);
1226
2121
  try {
1227
- await NodeFSP4.access(file);
2122
+ const runtime = JSON.parse(
2123
+ await NodeFSP6.readFile(NodePath6.join(paths.installDir, "manifest.json"), "utf8")
2124
+ );
2125
+ if (typeof runtime !== "object" || runtime === null || !("schemaVersion" in runtime) || runtime.schemaVersion !== 1 || !("target" in runtime) || runtime.target !== runtimeTarget)
2126
+ return false;
2127
+ await NodeFSP6.access(NodePath6.join(paths.installDir, "inventory.json"));
2128
+ const manifest = JSON.parse(await NodeFSP6.readFile(paths.sentinelPath, "utf8"));
2129
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest) || manifest.version !== spec.version)
2130
+ return false;
2131
+ await NodeFSP6.access(paths.entryPath);
1228
2132
  return true;
1229
2133
  } catch {
1230
2134
  return false;
1231
2135
  }
1232
2136
  };
1233
- var readSentinel = async (file) => {
1234
- try {
1235
- return (await NodeFSP4.readFile(file, "utf8")).trim();
1236
- } catch {
1237
- return null;
1238
- }
1239
- };
1240
- var isInstalled = async (paths, version) => await readSentinel(paths.sentinelPath) === version && await fileExists2(paths.entryPath);
1241
- var isToolInstalled = (dataDir, spec) => isInstalled(toolPaths(dataDir, spec), spec.version);
1242
- var installQueue = Promise.resolve();
1243
- var serialized = (task) => {
1244
- const next = installQueue.then(task, task);
1245
- installQueue = next.catch(() => void 0);
1246
- return next;
1247
- };
1248
- var installFailed = (spec, step, detail, cause) => new DeviceError(
1249
- "install-failed",
1250
- `Installing ${spec.packageName} ${spec.version} failed while ${step}.${detail ? ` ${detail}` : ""}`,
1251
- cause === void 0 ? void 0 : { cause }
1252
- );
1253
- var ensureTool = (input) => serialized(async () => {
1254
- const { dataDir, spec } = input;
1255
- const paths = toolPaths(dataDir, spec);
1256
- if (await isInstalled(paths, spec.version)) return paths;
1257
- const parentDir = NodePath4.dirname(paths.installDir);
1258
- await NodeFSP4.rm(paths.installDir, { recursive: true, force: true });
1259
- await NodeFSP4.mkdir(parentDir, { recursive: true });
1260
- const npm = await ensureManagedNpm({
1261
- dataDir,
1262
- helperPath: input.helperPath,
1263
- process: input.process,
1264
- fetch: input.fetch
1265
- });
1266
- const stagingDir = await NodeFSP4.mkdtemp(NodePath4.join(parentDir, ".staging-"));
1267
- try {
1268
- const result = await runManagedNpm({
1269
- helperPath: input.helperPath,
1270
- process: input.process,
1271
- npm,
1272
- args: [
1273
- "install",
1274
- "--prefix",
1275
- stagingDir,
1276
- "--no-fund",
1277
- "--no-audit",
1278
- `${spec.packageName}@${spec.version}`
1279
- ],
1280
- cwd: dataDir,
1281
- timeoutMs: INSTALL_TIMEOUT_MS
1282
- }).catch((error) => {
1283
- throw installFailed(spec, "running npm install", errorDetail(error), error);
1284
- });
1285
- if (result.timedOut) throw installFailed(spec, "running npm install", "npm timed out.");
1286
- if (result.exitCode !== 0) {
1287
- throw installFailed(
1288
- spec,
1289
- "running npm install",
1290
- `npm exited with code ${String(result.exitCode)}.`
1291
- );
1292
- }
1293
- const stagedEntry = NodePath4.join(
1294
- stagingDir,
1295
- "node_modules",
1296
- spec.packageName,
1297
- ...spec.entry
1298
- );
1299
- if (!await fileExists2(stagedEntry)) {
1300
- throw installFailed(spec, "verifying the installed entry point");
1301
- }
1302
- await NodeFSP4.writeFile(NodePath4.join(stagingDir, ".install-complete"), `${spec.version}
1303
- `);
1304
- try {
1305
- await NodeFSP4.rename(stagingDir, paths.installDir);
1306
- } catch (error) {
1307
- if (!await isInstalled(paths, spec.version)) {
1308
- throw installFailed(spec, "publishing the install", errorDetail(error), error);
1309
- }
1310
- }
1311
- return paths;
1312
- } finally {
1313
- await NodeFSP4.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1314
- }
1315
- });
1316
- var inspectTool = async (dataDir, spec, runningVersion) => {
1317
- const directory = NodePath4.join(toolsRoot(dataDir), spec.packageName);
1318
- let names = [];
1319
- try {
1320
- names = await NodeFSP4.readdir(directory);
1321
- } catch {
1322
- names = [];
1323
- }
1324
- const completed = [];
1325
- for (const version of names) {
1326
- if (!VERSION_PATTERN.test(version)) continue;
1327
- if (await isInstalled(toolPaths(dataDir, spec, version), version)) completed.push(version);
1328
- }
1329
- completed.sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
1330
- return { requiredVersion: spec.version, installedVersions: completed, runningVersion };
1331
- };
1332
-
1333
- // src/maintenance.ts
1334
- var TOOL_MAINTENANCE_SCRIPT = String.raw`
1335
- const maintenanceFs = require('node:fs');
1336
- const maintenancePath = require('node:path');
1337
- const maintenanceAlive = pid => {
1338
- try { process.kill(pid, 0); return true; }
1339
- catch (error) { return error.code !== 'ESRCH'; }
1340
- };
1341
- async function withToolMaintenance(root, operation) {
1342
- maintenanceFs.mkdirSync(root, { recursive: true });
1343
- const lock = maintenancePath.join(root, '.maintenance-lock');
1344
- const nonce = require('node:crypto').randomUUID();
1345
- const ownerFile = process.pid + '.' + nonce + '.json';
1346
- const candidate = lock + '.' + nonce;
1347
- const deadline = Date.now() + 30000;
1348
- const removeEmptyLock = () => {
1349
- try { maintenanceFs.rmdirSync(lock); }
1350
- catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST', 'EPERM'].includes(error.code)) throw error; }
1351
- };
1352
- maintenanceFs.mkdirSync(candidate);
1353
- try {
1354
- maintenanceFs.writeFileSync(maintenancePath.join(candidate, ownerFile), JSON.stringify({ pid: process.pid }));
1355
- while (true) {
1356
- try {
1357
- maintenanceFs.renameSync(candidate, lock);
1358
- break;
1359
- }
1360
- catch (error) {
1361
- if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
1362
- let files = [];
1363
- try { files = maintenanceFs.readdirSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
1364
- if (files.length === 1) {
1365
- const previousFile = maintenancePath.join(lock, files[0]);
1366
- let previous;
1367
- try { previous = JSON.parse(maintenanceFs.readFileSync(previousFile, 'utf8')); } catch {}
1368
- if (Number.isSafeInteger(previous?.pid) && previous.pid > 0 && !maintenanceAlive(previous.pid)) {
1369
- try { maintenanceFs.unlinkSync(previousFile); } catch (error) { if (error.code !== 'ENOENT') throw error; }
1370
- }
1371
- }
1372
- removeEmptyLock();
1373
- if (Date.now() >= deadline) throw Error('Device tool maintenance is locked. Retry when the other operation finishes.');
1374
- await new Promise(resolve => setTimeout(resolve, 50));
1375
- }
1376
- }
1377
- try { return operation(); }
1378
- finally {
1379
- maintenanceFs.unlinkSync(maintenancePath.join(lock, ownerFile));
1380
- removeEmptyLock();
1381
- }
1382
- } finally {
1383
- maintenanceFs.rmSync(candidate, { recursive: true, force: true });
1384
- }
1385
- }
1386
- function pruneTools(root, specs) {
1387
- return withToolMaintenance(root, () => {
1388
- const scan = process.platform === 'win32'
1389
- ? require('node:child_process').spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-CimInstance Win32_Process | Select-Object -ExpandProperty CommandLine'], { encoding: 'utf8', timeout: 10000, windowsHide: true })
1390
- : require('node:child_process').spawnSync('ps', ['-ax', '-o', 'command='], { encoding: 'utf8', timeout: 10000 });
1391
- if (scan.status !== 0 || !scan.stdout) return;
1392
- for (const [name, required] of specs) {
1393
- const parent = maintenancePath.join(root, name);
1394
- let names;
1395
- try { names = maintenanceFs.readdirSync(parent); } catch { continue; }
1396
- const completed = [];
1397
- for (const version of names) {
1398
- if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.-]+)?$/.test(version)) continue;
1399
- const directory = maintenancePath.join(parent, version);
1400
- try {
1401
- if (!maintenanceFs.lstatSync(directory).isDirectory()) continue;
1402
- if (maintenanceFs.readFileSync(maintenancePath.join(directory, '.install-complete'), 'utf8').trim() !== version) continue;
1403
- completed.push({ version, directory, modified: maintenanceFs.statSync(maintenancePath.join(directory, '.install-complete')).mtimeMs });
1404
- } catch {}
1405
- }
1406
- if (!completed.some(value => value.version === required)) continue;
1407
- const previous = completed.filter(value => value.version !== required).sort((a, b) => b.modified - a.modified || b.version.localeCompare(a.version, 'en', { numeric: true }))[0]?.version;
1408
- for (const { version, directory } of completed) {
1409
- if (version === required || version === previous || scan.stdout.includes(directory + maintenancePath.sep)) continue;
1410
- maintenanceFs.rmSync(directory, { recursive: true, force: true });
1411
- }
1412
- }
1413
- });
1414
- }
1415
- `;
1416
- var pruneTool = async (input) => {
1417
- const call = `pruneTools(${JSON.stringify(toolsRoot(input.dataDir))}, ${JSON.stringify([
1418
- [input.spec.packageName, input.spec.version]
1419
- ])})`;
1420
- const result = await input.process.run({
1421
- program: "host:node",
1422
- args: [
1423
- "-e",
1424
- `${TOOL_MAINTENANCE_SCRIPT}
1425
- ${call}.catch(error => { console.error(error.message); process.exitCode = 1; });`
1426
- ],
1427
- cwd: input.dataDir,
1428
- timeoutMs: 6e4
1429
- });
1430
- return { ok: result.exitCode === 0, detail: result.stderr.trim() };
1431
- };
1432
-
1433
- // src/probe.ts
1434
- var MIN_NODE_VERSION = [22, 12, 0];
1435
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1436
- var parseMachineProbe = (stdout) => {
1437
- const line = stdout.trim().split(/\r?\n/).at(-1) ?? "";
1438
- let value;
1439
- try {
1440
- value = JSON.parse(line);
1441
- } catch {
1442
- return null;
1443
- }
1444
- if (!isRecord2(value) || !isRecord2(value["android"])) return null;
1445
- const android = value["android"];
1446
- if (typeof value["node"] !== "string" || typeof value["execPath"] !== "string" || typeof value["platform"] !== "string") {
1447
- return null;
1448
- }
1449
- return {
1450
- node: value["node"],
1451
- execPath: value["execPath"],
1452
- electron: typeof value["electron"] === "string" ? value["electron"] : null,
1453
- platform: value["platform"],
1454
- xcrun: value["xcrun"] === true,
1455
- android: {
1456
- root: typeof android["root"] === "string" ? android["root"] : null,
1457
- adb: android["adb"] === true,
1458
- emulator: android["emulator"] === true,
1459
- avdmanager: android["avdmanager"] === true,
1460
- legacyAvdmanager: android["legacyAvdmanager"] === true
1461
- }
1462
- };
1463
- };
1464
- var nodeVersionSupported = (version) => {
1465
- const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
1466
- if (!match) return false;
1467
- const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
1468
- for (let index = 0; index < MIN_NODE_VERSION.length; index += 1) {
1469
- const actual = parts[index];
1470
- const required = MIN_NODE_VERSION[index];
1471
- if (actual !== required) return actual > required;
1472
- }
1473
- return true;
1474
- };
1475
- var unavailable = (platform, reasonCode, reason, sdkRoot) => ({
1476
- platform,
1477
- available: false,
1478
- reasonCode,
1479
- reason,
1480
- ...sdkRoot === void 0 ? {} : { sdkRoot }
1481
- });
1482
- var platformAvailability = (probe, platform) => {
1483
- if (platform === "ios") {
1484
- if (probe.platform !== "darwin") {
1485
- return unavailable("ios", "ios-needs-macos", "iOS Simulators need macOS with Xcode.");
1486
- }
1487
- if (!probe.xcrun) {
1488
- return unavailable("ios", "xcode-missing", "Xcode command line tools were not found.");
1489
- }
1490
- return { platform, available: true };
1491
- }
1492
- const sdk = probe.android;
1493
- if (!sdk.root) {
1494
- return unavailable(
1495
- "android",
1496
- "android-sdk-missing",
1497
- "Android SDK was not found. Install it with Android Studio or set its location in the Devices settings."
1498
- );
1499
- }
1500
- if (!sdk.adb) {
1501
- return unavailable(
1502
- "android",
1503
- "android-platform-tools-missing",
1504
- `Android SDK Platform-Tools are missing from ${sdk.root}. Install them in Android Studio's SDK Manager.`,
1505
- sdk.root
1506
- );
1507
- }
1508
- if (!sdk.emulator) {
1509
- return unavailable(
1510
- "android",
1511
- "android-emulator-missing",
1512
- `Android Emulator is missing from ${sdk.root}. Install it in Android Studio's SDK Manager.`,
1513
- sdk.root
1514
- );
1515
- }
1516
- if (!sdk.avdmanager) {
1517
- return sdk.legacyAvdmanager ? unavailable(
1518
- "android",
1519
- "android-cmdline-tools-legacy",
1520
- `The Android SDK command-line tools in ${sdk.root} appear to be an older, unsupported version. Install Android SDK Command-line Tools (latest) in Android Studio's SDK Manager under SDK Tools.`,
1521
- sdk.root
1522
- ) : unavailable(
1523
- "android",
1524
- "android-cmdline-tools-missing",
1525
- `Android SDK Command-line Tools (latest) are missing from ${sdk.root}. Install them in Android Studio's SDK Manager.`,
1526
- sdk.root
1527
- );
1528
- }
1529
- return { platform, available: true };
1530
- };
1531
- var probeMachine = async (input) => {
1532
- const args = [input.helperPath, "probe"];
1533
- if (input.sdkOverride.trim().length > 0) args.push("--sdk", input.sdkOverride.trim());
1534
- const result = await input.process.run({ program: "host:node", args, cwd: input.cwd, timeoutMs: 3e4 }).catch((error) => {
2137
+ var ensureTool = async (input) => {
2138
+ const pluginDir = input.pluginDir ?? NodePath6.dirname(NodePath6.dirname(input.helperPath));
2139
+ if (!await isToolInstalled(pluginDir, input.spec, input.runtimeDir)) {
1535
2140
  throw new DeviceError(
1536
- "node-unavailable",
1537
- `Could not run Node.js to check device support: ${errorDetail(error)}`,
1538
- { cause: error }
1539
- );
1540
- });
1541
- const probe = result.exitCode === 0 ? parseMachineProbe(result.stdout) : null;
1542
- if (!probe) {
1543
- throw new DeviceError(
1544
- "node-unavailable",
1545
- `Checking device support failed: ${result.stderr.trim() || "the helper returned no result"}.`
1546
- );
1547
- }
1548
- if (!nodeVersionSupported(probe.node)) {
1549
- throw new DeviceError(
1550
- "node-unavailable",
1551
- `Device support requires Node.js 22.12 or newer; this Ycodium runs on ${probe.node}. Update Ycodium on this machine.`
2141
+ "install-failed",
2142
+ `Bundled ${input.spec.packageName} ${input.spec.version} is missing or incomplete. Repair or update Ycodium on this machine.`
1552
2143
  );
1553
2144
  }
1554
- return probe;
2145
+ return bundledToolPaths(pluginDir, input.spec, input.runtimeDir);
1555
2146
  };
2147
+ var inspectTool = async (pluginDir, spec, runningVersion, runtimeDir) => ({
2148
+ requiredVersion: spec.version,
2149
+ installedVersions: await isToolInstalled(pluginDir, spec, runtimeDir) ? [spec.version] : [],
2150
+ runningVersion
2151
+ });
1556
2152
 
1557
2153
  // src/serveSimHelpers.ts
1558
- import * as NodeFSP5 from "node:fs/promises";
1559
- import * as NodePath5 from "node:path";
1560
- var existing = (file) => NodeFSP5.access(file).then(
2154
+ var existing = (file) => NodeFSP7.access(file).then(
1561
2155
  () => file,
1562
2156
  () => null
1563
2157
  );
1564
- var serveSimHelpers = async (dataDir) => {
1565
- const dist = NodePath5.join(
1566
- toolPaths(dataDir, HUB_SPEC).installDir,
2158
+ var serveSimHelpers = async (dataDir, runtimeDir) => {
2159
+ const dist = NodePath7.join(
2160
+ bundledToolPaths(dataDir, HUB_SPEC, runtimeDir).installDir,
1567
2161
  "node_modules",
1568
2162
  HUB_SPEC.packageName,
1569
2163
  "vendor",
@@ -1571,8 +2165,8 @@ var serveSimHelpers = async (dataDir) => {
1571
2165
  "dist"
1572
2166
  );
1573
2167
  const [axSettings2, cli] = await Promise.all([
1574
- existing(NodePath5.join(dist, "simax", "serve-sim-ax-settings")),
1575
- existing(NodePath5.join(dist, "serve-sim.js"))
2168
+ existing(NodePath7.join(dist, "simax", "serve-sim-ax-settings")),
2169
+ existing(NodePath7.join(dist, "serve-sim.js"))
1576
2170
  ]);
1577
2171
  return { axSettings: axSettings2, cli };
1578
2172
  };
@@ -1589,6 +2183,7 @@ var LocalDeviceHost = class {
1589
2183
  agent;
1590
2184
  agentEndpoint = null;
1591
2185
  lock = Promise.resolve();
2186
+ environmentMutating = false;
1592
2187
  options;
1593
2188
  constructor(options) {
1594
2189
  this.options = options;
@@ -1612,6 +2207,8 @@ var LocalDeviceHost = class {
1612
2207
  /** 生命周期操作串行:两个调用者同时要起 hub 时只起一次。 */
1613
2208
  installInput(spec) {
1614
2209
  return {
2210
+ pluginDir: this.pluginDir,
2211
+ ...this.options.runtimeDir ? { runtimeDir: this.options.runtimeDir } : {},
1615
2212
  dataDir: this.options.dataDir,
1616
2213
  spec,
1617
2214
  helperPath: this.options.helperPath,
@@ -1619,6 +2216,9 @@ var LocalDeviceHost = class {
1619
2216
  fetch: this.options.downloadFetch
1620
2217
  };
1621
2218
  }
2219
+ get pluginDir() {
2220
+ return this.options.pluginDir ?? NodePath8.dirname(NodePath8.dirname(this.options.helperPath));
2221
+ }
1622
2222
  serialized(task) {
1623
2223
  const next = this.lock.then(task, task);
1624
2224
  this.lock = next.catch(() => void 0);
@@ -1626,37 +2226,39 @@ var LocalDeviceHost = class {
1626
2226
  }
1627
2227
  async machine(refresh = false) {
1628
2228
  if (this.probeCache !== null && !refresh) return this.probeCache;
2229
+ const privateSdk = this.options.sdkOverride.trim() ? null : await selectedPrivateSdk(this.options.dataDir);
1629
2230
  this.probeCache = await probeMachine({
1630
2231
  helperPath: this.options.helperPath,
1631
2232
  process: this.options.process,
1632
2233
  cwd: this.options.dataDir,
1633
- sdkOverride: this.options.sdkOverride
2234
+ sdkOverride: this.options.sdkOverride.trim() || privateSdk || "",
2235
+ runtimeDir: bundledToolPaths(this.pluginDir, HUB_SPEC, this.options.runtimeDir).installDir
1634
2236
  });
1635
2237
  return this.probeCache;
1636
2238
  }
1637
- async platformAvailability(platform) {
1638
- return platformAvailability(await this.machine(), platform);
2239
+ async platformAvailability(platform2) {
2240
+ return platformAvailability(await this.machine(), platform2);
1639
2241
  }
1640
2242
  async platforms(refresh = false) {
1641
2243
  const probe = await this.machine(refresh);
1642
- return DEVICE_PLATFORMS.map((platform) => platformAvailability(probe, platform));
2244
+ return DEVICE_PLATFORMS.map((platform2) => platformAvailability(probe, platform2));
1643
2245
  }
1644
2246
  async toolVersion(spec) {
1645
2247
  const running = spec.tool === "hub" ? this.hub.running !== null ? HUB_SPEC.version : null : this.agentEndpoint?.version ?? null;
1646
- return inspectTool(this.options.dataDir, spec, running);
2248
+ return inspectTool(this.pluginDir, spec, running, this.options.runtimeDir);
1647
2249
  }
1648
2250
  /** 不装不起,只读现状。探测失败(比如没有 Node)时平台一律按不可用报。 */
1649
2251
  async summary(refresh = false) {
1650
2252
  const [platforms, hubInstalled, agentDeviceInstalled, hub, agent] = await Promise.all([
1651
2253
  this.platforms(refresh).catch(
1652
- (error) => DEVICE_PLATFORMS.map((platform) => ({
1653
- platform,
2254
+ (error) => DEVICE_PLATFORMS.map((platform2) => ({
2255
+ platform: platform2,
1654
2256
  available: false,
1655
2257
  reason: errorDetail(error)
1656
2258
  }))
1657
2259
  ),
1658
- isToolInstalled(this.options.dataDir, HUB_SPEC),
1659
- isToolInstalled(this.options.dataDir, AGENT_DEVICE_SPEC),
2260
+ isToolInstalled(this.pluginDir, HUB_SPEC, this.options.runtimeDir),
2261
+ isToolInstalled(this.pluginDir, AGENT_DEVICE_SPEC, this.options.runtimeDir),
1660
2262
  this.toolVersion(HUB_SPEC),
1661
2263
  this.toolVersion(AGENT_DEVICE_SPEC)
1662
2264
  ]);
@@ -1670,7 +2272,7 @@ var LocalDeviceHost = class {
1670
2272
  };
1671
2273
  }
1672
2274
  async installIfNeeded(spec, onPhase) {
1673
- if (!await isToolInstalled(this.options.dataDir, spec)) {
2275
+ if (!await isToolInstalled(this.pluginDir, spec, this.options.runtimeDir)) {
1674
2276
  onPhase(
1675
2277
  "installing",
1676
2278
  installPhaseDetail(spec.tool, await this.toolVersion(spec), spec.version)
@@ -1678,31 +2280,25 @@ var LocalDeviceHost = class {
1678
2280
  }
1679
2281
  return ensureTool(this.installInput(spec));
1680
2282
  }
1681
- async prune(spec) {
1682
- const result = await pruneTool({
1683
- dataDir: this.options.dataDir,
1684
- spec,
1685
- process: this.options.process
1686
- }).catch((error) => ({
1687
- ok: false,
1688
- detail: error instanceof Error ? error.message : String(error)
1689
- }));
1690
- if (!result.ok) {
1691
- this.options.log.warn("Reclaiming old device tool versions failed", {
1692
- tool: spec.tool,
1693
- detail: result.detail
1694
- });
1695
- }
1696
- }
1697
2283
  async startHub(onPhase) {
2284
+ if (this.environmentMutating)
2285
+ throw new DeviceError(
2286
+ "invalid-input",
2287
+ "Simulator environment preparation or cleanup is in progress"
2288
+ );
1698
2289
  const probe = await this.machine();
1699
2290
  const running = this.hub.running;
1700
- if (running !== null) return { hostId: this.id, hubOrigin: running.origin, probe };
2291
+ if (running !== null)
2292
+ return {
2293
+ hostId: this.id,
2294
+ hubOrigin: running.origin,
2295
+ probe,
2296
+ helperPath: this.options.helperPath
2297
+ };
1701
2298
  const tool = await this.installIfNeeded(HUB_SPEC, onPhase);
1702
2299
  onPhase("starting");
1703
2300
  const hub = await this.hub.ensure({ entryPath: tool.entryPath, sdkRoot: probe.android.root });
1704
- await this.prune(HUB_SPEC);
1705
- return { hostId: this.id, hubOrigin: hub.origin, probe };
2301
+ return { hostId: this.id, hubOrigin: hub.origin, probe, helperPath: this.options.helperPath };
1706
2302
  }
1707
2303
  /** 首次使用时装 hub 并起来;幂等,hub 已在跑就直接返回。 */
1708
2304
  ensureReady(onPhase) {
@@ -1720,7 +2316,6 @@ var LocalDeviceHost = class {
1720
2316
  sdkRoot: ready.probe.android.root
1721
2317
  });
1722
2318
  this.agentEndpoint = endpoint;
1723
- await this.prune(AGENT_DEVICE_SPEC);
1724
2319
  return { ...ready, agentDevice: endpoint };
1725
2320
  });
1726
2321
  }
@@ -1739,6 +2334,11 @@ var LocalDeviceHost = class {
1739
2334
  * Node 起脚本。只传 argv,不过 shell。
1740
2335
  */
1741
2336
  async runTool(program, args, options = {}) {
2337
+ if (this.environmentMutating)
2338
+ throw new DeviceError(
2339
+ "invalid-input",
2340
+ "Simulator environment preparation or cleanup is in progress"
2341
+ );
1742
2342
  const stdin = options.stdin === void 0 ? {} : { stdin: options.stdin };
1743
2343
  const base = {
1744
2344
  program: "host:node",
@@ -1770,7 +2370,7 @@ var LocalDeviceHost = class {
1770
2370
  async actionTools() {
1771
2371
  return {
1772
2372
  run: (program, args, options) => this.runTool(program, args, options),
1773
- helpers: await serveSimHelpers(this.options.dataDir)
2373
+ helpers: await serveSimHelpers(this.pluginDir, this.options.runtimeDir)
1774
2374
  };
1775
2375
  }
1776
2376
  /** 只停 agent-device;用户自己在面板里看设备不受影响。 */
@@ -1788,6 +2388,25 @@ var LocalDeviceHost = class {
1788
2388
  this.agentEndpoint = null;
1789
2389
  });
1790
2390
  }
2391
+ beginEnvironmentMutation() {
2392
+ return this.serialized(async () => {
2393
+ if (this.environmentMutating)
2394
+ throw new DeviceError("invalid-input", "Another environment mutation is in progress");
2395
+ this.environmentMutating = true;
2396
+ try {
2397
+ await this.hub.stop();
2398
+ await this.agent.stop();
2399
+ this.agentEndpoint = null;
2400
+ } catch (error) {
2401
+ this.environmentMutating = false;
2402
+ throw error;
2403
+ }
2404
+ return () => {
2405
+ this.probeCache = null;
2406
+ this.environmentMutating = false;
2407
+ };
2408
+ });
2409
+ }
1791
2410
  };
1792
2411
 
1793
2412
  // src/deviceActions.ts
@@ -2156,23 +2775,23 @@ var readDeviceDetail = async (tools, device) => {
2156
2775
  var BOOT_TIMEOUT_MS = 3 * 6e4;
2157
2776
  var SCREENSHOT_TIMEOUT_MS = 2e4;
2158
2777
  var DEFAULT_TIMEOUT_MS = 15e3;
2159
- var vendorPrefix = (platform) => platform === "ios" ? "/vendor/serve-sim" : "/vendor/serve-emu";
2778
+ var vendorPrefix = (platform2) => platform2 === "ios" ? "/vendor/serve-sim" : "/vendor/serve-emu";
2160
2779
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2161
2780
  var parseHubDevice = (value) => {
2162
2781
  if (!isRecord3(value)) return null;
2163
- const { id, name, version, platform, booted, physical } = value;
2164
- if (typeof id !== "string" || typeof name !== "string" || typeof version !== "string" || platform !== "ios" && platform !== "android" || typeof booted !== "boolean" || typeof physical !== "boolean") {
2782
+ const { id, name, version, platform: platform2, booted, physical } = value;
2783
+ if (typeof id !== "string" || typeof name !== "string" || typeof version !== "string" || platform2 !== "ios" && platform2 !== "android" || typeof booted !== "boolean" || typeof physical !== "boolean") {
2165
2784
  return null;
2166
2785
  }
2167
- return { id, name, version, platform, booted, physical };
2786
+ return { id, name, version, platform: platform2, booted, physical };
2168
2787
  };
2169
2788
  var parseHubDeviceList = (value) => {
2170
2789
  if (!isRecord3(value)) return null;
2171
2790
  const simulators = value["simulators"];
2172
- const emulators = value["emulators"];
2173
- if (!Array.isArray(simulators) || !Array.isArray(emulators)) return null;
2791
+ const emulators2 = value["emulators"];
2792
+ if (!Array.isArray(simulators) || !Array.isArray(emulators2)) return null;
2174
2793
  const devices = [];
2175
- for (const entry of [...simulators, ...emulators]) {
2794
+ for (const entry of [...simulators, ...emulators2]) {
2176
2795
  const device = parseHubDevice(entry);
2177
2796
  if (device === null) return null;
2178
2797
  devices.push(device);
@@ -2266,14 +2885,14 @@ var HubClient = class {
2266
2885
  shutdownIos(udid) {
2267
2886
  return this.action(`${vendorPrefix("ios")}/grid/api/shutdown`, "shutdown", { udid });
2268
2887
  }
2269
- shutdownDevice(platform, id) {
2270
- return this.action("/api/devices/shutdown", "shutdown", { platform, id });
2888
+ shutdownDevice(platform2, id) {
2889
+ return this.action("/api/devices/shutdown", "shutdown", { platform: platform2, id });
2271
2890
  }
2272
- async screenshot(platform, deviceId) {
2891
+ async screenshot(platform2, deviceId) {
2273
2892
  let response;
2274
2893
  try {
2275
2894
  response = await this.fetch(
2276
- `${this.origin}${vendorPrefix(platform)}/api/screenshot?device=${encodeURIComponent(deviceId)}`,
2895
+ `${this.origin}${vendorPrefix(platform2)}/api/screenshot?device=${encodeURIComponent(deviceId)}`,
2277
2896
  { method: "POST", signal: AbortSignal.timeout(SCREENSHOT_TIMEOUT_MS) }
2278
2897
  );
2279
2898
  } catch (cause) {
@@ -2306,6 +2925,9 @@ var classifyBootFailure = (error) => {
2306
2925
  // src/service.ts
2307
2926
  var sameDevice = (a, hostId, deviceId) => a.hostId === hostId && a.id === deviceId;
2308
2927
  var DeviceService = class {
2928
+ environmentMutating = false;
2929
+ environmentGeneration = 0;
2930
+ pendingOpens = 0;
2309
2931
  state = {
2310
2932
  hosts: [],
2311
2933
  hostStatuses: {},
@@ -2345,6 +2967,7 @@ var DeviceService = class {
2345
2967
  }));
2346
2968
  }
2347
2969
  onPhase = (phase, detail) => {
2970
+ if (this.environmentMutating) return;
2348
2971
  this.setHostStatus({ status: phase, ...detail });
2349
2972
  };
2350
2973
  resolveHost(hostId) {
@@ -2353,28 +2976,30 @@ var DeviceService = class {
2353
2976
  }
2354
2977
  }
2355
2978
  async withStatus(task) {
2979
+ const generation = this.environmentGeneration;
2356
2980
  try {
2357
2981
  return await task();
2358
2982
  } catch (error) {
2359
2983
  const message = error instanceof DeviceError ? error.message : errorDetail(error);
2360
- this.setHostStatus({
2361
- status: "failed",
2362
- code: error instanceof DeviceError ? error.code : "host-unavailable",
2363
- message
2364
- });
2984
+ if (!this.environmentMutating && generation === this.environmentGeneration)
2985
+ this.setHostStatus({
2986
+ status: "failed",
2987
+ code: error instanceof DeviceError ? error.code : "host-unavailable",
2988
+ message
2989
+ });
2365
2990
  throw error;
2366
2991
  }
2367
2992
  }
2368
2993
  /** 起 hub 并刷新设备。机器一个平台都跑不了时什么都不装,返回 null。 */
2369
2994
  async readinessIfSupported() {
2370
2995
  const platforms = await this.host.platforms();
2371
- if (!platforms.some((platform) => platform.available)) return null;
2996
+ if (!platforms.some((platform2) => platform2.available)) return null;
2372
2997
  return this.readiness();
2373
2998
  }
2374
2999
  async readiness() {
2375
3000
  return this.withStatus(async () => {
2376
3001
  const ready = await this.host.ensureReady(this.onPhase);
2377
- if (this.state.hostStatuses[this.host.id]?.status !== "ready") {
3002
+ if (!this.environmentMutating && this.state.hostStatuses[this.host.id]?.status !== "ready") {
2378
3003
  this.setHostStatus({ status: "ready" });
2379
3004
  }
2380
3005
  return ready;
@@ -2414,8 +3039,10 @@ var DeviceService = class {
2414
3039
  return list.errors.length > 0 ? { devices, detail: list.errors.join("\n") } : { devices };
2415
3040
  }
2416
3041
  async refresh(ready) {
3042
+ const generation = this.environmentGeneration;
2417
3043
  const { devices, detail } = await this.withStatus(() => this.fetchDevices(ready));
2418
3044
  const summary = await this.host.summary();
3045
+ if (this.environmentMutating || generation !== this.environmentGeneration) return this.state;
2419
3046
  return this.publish((state) => ({
2420
3047
  ...state,
2421
3048
  hosts: [summary],
@@ -2433,8 +3060,11 @@ var DeviceService = class {
2433
3060
  }
2434
3061
  /** 列设备:能跑的话起 hub 再刷新;跑不了只更新宿主现状。失败记在宿主状态里,不抛。 */
2435
3062
  async list() {
3063
+ if (this.environmentMutating) return this.state;
3064
+ const generation = this.environmentGeneration;
2436
3065
  try {
2437
3066
  const ready = await this.readinessIfSupported();
3067
+ if (this.environmentMutating || generation !== this.environmentGeneration) return this.state;
2438
3068
  if (ready === null) return await this.inspect();
2439
3069
  return await this.refresh(ready);
2440
3070
  } catch {
@@ -2442,6 +3072,19 @@ var DeviceService = class {
2442
3072
  }
2443
3073
  }
2444
3074
  async open(input) {
3075
+ if (this.environmentMutating)
3076
+ throw new DeviceError(
3077
+ "invalid-input",
3078
+ "Simulator environment preparation or cleanup is in progress"
3079
+ );
3080
+ this.pendingOpens++;
3081
+ try {
3082
+ return await this.openDevice(input);
3083
+ } finally {
3084
+ this.pendingOpens--;
3085
+ }
3086
+ }
3087
+ async openDevice(input) {
2445
3088
  this.resolveHost(input.hostId);
2446
3089
  const availability = await this.host.platformAvailability(input.platform);
2447
3090
  if (!availability.available) {
@@ -2520,11 +3163,11 @@ var DeviceService = class {
2520
3163
  }));
2521
3164
  return session;
2522
3165
  }
2523
- async shutdownDevice(deviceId, platform) {
3166
+ async shutdownDevice(deviceId, platform2) {
2524
3167
  const ready = await this.readiness();
2525
3168
  const hub = this.hub(ready);
2526
3169
  try {
2527
- const result = platform === "ios" ? await hub.shutdownIos(deviceId) : await hub.shutdownDevice(platform, deviceId);
3170
+ const result = platform2 === "ios" ? await hub.shutdownIos(deviceId) : await hub.shutdownDevice(platform2, deviceId);
2528
3171
  if (!result.ok) {
2529
3172
  throw new DeviceError(
2530
3173
  "hub-rejected",
@@ -2534,7 +3177,7 @@ var DeviceService = class {
2534
3177
  } catch (error) {
2535
3178
  const devices = await this.fetchDevices(ready).catch(() => null);
2536
3179
  const known = devices?.devices.find((device) => device.id === deviceId);
2537
- if (platform !== "ios" || known?.booted !== false) throw error;
3180
+ if (platform2 !== "ios" || known?.booted !== false) throw error;
2538
3181
  }
2539
3182
  this.publish((state) => ({
2540
3183
  ...state,
@@ -2640,7 +3283,8 @@ var DeviceService = class {
2640
3283
  entryPath: ready.agentDevice.entryPath,
2641
3284
  nodePath: ready.probe.execPath,
2642
3285
  electron: ready.probe.electron !== null,
2643
- platform: ready.probe.platform
3286
+ platform: ready.probe.platform,
3287
+ ...ready.helperPath ? { helperPath: ready.helperPath, sdkRoot: ready.probe.android.root } : {}
2644
3288
  });
2645
3289
  return {
2646
3290
  command,
@@ -2657,6 +3301,28 @@ var DeviceService = class {
2657
3301
  await this.host.installTool(tool);
2658
3302
  return this.inspect();
2659
3303
  }
3304
+ async beginEnvironmentMutation() {
3305
+ if (this.environmentMutating || this.pendingOpens > 0 || this.state.sessions.length > 0 || this.state.bootingDevices.length > 0)
3306
+ throw new DeviceError(
3307
+ "invalid-input",
3308
+ "Close device sessions and wait for device operations to finish before preparing or deleting an environment"
3309
+ );
3310
+ this.environmentMutating = true;
3311
+ this.environmentGeneration++;
3312
+ let release;
3313
+ try {
3314
+ release = await this.host.beginEnvironmentMutation();
3315
+ } catch (error) {
3316
+ this.environmentMutating = false;
3317
+ throw error;
3318
+ }
3319
+ this.setHostStatus({ status: "idle" });
3320
+ return async () => {
3321
+ release();
3322
+ this.environmentMutating = false;
3323
+ await this.inspect(true);
3324
+ };
3325
+ }
2660
3326
  /** 插件停用时:停辅助进程,模拟器本身不关。 */
2661
3327
  async stop() {
2662
3328
  await this.host.stop();
@@ -2730,7 +3396,7 @@ var deviceToolkit = (service) => ({
2730
3396
  tools: [
2731
3397
  {
2732
3398
  name: "device_list",
2733
- description: "List iOS Simulators and Android Emulators on this machine, which platforms it can run, and which devices are already open in this conversation's Devices panel. Call this before device_open when you do not know a device id.",
3399
+ description: "List iOS Simulators and Android Emulators on this machine for mobile app UI and interaction verification, including SwiftUI, UIKit, Jetpack Compose, Flutter and React Native apps. Reports which platforms this machine can run and which devices are already open in this conversation's Devices panel. Call this before device_open when you do not know a device id.",
2734
3400
  inputSchema: { type: "object", properties: { hostId: HOST_ID_PROPERTY } },
2735
3401
  handle: guard(async (input, invocation) => {
2736
3402
  const threadId = requireThread(invocation);
@@ -2746,7 +3412,7 @@ var deviceToolkit = (service) => ({
2746
3412
  },
2747
3413
  {
2748
3414
  name: "device_open",
2749
- description: "Open a simulator or emulator for this conversation: boots it if needed, starts its live stream, and shows it in the user's Devices panel. Returns the agent-device command line pinned to the device; drive the device with that command afterwards.",
3415
+ description: "Open an iOS simulator or Android emulator for this conversation, such as when checking a mobile app's screens, navigation, forms and interactions. Boots it if needed, starts its live stream, and shows it in the user's Devices panel. Returns the agent-device command line pinned to the device; use that command to inspect and drive the app afterwards.",
2750
3416
  inputSchema: {
2751
3417
  type: "object",
2752
3418
  properties: {
@@ -2857,12 +3523,14 @@ var deviceToolkit = (service) => ({
2857
3523
  var HUB_SERVICE = "hub";
2858
3524
  var index_default = definePlugin({
2859
3525
  id: "devices",
2860
- apiVersion: 43,
3526
+ apiVersion: 47,
2861
3527
  activate: async (ctx) => {
2862
3528
  const direct = (input, init) => ctx.proxies.fetch(PLUGIN_PROXY_DIRECT).then((fetchDirect) => fetchDirect(input, init));
2863
3529
  const host = new LocalDeviceHost({
3530
+ pluginDir: ctx.paths.pluginDir,
3531
+ ...ctx.paths.runtimeDir ? { runtimeDir: ctx.paths.runtimeDir } : {},
2864
3532
  dataDir: ctx.paths.dataDir,
2865
- helperPath: NodePath6.join(ctx.paths.pluginDir, "dist", "helper.mjs"),
3533
+ helperPath: NodePath9.join(ctx.paths.pluginDir, "dist", "helper.mjs"),
2866
3534
  process: ctx.process,
2867
3535
  fetch: direct,
2868
3536
  downloadFetch: ctx.http,
@@ -2884,8 +3552,31 @@ var index_default = definePlugin({
2884
3552
  });
2885
3553
  ctx.toolkit(deviceToolkit(service));
2886
3554
  registerCommands(ctx, service);
3555
+ const environments = new DeviceEnvironments({
3556
+ dataDir: ctx.paths.dataDir,
3557
+ helperPath: NodePath9.join(ctx.paths.pluginDir, "dist", "helper.mjs"),
3558
+ runtimeDir: bundledToolPaths(ctx.paths.pluginDir, HUB_SPEC, ctx.paths.runtimeDir).installDir,
3559
+ process: ctx.process,
3560
+ fetch: ctx.http,
3561
+ log: ctx.log,
3562
+ externalSdk: async () => {
3563
+ const probe = await host.machine(true);
3564
+ return {
3565
+ root: probe.android.root,
3566
+ available: platformAvailability(probe, "android").available
3567
+ };
3568
+ },
3569
+ beforeMutation: () => service.beginEnvironmentMutation(),
3570
+ afterPrepared: async () => {
3571
+ await service.inspect(true);
3572
+ }
3573
+ });
3574
+ registerEnvironmentCommands(ctx, environments);
2887
3575
  ctx.settingsCard(SETTINGS_CARD);
2888
- ctx.onDispose(() => service.stop());
3576
+ ctx.onDispose(async () => {
3577
+ await environments.dispose();
3578
+ await service.stop();
3579
+ });
2889
3580
  }
2890
3581
  });
2891
3582
  export {