@yoqa/cli 0.3.10 → 0.3.12

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/README.md +7 -1
  2. package/dist/main.js +444 -11
  3. package/package.json +15 -3
package/README.md CHANGED
@@ -10,6 +10,12 @@ npx @yoqa/cli health
10
10
 
11
11
  After a global install, the `yoqa` command is on your PATH.
12
12
 
13
- Requires a running Yoqa runner (desktop app, or `bun run runner` from a monorepo checkout) at `http://127.0.0.1:7420` by default.
13
+ The CLI talks to the local runner at `http://127.0.0.1:7420` by default. If nothing is listening, `yoqa` auto-starts `@yoqa/runner` (requires [Bun](https://bun.sh)). You can also run it in the foreground:
14
+
15
+ ```bash
16
+ yoqa serve
17
+ ```
18
+
19
+ Disable auto-start with `YOQA_NO_AUTOSTART=1`. Override the URL with `YOQA_RUNNER_HOST` / `YOQA_RUNNER_PORT` / `YOQA_RUNNER_URL`.
14
20
 
15
21
  See [CLI docs](https://yoqa.mintlify.app/docs/cli).
package/dist/main.js CHANGED
@@ -2122,7 +2122,7 @@ var require_commander = __commonJS((exports) => {
2122
2122
  });
2123
2123
 
2124
2124
  // src/main.ts
2125
- import { readFile, writeFile } from "node:fs/promises";
2125
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2126
2126
 
2127
2127
  // ../../node_modules/zod/v3/external.js
2128
2128
  var exports_external = {};
@@ -7850,14 +7850,16 @@ var {
7850
7850
  // package.json
7851
7851
  var package_default = {
7852
7852
  name: "@yoqa/cli",
7853
- version: "0.3.10",
7853
+ version: "0.3.12",
7854
7854
  description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
7855
7855
  license: "UNLICENSED",
7856
7856
  type: "module",
7857
7857
  bin: {
7858
7858
  yoqa: "dist/main.js"
7859
7859
  },
7860
- files: ["dist"],
7860
+ files: [
7861
+ "dist"
7862
+ ],
7861
7863
  engines: {
7862
7864
  node: ">=20"
7863
7865
  },
@@ -7870,7 +7872,14 @@ var package_default = {
7870
7872
  bugs: {
7871
7873
  url: "https://github.com/Aris-ngoy/qa-agent/issues"
7872
7874
  },
7873
- keywords: ["yoqa", "cli", "mobile", "qa", "appium", "testing"],
7875
+ keywords: [
7876
+ "yoqa",
7877
+ "cli",
7878
+ "mobile",
7879
+ "qa",
7880
+ "appium",
7881
+ "testing"
7882
+ ],
7874
7883
  scripts: {
7875
7884
  yoqa: "bun run src/main.ts",
7876
7885
  build: "bun run build.ts",
@@ -7878,6 +7887,9 @@ var package_default = {
7878
7887
  check: "tsc --noEmit",
7879
7888
  health: "bun run src/main.ts health"
7880
7889
  },
7890
+ dependencies: {
7891
+ "@yoqa/runner": "0.3.12"
7892
+ },
7881
7893
  devDependencies: {
7882
7894
  "@types/node": "^22.15.30",
7883
7895
  "@yoqa/runner-client": "workspace:*",
@@ -7890,15 +7902,402 @@ var package_default = {
7890
7902
  }
7891
7903
  };
7892
7904
 
7893
- // src/main.ts
7894
- function runnerBaseUrl() {
7895
- const host = process.env.YOQA_RUNNER_HOST ?? "127.0.0.1";
7896
- const port = Number(process.env.YOQA_RUNNER_PORT ?? "7420");
7905
+ // src/ensure-runner.ts
7906
+ import { spawn } from "node:child_process";
7907
+ import { existsSync } from "node:fs";
7908
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
7909
+ import { createRequire as createRequire2 } from "node:module";
7910
+ import { homedir as osHomedir } from "node:os";
7911
+ import { dirname, join } from "node:path";
7912
+ import { fileURLToPath } from "node:url";
7913
+ var DEFAULT_RUNNER_HOST = "127.0.0.1";
7914
+ var DEFAULT_RUNNER_PORT = 7420;
7915
+ var HEALTH_POLL_MS = 400;
7916
+ var HEALTH_TIMEOUT_MS = 20000;
7917
+
7918
+ class RunnerStartError extends Error {
7919
+ constructor(message) {
7920
+ super(message);
7921
+ this.name = "RunnerStartError";
7922
+ }
7923
+ }
7924
+ function runnerBaseUrlFromEnv(env = process.env) {
7925
+ const fromUrl = env.YOQA_RUNNER_URL?.trim();
7926
+ if (fromUrl)
7927
+ return fromUrl.replace(/\/$/, "");
7928
+ const host = env.YOQA_RUNNER_HOST ?? DEFAULT_RUNNER_HOST;
7929
+ const port = Number(env.YOQA_RUNNER_PORT ?? String(DEFAULT_RUNNER_PORT));
7897
7930
  if (!Number.isFinite(port) || port <= 0) {
7898
7931
  throw new Error("YOQA_RUNNER_PORT must be a positive number");
7899
7932
  }
7900
7933
  return `http://${host}:${port}`;
7901
7934
  }
7935
+ function shouldSkipAutostart(env = process.env) {
7936
+ const raw = env.YOQA_NO_AUTOSTART?.trim().toLowerCase();
7937
+ return raw === "1" || raw === "true" || raw === "yes";
7938
+ }
7939
+ function isRunnerHealth(value) {
7940
+ if (typeof value !== "object" || value === null)
7941
+ return false;
7942
+ const record = value;
7943
+ return record.ok === true && record.service === "yoqa-runner" && typeof record.version === "string" && record.version.length > 0;
7944
+ }
7945
+ function execRootsFrom(execPath, argv0) {
7946
+ const roots = [];
7947
+ for (const candidate of [execPath, argv0]) {
7948
+ if (!candidate)
7949
+ continue;
7950
+ const dir = dirname(candidate);
7951
+ if (!roots.includes(dir))
7952
+ roots.push(dir);
7953
+ }
7954
+ return roots;
7955
+ }
7956
+ function packagedYoqaRunnerCandidates(roots) {
7957
+ const fileName = "yoqa-runner";
7958
+ const paths = [];
7959
+ for (const root of roots) {
7960
+ paths.push(join(root, fileName), join(root, "../Resources/app.asar.unpacked/runner", fileName), join(root, "../Resources/runner", fileName), join(root, "Resources/app.asar.unpacked/runner", fileName), join(root, "Resources/runner", fileName));
7961
+ }
7962
+ return paths;
7963
+ }
7964
+ function yoqaRoot(homedir) {
7965
+ return join(homedir, ".yoqa");
7966
+ }
7967
+ function runnerPidPath(homedir) {
7968
+ return join(yoqaRoot(homedir), "runner.pid");
7969
+ }
7970
+ var BUN_MISSING_MESSAGE = `Could not find Bun, which is required to start the Yoqa runner.
7971
+ Install: https://bun.sh (curl -fsSL https://bun.sh/install | bash)
7972
+ Or set YOQA_RUNNER_BIN to a yoqa-runner binary.`;
7973
+ var RUNNER_NOT_FOUND_MESSAGE = "Could not locate the Yoqa runner. Install @yoqa/cli (includes @yoqa/runner) and Bun, or set YOQA_RUNNER_BIN.";
7974
+ var REPO_MARKER = join("services", "runner", "src", "index.ts");
7975
+ async function findRepoRoot(starts, pathExists) {
7976
+ for (const start of starts) {
7977
+ if (!start)
7978
+ continue;
7979
+ let dir = start;
7980
+ for (let i = 0;i < 14; i++) {
7981
+ if (await pathExists(join(dir, REPO_MARKER)))
7982
+ return dir;
7983
+ const parent = dirname(dir);
7984
+ if (parent === dir)
7985
+ break;
7986
+ dir = parent;
7987
+ }
7988
+ }
7989
+ return null;
7990
+ }
7991
+ async function resolvePublishedRunnerEntry(fromUrl) {
7992
+ try {
7993
+ const require2 = createRequire2(fromUrl);
7994
+ const pkgJson = require2.resolve("@yoqa/runner/package.json");
7995
+ return join(dirname(pkgJson), "dist", "index.js");
7996
+ } catch {
7997
+ return null;
7998
+ }
7999
+ }
8000
+ async function resolveRunnerLaunch(deps) {
8001
+ const binEnv = deps.env.YOQA_RUNNER_BIN?.trim();
8002
+ if (binEnv && await deps.pathExists(binEnv)) {
8003
+ return { command: [binEnv], source: "env" };
8004
+ }
8005
+ const onPath = await deps.which("yoqa-runner");
8006
+ if (onPath) {
8007
+ return { command: [onPath], source: "path" };
8008
+ }
8009
+ const packagedCandidates = packagedYoqaRunnerCandidates(execRootsFrom(deps.execPath, deps.argv0));
8010
+ for (const candidate of packagedCandidates) {
8011
+ if (await deps.pathExists(candidate)) {
8012
+ return { command: [candidate], source: "packaged" };
8013
+ }
8014
+ }
8015
+ const fromPath = fileURLToPath(deps.fromUrl);
8016
+ const repoRoot = await findRepoRoot([deps.env.YOQA_REPO_ROOT?.trim() ?? "", deps.cwd, dirname(fromPath)], deps.pathExists);
8017
+ if (repoRoot) {
8018
+ const bun = await deps.findBun();
8019
+ if (!bun)
8020
+ throw new RunnerStartError(BUN_MISSING_MESSAGE);
8021
+ const entry = join(repoRoot, REPO_MARKER);
8022
+ return {
8023
+ command: [bun, "run", entry],
8024
+ cwd: join(repoRoot, "services", "runner"),
8025
+ source: "monorepo"
8026
+ };
8027
+ }
8028
+ const pkgEntry = await deps.resolvePackageEntry();
8029
+ if (pkgEntry && await deps.pathExists(pkgEntry)) {
8030
+ const bun = await deps.findBun();
8031
+ if (!bun)
8032
+ throw new RunnerStartError(BUN_MISSING_MESSAGE);
8033
+ return { command: [bun, pkgEntry], source: "package" };
8034
+ }
8035
+ throw new RunnerStartError(RUNNER_NOT_FOUND_MESSAGE);
8036
+ }
8037
+ async function fetchRunnerHealth(baseUrl, timeoutMs = 1500) {
8038
+ try {
8039
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
8040
+ signal: AbortSignal.timeout(timeoutMs)
8041
+ });
8042
+ if (!response.ok)
8043
+ return null;
8044
+ const json = await response.json();
8045
+ return isRunnerHealth(json) ? json : null;
8046
+ } catch {
8047
+ return null;
8048
+ }
8049
+ }
8050
+ async function waitForRunnerHealth(fetchHealth, options) {
8051
+ const deadline = options.now() + options.timeoutMs;
8052
+ while (options.now() < deadline) {
8053
+ const health = await fetchHealth();
8054
+ if (health)
8055
+ return health;
8056
+ if (options.isAlive && !options.isAlive())
8057
+ return null;
8058
+ await options.sleep(options.intervalMs);
8059
+ }
8060
+ return fetchHealth();
8061
+ }
8062
+ function pathWithHostTools(env, homedir) {
8063
+ const extras = [
8064
+ "/opt/homebrew/bin",
8065
+ "/opt/homebrew/sbin",
8066
+ "/usr/local/bin",
8067
+ join(homedir, ".local", "bin"),
8068
+ join(homedir, ".bun", "bin")
8069
+ ];
8070
+ const existing = (env.PATH ?? "").split(":").filter(Boolean);
8071
+ const prepend = extras.filter((dir) => !existing.includes(dir));
8072
+ return [...prepend, ...existing].join(":");
8073
+ }
8074
+ async function defaultPathExists(path) {
8075
+ return existsSync(path);
8076
+ }
8077
+ async function defaultWhich(bin) {
8078
+ const { execFile } = await import("node:child_process");
8079
+ return new Promise((resolve) => {
8080
+ execFile("which", [bin], (error, stdout) => {
8081
+ if (error) {
8082
+ resolve(null);
8083
+ return;
8084
+ }
8085
+ const path = stdout.trim().split(`
8086
+ `)[0]?.trim();
8087
+ resolve(path || null);
8088
+ });
8089
+ });
8090
+ }
8091
+ async function defaultFindBun(pathExists, which, homedir, env) {
8092
+ const fromEnv = env.BUN_INSTALL ? join(env.BUN_INSTALL, "bin", "bun") : null;
8093
+ const candidates = [
8094
+ fromEnv,
8095
+ join(homedir, ".bun", "bin", "bun"),
8096
+ "/opt/homebrew/bin/bun",
8097
+ "/usr/local/bin/bun"
8098
+ ].filter((path) => Boolean(path));
8099
+ for (const candidate of candidates) {
8100
+ if (await pathExists(candidate))
8101
+ return candidate;
8102
+ }
8103
+ return which("bun");
8104
+ }
8105
+ function defaultIsPidAlive(pid) {
8106
+ try {
8107
+ process.kill(pid, 0);
8108
+ return true;
8109
+ } catch {
8110
+ return false;
8111
+ }
8112
+ }
8113
+ function spawnEnv(env, homedir) {
8114
+ return {
8115
+ ...env,
8116
+ PATH: pathWithHostTools(env, homedir),
8117
+ YOQA_RUNNER_HOST: env.YOQA_RUNNER_HOST ?? DEFAULT_RUNNER_HOST,
8118
+ YOQA_RUNNER_PORT: env.YOQA_RUNNER_PORT ?? String(DEFAULT_RUNNER_PORT)
8119
+ };
8120
+ }
8121
+ function defaultSpawnDetached(launch, env, homedir) {
8122
+ const [bin, ...args] = launch.command;
8123
+ if (!bin)
8124
+ throw new RunnerStartError("Runner launch command is empty");
8125
+ const child = spawn(bin, args, {
8126
+ cwd: launch.cwd,
8127
+ detached: true,
8128
+ stdio: "ignore",
8129
+ env: spawnEnv(env, homedir)
8130
+ });
8131
+ if (child.pid == null) {
8132
+ throw new RunnerStartError("Failed to spawn the Yoqa runner");
8133
+ }
8134
+ child.unref();
8135
+ return { pid: child.pid };
8136
+ }
8137
+ function defaultSpawnForeground(launch, env, homedir) {
8138
+ const [bin, ...args] = launch.command;
8139
+ if (!bin)
8140
+ throw new RunnerStartError("Runner launch command is empty");
8141
+ const child = spawn(bin, args, {
8142
+ cwd: launch.cwd,
8143
+ stdio: "inherit",
8144
+ env: spawnEnv(env, homedir)
8145
+ });
8146
+ if (child.pid == null) {
8147
+ throw new RunnerStartError("Failed to spawn the Yoqa runner");
8148
+ }
8149
+ const forward = (signal) => {
8150
+ try {
8151
+ child.kill(signal);
8152
+ } catch {}
8153
+ };
8154
+ process.on("SIGINT", forward);
8155
+ process.on("SIGTERM", forward);
8156
+ const exited = new Promise((resolve, reject) => {
8157
+ child.on("error", (error) => {
8158
+ process.off("SIGINT", forward);
8159
+ process.off("SIGTERM", forward);
8160
+ if ("code" in error && error.code === "ENOENT") {
8161
+ reject(new RunnerStartError(BUN_MISSING_MESSAGE));
8162
+ return;
8163
+ }
8164
+ reject(error);
8165
+ });
8166
+ child.on("exit", (code, signal) => {
8167
+ process.off("SIGINT", forward);
8168
+ process.off("SIGTERM", forward);
8169
+ if (signal) {
8170
+ resolve(1);
8171
+ return;
8172
+ }
8173
+ resolve(code ?? 1);
8174
+ });
8175
+ });
8176
+ return { pid: child.pid, exited };
8177
+ }
8178
+ async function defaultWritePid(homedir, pid) {
8179
+ const dir = yoqaRoot(homedir);
8180
+ await mkdir(dir, { recursive: true });
8181
+ await writeFile(runnerPidPath(homedir), `${pid}
8182
+ `, "utf8");
8183
+ }
8184
+ async function defaultReadPid(homedir) {
8185
+ try {
8186
+ const raw = (await readFile(runnerPidPath(homedir), "utf8")).trim();
8187
+ const pid = Number(raw);
8188
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
8189
+ } catch {
8190
+ return null;
8191
+ }
8192
+ }
8193
+ async function defaultRemovePid(homedir) {
8194
+ try {
8195
+ await unlink(runnerPidPath(homedir));
8196
+ } catch {}
8197
+ }
8198
+ function createDefaultEnsureDeps(overrides = {}) {
8199
+ const env = overrides.env ?? process.env;
8200
+ const homedir = overrides.homedir ?? osHomedir();
8201
+ const pathExists = overrides.pathExists ?? defaultPathExists;
8202
+ const which = overrides.which ?? defaultWhich;
8203
+ const fromUrl = overrides.fromUrl ?? import.meta.url;
8204
+ return {
8205
+ env,
8206
+ cwd: overrides.cwd ?? process.cwd(),
8207
+ execPath: overrides.execPath ?? process.execPath,
8208
+ argv0: overrides.argv0 ?? process.argv0,
8209
+ homedir,
8210
+ fromUrl,
8211
+ pathExists,
8212
+ which,
8213
+ findBun: overrides.findBun ?? (() => defaultFindBun(pathExists, which, homedir, env)),
8214
+ resolvePackageEntry: overrides.resolvePackageEntry ?? (() => resolvePublishedRunnerEntry(fromUrl)),
8215
+ fetchHealth: overrides.fetchHealth ?? ((baseUrl) => fetchRunnerHealth(baseUrl)),
8216
+ spawnDetached: overrides.spawnDetached ?? ((launch, spawnProcessEnv) => defaultSpawnDetached(launch, spawnProcessEnv, homedir)),
8217
+ spawnForeground: overrides.spawnForeground ?? ((launch, spawnProcessEnv) => defaultSpawnForeground(launch, spawnProcessEnv, homedir)),
8218
+ writePid: overrides.writePid ?? ((pid) => defaultWritePid(homedir, pid)),
8219
+ readPid: overrides.readPid ?? (() => defaultReadPid(homedir)),
8220
+ removePid: overrides.removePid ?? (() => defaultRemovePid(homedir)),
8221
+ isPidAlive: overrides.isPidAlive ?? defaultIsPidAlive,
8222
+ killPid: overrides.killPid ?? ((pid, signal = "SIGTERM") => {
8223
+ process.kill(pid, signal);
8224
+ }),
8225
+ sleep: overrides.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
8226
+ now: overrides.now ?? (() => Date.now()),
8227
+ log: overrides.log ?? ((message) => console.error(message))
8228
+ };
8229
+ }
8230
+ async function ensureRunner(options) {
8231
+ const mode = options?.mode ?? "detached";
8232
+ const deps = createDefaultEnsureDeps(options?.deps);
8233
+ const baseUrl = options?.baseUrl ?? runnerBaseUrlFromEnv(deps.env);
8234
+ const existing = await deps.fetchHealth(baseUrl);
8235
+ if (existing) {
8236
+ return { baseUrl, started: false };
8237
+ }
8238
+ const launch = await resolveRunnerLaunch(deps);
8239
+ deps.log(`yoqa: starting runner (${launch.source}) → ${baseUrl}`);
8240
+ if (mode === "foreground") {
8241
+ const child = deps.spawnForeground(launch, deps.env);
8242
+ await deps.writePid(child.pid);
8243
+ const code = await child.exited;
8244
+ await deps.removePid();
8245
+ if (code !== 0) {
8246
+ throw new RunnerStartError(`Runner exited with code ${code}`);
8247
+ }
8248
+ return { baseUrl, started: true, source: launch.source, pid: child.pid };
8249
+ }
8250
+ const spawned = deps.spawnDetached(launch, deps.env);
8251
+ await deps.writePid(spawned.pid);
8252
+ const health = await waitForRunnerHealth(() => deps.fetchHealth(baseUrl), {
8253
+ timeoutMs: HEALTH_TIMEOUT_MS,
8254
+ intervalMs: HEALTH_POLL_MS,
8255
+ sleep: deps.sleep,
8256
+ now: deps.now,
8257
+ isAlive: () => deps.isPidAlive(spawned.pid)
8258
+ });
8259
+ if (!health) {
8260
+ try {
8261
+ deps.killPid(spawned.pid);
8262
+ } catch {}
8263
+ await deps.removePid();
8264
+ throw new RunnerStartError(`Local runner failed to start at ${baseUrl}. Try: yoqa serve`);
8265
+ }
8266
+ return { baseUrl, started: true, source: launch.source, pid: spawned.pid };
8267
+ }
8268
+ async function stopOwnedRunner(depsPartial = {}) {
8269
+ const deps = createDefaultEnsureDeps(depsPartial);
8270
+ const pid = await deps.readPid();
8271
+ if (pid == null) {
8272
+ return { ok: true, message: "No yoqa-owned runner pid file" };
8273
+ }
8274
+ if (!deps.isPidAlive(pid)) {
8275
+ await deps.removePid();
8276
+ return { ok: true, message: `Stale pid ${pid} removed` };
8277
+ }
8278
+ try {
8279
+ deps.killPid(pid, "SIGTERM");
8280
+ } catch (error) {
8281
+ const message = error instanceof Error ? error.message : String(error);
8282
+ return { ok: false, message: `Failed to stop pid ${pid}: ${message}` };
8283
+ }
8284
+ await deps.removePid();
8285
+ return { ok: true, message: `Stopped runner pid ${pid}` };
8286
+ }
8287
+ function commandChainIncludes(command, name) {
8288
+ let current = command;
8289
+ while (current && typeof current.name === "function") {
8290
+ if (current.name() === name)
8291
+ return true;
8292
+ current = current.parent;
8293
+ }
8294
+ return false;
8295
+ }
8296
+
8297
+ // src/main.ts
8298
+ function runnerBaseUrl() {
8299
+ return runnerBaseUrlFromEnv();
8300
+ }
7902
8301
  function sleep(ms) {
7903
8302
  return new Promise((resolve) => setTimeout(resolve, ms));
7904
8303
  }
@@ -7910,9 +8309,43 @@ function client(baseUrl) {
7910
8309
  function fail(command, error) {
7911
8310
  const message = error instanceof Error ? error.message : String(error);
7912
8311
  console.error(`yoqa ${command} failed: ${message}`);
7913
- console.error("Is the runner up? Try: bun run runner");
8312
+ if (!message.includes("yoqa serve") && !message.includes("Bun")) {
8313
+ console.error("Is the runner up? Try: yoqa serve");
8314
+ }
7914
8315
  process.exitCode = 1;
7915
8316
  }
8317
+ program2.command("serve").description("Start the local Yoqa runner in the foreground (no desktop app required)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--stop", "Stop a runner previously started by yoqa (pid file)").action(async (options) => {
8318
+ try {
8319
+ if (options.stop) {
8320
+ const result = await stopOwnedRunner();
8321
+ console.log(result.message);
8322
+ if (!result.ok)
8323
+ process.exitCode = 1;
8324
+ return;
8325
+ }
8326
+ const baseUrl = options.baseUrl;
8327
+ const health = await fetchRunnerHealth(baseUrl);
8328
+ if (health) {
8329
+ console.log(`already running at ${baseUrl} (${health.version})`);
8330
+ return;
8331
+ }
8332
+ await ensureRunner({ mode: "foreground", baseUrl });
8333
+ } catch (error) {
8334
+ fail("serve", error);
8335
+ }
8336
+ });
8337
+ program2.hook("preAction", async (_thisCommand, actionCommand) => {
8338
+ if (commandChainIncludes(actionCommand, "serve"))
8339
+ return;
8340
+ if (shouldSkipAutostart())
8341
+ return;
8342
+ try {
8343
+ await ensureRunner({ mode: "detached" });
8344
+ } catch (error) {
8345
+ fail("autostart", error);
8346
+ throw error;
8347
+ }
8348
+ });
7916
8349
  async function resolveAppId(c, prefixOrId) {
7917
8350
  const apps = await c.listApps();
7918
8351
  const match = apps.find((a) => a.prefix === prefixOrId) ?? apps.find((a) => a.id === prefixOrId);
@@ -8735,7 +9168,7 @@ runsCmd.command("report").description("Export a detailed HTML or Markdown report
8735
9168
  const contents = format === "html" ? formatRunReportHtml(doc) : formatRunReportMarkdown(doc);
8736
9169
  const extension = format === "html" ? "html" : "md";
8737
9170
  const outputPath = options.output?.trim() || `${suggestedRunReportBasename(doc)}.${extension}`;
8738
- await writeFile(outputPath, contents, "utf8");
9171
+ await writeFile2(outputPath, contents, "utf8");
8739
9172
  console.log(`wrote ${outputPath} (${doc.status}, ${steps.length} steps)`);
8740
9173
  } catch (error) {
8741
9174
  fail("runs report", error);
@@ -8802,7 +9235,7 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
8802
9235
  var scriptCmd = program2.command("script").description("Replay exported CaseScript files");
8803
9236
  scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript JSON file").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON per step").action(async (file, options) => {
8804
9237
  try {
8805
- const raw = await readFile(file, "utf8");
9238
+ const raw = await readFile2(file, "utf8");
8806
9239
  const parsed = caseScriptSchema.safeParse(JSON.parse(raw));
8807
9240
  if (!parsed.success) {
8808
9241
  throw new Error(`Invalid CaseScript: ${parsed.error.message}`);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@yoqa/cli",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "yoqa": "dist/main.js"
9
9
  },
10
- "files": ["dist"],
10
+ "files": [
11
+ "dist"
12
+ ],
11
13
  "engines": {
12
14
  "node": ">=20"
13
15
  },
@@ -20,7 +22,14 @@
20
22
  "bugs": {
21
23
  "url": "https://github.com/Aris-ngoy/qa-agent/issues"
22
24
  },
23
- "keywords": ["yoqa", "cli", "mobile", "qa", "appium", "testing"],
25
+ "keywords": [
26
+ "yoqa",
27
+ "cli",
28
+ "mobile",
29
+ "qa",
30
+ "appium",
31
+ "testing"
32
+ ],
24
33
  "scripts": {
25
34
  "yoqa": "bun run src/main.ts",
26
35
  "build": "bun run build.ts",
@@ -28,6 +37,9 @@
28
37
  "check": "tsc --noEmit",
29
38
  "health": "bun run src/main.ts health"
30
39
  },
40
+ "dependencies": {
41
+ "@yoqa/runner": "0.3.12"
42
+ },
31
43
  "devDependencies": {
32
44
  "@types/node": "^22.15.30",
33
45
  "@yoqa/runner-client": "workspace:*",