@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.
- package/dist/cli.js +99 -14
- 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.
|
|
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("
|
|
727
|
+
child.on("close", (status) => resolve(status ?? 1));
|
|
648
728
|
});
|
|
649
|
-
|
|
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 =
|
|
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
|
|
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
|
});
|