surf-cli 2.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/LICENSE +21 -0
- package/README.md +426 -0
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/manifest.json +70 -0
- package/dist/options/options.html +30 -0
- package/dist/options/options.js +30 -0
- package/dist/options/options.js.map +1 -0
- package/dist/service-worker/index.js +156 -0
- package/dist/service-worker/index.js.map +1 -0
- package/dist/service-worker-loader.js +1 -0
- package/native/CHANGELOG.md +136 -0
- package/native/README.md +141 -0
- package/native/chatgpt-client.cjs +455 -0
- package/native/cli.cjs +2424 -0
- package/native/config.cjs +87 -0
- package/native/device-presets.cjs +211 -0
- package/native/formatters/network.cjs +402 -0
- package/native/gemini-client.cjs +637 -0
- package/native/host-helpers.cjs +989 -0
- package/native/host-wrapper.py +15 -0
- package/native/host.cjs +1271 -0
- package/native/host.sh +2 -0
- package/native/mcp-server.cjs +511 -0
- package/native/network-store.cjs +851 -0
- package/native/perplexity-client.cjs +561 -0
- package/native/protocol.cjs +27 -0
- package/native/test-host.py +41 -0
- package/native/tests/cli-tests.sh +115 -0
- package/package.json +70 -0
- package/scripts/install-native-host.cjs +308 -0
- package/scripts/uninstall-native-host.cjs +194 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const CONFIG_NAME = "surf.json";
|
|
6
|
+
|
|
7
|
+
let cachedConfig = null;
|
|
8
|
+
let cachedConfigPath = null;
|
|
9
|
+
|
|
10
|
+
const STARTER_CONFIG = {
|
|
11
|
+
routes: {
|
|
12
|
+
main: ["http://localhost:3000"]
|
|
13
|
+
},
|
|
14
|
+
selectors: {
|
|
15
|
+
chatgpt: {
|
|
16
|
+
input: "#prompt-textarea"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function findConfigPath() {
|
|
22
|
+
const cwdPath = path.join(process.cwd(), CONFIG_NAME);
|
|
23
|
+
if (fs.existsSync(cwdPath)) {
|
|
24
|
+
return cwdPath;
|
|
25
|
+
}
|
|
26
|
+
const homePath = path.join(os.homedir(), CONFIG_NAME);
|
|
27
|
+
if (fs.existsSync(homePath)) {
|
|
28
|
+
return homePath;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadConfig() {
|
|
34
|
+
if (cachedConfig !== null) {
|
|
35
|
+
return cachedConfig;
|
|
36
|
+
}
|
|
37
|
+
const configPath = findConfigPath();
|
|
38
|
+
if (!configPath) {
|
|
39
|
+
cachedConfig = {};
|
|
40
|
+
cachedConfigPath = null;
|
|
41
|
+
return cachedConfig;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
45
|
+
cachedConfig = JSON.parse(content);
|
|
46
|
+
cachedConfigPath = configPath;
|
|
47
|
+
return cachedConfig;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Warning: Failed to parse ${configPath}: ${err.message}`);
|
|
50
|
+
cachedConfig = {};
|
|
51
|
+
cachedConfigPath = null;
|
|
52
|
+
return cachedConfig;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getConfigPath() {
|
|
57
|
+
if (cachedConfig === null) {
|
|
58
|
+
loadConfig();
|
|
59
|
+
}
|
|
60
|
+
return cachedConfigPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createStarterConfig(targetDir = process.cwd()) {
|
|
64
|
+
const targetPath = path.join(targetDir, CONFIG_NAME);
|
|
65
|
+
if (fs.existsSync(targetPath)) {
|
|
66
|
+
return { success: false, error: "Config already exists", path: targetPath };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(targetPath, JSON.stringify(STARTER_CONFIG, null, 2) + "\n");
|
|
70
|
+
return { success: true, path: targetPath };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { success: false, error: err.message, path: targetPath };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clearCache() {
|
|
77
|
+
cachedConfig = null;
|
|
78
|
+
cachedConfigPath = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
loadConfig,
|
|
83
|
+
getConfigPath,
|
|
84
|
+
createStarterConfig,
|
|
85
|
+
clearCache,
|
|
86
|
+
STARTER_CONFIG,
|
|
87
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device presets for emulation (based on Chrome DevTools)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEVICE_PRESETS = {
|
|
6
|
+
// Apple devices
|
|
7
|
+
"iPhone 12": {
|
|
8
|
+
width: 390,
|
|
9
|
+
height: 844,
|
|
10
|
+
deviceScaleFactor: 3,
|
|
11
|
+
mobile: true,
|
|
12
|
+
touch: true,
|
|
13
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
|
14
|
+
},
|
|
15
|
+
"iPhone 13": {
|
|
16
|
+
width: 390,
|
|
17
|
+
height: 844,
|
|
18
|
+
deviceScaleFactor: 3,
|
|
19
|
+
mobile: true,
|
|
20
|
+
touch: true,
|
|
21
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
|
22
|
+
},
|
|
23
|
+
"iPhone 14": {
|
|
24
|
+
width: 390,
|
|
25
|
+
height: 844,
|
|
26
|
+
deviceScaleFactor: 3,
|
|
27
|
+
mobile: true,
|
|
28
|
+
touch: true,
|
|
29
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
|
30
|
+
},
|
|
31
|
+
"iPhone 14 Pro": {
|
|
32
|
+
width: 393,
|
|
33
|
+
height: 852,
|
|
34
|
+
deviceScaleFactor: 3,
|
|
35
|
+
mobile: true,
|
|
36
|
+
touch: true,
|
|
37
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
|
38
|
+
},
|
|
39
|
+
"iPhone 14 Pro Max": {
|
|
40
|
+
width: 430,
|
|
41
|
+
height: 932,
|
|
42
|
+
deviceScaleFactor: 3,
|
|
43
|
+
mobile: true,
|
|
44
|
+
touch: true,
|
|
45
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
|
46
|
+
},
|
|
47
|
+
"iPhone SE": {
|
|
48
|
+
width: 375,
|
|
49
|
+
height: 667,
|
|
50
|
+
deviceScaleFactor: 2,
|
|
51
|
+
mobile: true,
|
|
52
|
+
touch: true,
|
|
53
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
|
54
|
+
},
|
|
55
|
+
"iPad": {
|
|
56
|
+
width: 768,
|
|
57
|
+
height: 1024,
|
|
58
|
+
deviceScaleFactor: 2,
|
|
59
|
+
mobile: true,
|
|
60
|
+
touch: true,
|
|
61
|
+
userAgent: "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
|
62
|
+
},
|
|
63
|
+
"iPad Pro": {
|
|
64
|
+
width: 1024,
|
|
65
|
+
height: 1366,
|
|
66
|
+
deviceScaleFactor: 2,
|
|
67
|
+
mobile: true,
|
|
68
|
+
touch: true,
|
|
69
|
+
userAgent: "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
|
70
|
+
},
|
|
71
|
+
"iPad Mini": {
|
|
72
|
+
width: 768,
|
|
73
|
+
height: 1024,
|
|
74
|
+
deviceScaleFactor: 2,
|
|
75
|
+
mobile: true,
|
|
76
|
+
touch: true,
|
|
77
|
+
userAgent: "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Google devices
|
|
81
|
+
"Pixel 5": {
|
|
82
|
+
width: 393,
|
|
83
|
+
height: 851,
|
|
84
|
+
deviceScaleFactor: 2.75,
|
|
85
|
+
mobile: true,
|
|
86
|
+
touch: true,
|
|
87
|
+
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36",
|
|
88
|
+
},
|
|
89
|
+
"Pixel 6": {
|
|
90
|
+
width: 412,
|
|
91
|
+
height: 915,
|
|
92
|
+
deviceScaleFactor: 2.625,
|
|
93
|
+
mobile: true,
|
|
94
|
+
touch: true,
|
|
95
|
+
userAgent: "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36",
|
|
96
|
+
},
|
|
97
|
+
"Pixel 7": {
|
|
98
|
+
width: 412,
|
|
99
|
+
height: 915,
|
|
100
|
+
deviceScaleFactor: 2.625,
|
|
101
|
+
mobile: true,
|
|
102
|
+
touch: true,
|
|
103
|
+
userAgent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36",
|
|
104
|
+
},
|
|
105
|
+
"Pixel 7 Pro": {
|
|
106
|
+
width: 412,
|
|
107
|
+
height: 892,
|
|
108
|
+
deviceScaleFactor: 3.5,
|
|
109
|
+
mobile: true,
|
|
110
|
+
touch: true,
|
|
111
|
+
userAgent: "Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36",
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Samsung devices
|
|
115
|
+
"Galaxy S21": {
|
|
116
|
+
width: 360,
|
|
117
|
+
height: 800,
|
|
118
|
+
deviceScaleFactor: 3,
|
|
119
|
+
mobile: true,
|
|
120
|
+
touch: true,
|
|
121
|
+
userAgent: "Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36",
|
|
122
|
+
},
|
|
123
|
+
"Galaxy S22": {
|
|
124
|
+
width: 360,
|
|
125
|
+
height: 780,
|
|
126
|
+
deviceScaleFactor: 3,
|
|
127
|
+
mobile: true,
|
|
128
|
+
touch: true,
|
|
129
|
+
userAgent: "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36",
|
|
130
|
+
},
|
|
131
|
+
"Galaxy S23": {
|
|
132
|
+
width: 360,
|
|
133
|
+
height: 780,
|
|
134
|
+
deviceScaleFactor: 3,
|
|
135
|
+
mobile: true,
|
|
136
|
+
touch: true,
|
|
137
|
+
userAgent: "Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Mobile Safari/537.36",
|
|
138
|
+
},
|
|
139
|
+
"Galaxy Tab S7": {
|
|
140
|
+
width: 800,
|
|
141
|
+
height: 1280,
|
|
142
|
+
deviceScaleFactor: 2,
|
|
143
|
+
mobile: true,
|
|
144
|
+
touch: true,
|
|
145
|
+
userAgent: "Mozilla/5.0 (Linux; Android 10; SM-T870) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Safari/537.36",
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Other common devices
|
|
149
|
+
"Nest Hub": {
|
|
150
|
+
width: 1024,
|
|
151
|
+
height: 600,
|
|
152
|
+
deviceScaleFactor: 2,
|
|
153
|
+
mobile: false,
|
|
154
|
+
touch: true,
|
|
155
|
+
userAgent: "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.109 Safari/537.36 CrKey/1.54.248666",
|
|
156
|
+
},
|
|
157
|
+
"Nest Hub Max": {
|
|
158
|
+
width: 1280,
|
|
159
|
+
height: 800,
|
|
160
|
+
deviceScaleFactor: 2,
|
|
161
|
+
mobile: false,
|
|
162
|
+
touch: true,
|
|
163
|
+
userAgent: "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.109 Safari/537.36 CrKey/1.54.248666",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Aliases for common searches
|
|
168
|
+
const DEVICE_ALIASES = {
|
|
169
|
+
"iphone": "iPhone 14",
|
|
170
|
+
"iphone14": "iPhone 14",
|
|
171
|
+
"iphone13": "iPhone 13",
|
|
172
|
+
"iphone12": "iPhone 12",
|
|
173
|
+
"iphonese": "iPhone SE",
|
|
174
|
+
"pixel": "Pixel 7",
|
|
175
|
+
"pixel7": "Pixel 7",
|
|
176
|
+
"pixel6": "Pixel 6",
|
|
177
|
+
"galaxy": "Galaxy S23",
|
|
178
|
+
"galaxys23": "Galaxy S23",
|
|
179
|
+
"ipad": "iPad",
|
|
180
|
+
"ipadpro": "iPad Pro",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function findDevice(name) {
|
|
184
|
+
// Exact match first
|
|
185
|
+
if (DEVICE_PRESETS[name]) {
|
|
186
|
+
return { name, preset: DEVICE_PRESETS[name] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check aliases (case-insensitive, no spaces)
|
|
190
|
+
const normalized = name.toLowerCase().replace(/\s+/g, "");
|
|
191
|
+
if (DEVICE_ALIASES[normalized]) {
|
|
192
|
+
const deviceName = DEVICE_ALIASES[normalized];
|
|
193
|
+
return { name: deviceName, preset: DEVICE_PRESETS[deviceName] };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fuzzy match by partial name
|
|
197
|
+
const lowerName = name.toLowerCase();
|
|
198
|
+
for (const [deviceName, preset] of Object.entries(DEVICE_PRESETS)) {
|
|
199
|
+
if (deviceName.toLowerCase().includes(lowerName)) {
|
|
200
|
+
return { name: deviceName, preset };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function listDevices() {
|
|
208
|
+
return Object.keys(DEVICE_PRESETS).sort();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { DEVICE_PRESETS, findDevice, listDevices };
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Network request formatters for surf-cli
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format bytes to human readable size
|
|
5
|
+
*/
|
|
6
|
+
function formatSize(bytes) {
|
|
7
|
+
if (bytes === undefined || bytes === null) return '-';
|
|
8
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
9
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
10
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format milliseconds to human readable duration
|
|
15
|
+
*/
|
|
16
|
+
function formatDuration(ms) {
|
|
17
|
+
if (ms === undefined || ms === null) return '-';
|
|
18
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
19
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format timestamp to readable time
|
|
24
|
+
*/
|
|
25
|
+
function formatTimestamp(ts) {
|
|
26
|
+
if (!ts) return '-';
|
|
27
|
+
return new Date(ts).toLocaleTimeString('en-US', {
|
|
28
|
+
hour12: false,
|
|
29
|
+
hour: '2-digit',
|
|
30
|
+
minute: '2-digit',
|
|
31
|
+
second: '2-digit'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get content type shorthand
|
|
37
|
+
*/
|
|
38
|
+
function getContentTypeShort(contentType) {
|
|
39
|
+
if (!contentType) return '-';
|
|
40
|
+
const ct = contentType.toLowerCase();
|
|
41
|
+
if (ct.includes('json')) return 'json';
|
|
42
|
+
if (ct.includes('html')) return 'html';
|
|
43
|
+
if (ct.includes('javascript')) return 'js';
|
|
44
|
+
if (ct.includes('css')) return 'css';
|
|
45
|
+
if (ct.includes('image/')) return 'img';
|
|
46
|
+
if (ct.includes('font')) return 'font';
|
|
47
|
+
if (ct.includes('xml')) return 'xml';
|
|
48
|
+
if (ct.includes('text/plain')) return 'text';
|
|
49
|
+
if (ct.includes('protobuf') || ct.includes('proto')) return 'proto';
|
|
50
|
+
if (ct.includes('octet-stream')) return 'bin';
|
|
51
|
+
if (ct.includes('form')) return 'form';
|
|
52
|
+
return ct.split('/').pop()?.split(';')[0]?.slice(0, 6) || '-';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get status code style indicator
|
|
57
|
+
*/
|
|
58
|
+
function getStatusIndicator(status) {
|
|
59
|
+
if (!status) return '...';
|
|
60
|
+
if (status >= 200 && status < 300) return String(status);
|
|
61
|
+
if (status >= 300 && status < 400) return `${status}→`;
|
|
62
|
+
if (status >= 400 && status < 500) return `${status}!`;
|
|
63
|
+
if (status >= 500) return `${status}!!`;
|
|
64
|
+
return String(status);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compact table format (default)
|
|
69
|
+
* Groups by origin with time gaps
|
|
70
|
+
*/
|
|
71
|
+
function formatCompact(entries, options = {}) {
|
|
72
|
+
if (!entries || entries.length === 0) {
|
|
73
|
+
return 'No network requests captured';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = [];
|
|
77
|
+
const { verbose } = options;
|
|
78
|
+
|
|
79
|
+
// Header
|
|
80
|
+
const header = 'ID │ Method │ Status │ Type │ Size │ Time │ URL';
|
|
81
|
+
const separator = '─────────┼────────┼────────┼───────┼────────┼────────┼' + '─'.repeat(50);
|
|
82
|
+
lines.push(header);
|
|
83
|
+
lines.push(separator);
|
|
84
|
+
|
|
85
|
+
let lastOrigin = null;
|
|
86
|
+
let lastTime = null;
|
|
87
|
+
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const origin = e.origin || new URL(e.url).origin;
|
|
90
|
+
const timestamp = e.timestamp || e.startTime;
|
|
91
|
+
|
|
92
|
+
// Add origin separator if changed
|
|
93
|
+
if (lastOrigin && origin !== lastOrigin) {
|
|
94
|
+
lines.push('─────────┼────────┼────────┼───────┼────────┼────────┼' + '─'.repeat(50));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add time gap indicator (> 5 seconds)
|
|
98
|
+
if (lastTime && timestamp && (timestamp - lastTime) > 5000) {
|
|
99
|
+
const gap = formatDuration(timestamp - lastTime);
|
|
100
|
+
lines.push(` │ │ │ │ │ +${gap.padEnd(5)} │`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const id = (e.requestId || e.id || '-').slice(0, 8).padEnd(8);
|
|
104
|
+
const method = (e.method || 'GET').padEnd(6);
|
|
105
|
+
const status = getStatusIndicator(e.status).padEnd(6);
|
|
106
|
+
const type = getContentTypeShort(e.contentType || e.responseHeaders?.['content-type']).padEnd(5);
|
|
107
|
+
const size = formatSize(e.responseSize || e.encodedDataLength).padEnd(6);
|
|
108
|
+
const time = formatDuration(e.duration || e.time).padEnd(6);
|
|
109
|
+
const url = truncateUrl(e.url, 60);
|
|
110
|
+
|
|
111
|
+
lines.push(`${id} │ ${method} │ ${status} │ ${type} │ ${size} │ ${time} │ ${url}`);
|
|
112
|
+
|
|
113
|
+
lastOrigin = origin;
|
|
114
|
+
lastTime = timestamp;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Summary
|
|
118
|
+
lines.push('');
|
|
119
|
+
const totalSize = entries.reduce((acc, e) => acc + (e.responseSize || e.encodedDataLength || 0), 0);
|
|
120
|
+
lines.push(`Total: ${entries.length} requests, ${formatSize(totalSize)}`);
|
|
121
|
+
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Just URLs with method
|
|
127
|
+
*/
|
|
128
|
+
function formatUrls(entries) {
|
|
129
|
+
if (!entries || entries.length === 0) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
return entries.map(e => `${(e.method || 'GET').padEnd(6)} ${e.url}`).join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate curl command for a single request
|
|
137
|
+
*/
|
|
138
|
+
function formatCurl(entry) {
|
|
139
|
+
if (!entry) return '';
|
|
140
|
+
|
|
141
|
+
let cmd = `curl -X ${entry.method || 'GET'} '${entry.url}'`;
|
|
142
|
+
|
|
143
|
+
const headers = entry.requestHeaders || {};
|
|
144
|
+
const skipHeaders = ['host', 'content-length', 'connection', 'accept-encoding'];
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
147
|
+
if (!skipHeaders.includes(key.toLowerCase())) {
|
|
148
|
+
const escapedValue = String(value).replace(/'/g, "'\\''");
|
|
149
|
+
cmd += ` \\\n -H '${key}: ${escapedValue}'`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (entry.requestBody) {
|
|
154
|
+
const escapedBody = entry.requestBody.replace(/'/g, "'\\''");
|
|
155
|
+
cmd += ` \\\n -d '${escapedBody}'`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return cmd;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate curl commands for multiple entries
|
|
163
|
+
*/
|
|
164
|
+
function formatCurlBatch(entries) {
|
|
165
|
+
if (!entries || entries.length === 0) {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
return entries.map(e => formatCurl(e)).join('\n\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Full JSON output
|
|
173
|
+
*/
|
|
174
|
+
function formatRaw(entries) {
|
|
175
|
+
// Return as object with entries key so CLI can detect it
|
|
176
|
+
return JSON.stringify({ entries, _format: 'raw' }, null, 2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Verbose format with headers and body preview
|
|
181
|
+
*/
|
|
182
|
+
function formatVerbose(entries, level = 1) {
|
|
183
|
+
if (!entries || entries.length === 0) {
|
|
184
|
+
return 'No network requests captured';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lines = [];
|
|
188
|
+
const bodyLimit = level >= 2 ? Infinity : 2048;
|
|
189
|
+
|
|
190
|
+
for (const e of entries) {
|
|
191
|
+
lines.push('═'.repeat(80));
|
|
192
|
+
lines.push(`${e.method || 'GET'} ${e.url}`);
|
|
193
|
+
lines.push(`ID: ${e.requestId || e.id || '-'} Status: ${e.status || 'pending'} Time: ${formatDuration(e.duration || e.time)}`);
|
|
194
|
+
lines.push('');
|
|
195
|
+
|
|
196
|
+
// Request headers
|
|
197
|
+
if (e.requestHeaders && Object.keys(e.requestHeaders).length > 0) {
|
|
198
|
+
lines.push('▶ Request Headers:');
|
|
199
|
+
const reqHeaders = level >= 2
|
|
200
|
+
? e.requestHeaders
|
|
201
|
+
: pickHeaders(e.requestHeaders, ['content-type', 'authorization', 'cookie', 'user-agent', 'accept']);
|
|
202
|
+
for (const [k, v] of Object.entries(reqHeaders)) {
|
|
203
|
+
lines.push(` ${k}: ${truncateValue(v, 100)}`);
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Request body
|
|
209
|
+
if (e.requestBody) {
|
|
210
|
+
lines.push('▶ Request Body:');
|
|
211
|
+
lines.push(formatBody(e.requestBody, bodyLimit));
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Response headers
|
|
216
|
+
if (e.responseHeaders && Object.keys(e.responseHeaders).length > 0) {
|
|
217
|
+
lines.push('◀ Response Headers:');
|
|
218
|
+
const resHeaders = level >= 2
|
|
219
|
+
? e.responseHeaders
|
|
220
|
+
: pickHeaders(e.responseHeaders, ['content-type', 'content-length', 'set-cookie', 'location', 'cache-control']);
|
|
221
|
+
for (const [k, v] of Object.entries(resHeaders)) {
|
|
222
|
+
lines.push(` ${k}: ${truncateValue(v, 100)}`);
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Response body preview
|
|
228
|
+
if (e.responseBody) {
|
|
229
|
+
lines.push('◀ Response Body:');
|
|
230
|
+
lines.push(formatBody(e.responseBody, bodyLimit));
|
|
231
|
+
lines.push('');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Format a single entry in detail
|
|
240
|
+
*/
|
|
241
|
+
function formatEntry(entry) {
|
|
242
|
+
if (!entry) return 'Request not found';
|
|
243
|
+
return formatVerbose([entry], 2);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Origins summary table
|
|
248
|
+
* Accepts either array of {origin, count, totalSize, lastSeen} or
|
|
249
|
+
* object map of {origin: {count, size, lastSeen}}
|
|
250
|
+
*/
|
|
251
|
+
function formatOrigins(origins) {
|
|
252
|
+
if (!origins) {
|
|
253
|
+
return 'No origins captured';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Convert object map to array if needed
|
|
257
|
+
let originsArray;
|
|
258
|
+
if (Array.isArray(origins)) {
|
|
259
|
+
originsArray = origins;
|
|
260
|
+
} else if (typeof origins === 'object') {
|
|
261
|
+
originsArray = Object.entries(origins).map(([origin, data]) => ({
|
|
262
|
+
origin,
|
|
263
|
+
count: data.count || 0,
|
|
264
|
+
totalSize: data.size || data.totalSize || 0,
|
|
265
|
+
lastSeen: data.lastSeen || 0
|
|
266
|
+
}));
|
|
267
|
+
} else {
|
|
268
|
+
return 'No origins captured';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (originsArray.length === 0) {
|
|
272
|
+
return 'No origins captured';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const lines = [];
|
|
276
|
+
const header = 'Origin'.padEnd(40) + ' │ Requests │ Size │ Last Seen';
|
|
277
|
+
const separator = '─'.repeat(40) + '─┼──────────┼─────────┼' + '─'.repeat(20);
|
|
278
|
+
lines.push(header);
|
|
279
|
+
lines.push(separator);
|
|
280
|
+
|
|
281
|
+
for (const o of originsArray) {
|
|
282
|
+
const origin = truncateUrl(o.origin, 38).padEnd(40);
|
|
283
|
+
const count = String(o.count || 0).padEnd(8);
|
|
284
|
+
const size = formatSize(o.totalSize || 0).padEnd(7);
|
|
285
|
+
const lastSeen = formatTimestamp(o.lastSeen);
|
|
286
|
+
|
|
287
|
+
lines.push(`${origin} │ ${count} │ ${size} │ ${lastSeen}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return lines.join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Format network stats
|
|
295
|
+
*/
|
|
296
|
+
function formatStats(stats) {
|
|
297
|
+
if (!stats) return 'No stats available';
|
|
298
|
+
|
|
299
|
+
const lines = [];
|
|
300
|
+
lines.push('Network Capture Statistics');
|
|
301
|
+
lines.push('═'.repeat(40));
|
|
302
|
+
lines.push(`Total Requests: ${stats.totalRequests || 0}`);
|
|
303
|
+
lines.push(`Total Size: ${formatSize(stats.totalSize || 0)}`);
|
|
304
|
+
lines.push(`Unique Origins: ${stats.uniqueOrigins || 0}`);
|
|
305
|
+
lines.push(`Capture Start: ${formatTimestamp(stats.startTime)}`);
|
|
306
|
+
lines.push(`Duration: ${formatDuration(stats.duration)}`);
|
|
307
|
+
lines.push('');
|
|
308
|
+
|
|
309
|
+
if (stats.byMethod) {
|
|
310
|
+
lines.push('By Method:');
|
|
311
|
+
for (const [method, count] of Object.entries(stats.byMethod)) {
|
|
312
|
+
lines.push(` ${method.padEnd(8)} ${count}`);
|
|
313
|
+
}
|
|
314
|
+
lines.push('');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (stats.byStatus) {
|
|
318
|
+
lines.push('By Status:');
|
|
319
|
+
for (const [status, count] of Object.entries(stats.byStatus)) {
|
|
320
|
+
lines.push(` ${status.padEnd(8)} ${count}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return lines.join('\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Helper functions
|
|
328
|
+
|
|
329
|
+
function truncateUrl(url, maxLen = 60) {
|
|
330
|
+
if (!url) return '-';
|
|
331
|
+
if (url.length <= maxLen) return url;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const u = new URL(url);
|
|
335
|
+
const pathLen = maxLen - u.origin.length - 3;
|
|
336
|
+
if (pathLen > 10) {
|
|
337
|
+
return u.origin + u.pathname.slice(0, pathLen) + '...';
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
|
|
341
|
+
return url.slice(0, maxLen - 3) + '...';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function truncateValue(value, maxLen = 100) {
|
|
345
|
+
const str = String(value);
|
|
346
|
+
if (str.length <= maxLen) return str;
|
|
347
|
+
return str.slice(0, maxLen - 3) + '...';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function pickHeaders(headers, keys) {
|
|
351
|
+
const result = {};
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
const lowerKey = key.toLowerCase();
|
|
354
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
355
|
+
if (k.toLowerCase() === lowerKey) {
|
|
356
|
+
result[k] = v;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatBody(body, maxLen = 2048) {
|
|
365
|
+
if (!body) return ' (empty)';
|
|
366
|
+
|
|
367
|
+
let str = typeof body === 'string' ? body : JSON.stringify(body);
|
|
368
|
+
|
|
369
|
+
// Try to pretty-print JSON
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(str);
|
|
372
|
+
str = JSON.stringify(parsed, null, 2);
|
|
373
|
+
} catch {}
|
|
374
|
+
|
|
375
|
+
// Indent and truncate
|
|
376
|
+
const lines = str.split('\n');
|
|
377
|
+
const truncated = lines.slice(0, 50).map(l => ' ' + l);
|
|
378
|
+
|
|
379
|
+
if (str.length > maxLen) {
|
|
380
|
+
truncated.push(` ... (${formatSize(str.length)} total, truncated)`);
|
|
381
|
+
} else if (lines.length > 50) {
|
|
382
|
+
truncated.push(` ... (${lines.length - 50} more lines)`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return truncated.join('\n');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
formatCompact,
|
|
390
|
+
formatUrls,
|
|
391
|
+
formatCurl,
|
|
392
|
+
formatCurlBatch,
|
|
393
|
+
formatRaw,
|
|
394
|
+
formatVerbose,
|
|
395
|
+
formatEntry,
|
|
396
|
+
formatOrigins,
|
|
397
|
+
formatStats,
|
|
398
|
+
formatSize,
|
|
399
|
+
formatDuration,
|
|
400
|
+
formatTimestamp,
|
|
401
|
+
getContentTypeShort
|
|
402
|
+
};
|