Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e6b2c6
feat(runner): add runtimeEnv managed-deps domain
michael-pr Jun 22, 2026
9b51d57
fix(runner): harden managed runtime readiness checks
michael-pr Jun 22, 2026
225c519
refactor(runner): defer runtime install + share resolveDepsRoot helper
michael-pr Jun 22, 2026
445f49a
refactor(runner): dedupe runtime-deps helpers and extract flowsRun de…
michael-pr Jun 23, 2026
e9b905f
feat(runner): support QAWOLF_RUNTIME_DIR to relocate managed runtime
michael-pr Jun 23, 2026
6a5ba87
test(runner): assert resolve() in QAWOLF_RUNTIME_DIR absolute-path case
michael-pr Jun 23, 2026
e4722c6
feat(runner): add `install clear` to reset managed runtime cache
michael-pr Jun 23, 2026
a7ba8f4
fix(cli): keep install clear confirm on the Clack timeline
michael-pr Jun 23, 2026
7d5bf2e
fix(cli): use arrow-key confirm for destructive prompts
michael-pr Jun 23, 2026
e69f87f
fix(cli): show a spinner while clearing the runtime cache
michael-pr Jun 23, 2026
9a37746
fix(runner): resolve flow imports via managed-deps symlink and binary…
michael-pr Jun 23, 2026
0c5813e
fix(runner): use Windows junction for managed-deps symlink and assert…
michael-pr Jun 23, 2026
b22b080
fix(runner): isolate per-run staging and guard managed-runtime deletion
michael-pr Jun 23, 2026
de77600
feat(runner): resolve flow deps via layered run-dir with prefer-pinne…
michael-pr Jun 24, 2026
2ad2ff6
fix(runner): share executor instance in binary bundle and surface flo…
michael-pr Jun 24, 2026
390a4ab
fix(runner): prevent staged-flow basename collisions and harden run-d…
michael-pr Jun 24, 2026
99e06f3
refactor(cli): extract shared runStagedFlows orchestrator
michael-pr Jun 24, 2026
6a0e593
refactor(runner): dedupe npm-install spawn into canonical helper
michael-pr Jun 24, 2026
714316a
refactor(runner): decompose shimFlowsDeps into orchestrator + helpers
michael-pr Jun 24, 2026
0a99329
fix(runner): scope inner hop to pinned packages to stop transitive sh…
michael-pr Jun 24, 2026
3156574
fix(runner): relocate run staging out of managed base to fix install …
michael-pr Jun 24, 2026
833c6ce
fix(runner): register run cleanup before setup and drain npm stdout
michael-pr Jun 24, 2026
bdfe7c9
fix(runner): abort managed install when a flow-dep shim fails to build
michael-pr Jun 24, 2026
72a8bf1
test(runner): extract shared scaffoldManagedRuntime test helper
michael-pr Jun 24, 2026
49cd1c8
build(runner): support embedded worker bundle in compiled binary
michael-pr Jun 25, 2026
4646917
fix(runner): thread worker env through dispatch layer
michael-pr Jun 25, 2026
82cbb58
refactor(runner): extract flow execution dispatcher
michael-pr Jun 25, 2026
1b6b2ce
docs(runner): runtime deps target design
michael-pr Jun 25, 2026
28d187d
chore(runner): untrack runtime-deps design doc as local-only
michael-pr Jun 25, 2026
c6d51e6
docs(runner): apply comment-style to runtime-deps changes
michael-pr Jun 25, 2026
38f4903
chore(runner): add minor changeset for layered runtime-deps resolution
michael-pr Jun 25, 2026
e4ead06
test(runner): cover error formatter, staged-run orchestrator, and ins…
michael-pr Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/layered-runtime-deps-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@qawolf/cli": minor
---

Resolve flow runtime dependencies through a layered, project-isolated `node_modules` so flows run correctly in monorepos, single-package projects, and empty directories across both the Node and compiled-binary channels. The CLI-owned executor is always pinned and never pollutes or is shadowed by the surrounding project, while the flow's own declared dependencies still resolve. Adds `qawolf install clear` to wipe the managed runtime cache.
2 changes: 1 addition & 1 deletion knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const config: KnipConfig = {
ignoreDependencies: [
// TODO WIZ-10341 follow-up: consumed once the web-flow runner imports it.
"@playwright/test",
// Installed into the flow env dir at runtime by ensureFlowDeps; not imported by the CLI.
// Installed into the managed runtime dir at runtime by ensureRuntimeEnv; not imported by the CLI.
"appium",
"appium-xcuitest-driver",
"appium-uiautomator2-driver",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"scripts": {
"build": "bun run generate && bun scripts/build.ts",
"build:binary": "bun run generate && bun scripts/buildBinary.ts",
"build:binary": "bun run build && bun scripts/buildBinary.ts",
"dev": "bun run src/main.ts",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
Expand Down
25 changes: 24 additions & 1 deletion scripts/buildBinary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bun
// Compiles the standalone binary (default dist/qawolf); the release-binaries workflow passes --target/--outfile per platform.
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { parseArgs } from "node:util";

const { values } = parseArgs({
Expand All @@ -10,11 +11,33 @@ const { values } = parseArgs({
},
});

// Generate the compile entry in dist/ (excluded from tsc + knip): it embeds the
// already-built dist/cli.js as a Bun file asset, exports its extracted path via
// env, then runs the CLI from source. Generated rather than kept in src/ so
// neither tsc (which cannot model the file-asset import) nor knip (which cannot
// see a string-arg entry) trips on it. cli.js is the same bundle the runner
// spawns as a BUN_BE_BUN worker so flows resolve their own node_modules —
// including native modules like sharp — at runtime, which the in-process
// compiled resolver cannot. Never bundled into cli.js (build.ts builds
// src/main.ts), so there is no embed cycle.
const entryPath = "dist/binary-entry.ts";
writeFileSync(
entryPath,
[
'import cliAsset from "./cli.js" with { type: "file" };',
"",
"process.env.QAWOLF_EMBEDDED_CLI_PATH = cliAsset;",
"",
'await import("../src/main.ts");',
"",
].join("\n"),
);

const buildArgs = [
"build",
"--compile",
...(values.target ? [`--target=${values.target}`] : []),
"./src/main.ts",
entryPath,
"--outfile",
values.outfile,
// unlike the npm bundle (build.ts), only these stay external — ensureDeps installs them on demand
Expand Down
1 change: 1 addition & 0 deletions skills/qawolf-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ write on timeout: it may have reached the server the first time.
| `qawolf install` | local | Install every runtime dependency the project's flows need |
| `qawolf install android` | local | Install Android system images, AVDs, and the Appium driver used by the project's Android flows |
| `qawolf install browsers` | local | Install Playwright browsers used by the project's web flows |
| `qawolf install clear` | local | Remove the managed runtime cache (all installed runtime versions) |
| `qawolf run create` | write | Create a run for selected flows in an environment. |

<!-- commands-table:end -->
Expand Down
20 changes: 20 additions & 0 deletions src/commands/__snapshots__/help.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Commands:
flows
android [pattern] Install Android system images, AVDs, and the Appium driver
used by the project's Android flows
clear [options] Remove the managed runtime cache (all installed runtime
versions)

Examples:
$ qawolf install
Expand Down Expand Up @@ -90,6 +92,21 @@ Examples:
"
`;

exports[`--help output qawolf install clear 1`] = `
"Usage: qawolf install clear [options]

Remove the managed runtime cache (all installed runtime versions)

Options:
--yes Skip the confirmation prompt (default: false)
-h, --help display help for command

Examples:
$ qawolf install clear
$ qawolf install clear --yes
"
`;

exports[`--help output qawolf doctor 1`] = `
"Usage: qawolf doctor [options]

Expand Down Expand Up @@ -155,6 +172,9 @@ Options:
false)
--env <env> Pull and run a flow from this environment (UUID or slug)
if not cached locally
--deps <dir> Use this prepared dependency directory instead of
auto-installing the runtime; or set QAWOLF_RUNTIME_DIR
to relocate the managed runtime
-h, --help display help for command

Examples:
Expand Down
21 changes: 11 additions & 10 deletions src/commands/doctor/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { resolveApiKey } from "~/domains/auth/resolve.js";
import { runChecks } from "~/domains/doctor/checks/index.js";
import { renderResults } from "~/domains/doctor/render.js";
import type { CheckResult } from "~/domains/doctor/types.js";
import { resolveUniqueEnvDir } from "~/domains/flows/ensureDeps.js";
import { resolveProjectDirSafe } from "~/domains/flows/ensureDeps.js";
import { expandPatterns, makePeekFlowMeta } from "~/domains/flows/expand.js";
import { resolveDepsRootIfPresent } from "~/domains/runtimeEnv/index.js";
import { resolveAppiumBin } from "~/shell/appium/resolveAppiumBin.js";
import {
type CommandContext,
Expand Down Expand Up @@ -39,17 +40,17 @@ export async function handleDoctor(
const cwd = process.cwd();
const flowFiles = await expandPatterns([], cwd, undefined, fs);

// Playwright lives in the env dir (installed by ensureFlowDeps), not in cwd.
// Silently fall back to cwd when no env dir is found or flows span multiple packages.
let envDir: string | undefined;
try {
envDir = resolveUniqueEnvDir([...flowFiles], fs);
} catch {
// multiple env dirs — fall back to cwd
}
// Playwright/Appium live in the resolved runtime dir (managed env or project), not cwd.
const projectDir = resolveProjectDirSafe([...flowFiles], fs);
const envDir = resolveDepsRootIfPresent(
projectDir !== undefined ? { projectDir } : {},
fs,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
let playwrightCliPath: string | undefined;
try {
playwrightCliPath = resolvePlaywrightCli(envDir ?? cwd, process.platform);
playwrightCliPath = envDir
? resolvePlaywrightCli(envDir, process.platform)
: undefined;
} catch {
playwrightCliPath = undefined;
}
Expand Down
61 changes: 61 additions & 0 deletions src/commands/flows/buildFlowsRunDeps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { makePeekFlowMeta } from "~/domains/flows/expand.js";
import { findFlowStamp as defaultFindFlowStamp } from "~/shell/manifest/lookup.js";
import { installBrowserList } from "~/domains/install/browsers.js";
import { defaultSpawn } from "~/shell/spawn.js";
import { resolvePlaywrightCli } from "~/shell/playwright.js";
import { runAndroidFlow as defaultRunAndroidFlow } from "~/domains/runner/runAndroidFlow.js";
import { runWebFlow as defaultRunWebFlow } from "~/domains/runner/runWebFlow.js";
import { makePooledDispatch } from "~/domains/runner/makePooledDispatch.js";
import type { createAndroidDeps } from "~/domains/runner/runAndroidFlowDeps.js";
import type { RunWebFlowDeps } from "~/domains/runner/runWebFlow.js";
import type {
FlowsRunDeps,
FlowsRunFlags,
} from "~/domains/runner/runInternals.js";
import type { CommandContext } from "~/shell/commandContext.js";

import { buildRunReporter } from "./buildRunReporter.js";

type BuildFlowsRunDepsArgs = {
ctx: CommandContext;
resolvedDir: string;
android: ReturnType<typeof createAndroidDeps>;
runWebFlowDeps: RunWebFlowDeps;
flags: FlowsRunFlags;
};

/**
* Assembles the runner dependency bundle for `flowsRun`. Identical across the
* local (`flows run`) and hybrid (`--env`) entry points, so both share this
* single builder. `runWebFlowDeps` is resolved by the caller (it is async and
* the injection point for tests) and passed in already awaited.
*/
export function buildFlowsRunDeps(args: BuildFlowsRunDepsArgs): FlowsRunDeps {
const { ctx, resolvedDir, android, runWebFlowDeps, flags } = args;
return {
peekFlowMeta: makePeekFlowMeta(ctx.fs),
installBrowsers: (innerCtx, browsers) =>
installBrowserList(innerCtx, browsers, {
spawn: defaultSpawn,
platform: process.platform,
playwrightCliPath: resolvePlaywrightCli(resolvedDir, process.platform),
}),
runWebFlow: defaultRunWebFlow,
runWebFlowDeps,
runAndroidFlow: defaultRunAndroidFlow,
runAndroidFlowDeps: android.deps,
bootAndroid: android.boot,
shutdownAndroid: android.shutdown,
createPooledDispatch: makePooledDispatch(resolvedDir),
findFlowStamp: defaultFindFlowStamp,
warn: (message) => ctx.ui.warn(message),
logger: ctx.log("runner"),
// Route reporter output through ctx.ui so streamed test logs stay inside the run's timeline.
reporter: buildRunReporter(flags, {
fs: ctx.fs,
stdout: { write: (text: string) => ctx.ui.write(text) },
stderr: { write: (text: string) => ctx.ui.write(text) },
}),
now: () => Date.now(),
};
}
Loading