chrome-proc 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.
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
+ cd "$PROJECT_DIR"
7
+
8
+ echo "=== npm publish routine for chrome-proc ==="
9
+
10
+ # 1. Verify npm auth
11
+ echo ""
12
+ echo "[1/5] Checking npm authentication..."
13
+ if ! npm whoami > /dev/null 2>&1; then
14
+ echo "ERROR: You are not logged into npm."
15
+ echo "Please run: npm login"
16
+ echo "Then re-run this script."
17
+ exit 1
18
+ fi
19
+ USERNAME=$(npm whoami)
20
+ echo "Authenticated as: $USERNAME"
21
+
22
+ # 2. Build project
23
+ echo ""
24
+ echo "[2/5] Building project..."
25
+ npm run build
26
+
27
+ # 3. Verify artifacts
28
+ echo ""
29
+ echo "[3/5] Verifying build artifacts..."
30
+ if [ ! -f "dist/index.js" ]; then
31
+ echo "ERROR: dist/index.js not found. Build may have failed."
32
+ exit 1
33
+ fi
34
+ if [ ! -f "bin/chrome-proc" ]; then
35
+ echo "ERROR: bin/chrome-proc not found."
36
+ exit 1
37
+ fi
38
+ echo "Artifacts verified."
39
+
40
+ # 4. Check if version already exists
41
+ echo ""
42
+ echo "[4/5] Checking if version already exists on npm..."
43
+ VERSION=$(node -p "require('./package.json').version")
44
+ if npm view "chrome-proc@$VERSION" version > /dev/null 2>&1; then
45
+ echo "ERROR: chrome-proc@$VERSION already exists on npm."
46
+ echo "Please bump the version in package.json before publishing."
47
+ exit 1
48
+ fi
49
+ echo "Version $VERSION is available for publish."
50
+
51
+ # 5. Publish
52
+ echo ""
53
+ echo "[5/5] Publishing chrome-proc@$VERSION to npm..."
54
+ npm publish --access public
55
+
56
+ # 6. Verify
57
+ echo ""
58
+ echo "Verifying publish..."
59
+ sleep 2
60
+ PUBLISHED_VERSION=$(npm view chrome-proc version 2>/dev/null || echo "")
61
+ if [ "$PUBLISHED_VERSION" = "$VERSION" ]; then
62
+ echo "SUCCESS: chrome-proc@$VERSION is now live on npm!"
63
+ echo ""
64
+ echo "You can install it with:"
65
+ echo " npm install -g chrome-proc"
66
+ else
67
+ echo "WARNING: Publish command succeeded but verification failed."
68
+ echo "Published version: $PUBLISHED_VERSION"
69
+ echo "Expected version: $VERSION"
70
+ echo "Check https://www.npmjs.com/package/chrome-proc for status."
71
+ fi
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bing Tong
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 @@
1
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/index.js');
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cdpCommand = cdpCommand;
4
+ const process_1 = require("../utils/process");
5
+ const format_1 = require("../utils/format");
6
+ async function cdpCommand(options) {
7
+ const pids = (0, process_1.getChromePids)(true);
8
+ if (pids.length === 0) {
9
+ console.error("(no Chrome processes found)");
10
+ process.exit(1);
11
+ }
12
+ const ports = [];
13
+ for (const pid of pids) {
14
+ const args = (0, process_1.getProcessArgs)(pid);
15
+ const port = (0, process_1.extractDebugPort)(args);
16
+ if (port !== null && !ports.includes(port)) {
17
+ ports.push(port);
18
+ }
19
+ }
20
+ if (ports.length === 0) {
21
+ console.error("(no Chrome processes found with remote debugging enabled)");
22
+ process.exit(1);
23
+ }
24
+ let any = false;
25
+ let printedHeader = false;
26
+ for (const port of ports) {
27
+ const base = `http://localhost:${port}`;
28
+ let verResp = null;
29
+ let listResp = null;
30
+ try {
31
+ const ver = await fetchWithTimeout(`${base}/json/version`, 3000);
32
+ verResp = (await ver.json());
33
+ }
34
+ catch {
35
+ verResp = null;
36
+ }
37
+ try {
38
+ const list = await fetchWithTimeout(`${base}/json/list`, 3000);
39
+ listResp = (await list.json());
40
+ }
41
+ catch {
42
+ listResp = null;
43
+ }
44
+ if (!verResp && !listResp) {
45
+ console.error(`Warning: failed to query debug endpoint on port ${port}`);
46
+ continue;
47
+ }
48
+ any = true;
49
+ if (options.json) {
50
+ if (verResp) {
51
+ const browserUrl = typeof verResp.webSocketDebuggerUrl === "string" ? verResp.webSocketDebuggerUrl : "";
52
+ const browserName = typeof verResp.Browser === "string" ? verResp.Browser : "Browser";
53
+ if (browserUrl) {
54
+ console.log(JSON.stringify({ port, level: "browser", name: browserName, url: browserUrl }, null, 2));
55
+ }
56
+ }
57
+ if (listResp) {
58
+ for (const item of listResp) {
59
+ const name = item.title || item.url || "unknown";
60
+ const url = item.webSocketDebuggerUrl || "";
61
+ if (url) {
62
+ console.log(JSON.stringify({ port, level: "tab", name, url }, null, 2));
63
+ }
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ if (!printedHeader) {
69
+ console.log(`${(0, format_1.padEnd)("PORT", 6)} ${(0, format_1.padEnd)("LEVEL", 8)} ${(0, format_1.padEnd)("NAME", 30)} URL`);
70
+ console.log(`${(0, format_1.padEnd)("----", 6)} ${(0, format_1.padEnd)("-----", 8)} ${(0, format_1.padEnd)("----", 30)} ---`);
71
+ printedHeader = true;
72
+ }
73
+ if (verResp) {
74
+ const browserUrl = typeof verResp.webSocketDebuggerUrl === "string" ? verResp.webSocketDebuggerUrl : "";
75
+ const browserName = typeof verResp.Browser === "string" ? verResp.Browser : "Browser";
76
+ if (browserUrl) {
77
+ console.log(`${(0, format_1.padEnd)(String(port), 6)} ${(0, format_1.padEnd)("browser", 8)} ${(0, format_1.padEnd)(truncate(browserName, 30), 30)} ${browserUrl}`);
78
+ }
79
+ }
80
+ if (listResp) {
81
+ for (const item of listResp) {
82
+ const name = item.title || item.url || "unknown";
83
+ const url = item.webSocketDebuggerUrl || "";
84
+ if (url) {
85
+ console.log(`${(0, format_1.padEnd)(String(port), 6)} ${(0, format_1.padEnd)("tab", 8)} ${(0, format_1.padEnd)(truncate(name, 30), 30)} ${url}`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ if (!any) {
92
+ process.exit(1);
93
+ }
94
+ }
95
+ async function fetchWithTimeout(url, ms) {
96
+ const controller = new AbortController();
97
+ const timeout = setTimeout(() => controller.abort(), ms);
98
+ try {
99
+ const res = await fetch(url, { signal: controller.signal });
100
+ clearTimeout(timeout);
101
+ return res;
102
+ }
103
+ catch (err) {
104
+ clearTimeout(timeout);
105
+ throw err;
106
+ }
107
+ }
108
+ function truncate(str, max) {
109
+ if (str.length > max) {
110
+ return str.slice(0, max - 3) + "...";
111
+ }
112
+ return str;
113
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.killCommand = killCommand;
4
+ const process_1 = require("../utils/process");
5
+ function killCommand(options) {
6
+ const signal = options.force ? "KILL" : "TERM";
7
+ const exact = !options.all;
8
+ const pids = (0, process_1.getChromePids)(exact);
9
+ if (pids.length === 0) {
10
+ console.log("(no Chrome processes found)");
11
+ return;
12
+ }
13
+ let count = 0;
14
+ for (const pid of pids) {
15
+ if ((0, process_1.killPid)(pid, signal)) {
16
+ console.log(`Killed PID ${pid} (SIG${signal})`);
17
+ count++;
18
+ }
19
+ else {
20
+ console.error(`Failed to kill PID ${pid}`);
21
+ }
22
+ }
23
+ console.log("");
24
+ console.log(`Sent SIG${signal} to ${count} process(es)`);
25
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.launchCommand = launchCommand;
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const process_1 = require("../utils/process");
7
+ function launchCommand(options) {
8
+ const chromeBin = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
9
+ const profile = options.profile ?? process.env.CHROME_PROFILE ?? "";
10
+ const dataDir = options.dir ?? process.env.CHROME_DATA_DIR ?? "";
11
+ if (!(0, fs_1.existsSync)(chromeBin)) {
12
+ console.error(`Error: Chrome not found at ${chromeBin}`);
13
+ process.exit(1);
14
+ }
15
+ if (!dataDir) {
16
+ console.error("Error: CHROME_DATA_DIR environment variable is not set and --dir was not provided");
17
+ process.exit(1);
18
+ }
19
+ const args = [];
20
+ args.push(`--user-data-dir=${dataDir}`);
21
+ if (profile) {
22
+ args.push(`--profile-directory=${profile}`);
23
+ }
24
+ if (options.debug) {
25
+ const port = options.debuggingPort ?? "9222";
26
+ args.push(`--remote-debugging-port=${port}`);
27
+ }
28
+ else if (options.debuggingPort) {
29
+ console.error("Warning: --debugging-port is ignored without --debug");
30
+ }
31
+ if ((0, process_1.isChromeRunning)()) {
32
+ const { execSync } = require("child_process");
33
+ const existing = execSync('pgrep -x "Google Chrome" | tr "\\n" " " | sed "s/ $//"', { encoding: "utf-8" }).trim();
34
+ if (existing) {
35
+ console.error(`Warning: Chrome is already running (PIDs: ${existing})`);
36
+ }
37
+ }
38
+ console.log("Launching Chrome...");
39
+ console.log(` binary: ${chromeBin}`);
40
+ console.log(` data dir: ${dataDir}`);
41
+ if (profile) {
42
+ console.log(` profile: ${profile}`);
43
+ }
44
+ if (options.debug) {
45
+ console.log(` debug: port ${options.debuggingPort ?? "9222"}`);
46
+ }
47
+ // Spawn detached so it survives parent exit, with stdio ignored
48
+ const child = (0, child_process_1.spawn)(chromeBin, args, {
49
+ detached: true,
50
+ stdio: "ignore",
51
+ });
52
+ child.unref();
53
+ console.log(`Chrome started (pid=${child.pid})`);
54
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listCommand = listCommand;
4
+ const process_1 = require("../utils/process");
5
+ const format_1 = require("../utils/format");
6
+ function listCommand(options) {
7
+ const pids = (0, process_1.getChromePids)(true);
8
+ if (pids.length === 0) {
9
+ console.log("(no Chrome processes found)");
10
+ return;
11
+ }
12
+ if (options.json) {
13
+ for (const pid of pids) {
14
+ const cmd = (0, process_1.getProcessArgs)(pid);
15
+ const port = (0, process_1.extractDebugPort)(cmd);
16
+ const obj = { pid, cmd };
17
+ if (port !== null) {
18
+ obj.port = port;
19
+ }
20
+ else {
21
+ obj.port = null;
22
+ }
23
+ console.log(JSON.stringify(obj));
24
+ }
25
+ return;
26
+ }
27
+ if (options.verbose) {
28
+ console.log(`${(0, format_1.padEnd)("PID", 8)} ${(0, format_1.padEnd)("PORT", 6)} COMMAND`);
29
+ console.log(`${(0, format_1.padEnd)("---", 8)} ${(0, format_1.padEnd)("----", 6)} -------`);
30
+ for (const pid of pids) {
31
+ const cmd = (0, process_1.getProcessArgs)(pid);
32
+ const port = (0, process_1.extractDebugPort)(cmd);
33
+ console.log(`${(0, format_1.padEnd)(String(pid), 8)} ${(0, format_1.padEnd)(port !== null ? String(port) : "-", 6)} ${cmd}`);
34
+ }
35
+ }
36
+ else {
37
+ console.log(`${(0, format_1.padEnd)("PID", 8)} ${(0, format_1.padEnd)("PORT", 6)} NAME`);
38
+ console.log(`${(0, format_1.padEnd)("---", 8)} ${(0, format_1.padEnd)("----", 6)} ----`);
39
+ for (const pid of pids) {
40
+ const name = (0, process_1.getProcessName)(pid);
41
+ const cmd = (0, process_1.getProcessArgs)(pid);
42
+ const port = (0, process_1.extractDebugPort)(cmd);
43
+ console.log(`${(0, format_1.padEnd)(String(pid), 8)} ${(0, format_1.padEnd)(port !== null ? String(port) : "-", 6)} ${name}`);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.profileList = profileList;
4
+ exports.profileName = profileName;
5
+ exports.profileDelete = profileDelete;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const readline_1 = require("readline");
9
+ const localState_1 = require("../utils/localState");
10
+ const process_1 = require("../utils/process");
11
+ const format_1 = require("../utils/format");
12
+ function profileList(options) {
13
+ const dataDir = (0, localState_1.getChromeDataDir)();
14
+ const localStatePath = (0, path_1.join)(dataDir, "Local State");
15
+ if (!(0, fs_1.existsSync)(localStatePath)) {
16
+ console.error(`Error: Local State not found at ${localStatePath}`);
17
+ process.exit(1);
18
+ }
19
+ const state = (0, localState_1.readLocalState)();
20
+ const cache = state.profile?.info_cache ?? {};
21
+ if (options.json) {
22
+ for (const [dir, info] of Object.entries(cache)) {
23
+ console.log(JSON.stringify({ dir, name: info.name }));
24
+ }
25
+ return;
26
+ }
27
+ console.log(`${(0, format_1.padEnd)("DIR", 20)} NAME`);
28
+ console.log(`${(0, format_1.padEnd)("---", 20)} ----`);
29
+ for (const [dir, info] of Object.entries(cache)) {
30
+ console.log(`${(0, format_1.padEnd)(dir, 20)} ${info.name}`);
31
+ }
32
+ }
33
+ function profileName(profileDir, newName) {
34
+ const state = (0, localState_1.readLocalState)();
35
+ if (!(0, localState_1.profileExists)(profileDir, state)) {
36
+ console.error(`Error: profile directory '${profileDir}' not found in Local State`);
37
+ process.exit(1);
38
+ }
39
+ state.profile = state.profile ?? {};
40
+ state.profile.info_cache = state.profile.info_cache ?? {};
41
+ state.profile.info_cache[profileDir].name = newName;
42
+ (0, localState_1.writeLocalState)(state);
43
+ console.log(`Renamed '${profileDir}' to '${newName}'`);
44
+ }
45
+ async function profileDelete(profileDir) {
46
+ const dataDir = (0, localState_1.getChromeDataDir)();
47
+ const state = (0, localState_1.readLocalState)();
48
+ if (!(0, localState_1.profileExists)(profileDir, state)) {
49
+ console.error(`Error: profile directory '${profileDir}' not found in Local State`);
50
+ process.exit(1);
51
+ }
52
+ const profilePath = (0, path_1.join)(dataDir, profileDir);
53
+ if ((0, fs_1.existsSync)(profilePath)) {
54
+ (0, localState_1.validateProfileDir)(profileDir);
55
+ }
56
+ if ((0, process_1.isChromeRunning)()) {
57
+ console.error("Warning: Chrome is currently running. Deleting a profile while Chrome is active may cause data loss.");
58
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
59
+ const answer = await new Promise((resolve) => {
60
+ rl.question("Are you sure you want to continue? [y/N] ", (ans) => {
61
+ rl.close();
62
+ resolve(ans.trim());
63
+ });
64
+ });
65
+ if (answer !== "y" && answer !== "Y") {
66
+ console.error("Aborted.");
67
+ process.exit(1);
68
+ }
69
+ }
70
+ if ((0, fs_1.existsSync)(profilePath)) {
71
+ (0, fs_1.rmSync)(profilePath, { recursive: true, force: true });
72
+ console.log(`Deleted directory '${profilePath}'`);
73
+ }
74
+ delete state.profile.info_cache[profileDir];
75
+ (0, localState_1.writeLocalState)(state);
76
+ console.log(`Removed '${profileDir}' from Local State`);
77
+ }
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const list_1 = require("./commands/list");
6
+ const kill_1 = require("./commands/kill");
7
+ const launch_1 = require("./commands/launch");
8
+ const profile_1 = require("./commands/profile");
9
+ const cdp_1 = require("./commands/cdp");
10
+ const program = new commander_1.Command();
11
+ program
12
+ .name("chrome-proc")
13
+ .description("Manage Chrome browser processes, profiles, and CDP endpoints");
14
+ program
15
+ .command("list")
16
+ .description("List Chrome processes")
17
+ .option("-v, --verbose", "Show full command line")
18
+ .option("-j, --json", "Output as JSON lines")
19
+ .action((options) => {
20
+ (0, list_1.listCommand)(options);
21
+ });
22
+ program
23
+ .command("kill")
24
+ .description("Kill Chrome processes")
25
+ .option("-f, --force", "Use SIGKILL instead of SIGTERM")
26
+ .option("-a, --all", "Kill helper processes too (not just the main process)")
27
+ .action((options) => {
28
+ (0, kill_1.killCommand)(options);
29
+ });
30
+ program
31
+ .command("launch")
32
+ .description("Launch Chrome browser")
33
+ .option("--dir <dir>", "Chrome user data directory (overrides CHROME_DATA_DIR)")
34
+ .option("--profile <profile>", "Chrome profile name (default: $CHROME_PROFILE)")
35
+ .option("-d, --debug", "Enable remote debugging mode")
36
+ .option("-p, --debugging-port <port>", "Remote debugging port (default: 9222, only with --debug)")
37
+ .action((options) => {
38
+ (0, launch_1.launchCommand)(options);
39
+ });
40
+ const profileCmd = program
41
+ .command("profile")
42
+ .description("Manage Chrome profiles");
43
+ profileCmd
44
+ .command("list")
45
+ .description("List Chrome profiles")
46
+ .option("-j, --json", "Output as JSON lines")
47
+ .action((options) => {
48
+ (0, profile_1.profileList)(options);
49
+ });
50
+ profileCmd
51
+ .command("name <profile_dir> <new_name>")
52
+ .description("Rename a Chrome profile")
53
+ .action((profileDir, newName) => {
54
+ (0, profile_1.profileName)(profileDir, newName);
55
+ });
56
+ profileCmd
57
+ .command("delete <profile_dir>")
58
+ .description("Delete a Chrome profile")
59
+ .action(async (profileDir) => {
60
+ await (0, profile_1.profileDelete)(profileDir);
61
+ });
62
+ program
63
+ .command("cdp")
64
+ .description("List Chrome DevTools Protocol (CDP) WebSocket URLs")
65
+ .option("-j, --json", "Output as JSON lines")
66
+ .action(async (options) => {
67
+ await (0, cdp_1.cdpCommand)(options);
68
+ });
69
+ program.parse();
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.padEnd = padEnd;
4
+ exports.padStart = padStart;
5
+ function padEnd(str, length) {
6
+ return str.length >= length ? str : str + " ".repeat(length - str.length);
7
+ }
8
+ function padStart(str, length) {
9
+ return str.length >= length ? str : " ".repeat(length - str.length) + str;
10
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getChromeDataDir = getChromeDataDir;
4
+ exports.getLocalStatePath = getLocalStatePath;
5
+ exports.readLocalState = readLocalState;
6
+ exports.writeLocalState = writeLocalState;
7
+ exports.profileExists = profileExists;
8
+ exports.validateProfileDir = validateProfileDir;
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ function getChromeDataDir() {
12
+ const dir = process.env.CHROME_DATA_DIR;
13
+ if (!dir) {
14
+ throw new Error("CHROME_DATA_DIR environment variable is not set");
15
+ }
16
+ return dir;
17
+ }
18
+ function getLocalStatePath() {
19
+ return (0, path_1.join)(getChromeDataDir(), "Local State");
20
+ }
21
+ function readLocalState() {
22
+ const path = getLocalStatePath();
23
+ if (!(0, fs_1.existsSync)(path)) {
24
+ throw new Error(`Local State not found at ${path}`);
25
+ }
26
+ const raw = (0, fs_1.readFileSync)(path, "utf-8");
27
+ return JSON.parse(raw);
28
+ }
29
+ function writeLocalState(data) {
30
+ const path = getLocalStatePath();
31
+ const tmpPath = `${path}.tmp.${process.pid}`;
32
+ try {
33
+ (0, fs_1.writeFileSync)(tmpPath, JSON.stringify(data, null, 2), "utf-8");
34
+ // Atomic rename
35
+ const { renameSync } = require("fs");
36
+ renameSync(tmpPath, path);
37
+ }
38
+ catch (err) {
39
+ try {
40
+ (0, fs_1.rmSync)(tmpPath);
41
+ }
42
+ catch { }
43
+ throw new Error("failed to update Local State");
44
+ }
45
+ }
46
+ function profileExists(dir, state) {
47
+ const s = state || readLocalState();
48
+ return !!s.profile?.info_cache?.[dir];
49
+ }
50
+ function validateProfileDir(profileDir) {
51
+ const dataDir = getChromeDataDir();
52
+ const profilePath = (0, path_1.resolve)(dataDir, profileDir);
53
+ const realDataDir = (0, fs_1.realpathSync)(dataDir);
54
+ const realProfilePath = (0, fs_1.realpathSync)(profilePath);
55
+ if (!realProfilePath.startsWith(realDataDir)) {
56
+ throw new Error(`profile path '${profilePath}' is not inside CHROME_DATA_DIR`);
57
+ }
58
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getChromePids = getChromePids;
4
+ exports.getProcessArgs = getProcessArgs;
5
+ exports.getProcessName = getProcessName;
6
+ exports.extractDebugPort = extractDebugPort;
7
+ exports.killPid = killPid;
8
+ exports.isChromeRunning = isChromeRunning;
9
+ const child_process_1 = require("child_process");
10
+ function getChromePids(exact = false) {
11
+ try {
12
+ const flag = exact ? "-x" : "-f";
13
+ const output = (0, child_process_1.execSync)(`pgrep ${flag} "Google Chrome"`, { encoding: "utf-8" });
14
+ return output
15
+ .trim()
16
+ .split("\n")
17
+ .filter((line) => line.trim() !== "")
18
+ .map((line) => parseInt(line.trim(), 10))
19
+ .filter((pid) => !isNaN(pid));
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ function getProcessArgs(pid) {
26
+ try {
27
+ return (0, child_process_1.execSync)(`ps -p "${pid}" -o args=`, { encoding: "utf-8" }).trim();
28
+ }
29
+ catch {
30
+ return "?";
31
+ }
32
+ }
33
+ function getProcessName(pid) {
34
+ try {
35
+ return (0, child_process_1.execSync)(`ps -p "${pid}" -o comm=`, { encoding: "utf-8" }).trim();
36
+ }
37
+ catch {
38
+ return "?";
39
+ }
40
+ }
41
+ function extractDebugPort(args) {
42
+ const match = args.match(/--remote-debugging-port=(\d+)/);
43
+ return match ? parseInt(match[1], 10) : null;
44
+ }
45
+ function killPid(pid, signal) {
46
+ try {
47
+ (0, child_process_1.execSync)(`/bin/kill -${signal} "${pid}"`, { stdio: "pipe" });
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ function isChromeRunning() {
55
+ return getChromePids(true).length > 0;
56
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "chrome-proc",
3
+ "version": "1.0.0",
4
+ "description": "Manage Chrome browser processes, profiles, and CDP endpoints",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "chrome-proc": "./bin/chrome-proc"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch"
12
+ },
13
+ "keywords": ["chrome", "cdp", "devtools"],
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "commander": "^12.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "typescript": "^5.4.0"
21
+ }
22
+ }
@@ -0,0 +1,137 @@
1
+ import { getChromePids, getProcessArgs, extractDebugPort } from "../utils/process";
2
+ import { padEnd } from "../utils/format";
3
+
4
+ interface CdpOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ interface CdpEntry {
9
+ port: number;
10
+ level: string;
11
+ name: string;
12
+ url: string;
13
+ }
14
+
15
+ export async function cdpCommand(options: CdpOptions): Promise<void> {
16
+ const pids = getChromePids(true);
17
+
18
+ if (pids.length === 0) {
19
+ console.error("(no Chrome processes found)");
20
+ process.exit(1);
21
+ }
22
+
23
+ const ports: number[] = [];
24
+ for (const pid of pids) {
25
+ const args = getProcessArgs(pid);
26
+ const port = extractDebugPort(args);
27
+ if (port !== null && !ports.includes(port)) {
28
+ ports.push(port);
29
+ }
30
+ }
31
+
32
+ if (ports.length === 0) {
33
+ console.error("(no Chrome processes found with remote debugging enabled)");
34
+ process.exit(1);
35
+ }
36
+
37
+ let any = false;
38
+ let printedHeader = false;
39
+
40
+ for (const port of ports) {
41
+ const base = `http://localhost:${port}`;
42
+ let verResp: Record<string, unknown> | null = null;
43
+ let listResp: Array<Record<string, unknown>> | null = null;
44
+
45
+ try {
46
+ const ver = await fetchWithTimeout(`${base}/json/version`, 3000);
47
+ verResp = (await ver.json()) as Record<string, unknown>;
48
+ } catch {
49
+ verResp = null;
50
+ }
51
+
52
+ try {
53
+ const list = await fetchWithTimeout(`${base}/json/list`, 3000);
54
+ listResp = (await list.json()) as Array<Record<string, unknown>>;
55
+ } catch {
56
+ listResp = null;
57
+ }
58
+
59
+ if (!verResp && !listResp) {
60
+ console.error(`Warning: failed to query debug endpoint on port ${port}`);
61
+ continue;
62
+ }
63
+
64
+ any = true;
65
+
66
+ if (options.json) {
67
+ if (verResp) {
68
+ const browserUrl = typeof verResp.webSocketDebuggerUrl === "string" ? verResp.webSocketDebuggerUrl : "";
69
+ const browserName = typeof verResp.Browser === "string" ? verResp.Browser : "Browser";
70
+ if (browserUrl) {
71
+ console.log(JSON.stringify({ port, level: "browser", name: browserName, url: browserUrl }, null, 2));
72
+ }
73
+ }
74
+ if (listResp) {
75
+ for (const item of listResp) {
76
+ const name = (item.title as string) || (item.url as string) || "unknown";
77
+ const url = (item.webSocketDebuggerUrl as string) || "";
78
+ if (url) {
79
+ console.log(JSON.stringify({ port, level: "tab", name, url }, null, 2));
80
+ }
81
+ }
82
+ }
83
+ } else {
84
+ if (!printedHeader) {
85
+ console.log(`${padEnd("PORT", 6)} ${padEnd("LEVEL", 8)} ${padEnd("NAME", 30)} URL`);
86
+ console.log(`${padEnd("----", 6)} ${padEnd("-----", 8)} ${padEnd("----", 30)} ---`);
87
+ printedHeader = true;
88
+ }
89
+
90
+ if (verResp) {
91
+ const browserUrl = typeof verResp.webSocketDebuggerUrl === "string" ? verResp.webSocketDebuggerUrl : "";
92
+ const browserName = typeof verResp.Browser === "string" ? verResp.Browser : "Browser";
93
+ if (browserUrl) {
94
+ console.log(
95
+ `${padEnd(String(port), 6)} ${padEnd("browser", 8)} ${padEnd(truncate(browserName, 30), 30)} ${browserUrl}`
96
+ );
97
+ }
98
+ }
99
+
100
+ if (listResp) {
101
+ for (const item of listResp) {
102
+ const name = (item.title as string) || (item.url as string) || "unknown";
103
+ const url = (item.webSocketDebuggerUrl as string) || "";
104
+ if (url) {
105
+ console.log(
106
+ `${padEnd(String(port), 6)} ${padEnd("tab", 8)} ${padEnd(truncate(name, 30), 30)} ${url}`
107
+ );
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ if (!any) {
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
120
+ const controller = new AbortController();
121
+ const timeout = setTimeout(() => controller.abort(), ms);
122
+ try {
123
+ const res = await fetch(url, { signal: controller.signal });
124
+ clearTimeout(timeout);
125
+ return res;
126
+ } catch (err) {
127
+ clearTimeout(timeout);
128
+ throw err;
129
+ }
130
+ }
131
+
132
+ function truncate(str: string, max: number): string {
133
+ if (str.length > max) {
134
+ return str.slice(0, max - 3) + "...";
135
+ }
136
+ return str;
137
+ }
@@ -0,0 +1,30 @@
1
+ import { getChromePids, killPid } from "../utils/process";
2
+
3
+ interface KillOptions {
4
+ force?: boolean;
5
+ all?: boolean;
6
+ }
7
+
8
+ export function killCommand(options: KillOptions): void {
9
+ const signal: "TERM" | "KILL" = options.force ? "KILL" : "TERM";
10
+ const exact = !options.all;
11
+ const pids = getChromePids(exact);
12
+
13
+ if (pids.length === 0) {
14
+ console.log("(no Chrome processes found)");
15
+ return;
16
+ }
17
+
18
+ let count = 0;
19
+ for (const pid of pids) {
20
+ if (killPid(pid, signal)) {
21
+ console.log(`Killed PID ${pid} (SIG${signal})`);
22
+ count++;
23
+ } else {
24
+ console.error(`Failed to kill PID ${pid}`);
25
+ }
26
+ }
27
+
28
+ console.log("");
29
+ console.log(`Sent SIG${signal} to ${count} process(es)`);
30
+ }
@@ -0,0 +1,65 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { isChromeRunning } from "../utils/process";
4
+
5
+ interface LaunchOptions {
6
+ dir?: string;
7
+ profile?: string;
8
+ debug?: boolean;
9
+ debuggingPort?: string;
10
+ }
11
+
12
+ export function launchCommand(options: LaunchOptions): void {
13
+ const chromeBin = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
14
+ const profile = options.profile ?? process.env.CHROME_PROFILE ?? "";
15
+ const dataDir = options.dir ?? process.env.CHROME_DATA_DIR ?? "";
16
+
17
+ if (!existsSync(chromeBin)) {
18
+ console.error(`Error: Chrome not found at ${chromeBin}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ if (!dataDir) {
23
+ console.error("Error: CHROME_DATA_DIR environment variable is not set and --dir was not provided");
24
+ process.exit(1);
25
+ }
26
+
27
+ const args: string[] = [];
28
+ args.push(`--user-data-dir=${dataDir}`);
29
+ if (profile) {
30
+ args.push(`--profile-directory=${profile}`);
31
+ }
32
+
33
+ if (options.debug) {
34
+ const port = options.debuggingPort ?? "9222";
35
+ args.push(`--remote-debugging-port=${port}`);
36
+ } else if (options.debuggingPort) {
37
+ console.error("Warning: --debugging-port is ignored without --debug");
38
+ }
39
+
40
+ if (isChromeRunning()) {
41
+ const { execSync } = require("child_process");
42
+ const existing = execSync('pgrep -x "Google Chrome" | tr "\\n" " " | sed "s/ $//"', { encoding: "utf-8" }).trim();
43
+ if (existing) {
44
+ console.error(`Warning: Chrome is already running (PIDs: ${existing})`);
45
+ }
46
+ }
47
+
48
+ console.log("Launching Chrome...");
49
+ console.log(` binary: ${chromeBin}`);
50
+ console.log(` data dir: ${dataDir}`);
51
+ if (profile) {
52
+ console.log(` profile: ${profile}`);
53
+ }
54
+ if (options.debug) {
55
+ console.log(` debug: port ${options.debuggingPort ?? "9222"}`);
56
+ }
57
+
58
+ // Spawn detached so it survives parent exit, with stdio ignored
59
+ const child = spawn(chromeBin, args, {
60
+ detached: true,
61
+ stdio: "ignore",
62
+ });
63
+ child.unref();
64
+ console.log(`Chrome started (pid=${child.pid})`);
65
+ }
@@ -0,0 +1,50 @@
1
+ import { getChromePids, getProcessArgs, getProcessName, extractDebugPort } from "../utils/process";
2
+ import { padEnd } from "../utils/format";
3
+
4
+ interface ListOptions {
5
+ verbose?: boolean;
6
+ json?: boolean;
7
+ }
8
+
9
+ export function listCommand(options: ListOptions): void {
10
+ const pids = getChromePids(true);
11
+
12
+ if (pids.length === 0) {
13
+ console.log("(no Chrome processes found)");
14
+ return;
15
+ }
16
+
17
+ if (options.json) {
18
+ for (const pid of pids) {
19
+ const cmd = getProcessArgs(pid);
20
+ const port = extractDebugPort(cmd);
21
+ const obj: Record<string, unknown> = { pid, cmd };
22
+ if (port !== null) {
23
+ obj.port = port;
24
+ } else {
25
+ obj.port = null;
26
+ }
27
+ console.log(JSON.stringify(obj));
28
+ }
29
+ return;
30
+ }
31
+
32
+ if (options.verbose) {
33
+ console.log(`${padEnd("PID", 8)} ${padEnd("PORT", 6)} COMMAND`);
34
+ console.log(`${padEnd("---", 8)} ${padEnd("----", 6)} -------`);
35
+ for (const pid of pids) {
36
+ const cmd = getProcessArgs(pid);
37
+ const port = extractDebugPort(cmd);
38
+ console.log(`${padEnd(String(pid), 8)} ${padEnd(port !== null ? String(port) : "-", 6)} ${cmd}`);
39
+ }
40
+ } else {
41
+ console.log(`${padEnd("PID", 8)} ${padEnd("PORT", 6)} NAME`);
42
+ console.log(`${padEnd("---", 8)} ${padEnd("----", 6)} ----`);
43
+ for (const pid of pids) {
44
+ const name = getProcessName(pid);
45
+ const cmd = getProcessArgs(pid);
46
+ const port = extractDebugPort(cmd);
47
+ console.log(`${padEnd(String(pid), 8)} ${padEnd(port !== null ? String(port) : "-", 6)} ${name}`);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,96 @@
1
+ import { existsSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+ import { createInterface } from "readline";
4
+ import {
5
+ getChromeDataDir,
6
+ readLocalState,
7
+ writeLocalState,
8
+ profileExists,
9
+ validateProfileDir,
10
+ } from "../utils/localState";
11
+ import { isChromeRunning } from "../utils/process";
12
+ import { padEnd } from "../utils/format";
13
+
14
+ interface ProfileListOptions {
15
+ json?: boolean;
16
+ }
17
+
18
+ export function profileList(options: ProfileListOptions): void {
19
+ const dataDir = getChromeDataDir();
20
+ const localStatePath = join(dataDir, "Local State");
21
+
22
+ if (!existsSync(localStatePath)) {
23
+ console.error(`Error: Local State not found at ${localStatePath}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const state = readLocalState();
28
+ const cache = state.profile?.info_cache ?? {};
29
+
30
+ if (options.json) {
31
+ for (const [dir, info] of Object.entries(cache)) {
32
+ console.log(JSON.stringify({ dir, name: info.name }));
33
+ }
34
+ return;
35
+ }
36
+
37
+ console.log(`${padEnd("DIR", 20)} NAME`);
38
+ console.log(`${padEnd("---", 20)} ----`);
39
+ for (const [dir, info] of Object.entries(cache)) {
40
+ console.log(`${padEnd(dir, 20)} ${info.name}`);
41
+ }
42
+ }
43
+
44
+ export function profileName(profileDir: string, newName: string): void {
45
+ const state = readLocalState();
46
+
47
+ if (!profileExists(profileDir, state)) {
48
+ console.error(`Error: profile directory '${profileDir}' not found in Local State`);
49
+ process.exit(1);
50
+ }
51
+
52
+ state.profile = state.profile ?? {};
53
+ state.profile.info_cache = state.profile.info_cache ?? {};
54
+ state.profile.info_cache[profileDir].name = newName;
55
+ writeLocalState(state);
56
+ console.log(`Renamed '${profileDir}' to '${newName}'`);
57
+ }
58
+
59
+ export async function profileDelete(profileDir: string): Promise<void> {
60
+ const dataDir = getChromeDataDir();
61
+ const state = readLocalState();
62
+
63
+ if (!profileExists(profileDir, state)) {
64
+ console.error(`Error: profile directory '${profileDir}' not found in Local State`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const profilePath = join(dataDir, profileDir);
69
+ if (existsSync(profilePath)) {
70
+ validateProfileDir(profileDir);
71
+ }
72
+
73
+ if (isChromeRunning()) {
74
+ console.error("Warning: Chrome is currently running. Deleting a profile while Chrome is active may cause data loss.");
75
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
76
+ const answer = await new Promise<string>((resolve) => {
77
+ rl.question("Are you sure you want to continue? [y/N] ", (ans) => {
78
+ rl.close();
79
+ resolve(ans.trim());
80
+ });
81
+ });
82
+ if (answer !== "y" && answer !== "Y") {
83
+ console.error("Aborted.");
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ if (existsSync(profilePath)) {
89
+ rmSync(profilePath, { recursive: true, force: true });
90
+ console.log(`Deleted directory '${profilePath}'`);
91
+ }
92
+
93
+ delete state.profile!.info_cache![profileDir];
94
+ writeLocalState(state);
95
+ console.log(`Removed '${profileDir}' from Local State`);
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { listCommand } from "./commands/list";
4
+ import { killCommand } from "./commands/kill";
5
+ import { launchCommand } from "./commands/launch";
6
+ import { profileList, profileName, profileDelete } from "./commands/profile";
7
+ import { cdpCommand } from "./commands/cdp";
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name("chrome-proc")
13
+ .description("Manage Chrome browser processes, profiles, and CDP endpoints");
14
+
15
+ program
16
+ .command("list")
17
+ .description("List Chrome processes")
18
+ .option("-v, --verbose", "Show full command line")
19
+ .option("-j, --json", "Output as JSON lines")
20
+ .action((options) => {
21
+ listCommand(options);
22
+ });
23
+
24
+ program
25
+ .command("kill")
26
+ .description("Kill Chrome processes")
27
+ .option("-f, --force", "Use SIGKILL instead of SIGTERM")
28
+ .option("-a, --all", "Kill helper processes too (not just the main process)")
29
+ .action((options) => {
30
+ killCommand(options);
31
+ });
32
+
33
+ program
34
+ .command("launch")
35
+ .description("Launch Chrome browser")
36
+ .option("--dir <dir>", "Chrome user data directory (overrides CHROME_DATA_DIR)")
37
+ .option("--profile <profile>", "Chrome profile name (default: $CHROME_PROFILE)")
38
+ .option("-d, --debug", "Enable remote debugging mode")
39
+ .option("-p, --debugging-port <port>", "Remote debugging port (default: 9222, only with --debug)")
40
+ .action((options) => {
41
+ launchCommand(options);
42
+ });
43
+
44
+ const profileCmd = program
45
+ .command("profile")
46
+ .description("Manage Chrome profiles");
47
+
48
+ profileCmd
49
+ .command("list")
50
+ .description("List Chrome profiles")
51
+ .option("-j, --json", "Output as JSON lines")
52
+ .action((options) => {
53
+ profileList(options);
54
+ });
55
+
56
+ profileCmd
57
+ .command("name <profile_dir> <new_name>")
58
+ .description("Rename a Chrome profile")
59
+ .action((profileDir: string, newName: string) => {
60
+ profileName(profileDir, newName);
61
+ });
62
+
63
+ profileCmd
64
+ .command("delete <profile_dir>")
65
+ .description("Delete a Chrome profile")
66
+ .action(async (profileDir: string) => {
67
+ await profileDelete(profileDir);
68
+ });
69
+
70
+ program
71
+ .command("cdp")
72
+ .description("List Chrome DevTools Protocol (CDP) WebSocket URLs")
73
+ .option("-j, --json", "Output as JSON lines")
74
+ .action(async (options) => {
75
+ await cdpCommand(options);
76
+ });
77
+
78
+ program.parse();
@@ -0,0 +1,7 @@
1
+ export function padEnd(str: string, length: number): string {
2
+ return str.length >= length ? str : str + " ".repeat(length - str.length);
3
+ }
4
+
5
+ export function padStart(str: string, length: number): string {
6
+ return str.length >= length ? str : " ".repeat(length - str.length) + str;
7
+ }
@@ -0,0 +1,64 @@
1
+ import { readFileSync, writeFileSync, existsSync, realpathSync, rmSync } from "fs";
2
+ import { join, resolve } from "path";
3
+
4
+ export interface ProfileEntry {
5
+ name: string;
6
+ }
7
+
8
+ export interface LocalState {
9
+ profile?: {
10
+ info_cache?: Record<string, ProfileEntry>;
11
+ };
12
+ }
13
+
14
+ export function getChromeDataDir(): string {
15
+ const dir = process.env.CHROME_DATA_DIR;
16
+ if (!dir) {
17
+ throw new Error("CHROME_DATA_DIR environment variable is not set");
18
+ }
19
+ return dir;
20
+ }
21
+
22
+ export function getLocalStatePath(): string {
23
+ return join(getChromeDataDir(), "Local State");
24
+ }
25
+
26
+ export function readLocalState(): LocalState {
27
+ const path = getLocalStatePath();
28
+ if (!existsSync(path)) {
29
+ throw new Error(`Local State not found at ${path}`);
30
+ }
31
+ const raw = readFileSync(path, "utf-8");
32
+ return JSON.parse(raw) as LocalState;
33
+ }
34
+
35
+ export function writeLocalState(data: LocalState): void {
36
+ const path = getLocalStatePath();
37
+ const tmpPath = `${path}.tmp.${process.pid}`;
38
+ try {
39
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
40
+ // Atomic rename
41
+ const { renameSync } = require("fs");
42
+ renameSync(tmpPath, path);
43
+ } catch (err) {
44
+ try {
45
+ rmSync(tmpPath);
46
+ } catch {}
47
+ throw new Error("failed to update Local State");
48
+ }
49
+ }
50
+
51
+ export function profileExists(dir: string, state?: LocalState): boolean {
52
+ const s = state || readLocalState();
53
+ return !!s.profile?.info_cache?.[dir];
54
+ }
55
+
56
+ export function validateProfileDir(profileDir: string): void {
57
+ const dataDir = getChromeDataDir();
58
+ const profilePath = resolve(dataDir, profileDir);
59
+ const realDataDir = realpathSync(dataDir);
60
+ const realProfilePath = realpathSync(profilePath);
61
+ if (!realProfilePath.startsWith(realDataDir)) {
62
+ throw new Error(`profile path '${profilePath}' is not inside CHROME_DATA_DIR`);
63
+ }
64
+ }
@@ -0,0 +1,50 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export function getChromePids(exact = false): number[] {
4
+ try {
5
+ const flag = exact ? "-x" : "-f";
6
+ const output = execSync(`pgrep ${flag} "Google Chrome"`, { encoding: "utf-8" });
7
+ return output
8
+ .trim()
9
+ .split("\n")
10
+ .filter((line) => line.trim() !== "")
11
+ .map((line) => parseInt(line.trim(), 10))
12
+ .filter((pid) => !isNaN(pid));
13
+ } catch {
14
+ return [];
15
+ }
16
+ }
17
+
18
+ export function getProcessArgs(pid: number): string {
19
+ try {
20
+ return execSync(`ps -p "${pid}" -o args=`, { encoding: "utf-8" }).trim();
21
+ } catch {
22
+ return "?";
23
+ }
24
+ }
25
+
26
+ export function getProcessName(pid: number): string {
27
+ try {
28
+ return execSync(`ps -p "${pid}" -o comm=`, { encoding: "utf-8" }).trim();
29
+ } catch {
30
+ return "?";
31
+ }
32
+ }
33
+
34
+ export function extractDebugPort(args: string): number | null {
35
+ const match = args.match(/--remote-debugging-port=(\d+)/);
36
+ return match ? parseInt(match[1], 10) : null;
37
+ }
38
+
39
+ export function killPid(pid: number, signal: "TERM" | "KILL"): boolean {
40
+ try {
41
+ execSync(`/bin/kill -${signal} "${pid}"`, { stdio: "pipe" });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function isChromeRunning(): boolean {
49
+ return getChromePids(true).length > 0;
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }