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.
- package/.claude/scripts/publish-npm.sh +71 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/bin/chrome-proc +2 -0
- package/dist/commands/cdp.js +113 -0
- package/dist/commands/kill.js +25 -0
- package/dist/commands/launch.js +54 -0
- package/dist/commands/list.js +46 -0
- package/dist/commands/profile.js +77 -0
- package/dist/index.js +69 -0
- package/dist/utils/format.js +10 -0
- package/dist/utils/localState.js +58 -0
- package/dist/utils/process.js +56 -0
- package/package.json +22 -0
- package/src/commands/cdp.ts +137 -0
- package/src/commands/kill.ts +30 -0
- package/src/commands/launch.ts +65 -0
- package/src/commands/list.ts +50 -0
- package/src/commands/profile.ts +96 -0
- package/src/index.ts +78 -0
- package/src/utils/format.ts +7 -0
- package/src/utils/localState.ts +64 -0
- package/src/utils/process.ts +50 -0
- package/tsconfig.json +15 -0
|
@@ -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
|
+
|
package/bin/chrome-proc
ADDED
|
@@ -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
|
+
}
|