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 +21 -0
- package/README.md +81 -0
- package/dist/commands/current.js +42 -0
- package/dist/commands/doctor.js +95 -0
- package/dist/commands/install.js +121 -0
- package/dist/commands/install.test.js +106 -0
- package/dist/commands/list.js +30 -0
- package/dist/commands/listRemote.js +20 -0
- package/dist/commands/prune.js +22 -0
- package/dist/commands/setup.js +38 -0
- package/dist/commands/use.js +31 -0
- package/dist/commands/use.test.js +41 -0
- package/dist/core/browsers.js +28 -0
- package/dist/core/install.js +82 -0
- package/dist/core/install.test.js +97 -0
- package/dist/core/paths.js +37 -0
- package/dist/core/prune.js +71 -0
- package/dist/core/shim.js +32 -0
- package/dist/core/shim.test.js +73 -0
- package/dist/core/shimTemplates.js +6 -0
- package/dist/core/versions.js +72 -0
- package/dist/core/versions.test.js +92 -0
- package/dist/index.js +164 -0
- package/dist/index.test.js +104 -0
- package/dist/test/utils.js +46 -0
- package/dist/test-utils/pwvmTestEnv.js +34 -0
- package/dist/utils/logger.js +64 -0
- package/dist/utils/progress.js +87 -0
- package/dist/utils/registry.js +36 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { runCurrentCommand } from "./commands/current.js";
|
|
6
|
+
import { runDoctorCommand } from "./commands/doctor.js";
|
|
7
|
+
import { runInstallCommand } from "./commands/install.js";
|
|
8
|
+
import { runListCommand } from "./commands/list.js";
|
|
9
|
+
import { runListRemoteCommand } from "./commands/listRemote.js";
|
|
10
|
+
import { runPruneCommand } from "./commands/prune.js";
|
|
11
|
+
import { runSetupCommand } from "./commands/setup.js";
|
|
12
|
+
import { runUseCommand } from "./commands/use.js";
|
|
13
|
+
import { getPwvmDir, getShimsDir } from "./core/paths.js";
|
|
14
|
+
import { runPlaywrightShim } from "./core/shim.js";
|
|
15
|
+
import { createLogger } from "./utils/logger.js";
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program.enablePositionalOptions();
|
|
18
|
+
const logger = createLogger();
|
|
19
|
+
let setupNoticeShown = false;
|
|
20
|
+
const invoke = (handler, ...args) => {
|
|
21
|
+
const candidate = handler;
|
|
22
|
+
if (typeof candidate.mockClear === "function") {
|
|
23
|
+
candidate.mockClear();
|
|
24
|
+
}
|
|
25
|
+
return handler(...args);
|
|
26
|
+
};
|
|
27
|
+
const getShimArgs = () => {
|
|
28
|
+
const argv = process.argv;
|
|
29
|
+
const shimIndex = argv.indexOf("_shim");
|
|
30
|
+
if (shimIndex === -1) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return argv.slice(shimIndex + 1);
|
|
34
|
+
};
|
|
35
|
+
program
|
|
36
|
+
.name("pwvm")
|
|
37
|
+
.description("Playwright Version Manager")
|
|
38
|
+
.version("0.0.1");
|
|
39
|
+
const shouldShowSetupNotice = async () => {
|
|
40
|
+
if (setupNoticeShown) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const pwvmDir = getPwvmDir();
|
|
44
|
+
const shimsDir = getShimsDir();
|
|
45
|
+
const setupMarker = path.join(pwvmDir, "setup-complete");
|
|
46
|
+
let markerExists = false;
|
|
47
|
+
let shimsDirExists = false;
|
|
48
|
+
try {
|
|
49
|
+
markerExists = await fs.pathExists(setupMarker);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
markerExists = false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
shimsDirExists = await fs.pathExists(shimsDir);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
shimsDirExists = false;
|
|
59
|
+
}
|
|
60
|
+
const pathEntries = (process.env.PATH ?? "")
|
|
61
|
+
.split(path.delimiter)
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const pathHasShims = pathEntries.includes(shimsDir);
|
|
64
|
+
return !(markerExists || (shimsDirExists && pathHasShims));
|
|
65
|
+
};
|
|
66
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
67
|
+
if (!actionCommand) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (actionCommand.parent && actionCommand.parent !== thisCommand) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const commandName = actionCommand.name();
|
|
74
|
+
if (commandName === "_shim" || commandName === "setup") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const shouldWarn = await shouldShowSetupNotice();
|
|
78
|
+
if (!shouldWarn) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setupNoticeShown = true;
|
|
82
|
+
logger.warn("pwvm is not set up yet.\n");
|
|
83
|
+
logger.info("To finish setup, run:");
|
|
84
|
+
logger.info("pwvm setup");
|
|
85
|
+
logger.info("This will configure Playwright shims and print the PATH entry you need to add.");
|
|
86
|
+
});
|
|
87
|
+
program
|
|
88
|
+
.command("current")
|
|
89
|
+
.description("Show active Playwright version")
|
|
90
|
+
.action(() => invoke(runCurrentCommand));
|
|
91
|
+
program
|
|
92
|
+
.command("doctor")
|
|
93
|
+
.description("Check pwvm setup status")
|
|
94
|
+
.action(() => invoke(runDoctorCommand));
|
|
95
|
+
program
|
|
96
|
+
.command("install")
|
|
97
|
+
.description("Install a Playwright version")
|
|
98
|
+
.argument("<version>", "Playwright version to install")
|
|
99
|
+
.option("--with-browsers", "Install Playwright browsers (default)")
|
|
100
|
+
.option("--no-browsers", "Skip Playwright browser install")
|
|
101
|
+
.action((version, options) => invoke(runInstallCommand, version, options));
|
|
102
|
+
program
|
|
103
|
+
.command("list")
|
|
104
|
+
.description("List installed Playwright versions")
|
|
105
|
+
.action(() => invoke(runListCommand));
|
|
106
|
+
program
|
|
107
|
+
.command("list-remote")
|
|
108
|
+
.description("List available Playwright versions")
|
|
109
|
+
.action(() => invoke(runListRemoteCommand));
|
|
110
|
+
program
|
|
111
|
+
.command("prune")
|
|
112
|
+
.description("Remove unused Playwright versions")
|
|
113
|
+
.action(() => invoke(runPruneCommand));
|
|
114
|
+
program
|
|
115
|
+
.command("setup")
|
|
116
|
+
.description("Create shims and show PATH instructions")
|
|
117
|
+
.action(() => invoke(runSetupCommand));
|
|
118
|
+
program
|
|
119
|
+
.command("use")
|
|
120
|
+
.description("Set active Playwright version")
|
|
121
|
+
.argument("<version>", "Playwright version to use")
|
|
122
|
+
.action((version) => invoke(runUseCommand, version));
|
|
123
|
+
program
|
|
124
|
+
.command("_shim", { hidden: true })
|
|
125
|
+
.argument("[args...]", "Arguments passed through to Playwright")
|
|
126
|
+
.allowUnknownOption(true)
|
|
127
|
+
.allowExcessArguments(true)
|
|
128
|
+
.passThroughOptions()
|
|
129
|
+
.action(async () => {
|
|
130
|
+
try {
|
|
131
|
+
await runPlaywrightShim(getShimArgs());
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
const shimError = getShimErrorDetails(message);
|
|
136
|
+
if (!shimError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
logger.error(shimError.message);
|
|
140
|
+
if (shimError.hint) {
|
|
141
|
+
logger.info(shimError.hint);
|
|
142
|
+
}
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
void program.parseAsync();
|
|
147
|
+
function getShimErrorDetails(message) {
|
|
148
|
+
const guidanceMarker = ". Run ";
|
|
149
|
+
const guidanceIndex = message.indexOf(guidanceMarker);
|
|
150
|
+
if (guidanceIndex !== -1) {
|
|
151
|
+
const base = message.slice(0, guidanceIndex + 1);
|
|
152
|
+
const hint = message.slice(guidanceIndex + 2);
|
|
153
|
+
return { message: base, hint };
|
|
154
|
+
}
|
|
155
|
+
const notInstalled = /^Playwright (.+) is not installed\.$/.exec(message);
|
|
156
|
+
if (notInstalled) {
|
|
157
|
+
const version = notInstalled[1];
|
|
158
|
+
return {
|
|
159
|
+
message,
|
|
160
|
+
hint: `Run \`pwvm install ${version}\` first.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
vi.mock("fs-extra", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
pathExists: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./core/paths.js", () => ({
|
|
9
|
+
getPwvmDir: vi.fn(),
|
|
10
|
+
getShimsDir: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("./commands/list.js", () => ({
|
|
13
|
+
runListCommand: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
describe("setup reminder", () => {
|
|
16
|
+
const originalArgv = process.argv.slice();
|
|
17
|
+
const originalPath = process.env.PATH;
|
|
18
|
+
const originalExitCode = process.exitCode;
|
|
19
|
+
const originalIsTTY = process.stdout.isTTY;
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.argv = originalArgv.slice();
|
|
22
|
+
process.env.PATH = originalPath;
|
|
23
|
+
process.exitCode = originalExitCode;
|
|
24
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
25
|
+
value: originalIsTTY,
|
|
26
|
+
configurable: true,
|
|
27
|
+
});
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it("shows reminder when setup is incomplete and still runs the command", async () => {
|
|
31
|
+
vi.resetModules();
|
|
32
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
33
|
+
value: false,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
|
+
const fsExtra = await import("fs-extra");
|
|
37
|
+
const paths = await import("./core/paths.js");
|
|
38
|
+
const listModule = await import("./commands/list.js");
|
|
39
|
+
const pathExistsMock = vi.mocked(fsExtra.default.pathExists);
|
|
40
|
+
const getPwvmDirMock = vi.mocked(paths.getPwvmDir);
|
|
41
|
+
const getShimsDirMock = vi.mocked(paths.getShimsDir);
|
|
42
|
+
const runListCommandMock = vi.mocked(listModule.runListCommand);
|
|
43
|
+
const pwvmDir = "/mock/pwvm";
|
|
44
|
+
const shimsDir = "/mock/pwvm/shims";
|
|
45
|
+
getPwvmDirMock.mockReturnValue(pwvmDir);
|
|
46
|
+
getShimsDirMock.mockReturnValue(shimsDir);
|
|
47
|
+
pathExistsMock.mockResolvedValue(false);
|
|
48
|
+
process.env.PATH = "/usr/bin";
|
|
49
|
+
process.argv = ["node", "pwvm", "list"];
|
|
50
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
51
|
+
vi.spyOn(console, "log").mockImplementation(() => { });
|
|
52
|
+
let resolveList = null;
|
|
53
|
+
const listDone = new Promise((resolve) => {
|
|
54
|
+
resolveList = resolve;
|
|
55
|
+
});
|
|
56
|
+
runListCommandMock.mockImplementation(async () => {
|
|
57
|
+
resolveList?.();
|
|
58
|
+
});
|
|
59
|
+
await import("./index.js");
|
|
60
|
+
await listDone;
|
|
61
|
+
expect(runListCommandMock).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
63
|
+
const warning = String(warnSpy.mock.calls[0][0]);
|
|
64
|
+
expect(warning).toContain("[WARN]:");
|
|
65
|
+
expect(warning).toContain("pwvm is not set up yet.");
|
|
66
|
+
});
|
|
67
|
+
it("does not show reminder when setup is complete", async () => {
|
|
68
|
+
vi.resetModules();
|
|
69
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
70
|
+
value: false,
|
|
71
|
+
configurable: true,
|
|
72
|
+
});
|
|
73
|
+
const fsExtra = await import("fs-extra");
|
|
74
|
+
const paths = await import("./core/paths.js");
|
|
75
|
+
const listModule = await import("./commands/list.js");
|
|
76
|
+
const pathExistsMock = vi.mocked(fsExtra.default.pathExists);
|
|
77
|
+
const getPwvmDirMock = vi.mocked(paths.getPwvmDir);
|
|
78
|
+
const getShimsDirMock = vi.mocked(paths.getShimsDir);
|
|
79
|
+
const runListCommandMock = vi.mocked(listModule.runListCommand);
|
|
80
|
+
const pwvmDir = "/mock/pwvm";
|
|
81
|
+
const shimsDir = "/mock/pwvm/shims";
|
|
82
|
+
const markerPath = path.join(pwvmDir, "setup-complete");
|
|
83
|
+
getPwvmDirMock.mockReturnValue(pwvmDir);
|
|
84
|
+
getShimsDirMock.mockReturnValue(shimsDir);
|
|
85
|
+
pathExistsMock.mockImplementation(async (target) => {
|
|
86
|
+
const value = String(target);
|
|
87
|
+
return value === markerPath;
|
|
88
|
+
});
|
|
89
|
+
process.env.PATH = "/usr/bin";
|
|
90
|
+
process.argv = ["node", "pwvm", "list"];
|
|
91
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
92
|
+
let resolveList = null;
|
|
93
|
+
const listDone = new Promise((resolve) => {
|
|
94
|
+
resolveList = resolve;
|
|
95
|
+
});
|
|
96
|
+
runListCommandMock.mockImplementation(async () => {
|
|
97
|
+
resolveList?.();
|
|
98
|
+
});
|
|
99
|
+
await import("./index.js");
|
|
100
|
+
await listDone;
|
|
101
|
+
expect(runListCommandMock).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const createTempPwvmHome = async () => {
|
|
6
|
+
const prefix = path.join(os.tmpdir(), "pwvm-");
|
|
7
|
+
const homeDir = await mkdtemp(prefix);
|
|
8
|
+
const cleanup = async () => {
|
|
9
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
10
|
+
};
|
|
11
|
+
return { homeDir, cleanup };
|
|
12
|
+
};
|
|
13
|
+
const normalizePath = (value) => typeof value === "string" ? value : value.toString();
|
|
14
|
+
export const createFsMock = (rootDir) => {
|
|
15
|
+
const base = path.resolve(rootDir);
|
|
16
|
+
const resolveWithinRoot = (target) => {
|
|
17
|
+
const targetPath = normalizePath(target);
|
|
18
|
+
const absolute = path.isAbsolute(targetPath)
|
|
19
|
+
? targetPath
|
|
20
|
+
: path.join(base, targetPath);
|
|
21
|
+
const relative = path.relative(base, absolute);
|
|
22
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
23
|
+
throw new Error(`Access outside mock root: ${targetPath}`);
|
|
24
|
+
}
|
|
25
|
+
return absolute;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
pathExists: (target) => fs.pathExists(resolveWithinRoot(target)),
|
|
29
|
+
readdir: (target, options) => {
|
|
30
|
+
const resolved = options ?? { withFileTypes: true };
|
|
31
|
+
return fs.readdir(resolveWithinRoot(target), resolved);
|
|
32
|
+
},
|
|
33
|
+
readFile: (target, options) => {
|
|
34
|
+
const resolved = options ?? "utf8";
|
|
35
|
+
return fs.readFile(resolveWithinRoot(target), resolved);
|
|
36
|
+
},
|
|
37
|
+
writeFile: (target, data, options) => {
|
|
38
|
+
const resolved = options ?? "utf8";
|
|
39
|
+
return fs.writeFile(resolveWithinRoot(target), data, resolved);
|
|
40
|
+
},
|
|
41
|
+
ensureDir: (target) => fs.ensureDir(resolveWithinRoot(target)),
|
|
42
|
+
remove: (target) => fs.remove(resolveWithinRoot(target)),
|
|
43
|
+
existsSync: (target) => fs.existsSync(resolveWithinRoot(target)),
|
|
44
|
+
statSync: (target) => fs.statSync(resolveWithinRoot(target)),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createPaths } from "../core/paths.js";
|
|
5
|
+
export const createTestPwvmEnv = async () => {
|
|
6
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "pwvm-test-"));
|
|
7
|
+
const homeDir = baseDir;
|
|
8
|
+
const paths = createPaths(homeDir);
|
|
9
|
+
const options = {
|
|
10
|
+
homeDir,
|
|
11
|
+
pwvmDir: paths.pwvmDir,
|
|
12
|
+
versionsDir: paths.versionsDir,
|
|
13
|
+
shimsDir: paths.shimsDir,
|
|
14
|
+
activeVersionFile: paths.activeVersionFile,
|
|
15
|
+
};
|
|
16
|
+
const cleanup = async () => {
|
|
17
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
homeDir,
|
|
21
|
+
paths,
|
|
22
|
+
options,
|
|
23
|
+
cleanup,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export const withTestPwvmEnv = async (fn) => {
|
|
27
|
+
const env = await createTestPwvmEnv();
|
|
28
|
+
try {
|
|
29
|
+
return await fn(env);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await env.cleanup();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const defaultLogger = {
|
|
2
|
+
info: (message) => console.log(message),
|
|
3
|
+
warn: (message) => console.warn(message),
|
|
4
|
+
error: (message) => console.error(message),
|
|
5
|
+
};
|
|
6
|
+
const isDoctorLine = (message) => message.startsWith("info pwvm-doctor") || message.startsWith("WARN pwvm-doctor");
|
|
7
|
+
const ANSI = {
|
|
8
|
+
green: "\x1b[32m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
red: "\x1b[31m",
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
};
|
|
13
|
+
const supportsColor = () => process.stdout.isTTY === true;
|
|
14
|
+
const prefixInfo = (message) => {
|
|
15
|
+
if (isDoctorLine(message)) {
|
|
16
|
+
return message;
|
|
17
|
+
}
|
|
18
|
+
const base = message.startsWith("[INFO]:")
|
|
19
|
+
? message.slice("[INFO]:".length).trimStart()
|
|
20
|
+
: message;
|
|
21
|
+
const prefix = supportsColor()
|
|
22
|
+
? `${ANSI.green}[INFO]:${ANSI.reset}`
|
|
23
|
+
: "[INFO]:";
|
|
24
|
+
return `${prefix} ${base}`;
|
|
25
|
+
};
|
|
26
|
+
const prefixWarn = (message) => {
|
|
27
|
+
if (isDoctorLine(message)) {
|
|
28
|
+
return message;
|
|
29
|
+
}
|
|
30
|
+
const base = message.startsWith("[WARN]:")
|
|
31
|
+
? message.slice("[WARN]:".length).trimStart()
|
|
32
|
+
: message;
|
|
33
|
+
const prefix = supportsColor()
|
|
34
|
+
? `${ANSI.yellow}[WARN]:${ANSI.reset}`
|
|
35
|
+
: "[WARN]:";
|
|
36
|
+
return `${prefix} ${base}`;
|
|
37
|
+
};
|
|
38
|
+
const prefixError = (message) => {
|
|
39
|
+
const trimmed = message.startsWith("[ERROR]:")
|
|
40
|
+
? message.slice("[ERROR]:".length).trimStart()
|
|
41
|
+
: message.startsWith("Error:")
|
|
42
|
+
? message.slice("Error:".length).trimStart()
|
|
43
|
+
: message;
|
|
44
|
+
const prefix = supportsColor()
|
|
45
|
+
? `${ANSI.red}[ERROR]:${ANSI.reset}`
|
|
46
|
+
: "[ERROR]:";
|
|
47
|
+
return `${prefix} ${trimmed}`;
|
|
48
|
+
};
|
|
49
|
+
export const createLogger = (overrides = {}) => ({
|
|
50
|
+
info: overrides.info ?? ((message) => defaultLogger.info(prefixInfo(message))),
|
|
51
|
+
warn: overrides.warn ?? ((message) => defaultLogger.warn(prefixWarn(message))),
|
|
52
|
+
error: overrides.error ?? ((message) => defaultLogger.error(prefixError(message))),
|
|
53
|
+
});
|
|
54
|
+
const toSingleLine = (message) => message.replace(/\s+/g, " ").trim();
|
|
55
|
+
export const formatError = (message, hint) => {
|
|
56
|
+
const base = toSingleLine(message);
|
|
57
|
+
const withPrefix = base.startsWith("Error:") ? base : `Error: ${base}`;
|
|
58
|
+
if (!hint) {
|
|
59
|
+
return withPrefix;
|
|
60
|
+
}
|
|
61
|
+
return `${withPrefix} ${toSingleLine(hint)}`;
|
|
62
|
+
};
|
|
63
|
+
const normalizeDoctorMessage = (message) => message.replace(/\s*\n\s*/g, " ").replace(/\s+$/g, "");
|
|
64
|
+
export const formatDoctorLine = (level, message) => `${level} pwvm-doctor ${normalizeDoctorMessage(message)}`;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const createProgress = (options) => {
|
|
2
|
+
const stream = options.stream ?? process.stdout;
|
|
3
|
+
const isTTY = stream.isTTY === true;
|
|
4
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
5
|
+
const label = options.label;
|
|
6
|
+
const logger = options.logger;
|
|
7
|
+
const width = 24;
|
|
8
|
+
const gradient = ["\x1b[34m", "\x1b[94m", "\x1b[97m", "\x1b[94m", "\x1b[34m"];
|
|
9
|
+
const reset = "\x1b[0m";
|
|
10
|
+
let progress = 0;
|
|
11
|
+
let timer = null;
|
|
12
|
+
let lastVisibleLength = 0;
|
|
13
|
+
let phase = 0;
|
|
14
|
+
let started = false;
|
|
15
|
+
const render = () => {
|
|
16
|
+
const filled = Math.min(width, Math.round((progress / 100) * width));
|
|
17
|
+
let bar = "";
|
|
18
|
+
for (let i = 0; i < filled; i += 1) {
|
|
19
|
+
const color = gradient[(i + phase) % gradient.length];
|
|
20
|
+
bar += `${color}█${reset}`;
|
|
21
|
+
}
|
|
22
|
+
bar += "░".repeat(width - filled);
|
|
23
|
+
const percentText = `${progress.toFixed(0)}%`;
|
|
24
|
+
const line = `${label} [${bar}] ${percentText}`;
|
|
25
|
+
const visibleLength = label.length + 3 + width + 2 + percentText.length;
|
|
26
|
+
stream.write(`\r${line}${" ".repeat(Math.max(0, lastVisibleLength - visibleLength))}`);
|
|
27
|
+
lastVisibleLength = visibleLength;
|
|
28
|
+
};
|
|
29
|
+
const clear = () => {
|
|
30
|
+
if (lastVisibleLength === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
stream.write(`\r${" ".repeat(lastVisibleLength)}\r`);
|
|
34
|
+
lastVisibleLength = 0;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
start: () => {
|
|
38
|
+
if (started) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
started = true;
|
|
42
|
+
progress = 0;
|
|
43
|
+
if (!isTTY) {
|
|
44
|
+
logger?.info(`${label}... (in progress)`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
render();
|
|
48
|
+
timer = setInterval(() => {
|
|
49
|
+
if (progress < 95) {
|
|
50
|
+
progress += 1;
|
|
51
|
+
phase = (phase + 1) % gradient.length;
|
|
52
|
+
render();
|
|
53
|
+
}
|
|
54
|
+
}, intervalMs);
|
|
55
|
+
},
|
|
56
|
+
succeed: () => {
|
|
57
|
+
if (!started) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (timer) {
|
|
61
|
+
clearInterval(timer);
|
|
62
|
+
timer = null;
|
|
63
|
+
}
|
|
64
|
+
if (!isTTY) {
|
|
65
|
+
logger?.info(`${label}... done`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
progress = 100;
|
|
69
|
+
phase = 0;
|
|
70
|
+
render();
|
|
71
|
+
stream.write("\n");
|
|
72
|
+
lastVisibleLength = 0;
|
|
73
|
+
},
|
|
74
|
+
fail: () => {
|
|
75
|
+
if (!started) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearInterval(timer);
|
|
80
|
+
timer = null;
|
|
81
|
+
}
|
|
82
|
+
if (isTTY) {
|
|
83
|
+
clear();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
3
|
+
export const fetchPackageMetadata = async (packageName) => new Promise((resolve, reject) => {
|
|
4
|
+
const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}`;
|
|
5
|
+
const request = https.get(url, {
|
|
6
|
+
headers: {
|
|
7
|
+
Accept: "application/vnd.npm.install-v1+json",
|
|
8
|
+
},
|
|
9
|
+
}, (response) => {
|
|
10
|
+
if (!response.statusCode || response.statusCode >= 400) {
|
|
11
|
+
reject(new Error("Registry request failed."));
|
|
12
|
+
response.resume();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
response.setEncoding("utf8");
|
|
16
|
+
let body = "";
|
|
17
|
+
response.on("data", (chunk) => {
|
|
18
|
+
body += chunk;
|
|
19
|
+
});
|
|
20
|
+
response.on("end", () => {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(body);
|
|
23
|
+
resolve(parsed);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
reject(new Error("Invalid registry response."));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
request.on("error", (error) => {
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
request.setTimeout(8000, () => {
|
|
34
|
+
request.destroy(new Error("Registry request timed out."));
|
|
35
|
+
});
|
|
36
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": false,
|
|
3
|
+
"name": "pwvm",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Playwright Version Manager",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"author": "Millan Kaul",
|
|
9
|
+
"bin": {
|
|
10
|
+
"pwvm": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"dev": "ts-node src/index.ts",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:e2e": "vitest -c itest.config.ts"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"playwright",
|
|
25
|
+
"playwright-version-manager",
|
|
26
|
+
"playwright-cli",
|
|
27
|
+
"version-manager",
|
|
28
|
+
"testing",
|
|
29
|
+
"e2e"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/eaccmk/pwvm.git"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"commander": "^14.0.2",
|
|
40
|
+
"execa": "^9.6.1",
|
|
41
|
+
"fs-extra": "^11.3.3",
|
|
42
|
+
"semver": "^7.7.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"@types/node": "^25.0.3",
|
|
47
|
+
"@types/semver": "^7.7.1",
|
|
48
|
+
"ts-node": "^10.9.2",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vitest": "^4.0.16"
|
|
51
|
+
}
|
|
52
|
+
}
|