@touchtechclub/simbox 0.2.0 → 0.2.1
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 +139 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import pc4 from "picocolors";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
6
7
|
|
|
7
8
|
// src/config.ts
|
|
8
9
|
import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
@@ -223,32 +224,33 @@ function connectFlow(info, quiet = false) {
|
|
|
223
224
|
const base = daemonBaseUrl(info.tunnelUrl);
|
|
224
225
|
const bin = findAgentDevice();
|
|
225
226
|
if (bin) {
|
|
226
|
-
out.write(`${pc.green("▶")}
|
|
227
|
+
out.write(`${pc.green("▶")} Ready for ${pc.bold("agent-device")} via Simbox.
|
|
227
228
|
`);
|
|
228
|
-
const res = spawnSync(bin, ["connect", "proxy", "--daemon-base-url", base], {
|
|
229
|
-
stdio: quiet ? ["inherit", "ignore", "inherit"] : "inherit",
|
|
230
|
-
env: { ...process.env, AGENT_DEVICE_DAEMON_AUTH_TOKEN: info.daemonToken }
|
|
231
|
-
});
|
|
232
|
-
if (res.error || res.status !== 0) {
|
|
233
|
-
out.write(pc.yellow(` agent-device connect failed — use the manual commands below.
|
|
234
|
-
`));
|
|
235
|
-
} else {
|
|
236
|
-
out.write(`
|
|
237
|
-
Next: ${pc.cyan("agent-device open <app> --platform ios")} ${pc.dim("(or --platform android)")}
|
|
238
|
-
|
|
239
|
-
`);
|
|
240
|
-
}
|
|
241
229
|
} else {
|
|
242
230
|
out.write(`${pc.yellow("!")} ${pc.bold("agent-device")} not found on PATH — install it, then:
|
|
243
231
|
`);
|
|
244
232
|
}
|
|
233
|
+
out.write(`
|
|
234
|
+
${pc.cyan("simbox exec devices")}
|
|
235
|
+
`);
|
|
236
|
+
out.write(` ${pc.cyan('simbox exec open <app> --platform ios --device "<device from inventory>"')}
|
|
237
|
+
`);
|
|
238
|
+
out.write(` ${pc.cyan("simbox exec snapshot -i")}
|
|
239
|
+
`);
|
|
240
|
+
out.write(` Use --udid for iOS or --serial for Android when selecting a specific device.
|
|
241
|
+
`);
|
|
242
|
+
out.write(` exec supplies auth, refreshes tunnel URLs, and allows 5 minutes for cold startup.
|
|
243
|
+
|
|
244
|
+
`);
|
|
245
245
|
out.write(pc.dim(` manual connect:
|
|
246
246
|
`));
|
|
247
247
|
out.write(` ${pc.cyan(`export AGENT_DEVICE_DAEMON_AUTH_TOKEN=${info.daemonToken}`)}
|
|
248
248
|
`);
|
|
249
|
-
out.write(` ${pc.cyan(`
|
|
249
|
+
out.write(` ${pc.cyan(`export AGENT_DEVICE_DAEMON_BASE_URL=${base}`)}
|
|
250
|
+
`);
|
|
251
|
+
out.write(` ${pc.cyan(`agent-device open <app> --platform ios --udid <udid> --session simbox-${info.runId} --timeout 300000`)}
|
|
250
252
|
`);
|
|
251
|
-
out.write(`
|
|
253
|
+
out.write(` Raw agent-device commands need these exports again if the tunnel changes.
|
|
252
254
|
`);
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -356,6 +358,110 @@ class Spinner {
|
|
|
356
358
|
}
|
|
357
359
|
}
|
|
358
360
|
|
|
361
|
+
// src/remote.ts
|
|
362
|
+
import { spawn } from "node:child_process";
|
|
363
|
+
import { join as join2 } from "node:path";
|
|
364
|
+
var STARTUP_TIMEOUT_MS = 300000;
|
|
365
|
+
async function waitForRemote(initial, options = {}) {
|
|
366
|
+
const probe = options.probe ?? probeRemote;
|
|
367
|
+
const refresh = options.refresh ?? refreshRemote;
|
|
368
|
+
const deadline = Date.now() + (options.timeoutMs ?? 90000);
|
|
369
|
+
let info = initial;
|
|
370
|
+
do {
|
|
371
|
+
if (await probe(info))
|
|
372
|
+
return info;
|
|
373
|
+
const next = await refresh();
|
|
374
|
+
if (!next || next.runId !== initial.runId) {
|
|
375
|
+
throw new CliError("The run ended or was replaced. Run `simbox sim` to connect again.");
|
|
376
|
+
}
|
|
377
|
+
info = next;
|
|
378
|
+
await (options.pause?.() ?? sleep(1000));
|
|
379
|
+
} while (Date.now() < deadline);
|
|
380
|
+
throw new CliError("The run is live but its tunnel/daemon is not ready. Retry `simbox sim` or inspect the Actions logs. No device command was sent.");
|
|
381
|
+
}
|
|
382
|
+
async function probeRemote(info) {
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch(`${daemonBaseUrl(info.tunnelUrl)}/health`, {
|
|
385
|
+
signal: AbortSignal.timeout(15000)
|
|
386
|
+
});
|
|
387
|
+
if (!res.ok)
|
|
388
|
+
return false;
|
|
389
|
+
const body = await res.json();
|
|
390
|
+
return body.ok === true && body.upstream?.ok !== false;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function refreshRemote() {
|
|
396
|
+
const run = await currentRun();
|
|
397
|
+
if (run?.state !== "live" || !run.tunnelUrl || !run.daemonToken)
|
|
398
|
+
return null;
|
|
399
|
+
return {
|
|
400
|
+
runId: run.id,
|
|
401
|
+
tunnelUrl: run.tunnelUrl,
|
|
402
|
+
daemonToken: run.daemonToken,
|
|
403
|
+
expiresAt: run.expiresAt
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function remoteArgs(args, info, stateDir) {
|
|
407
|
+
const separator = args.indexOf("--");
|
|
408
|
+
const result = separator < 0 ? [...args] : args.slice(0, separator);
|
|
409
|
+
const tail = separator < 0 ? [] : args.slice(separator);
|
|
410
|
+
if (["open", "boot", "prepare"].includes(args[0] ?? "") && !hasFlag(args, "--timeout")) {
|
|
411
|
+
result.push("--timeout", String(STARTUP_TIMEOUT_MS));
|
|
412
|
+
}
|
|
413
|
+
if (!hasFlag(args, "--session"))
|
|
414
|
+
result.push("--session", `simbox-${info.runId}`);
|
|
415
|
+
result.push("--state-dir", stateDir, "--daemon-base-url", daemonBaseUrl(info.tunnelUrl), "--daemon-transport", "http");
|
|
416
|
+
return [...result, ...tail];
|
|
417
|
+
}
|
|
418
|
+
function hasFlag(args, flag) {
|
|
419
|
+
const end = args.indexOf("--");
|
|
420
|
+
return (end < 0 ? args : args.slice(0, end)).some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
421
|
+
}
|
|
422
|
+
async function cmdExec(args) {
|
|
423
|
+
if (!args.length)
|
|
424
|
+
throw new CliError("Usage: simbox exec <agent-device command> [arguments]");
|
|
425
|
+
const reserved = [
|
|
426
|
+
"--daemon-base-url",
|
|
427
|
+
"--daemon-auth-token",
|
|
428
|
+
"--daemon-transport",
|
|
429
|
+
"--remote-config",
|
|
430
|
+
"--state-dir"
|
|
431
|
+
];
|
|
432
|
+
if (reserved.some((flag) => hasFlag(args, flag))) {
|
|
433
|
+
throw new CliError("Simbox manages the remote URL, credentials and state directory for `exec`.");
|
|
434
|
+
}
|
|
435
|
+
if (["connect", "disconnect", "proxy", "auth"].includes(args[0])) {
|
|
436
|
+
throw new CliError("Use device commands with `simbox exec`, not connection/auth commands.");
|
|
437
|
+
}
|
|
438
|
+
const bin = findAgentDevice();
|
|
439
|
+
if (!bin)
|
|
440
|
+
throw new CliError("Install agent-device first: npm i -g agent-device@0.21.20");
|
|
441
|
+
const initial = await refreshRemote();
|
|
442
|
+
if (!initial)
|
|
443
|
+
throw new CliError("No live run. Start one with `simbox sim`.");
|
|
444
|
+
const info = await waitForRemote(initial);
|
|
445
|
+
const env = {
|
|
446
|
+
...process.env,
|
|
447
|
+
AGENT_DEVICE_DAEMON_AUTH_TOKEN: info.daemonToken
|
|
448
|
+
};
|
|
449
|
+
delete env.AGENT_DEVICE_REMOTE_CONFIG;
|
|
450
|
+
delete env.AGENT_DEVICE_DAEMON_BASE_URL;
|
|
451
|
+
const code = await new Promise((resolve, reject) => {
|
|
452
|
+
const child = spawn(bin, remoteArgs(args, info, join2(configDir(), "agent-device")), {
|
|
453
|
+
stdio: "inherit",
|
|
454
|
+
env
|
|
455
|
+
});
|
|
456
|
+
child.on("error", reject);
|
|
457
|
+
child.on("exit", (status) => resolve(status ?? 1));
|
|
458
|
+
});
|
|
459
|
+
if (code !== 0) {
|
|
460
|
+
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");
|
|
461
|
+
}
|
|
462
|
+
process.exitCode = code;
|
|
463
|
+
}
|
|
464
|
+
|
|
359
465
|
// src/commands.ts
|
|
360
466
|
async function cmdLogin() {
|
|
361
467
|
const flow = await startDeviceFlow();
|
|
@@ -484,6 +590,7 @@ async function cmdSim(opts) {
|
|
|
484
590
|
expiresAt: live.run.expiresAt
|
|
485
591
|
};
|
|
486
592
|
}
|
|
593
|
+
info = await waitForRemote(info);
|
|
487
594
|
if (json) {
|
|
488
595
|
const payload = {
|
|
489
596
|
tunnel_url: info.tunnelUrl,
|
|
@@ -545,17 +652,21 @@ async function cmdLogout() {
|
|
|
545
652
|
}
|
|
546
653
|
|
|
547
654
|
// src/cli.ts
|
|
548
|
-
var program = new Command;
|
|
549
|
-
program.name("simbox").description("On-demand iOS simulators & Android emulators on your own GitHub Actions minutes.").version(
|
|
655
|
+
var program = new Command().enablePositionalOptions();
|
|
656
|
+
program.name("simbox").description("On-demand iOS simulators & Android emulators on your own GitHub Actions minutes.").version(getVersion());
|
|
550
657
|
program.command("login").description("Authenticate via device flow").action(() => run(cmdLogin()));
|
|
551
658
|
program.command("init").description("Open onboarding and wait until a repo is connected").action(() => {
|
|
552
659
|
requireToken();
|
|
553
660
|
return run(cmdInit());
|
|
554
661
|
});
|
|
555
|
-
program.command("sim").description("Ensure a run
|
|
662
|
+
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").option("--json", "print {tunnel_url, daemon_token, run_id, expires_at} as JSON only").action((opts) => {
|
|
556
663
|
requireToken();
|
|
557
664
|
return run(cmdSim(opts));
|
|
558
665
|
});
|
|
666
|
+
program.command("exec").description("Run agent-device with fresh tunnel credentials and a cold-boot startup budget").argument("[args...]", "agent-device command and arguments").allowUnknownOption().allowExcessArguments().passThroughOptions().action((args) => {
|
|
667
|
+
requireToken();
|
|
668
|
+
return run(cmdExec(args));
|
|
669
|
+
});
|
|
559
670
|
program.command("ps").description("Show the current run").action(() => {
|
|
560
671
|
requireToken();
|
|
561
672
|
return run(cmdPs());
|
|
@@ -569,6 +680,15 @@ program.command("repair").description("Re-commit the workflow, rotate the repo s
|
|
|
569
680
|
return run(cmdRepair());
|
|
570
681
|
});
|
|
571
682
|
program.command("logout").description("Delete the saved auth token").action(() => run(cmdLogout()));
|
|
683
|
+
function getVersion() {
|
|
684
|
+
try {
|
|
685
|
+
const require2 = createRequire(import.meta.url);
|
|
686
|
+
const pkg = require2("../package.json");
|
|
687
|
+
return pkg.version ?? "0.0.0";
|
|
688
|
+
} catch {
|
|
689
|
+
return "0.0.0";
|
|
690
|
+
}
|
|
691
|
+
}
|
|
572
692
|
async function run(p) {
|
|
573
693
|
try {
|
|
574
694
|
await p;
|