@touchtechclub/simbox 0.0.0-stage → 0.1.7

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 (3) hide show
  1. package/dist/cli.js +585 -0
  2. package/package.json +36 -4
  3. package/README.md +0 -3
package/dist/cli.js ADDED
@@ -0,0 +1,585 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import pc4 from "picocolors";
6
+
7
+ // src/config.ts
8
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
9
+ import { homedir, platform } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ // ../../packages/shared/src/constants.ts
13
+ var PROD_API_URL = "https://api.simbox.touchtech.club";
14
+ // src/config.ts
15
+ var DEFAULT_WEB_URL = "https://simbox.touchtech.club";
16
+ function stripTrailingSlash(url) {
17
+ return url.replace(/\/+$/, "");
18
+ }
19
+ function apiBaseUrl() {
20
+ return stripTrailingSlash(process.env.SIMBOX_API_URL ?? PROD_API_URL);
21
+ }
22
+ function webBaseUrl() {
23
+ return stripTrailingSlash(process.env.SIMBOX_WEB_URL ?? DEFAULT_WEB_URL);
24
+ }
25
+ function configDir() {
26
+ const xdg = process.env.XDG_CONFIG_HOME;
27
+ if (xdg && xdg.length > 0)
28
+ return join(xdg, "simbox");
29
+ return join(homedir(), ".config", "simbox");
30
+ }
31
+ function authPath() {
32
+ return join(configDir(), "auth.json");
33
+ }
34
+ function loadToken() {
35
+ try {
36
+ const raw = readFileSync(authPath(), "utf8");
37
+ const parsed = JSON.parse(raw);
38
+ if (typeof parsed.token === "string" && parsed.token.length > 0) {
39
+ return parsed.token;
40
+ }
41
+ return null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function saveToken(token, login) {
47
+ mkdirSync(configDir(), { recursive: true });
48
+ const data = { token, login, createdAt: Date.now() };
49
+ writeFileSync(authPath(), JSON.stringify(data, null, 2) + `
50
+ `, {
51
+ encoding: "utf8",
52
+ mode: 384
53
+ });
54
+ try {
55
+ chmodSync(authPath(), 384);
56
+ } catch {
57
+ if (platform() === "win32")
58
+ return;
59
+ }
60
+ }
61
+ function requireToken() {
62
+ const token = loadToken();
63
+ if (!token) {
64
+ process.stderr.write("Not logged in. Run `simbox login` first.\n");
65
+ process.exit(1);
66
+ }
67
+ return token;
68
+ }
69
+
70
+ // src/client.ts
71
+ class CliError extends Error {
72
+ exitCode;
73
+ constructor(message, exitCode = 1) {
74
+ super(message);
75
+ this.exitCode = exitCode;
76
+ this.name = "CliError";
77
+ }
78
+ }
79
+
80
+ class ApiRequestError extends Error {
81
+ status;
82
+ code;
83
+ constructor(status, code, message) {
84
+ super(message);
85
+ this.status = status;
86
+ this.code = code;
87
+ this.name = "ApiRequestError";
88
+ }
89
+ }
90
+ async function api(path, opts = {}) {
91
+ const headers = {
92
+ accept: "application/json"
93
+ };
94
+ const useAuth = opts.auth !== false;
95
+ const token = opts.token ?? (useAuth ? loadToken() : null);
96
+ if (token)
97
+ headers.authorization = `Bearer ${token}`;
98
+ if (opts.body !== undefined)
99
+ headers["content-type"] = "application/json";
100
+ let res;
101
+ try {
102
+ res = await fetch(`${apiBaseUrl()}${path}`, {
103
+ method: opts.method ?? "GET",
104
+ headers,
105
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined
106
+ });
107
+ } catch (err) {
108
+ throw new CliError(`Could not reach ${apiBaseUrl()} — check your connection. (${err.message})`);
109
+ }
110
+ if (res.status === 401 || res.status === 403) {
111
+ throw new ApiRequestError(res.status, "unauthorized", "Your session is invalid or expired. Run `simbox login`.");
112
+ }
113
+ if (res.status === 404) {
114
+ const body = await parseErrorBody(res);
115
+ throw new ApiRequestError(404, body?.error ?? "not_found", body?.message ?? "Not found.");
116
+ }
117
+ if (!res.ok) {
118
+ const body = await parseErrorBody(res);
119
+ throw new ApiRequestError(res.status, body?.error ?? `http_${res.status}`, body?.message ?? `Request failed (HTTP ${res.status}).`);
120
+ }
121
+ return await res.json();
122
+ }
123
+ async function parseErrorBody(res) {
124
+ try {
125
+ return await res.json();
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ var CLI_CLIENT_ID = "simbox-cli";
131
+ var startDeviceFlow = () => api("/api/auth/device/code", {
132
+ method: "POST",
133
+ auth: false,
134
+ body: { client_id: CLI_CLIENT_ID }
135
+ });
136
+ async function pollDeviceToken(deviceCode) {
137
+ let res;
138
+ try {
139
+ res = await fetch(`${apiBaseUrl()}/api/auth/device/token`, {
140
+ method: "POST",
141
+ headers: { "content-type": "application/json" },
142
+ body: JSON.stringify({
143
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
144
+ device_code: deviceCode,
145
+ client_id: CLI_CLIENT_ID
146
+ })
147
+ });
148
+ } catch (err) {
149
+ throw new CliError(`Could not reach ${apiBaseUrl()} — check your connection. (${err.message})`);
150
+ }
151
+ const body = await res.json().catch(() => null);
152
+ if (res.ok && body?.access_token)
153
+ return { ok: true, token: body.access_token };
154
+ return {
155
+ ok: false,
156
+ error: body?.error ?? `http_${res.status}`,
157
+ description: body?.error_description
158
+ };
159
+ }
160
+ var me = async (token) => (await api("/v1/me", { token })).user;
161
+ var repoStatus = () => api("/v1/repo/status");
162
+ var repairRepo = () => api("/v1/repo/repair", {
163
+ method: "POST"
164
+ });
165
+ var ensureRun = (fresh) => api("/v1/runs/ensure", { method: "POST", body: { new: fresh } });
166
+ async function currentRun() {
167
+ try {
168
+ const res = await api("/v1/runs/current");
169
+ return res.run;
170
+ } catch (err) {
171
+ if (err instanceof ApiRequestError && (err.status === 404 || err.code === "no_run")) {
172
+ return null;
173
+ }
174
+ throw err;
175
+ }
176
+ }
177
+ var stopRun = (runId) => api(`/v1/runs/${encodeURIComponent(runId)}/stop`, {
178
+ method: "POST"
179
+ });
180
+ function friendlyError(err) {
181
+ if (err instanceof CliError)
182
+ return err;
183
+ if (err instanceof ApiRequestError) {
184
+ if (err.code === "repo_not_connected" || err.code === "no_repo") {
185
+ return new CliError(`${err.message}
186
+ Run \`simbox init\` to connect a repo.`);
187
+ }
188
+ if (err.code === "at_capacity") {
189
+ return new CliError(`${err.message}
190
+ That run is full — try \`simbox sim --new\` for a fresh one.`);
191
+ }
192
+ if (err.code === "needs_repair" || err.code === "repo_needs_repair" || err.code === "repo_not_ready" || err.code === "dispatch_failed") {
193
+ return new CliError(`${err.message}
194
+ Run \`simbox repair\` to fix the repo setup.`);
195
+ }
196
+ return new CliError(err.message);
197
+ }
198
+ if (err instanceof Error)
199
+ return new CliError(err.message);
200
+ return new CliError(String(err));
201
+ }
202
+
203
+ // src/commands.ts
204
+ import { rmSync } from "node:fs";
205
+ import pc3 from "picocolors";
206
+
207
+ // src/connect.ts
208
+ import { spawnSync } from "node:child_process";
209
+ import pc from "picocolors";
210
+ function daemonBaseUrl(tunnelUrl) {
211
+ return `${tunnelUrl.replace(/\/+$/, "")}/agent-device`;
212
+ }
213
+ function findAgentDevice() {
214
+ const which = process.platform === "win32" ? "where" : "which";
215
+ const res = spawnSync(which, ["agent-device"], { encoding: "utf8" });
216
+ if (res.status !== 0)
217
+ return null;
218
+ const first = (res.stdout ?? "").split(/\r?\n/).find((l) => l.trim().length > 0);
219
+ return first?.trim() ?? null;
220
+ }
221
+ function connectFlow(info, quiet = false) {
222
+ const out = quiet ? process.stderr : process.stdout;
223
+ const base = daemonBaseUrl(info.tunnelUrl);
224
+ const bin = findAgentDevice();
225
+ if (bin) {
226
+ out.write(`${pc.green("▶")} Connecting via ${pc.bold("agent-device")}…
227
+ `);
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
+ } else {
242
+ out.write(`${pc.yellow("!")} ${pc.bold("agent-device")} not found on PATH — install it, then:
243
+ `);
244
+ }
245
+ out.write(pc.dim(` manual connect:
246
+ `));
247
+ out.write(` ${pc.cyan(`export AGENT_DEVICE_DAEMON_AUTH_TOKEN=${info.daemonToken}`)}
248
+ `);
249
+ out.write(` ${pc.cyan(`agent-device connect proxy --daemon-base-url ${base}`)}
250
+ `);
251
+ out.write(` ${pc.cyan("agent-device open <app> --platform ios")}
252
+ `);
253
+ }
254
+
255
+ // src/open.ts
256
+ import open from "open";
257
+ async function openInBrowser(url) {
258
+ try {
259
+ await open(url);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+
266
+ // src/util.ts
267
+ import pc2 from "picocolors";
268
+ var isTTY = process.stderr.isTTY === true;
269
+ function sleep(ms) {
270
+ return new Promise((resolve) => setTimeout(resolve, ms));
271
+ }
272
+ function fmtElapsed(ms) {
273
+ const s = Math.max(0, Math.floor(ms / 1000));
274
+ const m = Math.floor(s / 60);
275
+ const r = s % 60;
276
+ return `${m}:${String(r).padStart(2, "0")}`;
277
+ }
278
+ function fmtCountdown(expiresAt) {
279
+ const ms = expiresAt - Date.now();
280
+ if (ms <= 0)
281
+ return "expired";
282
+ const s = Math.floor(ms / 1000);
283
+ const h = Math.floor(s / 3600);
284
+ const m = Math.floor(s % 3600 / 60);
285
+ const sec = s % 60;
286
+ if (h > 0)
287
+ return `${h}h ${m}m`;
288
+ if (m > 0)
289
+ return `${m}m ${sec}s`;
290
+ return `${sec}s`;
291
+ }
292
+ var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
293
+
294
+ class Spinner {
295
+ label;
296
+ frame = 0;
297
+ timer = null;
298
+ lastMsg = "";
299
+ start = Date.now();
300
+ constructor(label) {
301
+ this.label = label;
302
+ }
303
+ begin() {
304
+ this.start = Date.now();
305
+ if (!isTTY) {
306
+ process.stderr.write(`${this.label}
307
+ `);
308
+ this.lastMsg = this.label;
309
+ return;
310
+ }
311
+ this.render();
312
+ this.timer = setInterval(() => this.render(), 80);
313
+ }
314
+ set(label) {
315
+ if (label === this.label)
316
+ return;
317
+ this.label = label;
318
+ if (!isTTY) {
319
+ process.stderr.write(`${label}
320
+ `);
321
+ this.lastMsg = label;
322
+ return;
323
+ }
324
+ this.render();
325
+ }
326
+ render() {
327
+ const glyph = FRAMES[this.frame++ % FRAMES.length] ?? "⠋";
328
+ const elapsed = pc2.dim(`(${fmtElapsed(Date.now() - this.start)})`);
329
+ this.clearLine();
330
+ process.stderr.write(`${pc2.cyan(glyph)} ${this.label} ${elapsed}`);
331
+ }
332
+ clearLine() {
333
+ if (isTTY)
334
+ process.stderr.write("\r\x1B[K");
335
+ }
336
+ stop() {
337
+ if (this.timer)
338
+ clearInterval(this.timer);
339
+ this.timer = null;
340
+ this.clearLine();
341
+ }
342
+ succeed(msg) {
343
+ this.finish(pc2.green("✔"), msg);
344
+ }
345
+ fail(msg) {
346
+ this.finish(pc2.red("✖"), msg);
347
+ }
348
+ finish(symbol, msg) {
349
+ if (this.timer)
350
+ clearInterval(this.timer);
351
+ this.timer = null;
352
+ this.clearLine();
353
+ const elapsed = pc2.dim(`(${fmtElapsed(Date.now() - this.start)})`);
354
+ process.stderr.write(`${symbol} ${msg} ${elapsed}
355
+ `);
356
+ }
357
+ }
358
+
359
+ // src/commands.ts
360
+ async function cmdLogin() {
361
+ const flow = await startDeviceFlow();
362
+ console.log(`
363
+ Your login code: ${pc3.bold(pc3.cyan(flow.user_code))}
364
+ `);
365
+ const verifyUrl = flow.verification_uri_complete ?? flow.verification_uri;
366
+ const opened = await openInBrowser(verifyUrl);
367
+ console.log(opened ? `Opened ${pc3.underline(verifyUrl)} — approve the code in your browser.` : `Open ${pc3.underline(verifyUrl)} to approve the code.`);
368
+ const spinner = new Spinner("Waiting for approval");
369
+ spinner.begin();
370
+ const deadline = Date.now() + flow.expires_in * 1000;
371
+ let interval = Math.max(1, flow.interval) * 1000;
372
+ let token = null;
373
+ while (Date.now() < deadline) {
374
+ await sleep(interval);
375
+ const res = await pollDeviceToken(flow.device_code);
376
+ if (res.ok) {
377
+ token = res.token;
378
+ break;
379
+ }
380
+ switch (res.error) {
381
+ case "authorization_pending":
382
+ continue;
383
+ case "slow_down":
384
+ interval += 5000;
385
+ continue;
386
+ case "access_denied":
387
+ spinner.fail("Denied");
388
+ throw new CliError("Login was denied in the browser.");
389
+ case "expired_token":
390
+ spinner.fail("Device code expired");
391
+ throw new CliError("Login code expired — run `simbox login` again.");
392
+ default:
393
+ spinner.fail("Login failed");
394
+ throw new CliError(`Login failed: ${res.description ?? res.error}. Run \`simbox login\` again.`);
395
+ }
396
+ }
397
+ if (!token) {
398
+ spinner.fail("Timed out");
399
+ throw new CliError("Login timed out — run `simbox login` again.");
400
+ }
401
+ const user = await me(token);
402
+ saveToken(token, user.login);
403
+ spinner.succeed(`Logged in as ${pc3.bold(user.login)}`);
404
+ }
405
+ var INIT_TIMEOUT_MS = 10 * 60 * 1000;
406
+ var INIT_POLL_MS = 3000;
407
+ async function cmdInit() {
408
+ const first = await repoStatus();
409
+ if (first.connected && first.repo) {
410
+ printRepoConnected(first.repo);
411
+ return;
412
+ }
413
+ const url = `${webBaseUrl()}/onboarding`;
414
+ const opened = await openInBrowser(url);
415
+ console.log(opened ? `Opened ${pc3.underline(url)} — install the GitHub App and pick a repo.` : `Open ${pc3.underline(url)} to install the GitHub App and pick a repo.`);
416
+ const spinner = new Spinner("Waiting for repo connection");
417
+ spinner.begin();
418
+ const deadline = Date.now() + INIT_TIMEOUT_MS;
419
+ while (Date.now() < deadline) {
420
+ await sleep(INIT_POLL_MS);
421
+ const status = await repoStatus();
422
+ if (status.connected && status.repo) {
423
+ spinner.succeed("Repo connected");
424
+ printRepoConnected(status.repo);
425
+ return;
426
+ }
427
+ }
428
+ spinner.fail("Timed out waiting for repo connection");
429
+ throw new CliError(`No repo connected after 10 minutes. Finish onboarding at ${webBaseUrl()}/onboarding then re-run \`simbox init\`.`);
430
+ }
431
+ function printRepoConnected(repo) {
432
+ console.log(`Repo connected: ${pc3.bold(repo.fullName)} ${pc3.dim(`(${repo.state})`)}`);
433
+ if (repo.state === "pending_pr" && repo.prUrl) {
434
+ console.log(`Your default branch is protected — merge ${pc3.underline(repo.prUrl)} to activate Simbox.`);
435
+ }
436
+ if (repo.private) {
437
+ console.log(pc3.yellow("Note: private repos are billed at the 10× macOS minute multiplier by GitHub."));
438
+ }
439
+ console.log(`Run ${pc3.cyan("simbox sim")} to boot a simulator.`);
440
+ }
441
+ var SIM_TIMEOUT_MS = 8 * 60 * 1000;
442
+ var SIM_POLL_MS = 3000;
443
+ async function cmdSim(opts) {
444
+ const json = opts.json === true;
445
+ const out = json ? process.stderr : process.stdout;
446
+ const first = await ensureRun(opts.new === true);
447
+ let info;
448
+ if (first.state === "live") {
449
+ info = {
450
+ runId: first.runId,
451
+ tunnelUrl: first.tunnelUrl,
452
+ daemonToken: first.daemonToken,
453
+ expiresAt: first.expiresAt
454
+ };
455
+ } else {
456
+ const spinner = new Spinner(`Run ${first.state}`);
457
+ spinner.begin();
458
+ const deadline = Date.now() + SIM_TIMEOUT_MS;
459
+ let live = null;
460
+ while (Date.now() < deadline) {
461
+ await sleep(SIM_POLL_MS);
462
+ const run = await currentRun();
463
+ if (!run)
464
+ continue;
465
+ if (run.state === "live" && run.tunnelUrl && run.daemonToken) {
466
+ live = { run, tunnelUrl: run.tunnelUrl, daemonToken: run.daemonToken };
467
+ break;
468
+ }
469
+ if (run.state === "failed" || run.state === "ended" || run.state === "closing") {
470
+ spinner.fail(`Run ${run.state}`);
471
+ throw new CliError(`The run ${run.state} before coming online (${run.endReason ?? "no reason given"}). Try \`simbox sim --new\`.`);
472
+ }
473
+ spinner.set(`Run ${run.state}`);
474
+ }
475
+ if (!live) {
476
+ spinner.fail("Timed out");
477
+ throw new CliError("Run didn't come online within 8 minutes. Check the workflow run in your repo's Actions tab, then try `simbox sim` again (or `--new`).");
478
+ }
479
+ spinner.succeed("Run is live");
480
+ info = {
481
+ runId: live.run.id,
482
+ tunnelUrl: live.tunnelUrl,
483
+ daemonToken: live.daemonToken,
484
+ expiresAt: live.run.expiresAt
485
+ };
486
+ }
487
+ if (json) {
488
+ const payload = {
489
+ tunnel_url: info.tunnelUrl,
490
+ daemon_token: info.daemonToken,
491
+ run_id: info.runId,
492
+ expires_at: info.expiresAt
493
+ };
494
+ process.stdout.write(JSON.stringify(payload) + `
495
+ `);
496
+ return;
497
+ }
498
+ out.write(`Run ${pc3.bold(info.runId)} is ${pc3.green("live")}` + (info.expiresAt ? ` — expires in ${fmtCountdown(info.expiresAt)}` : "") + `
499
+
500
+ `);
501
+ connectFlow(info, false);
502
+ }
503
+ async function cmdPs() {
504
+ const run = await currentRun();
505
+ if (!run) {
506
+ console.log(`No active run. Start one with ${pc3.cyan("simbox sim")}.`);
507
+ return;
508
+ }
509
+ const stateColor = run.state === "live" ? pc3.green : run.state === "failed" ? pc3.red : pc3.yellow;
510
+ console.log(`${pc3.bold("run")} ${run.id}`);
511
+ console.log(` state: ${stateColor(run.state)}`);
512
+ console.log(` gh_run_id: ${run.ghRunId ?? pc3.dim("pending")}`);
513
+ console.log(` active_devices: ${run.activeDevices}`);
514
+ console.log(` android_ready: ${run.androidReady ? "yes" : "no"}`);
515
+ console.log(` expires: ${run.expiresAt ? fmtCountdown(run.expiresAt) : pc3.dim("n/a")}`);
516
+ if (run.endReason)
517
+ console.log(` end_reason: ${run.endReason}`);
518
+ }
519
+ async function cmdStop() {
520
+ const run = await currentRun();
521
+ if (!run) {
522
+ console.log("No active run to stop.");
523
+ return;
524
+ }
525
+ await stopRun(run.id);
526
+ console.log(`${pc3.green("✔")} Stop requested for run ${pc3.bold(run.id)}.`);
527
+ }
528
+ async function cmdRepair() {
529
+ const spinner = new Spinner("Repairing repo");
530
+ spinner.begin();
531
+ const res = await repairRepo();
532
+ spinner.succeed("Repair complete");
533
+ if (res.message)
534
+ console.log(res.message);
535
+ else
536
+ console.log("Workflow re-committed, secret rotated, installation verified.");
537
+ }
538
+ async function cmdLogout() {
539
+ try {
540
+ rmSync(authPath());
541
+ console.log(`${pc3.green("✔")} Logged out.`);
542
+ } catch {
543
+ console.log("Already logged out.");
544
+ }
545
+ }
546
+
547
+ // 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("0.1.0");
550
+ program.command("login").description("Authenticate via device flow").action(() => run(cmdLogin()));
551
+ program.command("init").description("Open onboarding and wait until a repo is connected").action(() => {
552
+ requireToken();
553
+ return run(cmdInit());
554
+ });
555
+ program.command("sim").description("Ensure a run is live and connect agent-device to it").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
+ requireToken();
557
+ return run(cmdSim(opts));
558
+ });
559
+ program.command("ps").description("Show the current run").action(() => {
560
+ requireToken();
561
+ return run(cmdPs());
562
+ });
563
+ program.command("stop").description("Cancel the active run").action(() => {
564
+ requireToken();
565
+ return run(cmdStop());
566
+ });
567
+ program.command("repair").description("Re-commit the workflow, rotate the repo secret, re-verify the installation").action(() => {
568
+ requireToken();
569
+ return run(cmdRepair());
570
+ });
571
+ program.command("logout").description("Delete the saved auth token").action(() => run(cmdLogout()));
572
+ async function run(p) {
573
+ try {
574
+ await p;
575
+ } catch (err) {
576
+ process.stderr.write(`${pc4.red("✖")} ${friendlyError(err).message}
577
+ `);
578
+ process.exitCode = 1;
579
+ }
580
+ }
581
+ program.parseAsync(process.argv).catch((err) => {
582
+ process.stderr.write(`${pc4.red("✖")} ${friendlyError(err).message}
583
+ `);
584
+ process.exitCode = 1;
585
+ });
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@touchtechclub/simbox",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.7",
4
+ "description": "On-demand iOS simulators & Android emulators on your own GitHub Actions minutes.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/TouchTechClub/simbox",
8
+ "directory": "apps/cli"
9
+ },
10
+ "bin": {
11
+ "simbox": "dist/cli.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "bun build src/cli.ts --outdir dist --target node --format esm --external commander --external open --external picocolors && chmod +x dist/cli.js",
22
+ "dev": "bun run src/cli.ts",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "commander": "^12.1.0",
27
+ "open": "^10.1.0",
28
+ "picocolors": "^1.1.1"
29
+ },
30
+ "devDependencies": {
31
+ "@simbox/shared": "*",
32
+ "@types/node": "^20.0.0",
33
+ "typescript": "catalog:"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.