@1dolinski/fastforms 0.3.0 → 0.4.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/lib/cdp.js +193 -0
- package/lib/personas.js +4 -9
- package/package.json +3 -3
package/lib/cdp.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
4
|
+
const SETTLE_MS = 1000;
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// CDP session — thin wrapper around a single WebSocket to a Chrome target
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
class CDPSession {
|
|
11
|
+
constructor(ws) {
|
|
12
|
+
this._ws = ws;
|
|
13
|
+
this._nextId = 0;
|
|
14
|
+
this._pending = new Map();
|
|
15
|
+
this._listeners = new Map();
|
|
16
|
+
|
|
17
|
+
ws.on("message", (raw) => {
|
|
18
|
+
const msg = JSON.parse(raw);
|
|
19
|
+
if ("id" in msg) {
|
|
20
|
+
const cb = this._pending.get(msg.id);
|
|
21
|
+
if (cb) {
|
|
22
|
+
this._pending.delete(msg.id);
|
|
23
|
+
msg.error ? cb.reject(new Error(msg.error.message)) : cb.resolve(msg.result);
|
|
24
|
+
}
|
|
25
|
+
} else if (msg.method) {
|
|
26
|
+
const fns = this._listeners.get(msg.method);
|
|
27
|
+
if (fns) for (const fn of fns) fn(msg.params);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
ws.on("close", () => {
|
|
32
|
+
for (const { reject } of this._pending.values()) reject(new Error("WebSocket closed"));
|
|
33
|
+
this._pending.clear();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
send(method, params = {}) {
|
|
38
|
+
const id = ++this._nextId;
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
this._pending.set(id, { resolve, reject });
|
|
41
|
+
this._ws.send(JSON.stringify({ id, method, params }));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
on(event, fn) {
|
|
46
|
+
if (!this._listeners.has(event)) this._listeners.set(event, []);
|
|
47
|
+
this._listeners.get(event).push(fn);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
off(event, fn) {
|
|
51
|
+
const fns = this._listeners.get(event);
|
|
52
|
+
if (!fns) return;
|
|
53
|
+
const idx = fns.indexOf(fn);
|
|
54
|
+
if (idx !== -1) fns.splice(idx, 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close() {
|
|
58
|
+
this._ws.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function openSession(wsUrl) {
|
|
63
|
+
const ws = new WebSocket(wsUrl);
|
|
64
|
+
await new Promise((resolve, reject) => {
|
|
65
|
+
ws.once("open", resolve);
|
|
66
|
+
ws.once("error", reject);
|
|
67
|
+
});
|
|
68
|
+
return new CDPSession(ws);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Page — mirrors the subset of puppeteer's Page API we actually use
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
class Page {
|
|
76
|
+
constructor(info, port) {
|
|
77
|
+
this._info = info;
|
|
78
|
+
this._port = port;
|
|
79
|
+
this._session = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
url() {
|
|
83
|
+
return this._info.url;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _ensureSession() {
|
|
87
|
+
if (!this._session) {
|
|
88
|
+
this._session = await openSession(this._info.webSocketDebuggerUrl);
|
|
89
|
+
}
|
|
90
|
+
return this._session;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Evaluate a function in the page context.
|
|
95
|
+
* Works the same as puppeteer's page.evaluate — pass a function + args,
|
|
96
|
+
* they get serialized and executed in the browser, result comes back.
|
|
97
|
+
*/
|
|
98
|
+
async evaluate(fn, ...args) {
|
|
99
|
+
const session = await this._ensureSession();
|
|
100
|
+
const expression = `(${fn.toString()})(${args.map((a) => JSON.stringify(a)).join(",")})`;
|
|
101
|
+
const { result, exceptionDetails } = await session.send("Runtime.evaluate", {
|
|
102
|
+
expression,
|
|
103
|
+
returnByValue: true,
|
|
104
|
+
awaitPromise: true,
|
|
105
|
+
});
|
|
106
|
+
if (exceptionDetails) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
exceptionDetails.text ||
|
|
109
|
+
exceptionDetails.exception?.description ||
|
|
110
|
+
"evaluate failed"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return result?.value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async goto(url, opts = {}) {
|
|
117
|
+
const session = await this._ensureSession();
|
|
118
|
+
await session.send("Page.enable");
|
|
119
|
+
|
|
120
|
+
const loaded = new Promise((resolve) => {
|
|
121
|
+
const timer = setTimeout(resolve, opts.timeout || DEFAULT_TIMEOUT);
|
|
122
|
+
const handler = () => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
session.off("Page.loadEventFired", handler);
|
|
125
|
+
setTimeout(resolve, SETTLE_MS);
|
|
126
|
+
};
|
|
127
|
+
session.on("Page.loadEventFired", handler);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await session.send("Page.navigate", { url });
|
|
131
|
+
await loaded;
|
|
132
|
+
this._info.url = url;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async bringToFront() {
|
|
136
|
+
const session = await this._ensureSession();
|
|
137
|
+
await session.send("Page.bringToFront");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async close() {
|
|
141
|
+
this._dispose();
|
|
142
|
+
try {
|
|
143
|
+
await fetch(`http://127.0.0.1:${this._port}/json/close/${this._info.id}`, { method: "PUT" });
|
|
144
|
+
} catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_dispose() {
|
|
148
|
+
if (this._session) {
|
|
149
|
+
this._session.close();
|
|
150
|
+
this._session = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// connectChrome — returns a browser-like object
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export async function connectChrome(port = 9222) {
|
|
160
|
+
const base = `http://127.0.0.1:${port}`;
|
|
161
|
+
|
|
162
|
+
const res = await fetch(`${base}/json/version`);
|
|
163
|
+
if (!res.ok) throw new Error(`Chrome not responding on port ${port}`);
|
|
164
|
+
|
|
165
|
+
const sessions = [];
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
async pages() {
|
|
169
|
+
const res = await fetch(`${base}/json/list`);
|
|
170
|
+
const targets = await res.json();
|
|
171
|
+
return targets
|
|
172
|
+
.filter((t) => t.type === "page")
|
|
173
|
+
.map((t) => {
|
|
174
|
+
const p = new Page(t, port);
|
|
175
|
+
sessions.push(p);
|
|
176
|
+
return p;
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async newPage() {
|
|
181
|
+
const res = await fetch(`${base}/json/new`, { method: "PUT" });
|
|
182
|
+
const target = await res.json();
|
|
183
|
+
const p = new Page(target, port);
|
|
184
|
+
sessions.push(p);
|
|
185
|
+
return p;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
disconnect() {
|
|
189
|
+
for (const p of sessions) p._dispose();
|
|
190
|
+
sessions.length = 0;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
package/lib/personas.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
2
|
import { join } from "path";
|
|
4
3
|
import { homedir } from "os";
|
|
5
4
|
import { createInterface } from "readline";
|
|
5
|
+
import { connectChrome } from "./cdp.js";
|
|
6
6
|
|
|
7
7
|
const FASTFORMS_URL = "https://293-fastforms.vercel.app/";
|
|
8
8
|
const PERSONA_URL = "https://293-fastforms.vercel.app/persona";
|
|
@@ -28,7 +28,7 @@ function ask(prompt) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export async function connectToChrome(port = 9222) {
|
|
31
|
-
return
|
|
31
|
+
return connectChrome(port);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function pullPersonas(browser) {
|
|
@@ -121,26 +121,23 @@ export function showPersonaDetails(user, biz, form) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// ---------------------------------------------------------------------------
|
|
124
|
-
// Interactive persona selection
|
|
124
|
+
// Interactive persona selection
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
async function pickFromList(label, list, hint, keys, defaultName) {
|
|
128
128
|
if (!list?.length) return null;
|
|
129
129
|
if (list.length === 1) return list[0];
|
|
130
130
|
|
|
131
|
-
// Try hint-based match
|
|
132
131
|
if (hint) {
|
|
133
132
|
const match = pickPersona(list, hint, keys);
|
|
134
133
|
if (match) return match;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
// Try default
|
|
138
136
|
if (defaultName) {
|
|
139
137
|
const match = list.find((p) => p.name === defaultName);
|
|
140
138
|
if (match) return match;
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
// Interactive selection
|
|
144
141
|
console.log(`\n ${label}:\n`);
|
|
145
142
|
list.forEach((p, i) => {
|
|
146
143
|
const detail = keys.map((k) => k.split(".").reduce((o, s) => o?.[s], p)).filter(Boolean)[0] || "";
|
|
@@ -168,7 +165,6 @@ export function matchFormByUrl(formPersonas, targetUrl) {
|
|
|
168
165
|
export async function selectFormPersona(formPersonas, targetUrl) {
|
|
169
166
|
if (!formPersonas?.length) return null;
|
|
170
167
|
|
|
171
|
-
// Auto-match by URL
|
|
172
168
|
const autoMatch = matchFormByUrl(formPersonas, targetUrl);
|
|
173
169
|
if (autoMatch) {
|
|
174
170
|
console.log(` Auto-matched form persona: ${autoMatch.name}`);
|
|
@@ -177,7 +173,6 @@ export async function selectFormPersona(formPersonas, targetUrl) {
|
|
|
177
173
|
|
|
178
174
|
if (formPersonas.length === 1) return formPersonas[0];
|
|
179
175
|
|
|
180
|
-
// Interactive
|
|
181
176
|
console.log(`\n Form personas:\n`);
|
|
182
177
|
formPersonas.forEach((f, i) => {
|
|
183
178
|
const org = f.profile?.organization || "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1dolinski/fastforms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Fill any form fast. Manage personas on the web, fill forms from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"
|
|
16
|
+
"ws": "^8.18.0"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=18"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"forms",
|
|
28
28
|
"automation",
|
|
29
29
|
"chrome",
|
|
30
|
-
"
|
|
30
|
+
"cdp",
|
|
31
31
|
"personas"
|
|
32
32
|
]
|
|
33
33
|
}
|