pwvm 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Millan Kaul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # pwvm
2
+
3
+ ### **Playwright Version Manager**
4
+
5
+ A simple *Playwright Version Manager* that solves a common pain point:
6
+
7
+ > Uncontrolled Playwright upgrades and breaking changes disrupt local setups and CI pipelines.
8
+
9
+ `pwvm` lets you install, manage, and switch Playwright versions reliably — **one command, predictable behavior**, just like `nvm` for Node.js.
10
+
11
+ ---
12
+
13
+ ## Why pwvm
14
+
15
+ Playwright evolves fast. That’s great — until a minor upgrade breaks tests you didn’t plan to touch.
16
+
17
+ `pwvm` keeps Playwright versions:
18
+
19
+ * isolated per version
20
+ * explicit and reproducible
21
+ * easy to switch locally and in CI
22
+
23
+ You upgrade **when you choose**, not when your tooling surprises you.
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ npm install -g pwvm
31
+ ```
32
+
33
+ Then run the one-time setup:
34
+
35
+ ```sh
36
+ pwvm setup
37
+ ```
38
+
39
+ Follow the printed instructions to add pwvm shims to your `PATH`.
40
+
41
+ ---
42
+
43
+ ## Common usage
44
+
45
+ ```sh
46
+ pwvm install 1.53.0
47
+ pwvm use 1.53.0
48
+ playwright test
49
+ ```
50
+
51
+ Pin versions per project with `.pwvmrc`:
52
+
53
+ ```text
54
+ 1.53.0
55
+ ```
56
+
57
+ ---
58
+
59
+ ## CI-friendly
60
+
61
+ `pwvm` works in GitHub Actions, Azure Pipelines, Bitbucket, and any CI where you control `PATH`.
62
+
63
+ Install → setup → select version → run Playwright.
64
+
65
+ ---
66
+
67
+ ## Support pwvm
68
+
69
+ `pwvm` is built and maintained independently to fix a problem many teams quietly struggle with.
70
+
71
+ If this tool saves you time, CI hours, or debugging frustration:
72
+
73
+ * ⭐ [Star the project](https://github.com/eaccmk/pwvm)
74
+ * ❤️ [Sponsor via GitHub](https://github.com/sponsors/eaccmk)
75
+ * 🔁 Share it with your team
76
+
77
+ Your support helps keep pwvm maintained and improving.
78
+
79
+ ---
80
+
81
+ 📘 Full documentation and contributing guide: [https://github.com/eaccmk/pwvm](https://github.com/eaccmk/pwvm)
@@ -0,0 +1,42 @@
1
+ import fs from "fs-extra";
2
+ import { resolveRcPath } from "../core/paths.js";
3
+ import { getResolvedActiveVersion, } from "../core/versions.js";
4
+ import { createLogger, formatError } from "../utils/logger.js";
5
+ export const runCurrentCommand = async (deps = {}) => {
6
+ const logger = deps.logger ?? createLogger();
7
+ const getActive = deps.getActive ?? getResolvedActiveVersion;
8
+ const resolveRc = deps.resolveRc ?? deps.activeOptions?.resolveRc ?? resolveRcPath;
9
+ const fsImpl = deps.fs ?? deps.activeOptions?.fs ?? fs;
10
+ try {
11
+ const currentVersion = await getActive(deps.activeOptions);
12
+ if (!currentVersion) {
13
+ logger.warn("No active Playwright version. Run `pwvm use <version>`.");
14
+ process.exitCode = 1;
15
+ return;
16
+ }
17
+ let suffix = "";
18
+ try {
19
+ const rcPath = resolveRc({
20
+ startDir: deps.activeOptions?.rcStartDir,
21
+ rcFileName: deps.activeOptions?.rcFileName,
22
+ fs: fsImpl,
23
+ });
24
+ if (rcPath) {
25
+ const contents = await fsImpl.readFile(rcPath, "utf8");
26
+ const trimmed = contents.trim();
27
+ if (trimmed.length > 0 && trimmed === currentVersion) {
28
+ suffix = " (from .pwvmrc)";
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ suffix = "";
34
+ }
35
+ logger.info(`${currentVersion}${suffix}`);
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ logger.error(formatError(`Failed to read current version: ${message}`, "Run `pwvm use <version>` to set one."));
40
+ process.exitCode = 1;
41
+ }
42
+ };
@@ -0,0 +1,95 @@
1
+ import fs from "fs-extra";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getPwvmDir, getShimsDir } from "../core/paths.js";
5
+ import { createLogger } from "../utils/logger.js";
6
+ const readVersion = () => {
7
+ try {
8
+ const entry = process.argv[1];
9
+ if (!entry) {
10
+ return "unknown";
11
+ }
12
+ const rootDir = path.resolve(path.dirname(entry), "..");
13
+ const pkgPath = path.join(rootDir, "package.json");
14
+ const contents = readFileSync(pkgPath, "utf8");
15
+ const parsed = JSON.parse(contents);
16
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
17
+ }
18
+ catch {
19
+ return "unknown";
20
+ }
21
+ };
22
+ export const runDoctorCommand = async (deps = {}) => {
23
+ const logger = deps.logger ?? createLogger();
24
+ const fsImpl = deps.fs ?? fs;
25
+ const pwvmDir = getPwvmDir();
26
+ const shimsDir = deps.shimsDir ?? getShimsDir();
27
+ const envPath = deps.envPath ?? process.env.PATH ?? "";
28
+ const shimPath = path.join(shimsDir, "playwright");
29
+ const useColor = process.stdout.isTTY === true;
30
+ const withScope = (message) => message.startsWith(" ") ? `pwvm-doctor${message}` : `pwvm-doctor ${message}`;
31
+ const logInfo = (message) => logger.info(withScope(message));
32
+ const logWarn = (message) => logger.warn(withScope(message));
33
+ const colorize = (symbol) => {
34
+ const value = symbol === "check" ? "✔" : "✖";
35
+ if (!useColor) {
36
+ return value;
37
+ }
38
+ const color = symbol === "check" ? "\x1b[32m" : "\x1b[31m";
39
+ const reset = "\x1b[39m";
40
+ return `${color}${value}${reset}`;
41
+ };
42
+ let shimsDirExists = false;
43
+ let shimExists = false;
44
+ let pwvmDirExists = false;
45
+ try {
46
+ pwvmDirExists = await fsImpl.pathExists(pwvmDir);
47
+ }
48
+ catch {
49
+ pwvmDirExists = false;
50
+ }
51
+ try {
52
+ shimsDirExists = await fsImpl.pathExists(shimsDir);
53
+ }
54
+ catch {
55
+ shimsDirExists = false;
56
+ }
57
+ try {
58
+ shimExists = await fsImpl.pathExists(shimPath);
59
+ }
60
+ catch {
61
+ shimExists = false;
62
+ }
63
+ const pathEntries = envPath.split(path.delimiter).filter(Boolean);
64
+ const pathHasShims = pathEntries.includes(shimsDir);
65
+ logInfo(`pwvm Doctor v.${readVersion()}`);
66
+ logInfo("### Diagnostic for required components ###");
67
+ if (pwvmDirExists) {
68
+ logInfo(` ${colorize("check")} pwvm home directory exists`);
69
+ }
70
+ else {
71
+ logWarn(` ${colorize("cross")} pwvm home directory exists`);
72
+ }
73
+ if (shimsDirExists) {
74
+ logInfo(` ${colorize("check")} shims directory exists`);
75
+ }
76
+ else {
77
+ logWarn(` ${colorize("cross")} shims directory exists`);
78
+ }
79
+ if (shimExists) {
80
+ logInfo(` ${colorize("check")} playwright shim found`);
81
+ }
82
+ else {
83
+ logWarn(` ${colorize("cross")} playwright shim found`);
84
+ }
85
+ if (pathHasShims) {
86
+ logInfo(` ${colorize("check")} shims directory is in PATH`);
87
+ }
88
+ else {
89
+ logWarn(` ${colorize("cross")} shims directory is not in PATH`);
90
+ }
91
+ logInfo("### Suggested fixes ###");
92
+ if (!pathHasShims) {
93
+ logInfo(' export PATH="$HOME/.pwvm/shims:$PATH"');
94
+ }
95
+ };
@@ -0,0 +1,121 @@
1
+ import { installPlaywrightVersion, } from "../core/install.js";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import { createProgress } from "../utils/progress.js";
4
+ import { fetchPackageMetadata } from "../utils/registry.js";
5
+ export const runInstallCommand = async (version, options = {}, deps = {}) => {
6
+ const logger = deps.logger ?? createLogger();
7
+ const install = deps.install ?? installPlaywrightVersion;
8
+ const progress = createProgress({ label: "Installing Playwright", logger });
9
+ const resolvedVersion = await resolveLatestVersion(version, logger);
10
+ if (!resolvedVersion) {
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+ let withBrowsers = options.browsers !== false;
15
+ if (options.withBrowsers) {
16
+ withBrowsers = true;
17
+ }
18
+ try {
19
+ if (resolvedVersion !== version) {
20
+ logger.info(`Resolved latest Playwright version to ${resolvedVersion}`);
21
+ }
22
+ logger.info(`Installing Playwright ${resolvedVersion}`);
23
+ if (withBrowsers) {
24
+ logger.info("Installing Playwright browsers");
25
+ }
26
+ else {
27
+ logger.info("Skipping Playwright browser install");
28
+ }
29
+ progress.start();
30
+ const result = await install(resolvedVersion, {
31
+ ...deps.installOptions,
32
+ withBrowsers,
33
+ });
34
+ progress.succeed();
35
+ if (!result.packageInstalled && !result.browsersInstalled) {
36
+ if (result.packageAlreadyInstalled && result.browsersAlreadyInstalled) {
37
+ logger.info(`Playwright ${version} is already installed (browsers already installed)`);
38
+ }
39
+ else {
40
+ logger.info(`Playwright ${version} is already installed`);
41
+ }
42
+ return;
43
+ }
44
+ if (!result.packageInstalled && result.browsersInstalled) {
45
+ logger.info(`🎉 Installed Playwright browsers for ${resolvedVersion}`);
46
+ return;
47
+ }
48
+ const suffix = result.browsersInstalled ? " (browsers installed)" : "";
49
+ logger.info(`🎉 Installed Playwright ${resolvedVersion}${suffix}`);
50
+ }
51
+ catch (error) {
52
+ progress.fail();
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ const code = getErrorCode(error);
55
+ if (isInvalidVersionError(code, message)) {
56
+ logger.error(`[ERROR]: Unable to install Playwright ${resolvedVersion}. The version does not exist.`);
57
+ logger.info("Available versions are published at https://github.com/microsoft/playwright/releases");
58
+ }
59
+ else if (isNetworkError(code, message)) {
60
+ logger.error("[ERROR]: Unable to install Playwright due to a network or registry issue.");
61
+ logger.info("Please check your internet connection and try again.");
62
+ }
63
+ else {
64
+ logger.error(`[ERROR]: Unable to install Playwright ${resolvedVersion}.`);
65
+ logger.info("Please try again or verify the version number.");
66
+ }
67
+ process.exitCode = 1;
68
+ }
69
+ };
70
+ const getErrorCode = (error) => {
71
+ if (typeof error !== "object" || error === null) {
72
+ return undefined;
73
+ }
74
+ if ("code" in error && typeof error.code === "string") {
75
+ return error.code;
76
+ }
77
+ return undefined;
78
+ };
79
+ const isInvalidVersionError = (code, message) => {
80
+ const text = message.toLowerCase();
81
+ if (code === "ETARGET" || code === "notarget") {
82
+ return true;
83
+ }
84
+ return (text.includes("etarget") ||
85
+ text.includes("notarget") ||
86
+ text.includes("no matching version") ||
87
+ text.includes("version not found"));
88
+ };
89
+ const isNetworkError = (code, message) => {
90
+ const text = message.toLowerCase();
91
+ if (code === "ECONNRESET" ||
92
+ code === "ENOTFOUND" ||
93
+ code === "ECONNREFUSED" ||
94
+ code === "ETIMEDOUT" ||
95
+ code === "EAI_AGAIN") {
96
+ return true;
97
+ }
98
+ return (text.includes("network") ||
99
+ text.includes("fetch failed") ||
100
+ text.includes("getaddrinfo") ||
101
+ text.includes("econnreset") ||
102
+ text.includes("enotfound"));
103
+ };
104
+ const resolveLatestVersion = async (version, logger) => {
105
+ if (version !== "latest") {
106
+ return version;
107
+ }
108
+ try {
109
+ const metadata = await fetchPackageMetadata("playwright");
110
+ const latest = metadata["dist-tags"]?.latest;
111
+ if (!latest) {
112
+ throw new Error("Missing latest tag.");
113
+ }
114
+ return latest;
115
+ }
116
+ catch {
117
+ logger.error("[ERROR]: Unable to reach npm registry.");
118
+ logger.info("Please check your internet connection and try again.");
119
+ return null;
120
+ }
121
+ };
@@ -0,0 +1,106 @@
1
+ import * as semver from "semver";
2
+ import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
3
+ import { runInstallCommand } from "./install.js";
4
+ import { fetchPackageMetadata } from "../utils/registry.js";
5
+ import { createProgress } from "../utils/progress.js";
6
+ vi.mock("../utils/registry.js", () => ({
7
+ fetchPackageMetadata: vi.fn(),
8
+ }));
9
+ vi.mock("../utils/progress.js", () => ({
10
+ createProgress: vi.fn(),
11
+ }));
12
+ describe("runInstallCommand", () => {
13
+ const originalExitCode = process.exitCode;
14
+ const originalIsTTY = process.stdout.isTTY;
15
+ let errorSpy;
16
+ let logSpy;
17
+ beforeEach(() => {
18
+ process.exitCode = 0;
19
+ Object.defineProperty(process.stdout, "isTTY", {
20
+ value: false,
21
+ configurable: true,
22
+ });
23
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
24
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => { });
25
+ vi.mocked(createProgress).mockReturnValue({
26
+ start: vi.fn(),
27
+ succeed: vi.fn(),
28
+ fail: vi.fn(),
29
+ });
30
+ });
31
+ afterEach(() => {
32
+ process.exitCode = originalExitCode;
33
+ Object.defineProperty(process.stdout, "isTTY", {
34
+ value: originalIsTTY,
35
+ configurable: true,
36
+ });
37
+ errorSpy.mockRestore();
38
+ logSpy.mockRestore();
39
+ vi.mocked(fetchPackageMetadata).mockReset();
40
+ vi.mocked(createProgress).mockReset();
41
+ });
42
+ it("resolves latest to a semver and installs with browsers by default", async () => {
43
+ const installMock = vi.fn(async () => ({
44
+ packageInstalled: true,
45
+ browsersInstalled: true,
46
+ packageAlreadyInstalled: false,
47
+ browsersAlreadyInstalled: false,
48
+ }));
49
+ vi.mocked(fetchPackageMetadata).mockResolvedValue({
50
+ "dist-tags": { latest: "1.42.0" },
51
+ });
52
+ await runInstallCommand("latest", {}, { install: installMock });
53
+ expect(installMock).toHaveBeenCalledTimes(1);
54
+ const [resolvedVersion, options] = installMock.mock.calls[0];
55
+ expect(semver.valid(resolvedVersion)).toBe(resolvedVersion);
56
+ expect(resolvedVersion).toBe("1.42.0");
57
+ expect(options).toEqual(expect.objectContaining({ withBrowsers: true }));
58
+ });
59
+ it("logs an error and exits when install fails for invalid version", async () => {
60
+ const installMock = vi.fn(async () => {
61
+ const error = Object.assign(new Error("npm ERR! code ETARGET"), {
62
+ code: "ETARGET",
63
+ });
64
+ throw error;
65
+ });
66
+ await runInstallCommand("1.9999.99999", {}, { install: installMock });
67
+ expect(process.exitCode).toBe(1);
68
+ expect(errorSpy).toHaveBeenCalled();
69
+ const combinedErrors = errorSpy.mock.calls
70
+ .map(([message]) => String(message))
71
+ .join("\n");
72
+ expect(combinedErrors).toContain("[ERROR]:");
73
+ expect(combinedErrors).toContain("Unable to install Playwright");
74
+ expect(combinedErrors).not.toContain("npm ERR");
75
+ });
76
+ it("installs browsers by default", async () => {
77
+ const installMock = vi.fn(async () => ({
78
+ packageInstalled: true,
79
+ browsersInstalled: true,
80
+ packageAlreadyInstalled: false,
81
+ browsersAlreadyInstalled: false,
82
+ }));
83
+ await runInstallCommand("1.2.3", {}, { install: installMock });
84
+ expect(installMock).toHaveBeenCalledWith("1.2.3", expect.objectContaining({ withBrowsers: true }));
85
+ });
86
+ it("respects --no-browsers", async () => {
87
+ const installMock = vi.fn(async () => ({
88
+ packageInstalled: true,
89
+ browsersInstalled: false,
90
+ packageAlreadyInstalled: false,
91
+ browsersAlreadyInstalled: false,
92
+ }));
93
+ await runInstallCommand("1.2.3", { browsers: false }, { install: installMock });
94
+ expect(installMock).toHaveBeenCalledWith("1.2.3", expect.objectContaining({ withBrowsers: false }));
95
+ });
96
+ it("accepts --with-browsers", async () => {
97
+ const installMock = vi.fn(async () => ({
98
+ packageInstalled: true,
99
+ browsersInstalled: true,
100
+ packageAlreadyInstalled: false,
101
+ browsersAlreadyInstalled: false,
102
+ }));
103
+ await runInstallCommand("1.2.3", { browsers: false, withBrowsers: true }, { install: installMock });
104
+ expect(installMock).toHaveBeenCalledWith("1.2.3", expect.objectContaining({ withBrowsers: true }));
105
+ });
106
+ });
@@ -0,0 +1,30 @@
1
+ import { getInstalledVersions, getResolvedActiveVersion, } from "../core/versions.js";
2
+ import { createLogger, formatError } from "../utils/logger.js";
3
+ export const runListCommand = async (deps = {}) => {
4
+ const logger = deps.logger ?? createLogger();
5
+ const getInstalled = deps.getInstalled ?? getInstalledVersions;
6
+ const getActive = deps.getActive ?? getResolvedActiveVersion;
7
+ try {
8
+ const [versions, currentVersion] = await Promise.all([
9
+ getInstalled(deps.installedOptions),
10
+ getActive(deps.activeOptions),
11
+ ]);
12
+ if (versions.length === 0) {
13
+ logger.info("No Playwright versions installed");
14
+ return;
15
+ }
16
+ for (const version of versions) {
17
+ if (version === currentVersion) {
18
+ logger.info(`* ${version}`);
19
+ }
20
+ else {
21
+ logger.info(` ${version}`);
22
+ }
23
+ }
24
+ }
25
+ catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ logger.error(formatError(`Failed to list versions: ${message}`, "Try again or run `pwvm install <version>` to add one."));
28
+ process.exitCode = 1;
29
+ }
30
+ };
@@ -0,0 +1,20 @@
1
+ import semver from "semver";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import { fetchPackageMetadata } from "../utils/registry.js";
4
+ export const runListRemoteCommand = async (deps = {}) => {
5
+ const logger = deps.logger ?? createLogger();
6
+ try {
7
+ const metadata = await fetchPackageMetadata("playwright");
8
+ const versions = Object.keys(metadata.versions ?? {}).filter((version) => semver.valid(version) !== null);
9
+ versions.sort((a, b) => semver.rcompare(a, b));
10
+ logger.info("Available Playwright versions:");
11
+ for (const version of versions) {
12
+ logger.info(version);
13
+ }
14
+ }
15
+ catch {
16
+ logger.error("[ERROR]: Unable to reach npm registry.");
17
+ logger.info("Please check your internet connection and try again.");
18
+ process.exitCode = 1;
19
+ }
20
+ };
@@ -0,0 +1,22 @@
1
+ import { pruneVersions } from "../core/prune.js";
2
+ import { createLogger, formatError } from "../utils/logger.js";
3
+ export const runPruneCommand = async (deps = {}) => {
4
+ const logger = deps.logger ?? createLogger();
5
+ const prune = deps.prune ?? pruneVersions;
6
+ try {
7
+ const removed = await prune(deps.pruneOptions);
8
+ if (removed.length === 0) {
9
+ logger.info("Nothing to prune");
10
+ return;
11
+ }
12
+ logger.info("Removed Playwright versions:");
13
+ for (const version of removed) {
14
+ logger.info(`- ${version}`);
15
+ }
16
+ }
17
+ catch (error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ logger.error(formatError(`Failed to prune versions: ${message}`, "Run `pwvm list` to review installed versions."));
20
+ process.exitCode = 1;
21
+ }
22
+ };
@@ -0,0 +1,38 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { getPwvmDir, getShimsDir } from "../core/paths.js";
4
+ import { unixShim, windowsShim } from "../core/shimTemplates.js";
5
+ import { createLogger, formatError } from "../utils/logger.js";
6
+ const buildPathInstructions = (shimsDir) => [
7
+ "",
8
+ "==> Next step:",
9
+ "- Run this command in your local terminal or add it to your CI pipeline:",
10
+ "",
11
+ `export PATH="${shimsDir}:$PATH"`,
12
+ ""
13
+ ];
14
+ export const runSetupCommand = async (deps = {}) => {
15
+ const logger = deps.logger ?? createLogger();
16
+ const fsImpl = deps.fs ?? fs;
17
+ const pwvmDir = getPwvmDir();
18
+ const shimsDir = deps.shimsDir ?? getShimsDir();
19
+ const pwvmBinPath = deps.pwvmBinPath ?? process.argv[1];
20
+ try {
21
+ await fsImpl.ensureDir(shimsDir);
22
+ const unixShimPath = path.join(shimsDir, "playwright");
23
+ const windowsShimPath = path.join(shimsDir, "playwright.cmd");
24
+ await fsImpl.writeFile(unixShimPath, unixShim(pwvmBinPath), "utf8");
25
+ await fsImpl.writeFile(windowsShimPath, windowsShim(pwvmBinPath), "utf8");
26
+ await fsImpl.chmod(unixShimPath, 0o755);
27
+ await fsImpl.ensureDir(pwvmDir);
28
+ await fsImpl.writeFile(path.join(pwvmDir, "setup-complete"), "ok\n", "utf8");
29
+ for (const line of buildPathInstructions(shimsDir)) {
30
+ logger.info(line);
31
+ }
32
+ }
33
+ catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ logger.error(formatError(`Setup failed: ${message}`, "Check filesystem permissions and try again."));
36
+ process.exitCode = 1;
37
+ }
38
+ };
@@ -0,0 +1,31 @@
1
+ import { setGlobalActiveVersion, } from "../core/versions.js";
2
+ import fs from "fs-extra";
3
+ import path from "node:path";
4
+ import { getVersionsDir } from "../core/paths.js";
5
+ import { createLogger, formatError } from "../utils/logger.js";
6
+ export const runUseCommand = async (version, deps = {}) => {
7
+ const logger = deps.logger ?? createLogger();
8
+ const setActive = deps.setActive ?? setGlobalActiveVersion;
9
+ const fsImpl = deps.fs ?? deps.setOptions?.fs ?? fs;
10
+ const resolveVersionsDir = deps.getVersionsDir ?? getVersionsDir;
11
+ const versionsDir = deps.versionsDir ??
12
+ deps.setOptions?.versionsDir ??
13
+ resolveVersionsDir(deps.setOptions?.homeDir);
14
+ const versionDir = path.join(versionsDir, version);
15
+ try {
16
+ const exists = await fsImpl.pathExists(versionDir);
17
+ if (!exists) {
18
+ logger.error(`Playwright version "${version}" is not installed.`);
19
+ logger.info(`Run \`pwvm install ${version}\` first.`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ await setActive(version, deps.setOptions);
24
+ logger.info(`Using Playwright ${version}`);
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ logger.error(formatError(`Failed to set active version: ${message}`, `Run \`pwvm install ${version}\` first.`));
29
+ process.exitCode = 1;
30
+ }
31
+ };
@@ -0,0 +1,41 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { runUseCommand } from "./use.js";
3
+ describe("runUseCommand", () => {
4
+ const originalExitCode = process.exitCode;
5
+ const originalIsTTY = process.stdout.isTTY;
6
+ let errorSpy;
7
+ beforeEach(() => {
8
+ process.exitCode = 0;
9
+ Object.defineProperty(process.stdout, "isTTY", {
10
+ value: false,
11
+ configurable: true,
12
+ });
13
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
14
+ });
15
+ afterEach(() => {
16
+ process.exitCode = originalExitCode;
17
+ Object.defineProperty(process.stdout, "isTTY", {
18
+ value: originalIsTTY,
19
+ configurable: true,
20
+ });
21
+ errorSpy.mockRestore();
22
+ });
23
+ it("logs an error and guidance when version is not installed", async () => {
24
+ const version = "1.9999.99999";
25
+ const setActive = vi.fn(async () => { });
26
+ const fsMock = {
27
+ pathExists: vi.fn(async () => false),
28
+ };
29
+ const versionsDir = "/mock/versions";
30
+ const infoSpy = vi.spyOn(console, "log").mockImplementation(() => { });
31
+ await runUseCommand(version, { setActive, fs: fsMock, versionsDir });
32
+ expect(process.exitCode).toBe(1);
33
+ expect(errorSpy).toHaveBeenCalled();
34
+ const message = String(errorSpy.mock.calls[0][0]);
35
+ expect(message).toContain("[ERROR]:");
36
+ const infoMessage = String(infoSpy.mock.calls[0][0]);
37
+ expect(infoMessage).toContain(`Run \`pwvm install ${version}\` first.`);
38
+ expect(setActive).not.toHaveBeenCalled();
39
+ infoSpy.mockRestore();
40
+ });
41
+ });