From ac2d20d81dfae2a678f3889cd34809c7c33ed7e9 Mon Sep 17 00:00:00 2001 From: Mateus Zitelli <201472+MateusZitelli@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:26:21 -0300 Subject: [PATCH 1/2] feat(cli): accept custom platform hosts --- .changeset/custom-api-host.md | 5 +++ skills/qawolf-cli/SKILL.md | 5 +++ src/commands/context.ts | 4 +-- src/commands/flows/runWorker.register.ts | 5 ++- src/core/messages/auth.ts | 2 +- src/shell/resolveHostUrl.test.ts | 39 ++++++++++++++++++++++++ src/shell/resolveHostUrl.ts | 10 ++++++ 7 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/custom-api-host.md create mode 100644 src/shell/resolveHostUrl.test.ts create mode 100644 src/shell/resolveHostUrl.ts diff --git a/.changeset/custom-api-host.md b/.changeset/custom-api-host.md new file mode 100644 index 000000000..41a898a61 --- /dev/null +++ b/.changeset/custom-api-host.md @@ -0,0 +1,5 @@ +--- +"@qawolf/cli": patch +--- + +Allow `QAWOLF_HOST_URL` to target a custom QA Wolf deployment host. diff --git a/skills/qawolf-cli/SKILL.md b/skills/qawolf-cli/SKILL.md index 2b8688762..b7b677565 100644 --- a/skills/qawolf-cli/SKILL.md +++ b/skills/qawolf-cli/SKILL.md @@ -19,6 +19,11 @@ environment variable (or stored credentials from `qawolf auth login`). their table entry notes a flag that switches them to `read`. Verify with `qawolf auth whoami`. Never print or log the key. +Commands use `https://app.qawolf.com` by default. Set `QAWOLF_HOST_URL` to +target another deployment host, for example +`https://app.staging.example.com`. `QAWOLF_API_URL` is a separate API endpoint +and does not select the deployment host used by CLI commands. + ## Output When consuming output programmatically, always pass `--json` (or `--agent`). diff --git a/src/commands/context.ts b/src/commands/context.ts index 7247ffab4..9190396a5 100644 --- a/src/commands/context.ts +++ b/src/commands/context.ts @@ -5,6 +5,7 @@ import { authMessages } from "~/core/messages/index.js"; import { getConfigDir } from "~/core/paths.js"; import { exitCodes } from "~/shell/exit.js"; import { makeDefaultFs } from "~/shell/fs.js"; +import { resolveHostUrl } from "~/shell/resolveHostUrl.js"; import { requireApiKey } from "~/domains/auth/index.js"; import { createLoggingSystem, @@ -59,8 +60,7 @@ export function buildBaseContext( logPath: defaultLogPath(), ...(verboseWrite ? { verboseWrite } : {}), }); - const apiBaseUrl = - env["QAWOLF_API_URL"]?.replace(/\/+$/, "") || "https://app.qawolf.com"; + const apiBaseUrl = resolveHostUrl(env); return { ctx: { ui: createUI(outputMode, { diff --git a/src/commands/flows/runWorker.register.ts b/src/commands/flows/runWorker.register.ts index 4559535e4..ce3163b04 100644 --- a/src/commands/flows/runWorker.register.ts +++ b/src/commands/flows/runWorker.register.ts @@ -6,6 +6,7 @@ import type { Reporter } from "~/shell/reporter/types.js"; import type { SignalRegistry } from "~/shell/signals/createSignalRegistry.js"; import { getConfigDir } from "~/core/paths.js"; import { makeDefaultFs } from "~/shell/fs.js"; +import { resolveHostUrl } from "~/shell/resolveHostUrl.js"; import { configureTestkit } from "~/shell/testkit.js"; import { executeWorkerFlow } from "~/domains/runner/executeWorkerFlow.js"; import { runAndroidFlow as defaultRunAndroidFlow } from "~/domains/runner/runAndroidFlow.js"; @@ -34,9 +35,7 @@ async function runWorker(signals: SignalRegistry): Promise { await configureTestkit(input.resolvedDir); const fs = makeDefaultFs(); - const apiBaseUrl = - process.env["QAWOLF_API_URL"]?.replace(/\/+$/, "") || - "https://app.qawolf.com"; + const apiBaseUrl = resolveHostUrl(process.env); const flowRuntimeDeps = await createFlowRuntimeDeps({ envDir: input.resolvedDir, ctx: { apiBaseUrl, configDir: getConfigDir(), fs }, diff --git a/src/core/messages/auth.ts b/src/core/messages/auth.ts index 7e3ffb374..9c93f68d8 100644 --- a/src/core/messages/auth.ts +++ b/src/core/messages/auth.ts @@ -47,7 +47,7 @@ export const authMessages = { failedWithStatus: (status: number, noun: string | undefined) => `QA Wolf API${noun ? ` ${noun}` : ""} request failed (HTTP ${status}).`, networkUnreachable: (baseUrl: string, noun: string | undefined) => - `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_API_URL.`, + `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_HOST_URL.`, unexpectedResponse: (noun: string | undefined) => `Unexpected${noun ? ` ${noun}` : ""} response from the QA Wolf API.`, }, diff --git a/src/shell/resolveHostUrl.test.ts b/src/shell/resolveHostUrl.test.ts new file mode 100644 index 000000000..66e8d43a9 --- /dev/null +++ b/src/shell/resolveHostUrl.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "bun:test"; + +import { resolveHostUrl } from "./resolveHostUrl.js"; + +describe("resolveHostUrl", () => { + it("returns the production host when QAWOLF_HOST_URL is unset", () => { + expect(resolveHostUrl({})).toBe("https://app.qawolf.com"); + }); + + it("accepts a custom host", () => { + expect( + resolveHostUrl({ + QAWOLF_HOST_URL: "https://app.preview.example.com", + }), + ).toBe("https://app.preview.example.com"); + }); + + it("does not treat QAWOLF_API_URL as the host URL", () => { + expect( + resolveHostUrl({ + QAWOLF_API_URL: "https://api.staging.example.com/api", + }), + ).toBe("https://app.qawolf.com"); + }); + + it("trims whitespace and trailing slashes", () => { + expect( + resolveHostUrl({ + QAWOLF_HOST_URL: " http://localhost:3000/// ", + }), + ).toBe("http://localhost:3000"); + }); + + it("falls back to the production host for whitespace", () => { + expect(resolveHostUrl({ QAWOLF_HOST_URL: " " })).toBe( + "https://app.qawolf.com", + ); + }); +}); diff --git a/src/shell/resolveHostUrl.ts b/src/shell/resolveHostUrl.ts new file mode 100644 index 000000000..f6a2d8470 --- /dev/null +++ b/src/shell/resolveHostUrl.ts @@ -0,0 +1,10 @@ +const defaultHostUrl = "https://app.qawolf.com"; + +export function resolveHostUrl( + env: Record, +): string { + const configuredHostUrl = env["QAWOLF_HOST_URL"]?.trim(); + if (!configuredHostUrl) return defaultHostUrl; + + return configuredHostUrl.replace(/\/+$/, ""); +} From f4a2bc56c23e961ee7a5f0b35202f9a725067465 Mon Sep 17 00:00:00 2001 From: Mateus Zitelli <201472+MateusZitelli@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:15:53 -0300 Subject: [PATCH 2/2] chore(cli): bump version to 1.3.1 --- .changeset/custom-api-host.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/custom-api-host.md diff --git a/.changeset/custom-api-host.md b/.changeset/custom-api-host.md deleted file mode 100644 index 41a898a61..000000000 --- a/.changeset/custom-api-host.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@qawolf/cli": patch ---- - -Allow `QAWOLF_HOST_URL` to target a custom QA Wolf deployment host. diff --git a/CHANGELOG.md b/CHANGELOG.md index abed858dd..0329647a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @qawolf/cli +## 1.3.1 + +### Patch Changes + +- ac2d20d: Allow `QAWOLF_HOST_URL` to target a custom QA Wolf deployment host. + ## 1.3.0 ### Minor Changes diff --git a/package.json b/package.json index cbaf3642b..cb9db7e37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qawolf/cli", - "version": "1.3.0", + "version": "1.3.1", "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent", "keywords": [ "automation",