@touchtechclub/simbox 0.4.8 → 0.4.10

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.
Files changed (2) hide show
  1. package/dist/cli.js +99 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -81,6 +81,35 @@ function parseRunnerInput(input, platform) {
81
81
  function formatRunner(runner) {
82
82
  return runner ? runner.labels.join(" + ") : "unknown (legacy workflow)";
83
83
  }
84
+ // ../../packages/shared/src/device-profile.ts
85
+ var XCODE_VERSION = /^\d{1,3}(\.\d{1,3}){0,2}$/;
86
+ function validateDeviceProfile(value, platform) {
87
+ if (value === undefined || value === null)
88
+ return null;
89
+ if (typeof value !== "object" || Array.isArray(value))
90
+ throw new Error("Device must be {foldable?: boolean, xcode?: string}.");
91
+ const keys = Object.keys(value);
92
+ if (keys.some((key) => key !== "foldable" && key !== "xcode"))
93
+ throw new Error("Device only accepts foldable and xcode.");
94
+ const { foldable, xcode } = value;
95
+ if (foldable !== undefined && typeof foldable !== "boolean")
96
+ throw new Error("foldable must be true or false.");
97
+ if (xcode !== undefined) {
98
+ if (typeof xcode !== "string" || !XCODE_VERSION.test(xcode))
99
+ throw new Error('Xcode must be a version such as "27.1".');
100
+ if (platform !== "ios")
101
+ throw new Error("Xcode selection only applies to iOS runs.");
102
+ }
103
+ const profile = {
104
+ ...foldable === true ? { foldable: true } : {},
105
+ ...xcode !== undefined ? { xcode } : {}
106
+ };
107
+ return Object.keys(profile).length > 0 ? profile : null;
108
+ }
109
+ function formatDeviceProfile(profile, platform) {
110
+ const device = profile?.foldable ? platform === "ios" ? "iPhone Duo" : "Android foldable" : "default";
111
+ return profile?.xcode ? `${device}, Xcode ${profile.xcode}` : device;
112
+ }
84
113
  // ../../packages/shared/src/preview.ts
85
114
  var PREVIEW_SESSION_MS = 5 * 60000;
86
115
  // src/config.ts
@@ -234,9 +263,9 @@ var repoStatus = () => api("/v1/repo/status");
234
263
  var repairRepo = () => api("/v1/repo/repair", {
235
264
  method: "POST"
236
265
  });
237
- var ensureRun = (fresh, platform2, runner) => api("/v1/runs/ensure", {
266
+ var ensureRun = (fresh, platform2, runner, device) => api("/v1/runs/ensure", {
238
267
  method: "POST",
239
- body: { new: fresh, platform: platform2, runner }
268
+ body: { new: fresh, platform: platform2, runner, device }
240
269
  });
241
270
  var runnerSettings = () => api("/v1/runners");
242
271
  var runnerDiagnostics = () => api("/v1/runners/doctor");
@@ -594,6 +623,22 @@ function remoteArgs(args, info, stateDir) {
594
623
  result.push("--state-dir", stateDir, "--daemon-base-url", daemonBaseUrl(info.tunnelUrl), "--daemon-transport", "http");
595
624
  return [...result, ...tail];
596
625
  }
626
+ var REPLAY_SAFE = new Set([
627
+ "devices",
628
+ "snapshot",
629
+ "screenshot",
630
+ "fold",
631
+ "session",
632
+ "appstate",
633
+ "apps",
634
+ "get",
635
+ "is"
636
+ ]);
637
+ var TRANSPORT_ERROR = /Remote daemon is unavailable|Daemon request timed out|Failed to read daemon response|fetch failed|ECONNRESET|socket hang up/;
638
+ function shouldReplay(args, code, output) {
639
+ return code !== 0 && REPLAY_SAFE.has(args[0] ?? "") && TRANSPORT_ERROR.test(output);
640
+ }
641
+ var REPLAY_ATTEMPTS = 3;
597
642
  function hasFlag(args, flag) {
598
643
  const end = args.indexOf("--");
599
644
  return (end < 0 ? args : args.slice(0, end)).some((arg) => arg === flag || arg.startsWith(`${flag}=`));
@@ -616,7 +661,7 @@ async function cmdExec(args) {
616
661
  }
617
662
  const bin = findAgentDevice();
618
663
  if (!bin)
619
- throw new CliError("Install agent-device first: npm i -g agent-device@0.21.20");
664
+ throw new CliError("Install agent-device first: npm i -g agent-device@0.21.28");
620
665
  const android = args.some((arg, i) => arg === "--platform=android" || arg === "--platform" && args[i + 1] === "android");
621
666
  let info = android ? null : readCachedRun();
622
667
  if (info && !await probeRemote(info))
@@ -632,26 +677,56 @@ async function cmdExec(args) {
632
677
  info = await waitForRemote(initial);
633
678
  }
634
679
  writeCachedRun(info);
680
+ const replaySafe = REPLAY_SAFE.has(args[0]);
681
+ let connected = info;
682
+ let result = await runRemote(bin, args, connected, replaySafe);
683
+ for (let attempt = 1;attempt < REPLAY_ATTEMPTS; attempt++) {
684
+ if (!shouldReplay(args, result.code, result.output))
685
+ break;
686
+ process.stderr.write(`Tunnel dropped during \`${args[0]}\`; retrying (${attempt}/${REPLAY_ATTEMPTS - 1}).
687
+ `);
688
+ await sleep(attempt * 5000);
689
+ if (!await probeRemote(connected)) {
690
+ const refreshed = await refreshRemote();
691
+ if (!refreshed)
692
+ break;
693
+ connected = await waitForRemote(refreshed);
694
+ writeCachedRun(connected);
695
+ }
696
+ result = await runRemote(bin, args, connected, replaySafe);
697
+ }
698
+ if (replaySafe) {
699
+ process.stdout.write(result.stdout);
700
+ process.stderr.write(result.stderr);
701
+ }
702
+ const code = result.code;
703
+ if (code === 0 && !args.some((arg) => ["help", "--help", "-h", "--version", "version"].includes(arg)))
704
+ registerDevice(connected, { args });
705
+ if (code !== 0 && !replaySafe) {
706
+ process.stderr.write("Simbox did not retry the device command (it may already have executed). Use `simbox exec session list` to inspect it; the next command refreshes the tunnel automatically.\n");
707
+ }
708
+ process.exitCode = code;
709
+ }
710
+ async function runRemote(bin, args, info, capture) {
635
711
  const env = {
636
712
  ...process.env,
637
713
  AGENT_DEVICE_DAEMON_AUTH_TOKEN: info.daemonToken
638
714
  };
639
715
  delete env.AGENT_DEVICE_REMOTE_CONFIG;
640
716
  delete env.AGENT_DEVICE_DAEMON_BASE_URL;
717
+ let stdout = "";
718
+ let stderr = "";
641
719
  const code = await new Promise((resolve, reject) => {
642
720
  const child = spawn(bin, remoteArgs(args, info, join4(configDir(), "agent-device")), {
643
- stdio: "inherit",
721
+ stdio: capture ? ["inherit", "pipe", "pipe"] : "inherit",
644
722
  env
645
723
  });
724
+ child.stdout?.on("data", (chunk) => stdout += chunk.toString());
725
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
646
726
  child.on("error", reject);
647
- child.on("exit", (status) => resolve(status ?? 1));
727
+ child.on("close", (status) => resolve(status ?? 1));
648
728
  });
649
- if (code === 0 && !args.some((arg) => ["help", "--help", "-h", "--version", "version"].includes(arg)))
650
- registerDevice(info, { args });
651
- if (code !== 0) {
652
- process.stderr.write("Simbox did not retry the device command (it may already have executed). Use `simbox exec session list` to inspect it; the next command refreshes the tunnel automatically.\n");
653
- }
654
- process.exitCode = code;
729
+ return { code, output: stdout + stderr, stdout, stderr };
655
730
  }
656
731
 
657
732
  // src/commands.ts
@@ -736,7 +811,7 @@ function printRepoConnected(repo) {
736
811
  }
737
812
  console.log(`Run ${pc3.cyan("simbox sim")} to boot a simulator.`);
738
813
  }
739
- var SIM_TIMEOUT_MS = 12 * 60 * 1000;
814
+ var SIM_TIMEOUT_MS = 25 * 60 * 1000;
740
815
  var SIM_POLL_MS = 3000;
741
816
  async function cmdSim(opts) {
742
817
  const json = opts.json === true;
@@ -744,7 +819,16 @@ async function cmdSim(opts) {
744
819
  if (opts.runner !== undefined && !opts.platform)
745
820
  throw new CliError("Use --platform ios or android with --runner.");
746
821
  const runner = opts.runner === undefined ? undefined : parseRunnerInput(opts.runner, opts.platform);
747
- const first = await ensureRun(opts.new === true, opts.platform, runner);
822
+ const wantsDevice = opts.foldable === true || opts.xcode !== undefined;
823
+ if (wantsDevice && !opts.platform)
824
+ throw new CliError("Use --platform ios or android with --foldable or --xcode.");
825
+ let device;
826
+ try {
827
+ device = wantsDevice ? validateDeviceProfile({ foldable: opts.foldable, xcode: opts.xcode }, opts.platform) ?? undefined : undefined;
828
+ } catch (err) {
829
+ throw new CliError(err.message);
830
+ }
831
+ const first = await ensureRun(opts.new === true, opts.platform, runner, device);
748
832
  let info;
749
833
  if (first.state === "live") {
750
834
  info = {
@@ -823,6 +907,7 @@ async function cmdPs() {
823
907
  console.log(` state: ${stateColor(run.state)}`);
824
908
  console.log(` platform: ${run.platform}`);
825
909
  console.log(` runner: ${formatRunner(run.runner)}`);
910
+ console.log(` device: ${formatDeviceProfile(run.device ?? null, run.platform)}`);
826
911
  console.log(` gh_run_id: ${run.ghRunId ?? pc3.dim("pending")}`);
827
912
  console.log(` active_devices: ${run.activeDevices}`);
828
913
  console.log(` android_ready: ${run.androidReady ? "yes" : "no"}`);
@@ -1011,7 +1096,7 @@ program.command("init").description("Open onboarding and wait until a repo is co
1011
1096
  program.command("sim").description("Ensure a run and its tunnel are ready for device commands").option("--new", "force a fresh run even if one is live").addOption(new Option("--platform <platform>", "ios: macOS runner; android: Linux/KVM runner").choices([
1012
1097
  "ios",
1013
1098
  "android"
1014
- ])).option("--runner <label-or-json>", "override runner label or JSON label array; requires --platform").option("--json", "print {tunnel_url, daemon_token, run_id, expires_at} as JSON only").action((opts) => {
1099
+ ])).option("--runner <label-or-json>", "override runner label or JSON label array; requires --platform").option("--foldable", "boot a foldable: iPhone Duo (needs Xcode 27.1+, e.g. --runner xcode-27) or a 7.6in Android foldable; requires --platform").option("--xcode <version>", "iOS: select this Xcode (e.g. 27.1) before the device daemon starts").option("--json", "print {tunnel_url, daemon_token, run_id, expires_at} as JSON only").action((opts) => {
1015
1100
  requireToken();
1016
1101
  return run(cmdSim(opts));
1017
1102
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@touchtechclub/simbox",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "On-demand iOS simulators & Android emulators on your own GitHub Actions minutes.",
5
5
  "repository": {
6
6
  "type": "git",