@maxiaochao/pi-toolkit 0.1.9 → 0.2.1
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/CHANGELOG.md +11 -0
- package/README.md +11 -2
- package/global/AGENTS.md +5 -6
- package/package.json +5 -2
- package/skills/web-browser/LICENSE +201 -0
- package/skills/web-browser/NOTICE +15 -0
- package/skills/web-browser/SKILL.md +148 -0
- package/skills/web-browser/scripts/cdp.js +305 -0
- package/skills/web-browser/scripts/devices.js +141 -0
- package/skills/web-browser/scripts/dismiss-cookies.js +377 -0
- package/skills/web-browser/scripts/emulate.js +128 -0
- package/skills/web-browser/scripts/emulation-state.js +74 -0
- package/skills/web-browser/scripts/eval.js +84 -0
- package/skills/web-browser/scripts/logs-tail.js +69 -0
- package/skills/web-browser/scripts/nav.js +74 -0
- package/skills/web-browser/scripts/net-summary.js +94 -0
- package/skills/web-browser/scripts/pick.js +169 -0
- package/skills/web-browser/scripts/screenshot.js +185 -0
- package/skills/web-browser/scripts/start.js +325 -0
- package/skills/web-browser/scripts/watch.js +266 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const LOG_ROOT = join(homedir(), ".cache/agent-web/logs");
|
|
8
|
+
|
|
9
|
+
function findLatestFile() {
|
|
10
|
+
if (!existsSync(LOG_ROOT)) return null;
|
|
11
|
+
const dirs = readdirSync(LOG_ROOT)
|
|
12
|
+
.filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name))
|
|
13
|
+
.map((name) => join(LOG_ROOT, name))
|
|
14
|
+
.filter((path) => statSafe(path)?.isDirectory())
|
|
15
|
+
.sort();
|
|
16
|
+
if (dirs.length === 0) return null;
|
|
17
|
+
const latestDir = dirs[dirs.length - 1];
|
|
18
|
+
const files = readdirSync(latestDir)
|
|
19
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
20
|
+
.map((name) => join(latestDir, name))
|
|
21
|
+
.map((path) => ({ path, mtime: statSafe(path)?.mtimeMs || 0 }))
|
|
22
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
23
|
+
return files[0]?.path || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function statSafe(path) {
|
|
27
|
+
try {
|
|
28
|
+
return statSync(path);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const argIndex = process.argv.indexOf("--file");
|
|
35
|
+
const filePath = argIndex !== -1 ? process.argv[argIndex + 1] : findLatestFile();
|
|
36
|
+
const follow = process.argv.includes("--follow");
|
|
37
|
+
|
|
38
|
+
if (!filePath) {
|
|
39
|
+
console.error("✗ No log file found");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readAll() {
|
|
44
|
+
if (!existsSync(filePath)) return;
|
|
45
|
+
const data = readFileSync(filePath, "utf8");
|
|
46
|
+
if (data.length > 0) process.stdout.write(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let offset = 0;
|
|
50
|
+
|
|
51
|
+
function readNew() {
|
|
52
|
+
if (!existsSync(filePath)) return;
|
|
53
|
+
const data = readFileSync(filePath, "utf8");
|
|
54
|
+
if (data.length <= offset) return;
|
|
55
|
+
const chunk = data.slice(offset);
|
|
56
|
+
offset = data.length;
|
|
57
|
+
process.stdout.write(chunk);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
readAll();
|
|
62
|
+
if (!follow) process.exit(0);
|
|
63
|
+
offset = statSafe(filePath)?.size || 0;
|
|
64
|
+
watch(filePath, { persistent: true }, () => readNew());
|
|
65
|
+
console.log(`✓ tailing ${filePath}`);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error("✗ tail failed:", e.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { connect } from "./cdp.js";
|
|
4
|
+
import { applyActiveEmulation } from "./emulation-state.js";
|
|
5
|
+
|
|
6
|
+
const DEBUG = process.env.DEBUG === "1";
|
|
7
|
+
const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
|
|
8
|
+
|
|
9
|
+
const url = process.argv[2];
|
|
10
|
+
const newTab = process.argv[3] === "--new";
|
|
11
|
+
|
|
12
|
+
if (!url) {
|
|
13
|
+
console.log("Usage: nav.js <url> [--new]");
|
|
14
|
+
console.log("\nExamples:");
|
|
15
|
+
console.log(" nav.js https://example.com # Navigate current tab");
|
|
16
|
+
console.log(" nav.js https://example.com --new # Open in new tab");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Global timeout
|
|
21
|
+
const globalTimeout = setTimeout(() => {
|
|
22
|
+
console.error("✗ Global timeout exceeded (45s)");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}, 45000);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
log("connecting...");
|
|
28
|
+
const cdp = await connect(5000);
|
|
29
|
+
|
|
30
|
+
log("getting pages...");
|
|
31
|
+
let targetId;
|
|
32
|
+
|
|
33
|
+
if (newTab) {
|
|
34
|
+
log("creating new tab...");
|
|
35
|
+
const { targetId: newTargetId } = await cdp.send("Target.createTarget", {
|
|
36
|
+
url: "about:blank",
|
|
37
|
+
});
|
|
38
|
+
targetId = newTargetId;
|
|
39
|
+
} else {
|
|
40
|
+
const pages = await cdp.getPages();
|
|
41
|
+
const page = pages.at(-1);
|
|
42
|
+
if (!page) {
|
|
43
|
+
console.error("✗ No active tab found");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
targetId = page.targetId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log("attaching to page...");
|
|
50
|
+
const sessionId = await cdp.attachToPage(targetId);
|
|
51
|
+
|
|
52
|
+
log("applying active emulation (if configured)...");
|
|
53
|
+
const activeEmulation = await applyActiveEmulation(cdp, sessionId);
|
|
54
|
+
|
|
55
|
+
log("navigating...");
|
|
56
|
+
await cdp.navigate(sessionId, url);
|
|
57
|
+
|
|
58
|
+
console.log(newTab ? "✓ Opened:" : "✓ Navigated to:", url);
|
|
59
|
+
if (activeEmulation) {
|
|
60
|
+
console.log(
|
|
61
|
+
` emulation: ${activeEmulation.preset.id}${activeEmulation.landscape ? " (landscape)" : ""}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log("closing...");
|
|
66
|
+
cdp.close();
|
|
67
|
+
log("done");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error("✗", e.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(globalTimeout);
|
|
73
|
+
setTimeout(() => process.exit(0), 100);
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const LOG_ROOT = join(homedir(), ".cache/agent-web/logs");
|
|
8
|
+
|
|
9
|
+
function statSafe(path) {
|
|
10
|
+
try {
|
|
11
|
+
return statSync(path);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findLatestFile() {
|
|
18
|
+
if (!existsSync(LOG_ROOT)) return null;
|
|
19
|
+
const dirs = readdirSync(LOG_ROOT)
|
|
20
|
+
.filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name))
|
|
21
|
+
.map((name) => join(LOG_ROOT, name))
|
|
22
|
+
.filter((path) => statSafe(path)?.isDirectory())
|
|
23
|
+
.sort();
|
|
24
|
+
if (dirs.length === 0) return null;
|
|
25
|
+
const latestDir = dirs[dirs.length - 1];
|
|
26
|
+
const files = readdirSync(latestDir)
|
|
27
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
28
|
+
.map((name) => join(latestDir, name))
|
|
29
|
+
.map((path) => ({ path, mtime: statSafe(path)?.mtimeMs || 0 }))
|
|
30
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
31
|
+
return files[0]?.path || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const argIndex = process.argv.indexOf("--file");
|
|
35
|
+
const filePath = argIndex !== -1 ? process.argv[argIndex + 1] : findLatestFile();
|
|
36
|
+
|
|
37
|
+
if (!filePath) {
|
|
38
|
+
console.error("✗ No log file found");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const statusCounts = new Map();
|
|
43
|
+
const failures = [];
|
|
44
|
+
let totalResponses = 0;
|
|
45
|
+
let totalRequests = 0;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const data = readFileSync(filePath, "utf8");
|
|
49
|
+
const lines = data.split("\n").filter(Boolean);
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
let entry;
|
|
52
|
+
try {
|
|
53
|
+
entry = JSON.parse(line);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (entry.type === "network.request") {
|
|
58
|
+
totalRequests += 1;
|
|
59
|
+
} else if (entry.type === "network.response") {
|
|
60
|
+
totalResponses += 1;
|
|
61
|
+
const status = String(entry.status ?? "unknown");
|
|
62
|
+
statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
|
|
63
|
+
} else if (entry.type === "network.failure") {
|
|
64
|
+
failures.push({
|
|
65
|
+
requestId: entry.requestId,
|
|
66
|
+
errorText: entry.errorText,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error("✗ summary failed:", e.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`file: ${filePath}`);
|
|
76
|
+
console.log(`requests: ${totalRequests}`);
|
|
77
|
+
console.log(`responses: ${totalResponses}`);
|
|
78
|
+
|
|
79
|
+
const statuses = Array.from(statusCounts.entries()).sort(
|
|
80
|
+
(a, b) => Number(a[0]) - Number(b[0]),
|
|
81
|
+
);
|
|
82
|
+
for (const [status, count] of statuses) {
|
|
83
|
+
console.log(`status ${status}: ${count}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (failures.length > 0) {
|
|
87
|
+
console.log("failures:");
|
|
88
|
+
for (const failure of failures.slice(0, 10)) {
|
|
89
|
+
console.log(`- ${failure.errorText || "unknown"} (${failure.requestId})`);
|
|
90
|
+
}
|
|
91
|
+
if (failures.length > 10) {
|
|
92
|
+
console.log(`- ... ${failures.length - 10} more`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { connect } from "./cdp.js";
|
|
4
|
+
import { applyActiveEmulation } from "./emulation-state.js";
|
|
5
|
+
|
|
6
|
+
const DEBUG = process.env.DEBUG === "1";
|
|
7
|
+
const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
|
|
8
|
+
|
|
9
|
+
const message = process.argv.slice(2).join(" ");
|
|
10
|
+
if (!message) {
|
|
11
|
+
console.log("Usage: pick.js 'message'");
|
|
12
|
+
console.log("\nExample:");
|
|
13
|
+
console.log(' pick.js "Click the submit button"');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Global timeout - 5 minutes for interactive picking
|
|
18
|
+
const globalTimeout = setTimeout(() => {
|
|
19
|
+
console.error("✗ Global timeout exceeded (5m)");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}, 300000);
|
|
22
|
+
|
|
23
|
+
const PICK_SCRIPT = `(message) => {
|
|
24
|
+
if (!message) throw new Error("pick() requires a message parameter");
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const selections = [];
|
|
27
|
+
const selectedElements = new Set();
|
|
28
|
+
|
|
29
|
+
const overlay = document.createElement("div");
|
|
30
|
+
overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
|
|
31
|
+
|
|
32
|
+
const highlight = document.createElement("div");
|
|
33
|
+
highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
|
|
34
|
+
overlay.appendChild(highlight);
|
|
35
|
+
|
|
36
|
+
const banner = document.createElement("div");
|
|
37
|
+
banner.style.cssText = "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";
|
|
38
|
+
|
|
39
|
+
const updateBanner = () => {
|
|
40
|
+
banner.textContent = message + " (" + selections.length + " selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)";
|
|
41
|
+
};
|
|
42
|
+
updateBanner();
|
|
43
|
+
|
|
44
|
+
document.body.append(banner, overlay);
|
|
45
|
+
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
48
|
+
document.removeEventListener("click", onClick, true);
|
|
49
|
+
document.removeEventListener("keydown", onKey, true);
|
|
50
|
+
overlay.remove();
|
|
51
|
+
banner.remove();
|
|
52
|
+
selectedElements.forEach((el) => { el.style.outline = ""; });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onMove = (e) => {
|
|
56
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
57
|
+
if (!el || overlay.contains(el) || banner.contains(el)) return;
|
|
58
|
+
const r = el.getBoundingClientRect();
|
|
59
|
+
highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:" + r.top + "px;left:" + r.left + "px;width:" + r.width + "px;height:" + r.height + "px";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const buildElementInfo = (el) => {
|
|
63
|
+
const parents = [];
|
|
64
|
+
let current = el.parentElement;
|
|
65
|
+
while (current && current !== document.body) {
|
|
66
|
+
const parentInfo = current.tagName.toLowerCase();
|
|
67
|
+
const id = current.id ? "#" + current.id : "";
|
|
68
|
+
const cls = current.className ? "." + current.className.trim().split(/\\s+/).join(".") : "";
|
|
69
|
+
parents.push(parentInfo + id + cls);
|
|
70
|
+
current = current.parentElement;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
tag: el.tagName.toLowerCase(),
|
|
74
|
+
id: el.id || null,
|
|
75
|
+
class: el.className || null,
|
|
76
|
+
text: (el.textContent || "").trim().slice(0, 200) || null,
|
|
77
|
+
html: el.outerHTML.slice(0, 500),
|
|
78
|
+
parents: parents.join(" > "),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const onClick = (e) => {
|
|
83
|
+
if (banner.contains(e.target)) return;
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
87
|
+
if (!el || overlay.contains(el) || banner.contains(el)) return;
|
|
88
|
+
|
|
89
|
+
if (e.metaKey || e.ctrlKey) {
|
|
90
|
+
if (!selectedElements.has(el)) {
|
|
91
|
+
selectedElements.add(el);
|
|
92
|
+
el.style.outline = "3px solid #10b981";
|
|
93
|
+
selections.push(buildElementInfo(el));
|
|
94
|
+
updateBanner();
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
cleanup();
|
|
98
|
+
const info = buildElementInfo(el);
|
|
99
|
+
resolve(selections.length > 0 ? selections : info);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onKey = (e) => {
|
|
104
|
+
if (e.key === "Escape") {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve(null);
|
|
108
|
+
} else if (e.key === "Enter" && selections.length > 0) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
cleanup();
|
|
111
|
+
resolve(selections);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
document.addEventListener("mousemove", onMove, true);
|
|
116
|
+
document.addEventListener("click", onClick, true);
|
|
117
|
+
document.addEventListener("keydown", onKey, true);
|
|
118
|
+
});
|
|
119
|
+
}`;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
log("connecting...");
|
|
123
|
+
const cdp = await connect(5000);
|
|
124
|
+
|
|
125
|
+
log("getting pages...");
|
|
126
|
+
const pages = await cdp.getPages();
|
|
127
|
+
const page = pages.at(-1);
|
|
128
|
+
|
|
129
|
+
if (!page) {
|
|
130
|
+
console.error("✗ No active tab found");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log("attaching to page...");
|
|
135
|
+
const sessionId = await cdp.attachToPage(page.targetId);
|
|
136
|
+
|
|
137
|
+
log("applying active emulation (if configured)...");
|
|
138
|
+
await applyActiveEmulation(cdp, sessionId);
|
|
139
|
+
|
|
140
|
+
log("waiting for user pick...");
|
|
141
|
+
const expression = `(${PICK_SCRIPT})(${JSON.stringify(message)})`;
|
|
142
|
+
const result = await cdp.evaluate(sessionId, expression, 300000);
|
|
143
|
+
|
|
144
|
+
log("formatting result...");
|
|
145
|
+
if (Array.isArray(result)) {
|
|
146
|
+
for (let i = 0; i < result.length; i++) {
|
|
147
|
+
if (i > 0) console.log("");
|
|
148
|
+
for (const [key, value] of Object.entries(result[i])) {
|
|
149
|
+
console.log(`${key}: ${value}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else if (typeof result === "object" && result !== null) {
|
|
153
|
+
for (const [key, value] of Object.entries(result)) {
|
|
154
|
+
console.log(`${key}: ${value}`);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.log(result);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log("closing...");
|
|
161
|
+
cdp.close();
|
|
162
|
+
log("done");
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error("✗", e.message);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(globalTimeout);
|
|
168
|
+
setTimeout(() => process.exit(0), 100);
|
|
169
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { connect } from "./cdp.js";
|
|
7
|
+
import {
|
|
8
|
+
applyDevicePreset,
|
|
9
|
+
clearDeviceEmulation,
|
|
10
|
+
listDevicePresets,
|
|
11
|
+
resolveDevicePreset,
|
|
12
|
+
} from "./devices.js";
|
|
13
|
+
import { applyActiveEmulation } from "./emulation-state.js";
|
|
14
|
+
|
|
15
|
+
const DEBUG = process.env.DEBUG === "1";
|
|
16
|
+
const log = DEBUG ? (...args) => console.error("[debug]", ...args) : () => {};
|
|
17
|
+
|
|
18
|
+
function printUsage() {
|
|
19
|
+
console.log("Usage: screenshot.js [--full-page] [--device <preset>] [--landscape]");
|
|
20
|
+
console.log("\nExamples:");
|
|
21
|
+
console.log(" screenshot.js");
|
|
22
|
+
console.log(" screenshot.js --full-page");
|
|
23
|
+
console.log(" screenshot.js --device iphone-14");
|
|
24
|
+
console.log(" screenshot.js --device pixel-7 --full-page");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
let fullPage = false;
|
|
29
|
+
let landscape = false;
|
|
30
|
+
let deviceName = null;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i];
|
|
34
|
+
|
|
35
|
+
if (arg === "--full-page") {
|
|
36
|
+
fullPage = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (arg === "--landscape") {
|
|
41
|
+
landscape = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (arg === "--device") {
|
|
46
|
+
const value = args[i + 1];
|
|
47
|
+
if (!value || value.startsWith("--")) {
|
|
48
|
+
console.error("✗ --device requires a preset name");
|
|
49
|
+
printUsage();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
deviceName = value;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (arg === "--help") {
|
|
58
|
+
printUsage();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.error(`✗ Unknown argument: ${arg}`);
|
|
63
|
+
printUsage();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const preset = deviceName ? resolveDevicePreset(deviceName) : null;
|
|
68
|
+
if (landscape && !preset) {
|
|
69
|
+
console.error("✗ --landscape requires --device <preset>");
|
|
70
|
+
printUsage();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (deviceName && !preset) {
|
|
75
|
+
console.error(`✗ Unknown device preset: ${deviceName}`);
|
|
76
|
+
console.error(" Available presets:");
|
|
77
|
+
for (const item of listDevicePresets()) {
|
|
78
|
+
console.error(` - ${item.id}`);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Global timeout
|
|
84
|
+
const globalTimeout = setTimeout(() => {
|
|
85
|
+
console.error("✗ Global timeout exceeded (30s)");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}, 30000);
|
|
88
|
+
|
|
89
|
+
let cdp = null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
log("connecting...");
|
|
93
|
+
cdp = await connect(5000);
|
|
94
|
+
|
|
95
|
+
log("getting pages...");
|
|
96
|
+
const pages = await cdp.getPages();
|
|
97
|
+
const page = pages.at(-1);
|
|
98
|
+
|
|
99
|
+
if (!page) {
|
|
100
|
+
console.error("✗ No active tab found");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
log("attaching to page...");
|
|
105
|
+
const sessionId = await cdp.attachToPage(page.targetId);
|
|
106
|
+
|
|
107
|
+
let temporaryEmulationApplied = false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (preset) {
|
|
111
|
+
log("applying temporary device emulation...");
|
|
112
|
+
await applyDevicePreset(cdp, sessionId, preset, { landscape });
|
|
113
|
+
temporaryEmulationApplied = true;
|
|
114
|
+
} else {
|
|
115
|
+
log("applying active emulation (if configured)...");
|
|
116
|
+
await applyActiveEmulation(cdp, sessionId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let params = { format: "png" };
|
|
120
|
+
|
|
121
|
+
if (fullPage) {
|
|
122
|
+
log("reading layout metrics...");
|
|
123
|
+
const metrics = await cdp.send("Page.getLayoutMetrics", {}, sessionId, 10000);
|
|
124
|
+
const contentSize = metrics.cssContentSize || metrics.contentSize;
|
|
125
|
+
|
|
126
|
+
if (!contentSize) {
|
|
127
|
+
throw new Error("Could not determine page size for full-page screenshot");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
params = {
|
|
131
|
+
...params,
|
|
132
|
+
fromSurface: true,
|
|
133
|
+
captureBeyondViewport: true,
|
|
134
|
+
clip: {
|
|
135
|
+
x: 0,
|
|
136
|
+
y: 0,
|
|
137
|
+
width: Math.max(1, Math.ceil(contentSize.width)),
|
|
138
|
+
height: Math.max(1, Math.ceil(contentSize.height)),
|
|
139
|
+
scale: 1,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
log("taking screenshot...");
|
|
145
|
+
const { data } = await cdp.send(
|
|
146
|
+
"Page.captureScreenshot",
|
|
147
|
+
params,
|
|
148
|
+
sessionId,
|
|
149
|
+
fullPage ? 20000 : 10000,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
153
|
+
const filename = `screenshot-${timestamp}.png`;
|
|
154
|
+
const filepath = join(tmpdir(), filename);
|
|
155
|
+
|
|
156
|
+
writeFileSync(filepath, Buffer.from(data, "base64"));
|
|
157
|
+
console.log(filepath);
|
|
158
|
+
} finally {
|
|
159
|
+
if (temporaryEmulationApplied) {
|
|
160
|
+
try {
|
|
161
|
+
log("clearing temporary device emulation...");
|
|
162
|
+
await clearDeviceEmulation(cdp, sessionId);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
log("failed to clear device emulation", e.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
log("closing...");
|
|
170
|
+
cdp.close();
|
|
171
|
+
log("done");
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error("✗", e.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
} finally {
|
|
176
|
+
clearTimeout(globalTimeout);
|
|
177
|
+
if (cdp) {
|
|
178
|
+
try {
|
|
179
|
+
cdp.close();
|
|
180
|
+
} catch {
|
|
181
|
+
// ignore
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
setTimeout(() => process.exit(0), 100);
|
|
185
|
+
}
|