pwvm 0.1.0 → 1.0.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pwvm
2
2
 
3
+ ![CI](https://github.com/eaccmk/pwvm/actions/workflows/ci.yml/badge.svg)
4
+
3
5
  ### **Playwright Version Manager**
4
6
 
5
7
  A simple *Playwright Version Manager* that solves a common pain point:
@@ -8,6 +10,8 @@ A simple *Playwright Version Manager* that solves a common pain point:
8
10
 
9
11
  `pwvm` lets you install, manage, and switch Playwright versions reliably — **one command, predictable behavior**, just like `nvm` for Node.js.
10
12
 
13
+ ![pwvm icon ](./wiki/pwvm_icon_white.png)
14
+
11
15
  ---
12
16
 
13
17
  ## Why pwvm
@@ -43,25 +47,41 @@ Follow the printed instructions to add pwvm shims to your `PATH`.
43
47
  ## Common usage
44
48
 
45
49
  ```sh
46
- pwvm install 1.53.0
47
- pwvm use 1.53.0
50
+ pwvm install 1.57.0
51
+ pwvm use 1.57.0
52
+ playwright --version
48
53
  playwright test
49
54
  ```
50
55
 
56
+ ![pwvm commmands demo](./wiki/switching_playwright_vesions.gif)
57
+
51
58
  Pin versions per project with `.pwvmrc`:
52
59
 
53
60
  ```text
54
- 1.53.0
61
+ 1.57.0
55
62
  ```
56
63
 
64
+ ### Deterministic guarantees pwvm already provides
65
+ • Playwright installed by exact version (`playwright@x.y.z`)
66
+ • Versions stored in isolated directories (`~/.pwvm/versions/<version>`)
67
+ • Browser binaries scoped per Playwright version
68
+ • `.pwvmrc` pins version per repo
69
+ • No implicit upgrades (no latest unless explicitly requested)
70
+
71
+ > [!NOTE] pwvm performs no background network activity and only installs software when explicitly requested.
72
+
57
73
  ---
58
74
 
59
- ## CI-friendly
75
+ ### CI-friendly
60
76
 
61
77
  `pwvm` works in GitHub Actions, Azure Pipelines, Bitbucket, and any CI where you control `PATH`.
62
78
 
63
79
  Install → setup → select version → run Playwright.
64
80
 
81
+ ### Dockerized
82
+
83
+ `pwvm` works seamlessly in Docker containers or local development.
84
+
65
85
  ---
66
86
 
67
87
  ## Support pwvm
@@ -72,7 +92,7 @@ If this tool saves you time, CI hours, or debugging frustration:
72
92
 
73
93
  * ⭐ [Star the project](https://github.com/eaccmk/pwvm)
74
94
  * ❤️ [Sponsor via GitHub](https://github.com/sponsors/eaccmk)
75
- * 🔁 Share it with your team
95
+ * 🔁 Share it with your team and tag #QualityWithMillan
76
96
 
77
97
  Your support helps keep pwvm maintained and improving.
78
98
 
@@ -24,7 +24,7 @@ export const runDoctorCommand = async (deps = {}) => {
24
24
  const fsImpl = deps.fs ?? fs;
25
25
  const pwvmDir = getPwvmDir();
26
26
  const shimsDir = deps.shimsDir ?? getShimsDir();
27
- const envPath = deps.envPath ?? process.env.PATH ?? "";
27
+ const envPath = deps.envPath ?? process.env.PATH ?? process.env.DOCKER_PATH ?? "";
28
28
  const shimPath = path.join(shimsDir, "playwright");
29
29
  const useColor = process.stdout.isTTY === true;
30
30
  const withScope = (message) => message.startsWith(" ") ? `pwvm-doctor${message}` : `pwvm-doctor ${message}`;
@@ -0,0 +1,25 @@
1
+ import { uninstallPlaywrightVersion, } from "../core/uninstall.js";
2
+ import { createLogger, formatError } from "../utils/logger.js";
3
+ export const runUninstallCommand = async (version, deps = {}) => {
4
+ const logger = deps.logger ?? createLogger();
5
+ const uninstall = deps.uninstall ?? uninstallPlaywrightVersion;
6
+ try {
7
+ await uninstall(version, deps.uninstallOptions);
8
+ logger.info(`Uninstalled Playwright ${version}`);
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ let hint;
13
+ if (message.includes("is currently active")) {
14
+ hint = "Run `pwvm use <version>` to switch first.";
15
+ }
16
+ else if (message.includes("is not installed")) {
17
+ hint = "Run `pwvm list` to see installed versions.";
18
+ }
19
+ else if (message.includes("Invalid Playwright version")) {
20
+ hint = "Use a valid semver like 1.2.3.";
21
+ }
22
+ logger.error(formatError(message, hint));
23
+ process.exitCode = 1;
24
+ }
25
+ };
@@ -0,0 +1,27 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import semver from "semver";
4
+ import { getVersionsDir } from "./paths.js";
5
+ import { getGlobalActiveVersion } from "./versions.js";
6
+ export const uninstallPlaywrightVersion = async (version, options = {}) => {
7
+ if (!semver.valid(version)) {
8
+ throw new Error(`Invalid Playwright version "${version}".`);
9
+ }
10
+ const fsImpl = options.fs ?? fs;
11
+ const activeVersion = await getGlobalActiveVersion({
12
+ homeDir: options.homeDir,
13
+ activeVersionFile: options.activeVersionFile,
14
+ fs: fsImpl,
15
+ });
16
+ if (activeVersion === version) {
17
+ throw new Error(`Playwright version "${version}" is currently active.`);
18
+ }
19
+ const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
20
+ const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
21
+ const targetDir = path.join(versionsDir, version);
22
+ const exists = await fsImpl.pathExists(targetDir);
23
+ if (!exists) {
24
+ throw new Error(`Playwright version "${version}" is not installed.`);
25
+ }
26
+ await fsImpl.remove(targetDir);
27
+ };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import fs from "fs-extra";
4
+ import { createRequire } from "node:module";
4
5
  import path from "node:path";
5
6
  import { runCurrentCommand } from "./commands/current.js";
6
7
  import { runDoctorCommand } from "./commands/doctor.js";
@@ -9,10 +10,13 @@ import { runListCommand } from "./commands/list.js";
9
10
  import { runListRemoteCommand } from "./commands/listRemote.js";
10
11
  import { runPruneCommand } from "./commands/prune.js";
11
12
  import { runSetupCommand } from "./commands/setup.js";
13
+ import { runUninstallCommand } from "./commands/uninstall.js";
12
14
  import { runUseCommand } from "./commands/use.js";
13
15
  import { getPwvmDir, getShimsDir } from "./core/paths.js";
14
16
  import { runPlaywrightShim } from "./core/shim.js";
15
17
  import { createLogger } from "./utils/logger.js";
18
+ const require = createRequire(import.meta.url);
19
+ const { version: pwvmVersion } = require("../package.json");
16
20
  const program = new Command();
17
21
  program.enablePositionalOptions();
18
22
  const logger = createLogger();
@@ -34,8 +38,78 @@ const getShimArgs = () => {
34
38
  };
35
39
  program
36
40
  .name("pwvm")
37
- .description("Playwright Version Manager")
38
- .version("0.0.1");
41
+ .description(`Playwright Version Manager (v${pwvmVersion})`)
42
+ .version(pwvmVersion, "-v, --version");
43
+ program.configureOutput({
44
+ outputError: () => { },
45
+ });
46
+ program.exitOverride((err) => {
47
+ // Commander control-flow exits (help / version)
48
+ if (err.code === "commander.helpDisplayed" ||
49
+ err.code === "commander.versionDisplayed" ||
50
+ err.code === "commander.help" ||
51
+ err.exitCode === 0) {
52
+ process.exitCode = err.exitCode || 0;
53
+ return;
54
+ }
55
+ let message = err.message;
56
+ if (message.startsWith("error: ")) {
57
+ message = message.slice("error: ".length);
58
+ }
59
+ message = message.replace(/'([^']+)'/g, "\"$1\"");
60
+ if (message.length > 0) {
61
+ message = `${message[0].toUpperCase()}${message.slice(1)}`;
62
+ }
63
+ if (!message.endsWith(".")) {
64
+ message = `${message}.`;
65
+ }
66
+ logger.error(message);
67
+ process.exitCode = err.exitCode || 1;
68
+ });
69
+ program.configureHelp({
70
+ formatHelp: (cmd, helper) => {
71
+ const help = Object.getPrototypeOf(helper).formatHelp.call(helper, cmd, helper);
72
+ if (cmd !== program) {
73
+ return help;
74
+ }
75
+ const sections = help.split("\n\n");
76
+ if (sections.length < 2 || !sections[0].startsWith("Usage:")) {
77
+ return help;
78
+ }
79
+ let reordered = [sections[1], sections[0], ...sections.slice(2)].join("\n\n");
80
+ const installLine = " install Install a Playwright version";
81
+ const uninstallLine = " uninstall <version> Uninstall a specific Playwright version";
82
+ if (!reordered.includes(uninstallLine) && reordered.includes(installLine)) {
83
+ reordered = reordered.replace(`${installLine}\n`, `${installLine}\n${uninstallLine}\n`);
84
+ }
85
+ const lines = reordered.split("\n");
86
+ let inCommands = false;
87
+ for (let i = 0; i < lines.length; i += 1) {
88
+ const line = lines[i];
89
+ if (line === "Commands:") {
90
+ inCommands = true;
91
+ continue;
92
+ }
93
+ if (inCommands) {
94
+ if (line.trim().length === 0) {
95
+ inCommands = false;
96
+ continue;
97
+ }
98
+ const match = /^ (.+?)( {2,})(\S.*)$/.exec(line);
99
+ if (match) {
100
+ const prefix = "pwvm ";
101
+ const term = match[1];
102
+ const spacing = match[2];
103
+ const desc = match[3];
104
+ const adjusted = Math.max(2, spacing.length - prefix.length);
105
+ lines[i] = ` ${prefix}${term}${" ".repeat(adjusted)}${desc}`;
106
+ }
107
+ }
108
+ }
109
+ reordered = lines.join("\n");
110
+ return `\n${reordered}`;
111
+ },
112
+ });
39
113
  const shouldShowSetupNotice = async () => {
40
114
  if (setupNoticeShown) {
41
115
  return false;
@@ -99,6 +173,11 @@ program
99
173
  .option("--with-browsers", "Install Playwright browsers (default)")
100
174
  .option("--no-browsers", "Skip Playwright browser install")
101
175
  .action((version, options) => invoke(runInstallCommand, version, options));
176
+ program
177
+ .command("uninstall")
178
+ .description("Uninstall a specific Playwright version")
179
+ .argument("<version>")
180
+ .action((version) => invoke(runUninstallCommand, version));
102
181
  program
103
182
  .command("list")
104
183
  .description("List installed Playwright versions")
@@ -143,7 +222,9 @@ program
143
222
  process.exitCode = 1;
144
223
  }
145
224
  });
146
- void program.parseAsync();
225
+ program.parseAsync().catch(() => {
226
+ // All commander control-flow exits are handled by exitOverride.
227
+ });
147
228
  function getShimErrorDetails(message) {
148
229
  const guidanceMarker = ". Run ";
149
230
  const guidanceIndex = message.indexOf(guidanceMarker);
@@ -1,5 +1,7 @@
1
1
  import https from "node:https";
2
2
  const REGISTRY_URL = "https://registry.npmjs.org";
3
+ // NOTE: This function MUST only be called from user-invoked commands
4
+ // such as `list-remote` or `install latest`. No background usage allowed.
3
5
  export const fetchPackageMetadata = async (packageName) => new Promise((resolve, reject) => {
4
6
  const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}`;
5
7
  const request = https.get(url, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "pwvm",
4
- "version": "0.1.0",
4
+ "version": "1.0.0",
5
5
  "description": "Playwright Version Manager",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "dist",
14
14
  "README.md",
15
+ "wiki",
15
16
  "LICENSE"
16
17
  ],
17
18
  "scripts": {
@@ -25,8 +26,11 @@
25
26
  "playwright-version-manager",
26
27
  "playwright-cli",
27
28
  "version-manager",
29
+ "version",
30
+ "manager",
28
31
  "testing",
29
- "e2e"
32
+ "ci",
33
+ "cli"
30
34
  ],
31
35
  "engines": {
32
36
  "node": ">=18"
Binary file
Binary file