@touchtechclub/simbox 0.2.10 → 0.3.0
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 +250 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command, Option } from "commander";
|
|
4
|
+
import { Argument, Command, Option } from "commander";
|
|
5
5
|
import pc4 from "picocolors";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
|
|
@@ -12,6 +12,77 @@ import { join } from "node:path";
|
|
|
12
12
|
|
|
13
13
|
// ../../packages/shared/src/constants.ts
|
|
14
14
|
var PROD_API_URL = "https://api.simbox.touchtech.club";
|
|
15
|
+
// ../../packages/shared/src/runners.ts
|
|
16
|
+
var DEFAULT_RUNNERS = {
|
|
17
|
+
ios: { labels: ["macos-latest"] },
|
|
18
|
+
android: { labels: ["ubuntu-latest"] }
|
|
19
|
+
};
|
|
20
|
+
var RUNNER_PRESETS = [
|
|
21
|
+
{
|
|
22
|
+
name: "GitHub macOS (ARM64)",
|
|
23
|
+
platform: "ios",
|
|
24
|
+
provider: "github",
|
|
25
|
+
runner: DEFAULT_RUNNERS.ios
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "GitHub Ubuntu (x64/KVM)",
|
|
29
|
+
platform: "android",
|
|
30
|
+
provider: "github",
|
|
31
|
+
runner: DEFAULT_RUNNERS.android
|
|
32
|
+
},
|
|
33
|
+
...[2, 4, 8].map((cpu) => ({
|
|
34
|
+
name: `Blacksmith Ubuntu · ${cpu} vCPU`,
|
|
35
|
+
platform: "android",
|
|
36
|
+
provider: "blacksmith",
|
|
37
|
+
runner: { labels: [`blacksmith-${cpu}vcpu-ubuntu-2404`] }
|
|
38
|
+
})),
|
|
39
|
+
...[6, 12].map((cpu) => ({
|
|
40
|
+
name: `Blacksmith macOS · ${cpu} vCPU`,
|
|
41
|
+
platform: "ios",
|
|
42
|
+
provider: "blacksmith",
|
|
43
|
+
runner: { labels: [`blacksmith-${cpu}vcpu-macos-latest`] }
|
|
44
|
+
}))
|
|
45
|
+
];
|
|
46
|
+
function validateRunner(value, platform) {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "labels")) {
|
|
48
|
+
throw new Error('Runner must be {labels: ["runner-label"]}.');
|
|
49
|
+
}
|
|
50
|
+
const labels = value.labels;
|
|
51
|
+
if (!Array.isArray(labels) || labels.length < 1 || labels.length > 8 || labels.some((label) => typeof label !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,99}$/.test(label) || label !== label.trim())) {
|
|
52
|
+
throw new Error("Use 1–8 runner labels (1–100 letters, numbers, spaces, dots, underscores or hyphens each).");
|
|
53
|
+
}
|
|
54
|
+
if (new Set(labels.map((label) => label.toLowerCase())).size !== labels.length)
|
|
55
|
+
throw new Error("Runner labels must be unique (case-insensitive).");
|
|
56
|
+
for (const label of labels) {
|
|
57
|
+
const lower = label.toLowerCase();
|
|
58
|
+
if (lower.includes("windows") || lower === "ubuntu-slim")
|
|
59
|
+
throw new Error("Windows and container-only ubuntu-slim runners are unsupported.");
|
|
60
|
+
if (platform === "android" && (/macos|darwin/.test(lower) || /(?:^|[-_ ])(?:arm|arm64|aarch64)(?:$|[-_ ])/.test(lower))) {
|
|
61
|
+
throw new Error("Android requires a Linux x64 runner with KVM, not macOS or ARM.");
|
|
62
|
+
}
|
|
63
|
+
if (platform === "ios" && (/ubuntu|linux/.test(lower) || /intel|x64|x86_64/.test(lower))) {
|
|
64
|
+
throw new Error("iOS requires a macOS ARM64 runner with Xcode.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { labels: [...labels] };
|
|
68
|
+
}
|
|
69
|
+
function parseRunnerInput(input, platform) {
|
|
70
|
+
const text = input.trim();
|
|
71
|
+
let labels = [text];
|
|
72
|
+
if (text.startsWith("[")) {
|
|
73
|
+
try {
|
|
74
|
+
labels = JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error("Custom runner label list must be valid JSON.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return validateRunner({ labels }, platform);
|
|
80
|
+
}
|
|
81
|
+
function formatRunner(runner) {
|
|
82
|
+
return runner ? runner.labels.join(" + ") : "unknown (legacy workflow)";
|
|
83
|
+
}
|
|
84
|
+
// ../../packages/shared/src/preview.ts
|
|
85
|
+
var PREVIEW_SESSION_MS = 5 * 60000;
|
|
15
86
|
// src/config.ts
|
|
16
87
|
var DEFAULT_WEB_URL = "https://simbox.touchtech.club";
|
|
17
88
|
function stripTrailingSlash(url) {
|
|
@@ -163,7 +234,13 @@ var repoStatus = () => api("/v1/repo/status");
|
|
|
163
234
|
var repairRepo = () => api("/v1/repo/repair", {
|
|
164
235
|
method: "POST"
|
|
165
236
|
});
|
|
166
|
-
var ensureRun = (fresh, platform2) => api("/v1/runs/ensure", {
|
|
237
|
+
var ensureRun = (fresh, platform2, runner) => api("/v1/runs/ensure", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: { new: fresh, platform: platform2, runner }
|
|
240
|
+
});
|
|
241
|
+
var runnerSettings = () => api("/v1/runners");
|
|
242
|
+
var runnerDiagnostics = () => api("/v1/runners/doctor");
|
|
243
|
+
var updateRunners = (scope, patch) => api(`/v1/runners/${scope}`, { method: "POST", body: patch });
|
|
167
244
|
async function currentRun() {
|
|
168
245
|
try {
|
|
169
246
|
const res = await api("/v1/runs/current");
|
|
@@ -560,7 +637,7 @@ function printRepoConnected(repo) {
|
|
|
560
637
|
console.log(`Your default branch is protected — merge ${pc3.underline(repo.prUrl)} to activate Simbox.`);
|
|
561
638
|
}
|
|
562
639
|
if (repo.private) {
|
|
563
|
-
console.log(pc3.yellow("Note: private
|
|
640
|
+
console.log(pc3.yellow("Note: private repositories may consume paid runner minutes. Review GitHub or your selected provider's billing."));
|
|
564
641
|
}
|
|
565
642
|
console.log(`Run ${pc3.cyan("simbox sim")} to boot a simulator.`);
|
|
566
643
|
}
|
|
@@ -569,7 +646,10 @@ var SIM_POLL_MS = 3000;
|
|
|
569
646
|
async function cmdSim(opts) {
|
|
570
647
|
const json = opts.json === true;
|
|
571
648
|
const out = json ? process.stderr : process.stdout;
|
|
572
|
-
|
|
649
|
+
if (opts.runner !== undefined && !opts.platform)
|
|
650
|
+
throw new CliError("Use --platform ios or android with --runner.");
|
|
651
|
+
const runner = opts.runner === undefined ? undefined : parseRunnerInput(opts.runner, opts.platform);
|
|
652
|
+
const first = await ensureRun(opts.new === true, opts.platform, runner);
|
|
573
653
|
let info;
|
|
574
654
|
if (first.state === "live") {
|
|
575
655
|
info = {
|
|
@@ -645,6 +725,7 @@ async function cmdPs() {
|
|
|
645
725
|
console.log(`${pc3.bold("run")} ${run.id}`);
|
|
646
726
|
console.log(` state: ${stateColor(run.state)}`);
|
|
647
727
|
console.log(` platform: ${run.platform}`);
|
|
728
|
+
console.log(` runner: ${formatRunner(run.runner)}`);
|
|
648
729
|
console.log(` gh_run_id: ${run.ghRunId ?? pc3.dim("pending")}`);
|
|
649
730
|
console.log(` active_devices: ${run.activeDevices}`);
|
|
650
731
|
console.log(` android_ready: ${run.androidReady ? "yes" : "no"}`);
|
|
@@ -680,6 +761,140 @@ async function cmdLogout() {
|
|
|
680
761
|
}
|
|
681
762
|
}
|
|
682
763
|
|
|
764
|
+
// src/runners.ts
|
|
765
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
766
|
+
function printRunnerSettings(settings, json = false) {
|
|
767
|
+
if (json) {
|
|
768
|
+
console.log(JSON.stringify(settings));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
console.log(`Repository: ${settings.repoFullName ?? "not connected"}`);
|
|
772
|
+
for (const platform2 of ["ios", "android"]) {
|
|
773
|
+
console.log(`${platform2}: ${formatRunner(settings.effective[platform2].runner)} (${settings.effective[platform2].source})`);
|
|
774
|
+
console.log(` account: ${settings.account[platform2] ? formatRunner(settings.account[platform2]) : "GitHub default"}`);
|
|
775
|
+
console.log(` repository: ${settings.repository?.[platform2] ? formatRunner(settings.repository[platform2]) : "inherit account"}`);
|
|
776
|
+
}
|
|
777
|
+
console.log("Settings apply to future runs only. Run `simbox repair` once to upgrade older workflows.");
|
|
778
|
+
console.log("Blacksmith requires its GitHub App on the repository and bills separately. Custom runners need Linux x64/KVM or macOS ARM64/Xcode.");
|
|
779
|
+
}
|
|
780
|
+
async function cmdRunnersShow(json = false) {
|
|
781
|
+
printRunnerSettings(await runnerSettings(), json);
|
|
782
|
+
}
|
|
783
|
+
function cmdRunnerPresets() {
|
|
784
|
+
for (const preset of RUNNER_PRESETS)
|
|
785
|
+
console.log(`${preset.platform.padEnd(7)} ${preset.runner.labels[0]} — ${preset.name}`);
|
|
786
|
+
}
|
|
787
|
+
async function cmdRunnersSet(platform2, input, scope, json = false) {
|
|
788
|
+
const runner = input === null ? null : parseRunnerInput(input, platform2);
|
|
789
|
+
printRunnerSettings(await updateRunners(scope, { [platform2]: runner }), json);
|
|
790
|
+
}
|
|
791
|
+
async function cmdDoctor(json = false) {
|
|
792
|
+
const diagnostics = await runnerDiagnostics();
|
|
793
|
+
const tool = (name, required) => {
|
|
794
|
+
const result = spawnSync2(name, ["--version"], {
|
|
795
|
+
encoding: "utf8",
|
|
796
|
+
timeout: 1e4,
|
|
797
|
+
shell: false
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
name,
|
|
801
|
+
required,
|
|
802
|
+
ok: !result.error && result.status === 0,
|
|
803
|
+
message: result.error ? `Missing or failed ${name}; install it locally.` : (result.stdout || result.stderr).trim().slice(0, 200)
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
const local = [tool("agent-device", true), tool("bunx", false)];
|
|
807
|
+
if (json)
|
|
808
|
+
console.log(JSON.stringify({ ...diagnostics, local }));
|
|
809
|
+
else {
|
|
810
|
+
for (const check of [...local, ...diagnostics.checks])
|
|
811
|
+
console.log(`${check.ok ? "PASS" : ("required" in check) && !check.required ? "WARN" : "FAIL"} ${check.name}: ${check.message}`);
|
|
812
|
+
printRunnerSettings(diagnostics.settings);
|
|
813
|
+
console.log("No job was started. Actual KVM/Xcode capability is checked on the runner, not this machine.");
|
|
814
|
+
}
|
|
815
|
+
if (diagnostics.checks.some((check) => !check.ok) || local.some((check) => check.required && !check.ok))
|
|
816
|
+
process.exitCode = 1;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/skills.ts
|
|
820
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
821
|
+
import { createInterface } from "node:readline/promises";
|
|
822
|
+
function skillInstallArgs(opts) {
|
|
823
|
+
if (opts.project && opts.global)
|
|
824
|
+
throw new CliError("Choose either --project or --global, not both.");
|
|
825
|
+
if (!opts.project && !opts.global)
|
|
826
|
+
throw new CliError("Choose --project or --global (scope is never inferred).");
|
|
827
|
+
if (opts.agent?.some((agent) => !/^[a-z][a-z0-9-]*$/.test(agent) && agent !== "*"))
|
|
828
|
+
throw new CliError("Invalid agent ID. Use skills CLI agent names, e.g. claude-code or codex.");
|
|
829
|
+
if (opts.yes && !opts.agent?.length)
|
|
830
|
+
throw new CliError("Non-interactive installs require --agent; Simbox will not choose agents for you.");
|
|
831
|
+
return [
|
|
832
|
+
"skills",
|
|
833
|
+
"add",
|
|
834
|
+
"TouchTechClub/simbox",
|
|
835
|
+
"--skill",
|
|
836
|
+
"simbox",
|
|
837
|
+
...opts.global ? ["--global"] : [],
|
|
838
|
+
...opts.agent?.length ? ["--agent", ...opts.agent] : [],
|
|
839
|
+
...opts.yes ? ["--yes"] : []
|
|
840
|
+
];
|
|
841
|
+
}
|
|
842
|
+
async function selectSkillScope(opts, interactive, ask) {
|
|
843
|
+
if (opts.project && opts.global)
|
|
844
|
+
throw new CliError("Choose either --project or --global, not both.");
|
|
845
|
+
if (opts.project || opts.global)
|
|
846
|
+
return opts;
|
|
847
|
+
if (!interactive || opts.yes)
|
|
848
|
+
throw new CliError("Specify --project or --global. For automation also provide --agent <id> --yes.");
|
|
849
|
+
while (true) {
|
|
850
|
+
const answer = (await ask(`Install the Simbox skill where?
|
|
851
|
+
1. Current project (${process.cwd()})
|
|
852
|
+
2. Global (your user account)
|
|
853
|
+
Choose 1 or 2 (q to cancel): `)).trim().toLowerCase();
|
|
854
|
+
if (answer === "1" || answer === "project")
|
|
855
|
+
return { ...opts, project: true };
|
|
856
|
+
if (answer === "2" || answer === "global")
|
|
857
|
+
return { ...opts, global: true };
|
|
858
|
+
if (answer === "q" || answer === "cancel")
|
|
859
|
+
throw new CliError("Skill installation canceled.");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function cmdSkillsInstall(opts) {
|
|
863
|
+
let selected = opts;
|
|
864
|
+
if (!opts.project && !opts.global) {
|
|
865
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || opts.yes) {
|
|
866
|
+
selected = await selectSkillScope(opts, false, async () => "");
|
|
867
|
+
} else {
|
|
868
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
869
|
+
try {
|
|
870
|
+
selected = await selectSkillScope(opts, true, (q) => prompt.question(q));
|
|
871
|
+
} finally {
|
|
872
|
+
prompt.close();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const args = skillInstallArgs(selected);
|
|
877
|
+
if (!opts.dryRun && !opts.yes && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
878
|
+
throw new CliError("Interactive skills installation needs a terminal. Use --project/--global --agent <id> --yes for automation.");
|
|
879
|
+
}
|
|
880
|
+
console.log(`Scope: ${selected.global ? "global" : `current project (${process.cwd()})`}`);
|
|
881
|
+
console.log(`Command: bunx ${args.map((arg) => arg === "*" ? "'*'" : arg).join(" ")}`);
|
|
882
|
+
console.log("This runs the third-party skills CLI. It handles agent selection and install confirmations; --yes accepts its installation/overwrite prompts.");
|
|
883
|
+
console.log("Set DISABLE_TELEMETRY=1 to opt out of skills CLI telemetry.");
|
|
884
|
+
if (opts.dryRun)
|
|
885
|
+
return;
|
|
886
|
+
await new Promise((resolve, reject) => {
|
|
887
|
+
const child = spawn2("bunx", args, {
|
|
888
|
+
cwd: process.cwd(),
|
|
889
|
+
env: process.env,
|
|
890
|
+
stdio: "inherit",
|
|
891
|
+
shell: false
|
|
892
|
+
});
|
|
893
|
+
child.once("error", (err) => reject(new CliError(`Could not start bunx. Install Bun or run the printed command manually. (${err.message})`)));
|
|
894
|
+
child.once("exit", (code, signal) => code === 0 ? resolve() : reject(new CliError(`Skills installer ${signal ? `stopped by ${signal}` : `exited with code ${code}`}.`, code ?? 1)));
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
683
898
|
// src/cli.ts
|
|
684
899
|
var program = new Command().enablePositionalOptions();
|
|
685
900
|
program.name("simbox").description("On-demand iOS simulators & Android emulators on your own GitHub Actions minutes.").version(getVersion());
|
|
@@ -691,7 +906,7 @@ program.command("init").description("Open onboarding and wait until a repo is co
|
|
|
691
906
|
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([
|
|
692
907
|
"ios",
|
|
693
908
|
"android"
|
|
694
|
-
])).option("--json", "print {tunnel_url, daemon_token, run_id, expires_at} as JSON only").action((opts) => {
|
|
909
|
+
])).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) => {
|
|
695
910
|
requireToken();
|
|
696
911
|
return run(cmdSim(opts));
|
|
697
912
|
});
|
|
@@ -712,6 +927,33 @@ program.command("repair").description("Re-commit the workflow, rotate the repo s
|
|
|
712
927
|
return run(cmdRepair());
|
|
713
928
|
});
|
|
714
929
|
program.command("logout").description("Delete the saved auth token").action(() => run(cmdLogout()));
|
|
930
|
+
var runners2 = program.command("runners").description("Account runner defaults and connected-repository overrides");
|
|
931
|
+
program.command("doctor").description("Check tools, runner settings and workflow without starting a job").option("--json", "print diagnostics as JSON").action((opts) => {
|
|
932
|
+
requireToken();
|
|
933
|
+
return run(cmdDoctor(opts.json));
|
|
934
|
+
});
|
|
935
|
+
runners2.command("show").option("--json", "print settings as JSON").action((opts) => {
|
|
936
|
+
requireToken();
|
|
937
|
+
return run(cmdRunnersShow(opts.json));
|
|
938
|
+
});
|
|
939
|
+
runners2.command("presets").description("List GitHub and Blacksmith presets").action(cmdRunnerPresets);
|
|
940
|
+
for (const command of ["set", "reset"]) {
|
|
941
|
+
const cmd = runners2.command(command).addArgument(new Argument("<platform>").choices(["ios", "android"]));
|
|
942
|
+
if (command === "set")
|
|
943
|
+
cmd.argument("<label-or-json>", "runner label or JSON label array");
|
|
944
|
+
cmd.addOption(new Option("--scope <scope>", "account defaults or connected repository override").choices(["account", "repo"]).makeOptionMandatory()).option("--json", "print settings as JSON");
|
|
945
|
+
if (command === "set")
|
|
946
|
+
cmd.action((platform2, input, opts) => {
|
|
947
|
+
requireToken();
|
|
948
|
+
return run(cmdRunnersSet(platform2, input, opts.scope, opts.json));
|
|
949
|
+
});
|
|
950
|
+
else
|
|
951
|
+
cmd.action((platform2, opts) => {
|
|
952
|
+
requireToken();
|
|
953
|
+
return run(cmdRunnersSet(platform2, null, opts.scope, opts.json));
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
program.command("skills").description("Set up the small Simbox agent skill").command("install").description("Install via bunx skills; asks current project or global").option("--project", "install to the current project").option("--global", "install to your user account").option("--agent <agents...>", "target skills CLI agent IDs; otherwise let the installer ask").option("--yes", "accept installer confirmations; requires explicit scope and agents").option("--dry-run", "print the command without installing").action((opts) => run(cmdSkillsInstall(opts)));
|
|
715
957
|
function getVersion() {
|
|
716
958
|
try {
|
|
717
959
|
const require2 = createRequire(import.meta.url);
|
|
@@ -725,9 +967,10 @@ async function run(p) {
|
|
|
725
967
|
try {
|
|
726
968
|
await p;
|
|
727
969
|
} catch (err) {
|
|
728
|
-
|
|
970
|
+
const error = friendlyError(err);
|
|
971
|
+
process.stderr.write(`${pc4.red("✖")} ${error.message}
|
|
729
972
|
`);
|
|
730
|
-
process.exitCode =
|
|
973
|
+
process.exitCode = error.exitCode;
|
|
731
974
|
}
|
|
732
975
|
}
|
|
733
976
|
program.parseAsync(process.argv).catch((err) => {
|