glidercli 0.3.1 → 0.3.2
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/bin/glider.js +177 -0
- package/lib/bfavicon.js +252 -0
- package/lib/registry.json +73 -0
- package/package.json +1 -1
package/bin/glider.js
CHANGED
|
@@ -37,6 +37,15 @@ const DEBUG_URL = `http://127.0.0.1:${DEBUG_PORT}`;
|
|
|
37
37
|
const LIB_DIR = path.join(__dirname, '..', 'lib');
|
|
38
38
|
const STATE_FILE = '/tmp/glider-state.json';
|
|
39
39
|
const LOG_FILE = '/tmp/glider.log';
|
|
40
|
+
const REGISTRY_FILE = path.join(LIB_DIR, 'registry.json');
|
|
41
|
+
|
|
42
|
+
// Load pattern registry
|
|
43
|
+
let REGISTRY = {};
|
|
44
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
45
|
+
try {
|
|
46
|
+
REGISTRY = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
|
47
|
+
} catch (e) { /* ignore parse errors */ }
|
|
48
|
+
}
|
|
40
49
|
|
|
41
50
|
// Direct CDP module
|
|
42
51
|
const { DirectCDP, checkChrome } = require(path.join(LIB_DIR, 'cdp-direct.js'));
|
|
@@ -982,6 +991,164 @@ async function cmdExtract(opts = []) {
|
|
|
982
991
|
}
|
|
983
992
|
}
|
|
984
993
|
|
|
994
|
+
// Registry pattern execution - bulletproof extraction using predefined patterns
|
|
995
|
+
async function cmdRegistry(patternName, opts = []) {
|
|
996
|
+
if (!patternName) {
|
|
997
|
+
// List all patterns
|
|
998
|
+
const patterns = Object.keys(REGISTRY);
|
|
999
|
+
if (patterns.length === 0) {
|
|
1000
|
+
log.warn('No patterns in registry');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
console.log(`${GREEN}${patterns.length}${NC} pattern(s) available:\n`);
|
|
1004
|
+
for (const name of patterns) {
|
|
1005
|
+
const p = REGISTRY[name];
|
|
1006
|
+
console.log(` ${CYAN}${name}${NC}`);
|
|
1007
|
+
console.log(` ${DIM}${p.description || 'No description'}${NC}`);
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const pattern = REGISTRY[patternName];
|
|
1013
|
+
if (!pattern) {
|
|
1014
|
+
log.fail(`Pattern not found: ${patternName}`);
|
|
1015
|
+
log.info('Run "glider registry" to see available patterns');
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Parse options - for favicon: glider favicon [output.webp]
|
|
1020
|
+
// The first arg that looks like a file path is output, anything else is URL
|
|
1021
|
+
let outputFile = null;
|
|
1022
|
+
let url = null;
|
|
1023
|
+
for (let i = 0; i < opts.length; i++) {
|
|
1024
|
+
const arg = opts[i];
|
|
1025
|
+
if (arg === '--output' || arg === '-o') {
|
|
1026
|
+
outputFile = opts[++i];
|
|
1027
|
+
} else if (arg.startsWith('-')) {
|
|
1028
|
+
// skip flags
|
|
1029
|
+
} else if (arg.includes('/') && !arg.startsWith('http') && (arg.endsWith('.webp') || arg.endsWith('.png') || arg.endsWith('.ico'))) {
|
|
1030
|
+
// Looks like a file path
|
|
1031
|
+
outputFile = arg;
|
|
1032
|
+
} else if (!url) {
|
|
1033
|
+
url = arg;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// If URL provided, navigate first
|
|
1038
|
+
if (url) {
|
|
1039
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
1040
|
+
log.info(`Navigating to: ${url}`);
|
|
1041
|
+
await cmdGoto(url);
|
|
1042
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
log.info(`Running pattern: ${patternName}`);
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
const result = await httpPost('/cdp', {
|
|
1049
|
+
method: 'Runtime.evaluate',
|
|
1050
|
+
params: {
|
|
1051
|
+
expression: pattern.pattern,
|
|
1052
|
+
returnByValue: true,
|
|
1053
|
+
awaitPromise: true,
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
let value = result?.result?.value;
|
|
1058
|
+
|
|
1059
|
+
if (value === undefined || value === null) {
|
|
1060
|
+
log.fail('Pattern returned no value');
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Handle postprocessing for favicon
|
|
1065
|
+
if (patternName === 'favicon' && pattern.postprocess) {
|
|
1066
|
+
const base64 = value;
|
|
1067
|
+
if (!base64 || base64.length < 50) {
|
|
1068
|
+
log.fail('No favicon data received');
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Determine output path
|
|
1073
|
+
if (!outputFile) {
|
|
1074
|
+
const currentUrl = await httpPost('/cdp', {
|
|
1075
|
+
method: 'Runtime.evaluate',
|
|
1076
|
+
params: { expression: 'window.location.hostname', returnByValue: true }
|
|
1077
|
+
});
|
|
1078
|
+
const hostname = currentUrl?.result?.value?.replace(/^www\./, '').split('.')[0] || 'favicon';
|
|
1079
|
+
outputFile = `/tmp/${hostname}-favicon.webp`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Save and convert
|
|
1083
|
+
const tempFile = `/tmp/favicon-temp-${Date.now()}`;
|
|
1084
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
1085
|
+
|
|
1086
|
+
// Detect if ICO
|
|
1087
|
+
const isIco = buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 1;
|
|
1088
|
+
const tempPath = isIco ? `${tempFile}.ico` : `${tempFile}.png`;
|
|
1089
|
+
fs.writeFileSync(tempPath, buffer);
|
|
1090
|
+
log.ok(`Downloaded: ${buffer.length} bytes`);
|
|
1091
|
+
|
|
1092
|
+
// Convert to webp
|
|
1093
|
+
if (outputFile.endsWith('.webp')) {
|
|
1094
|
+
try {
|
|
1095
|
+
let pngPath = tempPath;
|
|
1096
|
+
if (isIco) {
|
|
1097
|
+
pngPath = `${tempFile}.png`;
|
|
1098
|
+
execSync(`magick "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null || convert "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null`);
|
|
1099
|
+
}
|
|
1100
|
+
execSync(`cwebp "${pngPath}" -o "${outputFile}" -q 90 2>/dev/null`);
|
|
1101
|
+
log.ok(`Saved: ${outputFile}`);
|
|
1102
|
+
|
|
1103
|
+
// Cleanup
|
|
1104
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
1105
|
+
if (pngPath !== tempPath) try { fs.unlinkSync(pngPath); } catch {}
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
// Fallback - save as-is
|
|
1108
|
+
const fallback = outputFile.replace('.webp', isIco ? '.ico' : '.png');
|
|
1109
|
+
fs.copyFileSync(tempPath, fallback);
|
|
1110
|
+
log.warn(`Conversion failed, saved as: ${fallback}`);
|
|
1111
|
+
outputFile = fallback;
|
|
1112
|
+
}
|
|
1113
|
+
} else {
|
|
1114
|
+
fs.copyFileSync(tempPath, outputFile);
|
|
1115
|
+
log.ok(`Saved: ${outputFile}`);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Also copy to dist if in spoonfeeder project
|
|
1119
|
+
const distPath = outputFile.replace('/public/', '/dist/web/');
|
|
1120
|
+
if (distPath !== outputFile && fs.existsSync(outputFile)) {
|
|
1121
|
+
try {
|
|
1122
|
+
const distDir = path.dirname(distPath);
|
|
1123
|
+
if (fs.existsSync(distDir)) {
|
|
1124
|
+
fs.copyFileSync(outputFile, distPath);
|
|
1125
|
+
log.ok(`Copied to dist: ${distPath}`);
|
|
1126
|
+
}
|
|
1127
|
+
} catch {}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
console.log(outputFile);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Standard output
|
|
1135
|
+
if (outputFile) {
|
|
1136
|
+
const output = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
|
1137
|
+
fs.writeFileSync(outputFile, output);
|
|
1138
|
+
log.ok(`Saved to ${outputFile}`);
|
|
1139
|
+
} else {
|
|
1140
|
+
if (typeof value === 'object') {
|
|
1141
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1142
|
+
} else {
|
|
1143
|
+
console.log(value);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
log.fail(`Pattern failed: ${e.message}`);
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
985
1152
|
// Explore site (clicks around, captures network)
|
|
986
1153
|
async function cmdExplore(url, opts = []) {
|
|
987
1154
|
if (!url) {
|
|
@@ -1337,6 +1504,7 @@ ${B5}MULTI-TAB${NC}
|
|
|
1337
1504
|
${BW}spawn${NC} <urls...> Open multiple tabs
|
|
1338
1505
|
${BW}extract${NC} [opts] Extract from all tabs
|
|
1339
1506
|
${BW}explore${NC} <url> Crawl site, capture network
|
|
1507
|
+
${BW}favicon${NC} <url> [out] Extract favicon from site ${DIM}(webp)${NC}
|
|
1340
1508
|
|
|
1341
1509
|
${B5}AUTOMATION${NC}
|
|
1342
1510
|
${BW}run${NC} <task.yaml> Execute YAML task file
|
|
@@ -1516,6 +1684,15 @@ async function main() {
|
|
|
1516
1684
|
case 'explore':
|
|
1517
1685
|
await cmdExplore(args[1], args.slice(2));
|
|
1518
1686
|
break;
|
|
1687
|
+
case 'favicon':
|
|
1688
|
+
// Use registry pattern - bulletproof method
|
|
1689
|
+
await cmdRegistry('favicon', args.slice(1));
|
|
1690
|
+
break;
|
|
1691
|
+
case 'registry':
|
|
1692
|
+
case 'reg':
|
|
1693
|
+
// Run a registry pattern
|
|
1694
|
+
await cmdRegistry(args[1], args.slice(2));
|
|
1695
|
+
break;
|
|
1519
1696
|
case 'loop':
|
|
1520
1697
|
case 'ralph': // alias for loop - Ralph Wiggum pattern
|
|
1521
1698
|
// Parse loop options
|
package/lib/bfavicon.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bfavicon.js - Bulletproof favicon extractor via CDP
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ./bfavicon.js <url> [output.webp]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
14
|
+
|
|
15
|
+
const GREEN = '\x1b[32m';
|
|
16
|
+
const RED = '\x1b[31m';
|
|
17
|
+
const CYAN = '\x1b[36m';
|
|
18
|
+
const YELLOW = '\x1b[33m';
|
|
19
|
+
const NC = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
const log = {
|
|
22
|
+
ok: (msg) => console.error(`${GREEN}✓${NC} ${msg}`),
|
|
23
|
+
fail: (msg) => console.error(`${RED}✗${NC} ${msg}`),
|
|
24
|
+
info: (msg) => console.error(`${CYAN}→${NC} ${msg}`),
|
|
25
|
+
warn: (msg) => console.error(`${YELLOW}⚠${NC} ${msg}`),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class FaviconExtractor {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.ws = null;
|
|
31
|
+
this.messageId = 0;
|
|
32
|
+
this.pending = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async connect() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
38
|
+
this.ws.on('open', () => resolve());
|
|
39
|
+
this.ws.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
40
|
+
this.ws.on('message', (data) => {
|
|
41
|
+
const msg = JSON.parse(data.toString());
|
|
42
|
+
if (msg.id !== undefined) {
|
|
43
|
+
const pending = this.pending.get(msg.id);
|
|
44
|
+
if (pending) {
|
|
45
|
+
this.pending.delete(msg.id);
|
|
46
|
+
msg.error ? pending.reject(new Error(msg.error.message)) : pending.resolve(msg.result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async send(method, params = {}) {
|
|
54
|
+
const id = ++this.messageId;
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
this.pending.set(id, { resolve, reject });
|
|
57
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
if (this.pending.has(id)) {
|
|
60
|
+
this.pending.delete(id);
|
|
61
|
+
reject(new Error('Timeout'));
|
|
62
|
+
}
|
|
63
|
+
}, 30000);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async navigate(url) {
|
|
68
|
+
await this.send('Page.navigate', { url });
|
|
69
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async extractFavicon() {
|
|
73
|
+
// Bulletproof extraction - try everything, return first success
|
|
74
|
+
const script = `
|
|
75
|
+
(async () => {
|
|
76
|
+
const origin = window.location.origin;
|
|
77
|
+
const results = [];
|
|
78
|
+
|
|
79
|
+
// Build list of ALL possible favicon URLs
|
|
80
|
+
const urls = new Set();
|
|
81
|
+
|
|
82
|
+
// 1. From link tags
|
|
83
|
+
document.querySelectorAll('link[rel*="icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"], link[rel="mask-icon"]')
|
|
84
|
+
.forEach(l => l.href && urls.add(l.href));
|
|
85
|
+
|
|
86
|
+
// 2. From meta tags
|
|
87
|
+
document.querySelectorAll('meta[property="og:image"], meta[name="msapplication-TileImage"]')
|
|
88
|
+
.forEach(m => m.content && urls.add(m.content));
|
|
89
|
+
|
|
90
|
+
// 3. Default locations - try ALL common paths
|
|
91
|
+
const defaults = [
|
|
92
|
+
'/favicon.ico', '/favicon.png', '/favicon.svg', '/favicon.webp',
|
|
93
|
+
'/apple-touch-icon.png', '/apple-touch-icon-precomposed.png',
|
|
94
|
+
'/apple-touch-icon-180x180.png', '/apple-touch-icon-152x152.png',
|
|
95
|
+
'/icon.png', '/icon.ico', '/logo.png', '/logo.ico',
|
|
96
|
+
'/images/favicon.ico', '/images/favicon.png',
|
|
97
|
+
'/assets/favicon.ico', '/assets/favicon.png',
|
|
98
|
+
'/static/favicon.ico', '/static/favicon.png',
|
|
99
|
+
];
|
|
100
|
+
defaults.forEach(p => urls.add(origin + p));
|
|
101
|
+
|
|
102
|
+
// 4. Check manifest
|
|
103
|
+
const manifest = document.querySelector('link[rel="manifest"]');
|
|
104
|
+
if (manifest?.href) {
|
|
105
|
+
try {
|
|
106
|
+
const m = await fetch(manifest.href).then(r => r.json());
|
|
107
|
+
(m.icons || []).forEach(i => urls.add(new URL(i.src, manifest.href).href));
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 5. Look for any img with icon/logo/favicon in src
|
|
112
|
+
document.querySelectorAll('img[src*="icon"], img[src*="logo"], img[src*="favicon"]')
|
|
113
|
+
.forEach(img => img.src && urls.add(img.src));
|
|
114
|
+
|
|
115
|
+
// Try each URL - fetch directly, no HEAD check
|
|
116
|
+
for (const url of urls) {
|
|
117
|
+
try {
|
|
118
|
+
const resp = await fetch(url, { mode: 'cors', credentials: 'include' });
|
|
119
|
+
if (!resp.ok) continue;
|
|
120
|
+
|
|
121
|
+
const blob = await resp.blob();
|
|
122
|
+
if (blob.size < 50) continue; // Skip tiny/empty
|
|
123
|
+
|
|
124
|
+
// Convert to base64
|
|
125
|
+
const base64 = await new Promise((resolve, reject) => {
|
|
126
|
+
const reader = new FileReader();
|
|
127
|
+
reader.onload = () => resolve(reader.result.split(',')[1]);
|
|
128
|
+
reader.onerror = reject;
|
|
129
|
+
reader.readAsDataURL(blob);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { url, base64, size: blob.size, type: blob.type };
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// Silent fail, try next
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
})()
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const result = await this.send('Runtime.evaluate', {
|
|
143
|
+
expression: script,
|
|
144
|
+
awaitPromise: true,
|
|
145
|
+
returnByValue: true
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return result?.value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
close() {
|
|
152
|
+
if (this.ws) this.ws.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function main() {
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
|
|
159
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
160
|
+
console.log(`Usage: glider favicon <url> [output.webp]`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let url = args[0];
|
|
165
|
+
let outputPath = args[1];
|
|
166
|
+
|
|
167
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
168
|
+
|
|
169
|
+
log.info(`Extracting favicon from: ${url}`);
|
|
170
|
+
|
|
171
|
+
const extractor = new FaviconExtractor();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await extractor.connect();
|
|
175
|
+
log.ok('Connected to relay');
|
|
176
|
+
|
|
177
|
+
await extractor.navigate(url);
|
|
178
|
+
log.ok('Page loaded');
|
|
179
|
+
|
|
180
|
+
const favicon = await extractor.extractFavicon();
|
|
181
|
+
|
|
182
|
+
if (!favicon) {
|
|
183
|
+
log.fail('No favicon found');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
log.ok(`Found: ${favicon.url} (${favicon.size} bytes, ${favicon.type})`);
|
|
188
|
+
|
|
189
|
+
// Determine output path
|
|
190
|
+
if (!outputPath) {
|
|
191
|
+
const hostname = new URL(url).hostname.replace(/^www\./, '').split('.')[0];
|
|
192
|
+
outputPath = `/tmp/${hostname}-favicon.png`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Save the file
|
|
196
|
+
const buffer = Buffer.from(favicon.base64, 'base64');
|
|
197
|
+
const tempFile = `/tmp/favicon-temp-${Date.now()}`;
|
|
198
|
+
|
|
199
|
+
// Detect format and save appropriately
|
|
200
|
+
const isIco = favicon.type.includes('icon') || favicon.url.endsWith('.ico');
|
|
201
|
+
const tempPath = isIco ? `${tempFile}.ico` : `${tempFile}.png`;
|
|
202
|
+
fs.writeFileSync(tempPath, buffer);
|
|
203
|
+
|
|
204
|
+
// Convert to webp if requested
|
|
205
|
+
if (outputPath.endsWith('.webp')) {
|
|
206
|
+
try {
|
|
207
|
+
// If ico, convert to png first
|
|
208
|
+
let pngPath = tempPath;
|
|
209
|
+
if (isIco) {
|
|
210
|
+
pngPath = `${tempFile}.png`;
|
|
211
|
+
execSync(`magick "${tempPath}" -resize 32x32 "${pngPath}" 2>/dev/null || convert "${tempPath}" -resize 32x32 "${pngPath}" 2>/dev/null`);
|
|
212
|
+
}
|
|
213
|
+
execSync(`cwebp "${pngPath}" -o "${outputPath}" -q 90 2>/dev/null`);
|
|
214
|
+
log.ok(`Saved as WebP: ${outputPath}`);
|
|
215
|
+
// Cleanup
|
|
216
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
217
|
+
if (pngPath !== tempPath) try { fs.unlinkSync(pngPath); } catch {}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Fallback - just save as-is
|
|
220
|
+
const fallbackPath = outputPath.replace('.webp', isIco ? '.ico' : '.png');
|
|
221
|
+
fs.copyFileSync(tempPath, fallbackPath);
|
|
222
|
+
log.warn(`Conversion failed, saved as: ${fallbackPath}`);
|
|
223
|
+
outputPath = fallbackPath;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
fs.copyFileSync(tempPath, outputPath);
|
|
227
|
+
log.ok(`Saved: ${outputPath}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Copy to dist too
|
|
231
|
+
const distPath = outputPath.replace('/public/', '/dist/web/');
|
|
232
|
+
if (distPath !== outputPath && fs.existsSync(outputPath)) {
|
|
233
|
+
try {
|
|
234
|
+
const distDir = require('path').dirname(distPath);
|
|
235
|
+
if (fs.existsSync(distDir)) {
|
|
236
|
+
fs.copyFileSync(outputPath, distPath);
|
|
237
|
+
log.ok(`Copied to dist: ${distPath}`);
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(outputPath);
|
|
243
|
+
|
|
244
|
+
} catch (e) {
|
|
245
|
+
log.fail(e.message);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
} finally {
|
|
248
|
+
extractor.close();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"favicon": {
|
|
3
|
+
"description": "Extract favicon from current page using browser session",
|
|
4
|
+
"pattern": "(async () => { const resp = await fetch(document.querySelector('link[rel*=icon]')?.href || window.location.origin + '/favicon.ico', { credentials: 'include' }); const blob = await resp.blob(); const reader = new FileReader(); return await new Promise(resolve => { reader.onload = () => resolve(reader.result.split(',')[1]); reader.readAsDataURL(blob); }); })()",
|
|
5
|
+
"output": "base64",
|
|
6
|
+
"postprocess": ["base64-decode", "convert-webp"]
|
|
7
|
+
},
|
|
8
|
+
"favicon-all": {
|
|
9
|
+
"description": "Get all favicon URLs from current page",
|
|
10
|
+
"pattern": "(() => { const urls = []; document.querySelectorAll('link[rel*=icon]').forEach(l => urls.push(l.href)); if (urls.length === 0) urls.push(window.location.origin + '/favicon.ico'); return urls; })()",
|
|
11
|
+
"output": "json"
|
|
12
|
+
},
|
|
13
|
+
"title": {
|
|
14
|
+
"description": "Get page title",
|
|
15
|
+
"pattern": "document.title",
|
|
16
|
+
"output": "string"
|
|
17
|
+
},
|
|
18
|
+
"url": {
|
|
19
|
+
"description": "Get current URL",
|
|
20
|
+
"pattern": "window.location.href",
|
|
21
|
+
"output": "string"
|
|
22
|
+
},
|
|
23
|
+
"links": {
|
|
24
|
+
"description": "Get all links on page",
|
|
25
|
+
"pattern": "Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim(), href: a.href })).filter(l => l.href.startsWith('http'))",
|
|
26
|
+
"output": "json"
|
|
27
|
+
},
|
|
28
|
+
"images": {
|
|
29
|
+
"description": "Get all image URLs",
|
|
30
|
+
"pattern": "Array.from(document.querySelectorAll('img[src]')).map(i => i.src)",
|
|
31
|
+
"output": "json"
|
|
32
|
+
},
|
|
33
|
+
"text": {
|
|
34
|
+
"description": "Get page text content",
|
|
35
|
+
"pattern": "document.body.innerText",
|
|
36
|
+
"output": "string"
|
|
37
|
+
},
|
|
38
|
+
"meta": {
|
|
39
|
+
"description": "Get all meta tags",
|
|
40
|
+
"pattern": "Array.from(document.querySelectorAll('meta')).map(m => ({ name: m.name || m.property, content: m.content })).filter(m => m.content)",
|
|
41
|
+
"output": "json"
|
|
42
|
+
},
|
|
43
|
+
"og": {
|
|
44
|
+
"description": "Get Open Graph metadata",
|
|
45
|
+
"pattern": "(() => { const og = {}; document.querySelectorAll('meta[property^=\"og:\"]').forEach(m => og[m.property.replace('og:','')] = m.content); return og; })()",
|
|
46
|
+
"output": "json"
|
|
47
|
+
},
|
|
48
|
+
"forms": {
|
|
49
|
+
"description": "Get all forms and their inputs",
|
|
50
|
+
"pattern": "Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,select,textarea')).map(i => ({ name: i.name, type: i.type, id: i.id })) }))",
|
|
51
|
+
"output": "json"
|
|
52
|
+
},
|
|
53
|
+
"cookies": {
|
|
54
|
+
"description": "Get all cookies",
|
|
55
|
+
"pattern": "document.cookie.split(';').map(c => c.trim()).filter(c => c).map(c => { const [k,...v] = c.split('='); return { name: k, value: v.join('=') }; })",
|
|
56
|
+
"output": "json"
|
|
57
|
+
},
|
|
58
|
+
"storage": {
|
|
59
|
+
"description": "Get localStorage keys",
|
|
60
|
+
"pattern": "Object.keys(localStorage)",
|
|
61
|
+
"output": "json"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"description": "Get all script sources",
|
|
65
|
+
"pattern": "Array.from(document.querySelectorAll('script[src]')).map(s => s.src)",
|
|
66
|
+
"output": "json"
|
|
67
|
+
},
|
|
68
|
+
"styles": {
|
|
69
|
+
"description": "Get all stylesheet links",
|
|
70
|
+
"pattern": "Array.from(document.querySelectorAll('link[rel=stylesheet]')).map(l => l.href)",
|
|
71
|
+
"output": "json"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
CHANGED