standout 0.1.0 → 0.2.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.
@@ -0,0 +1,267 @@
1
+ import { chromium } from "playwright-core";
2
+ import { existsSync, readdirSync, statSync, mkdtempSync, mkdirSync, copyFileSync, rmSync, readFileSync, } from "fs";
3
+ import { homedir, platform, tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { spawn } from "child_process";
6
+ const DEBUG_PORT_WAIT_MS = 10000;
7
+ const NAV_TIMEOUT_MS = 20000;
8
+ const TOTAL_TIMEOUT_MS = 40000;
9
+ function wait(ms) {
10
+ return new Promise((r) => setTimeout(r, ms));
11
+ }
12
+ function findChromeExecutable() {
13
+ const os = platform();
14
+ const candidates = os === "darwin"
15
+ ? [
16
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
17
+ "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
18
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
19
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
20
+ ]
21
+ : os === "linux"
22
+ ? [
23
+ "/usr/bin/google-chrome",
24
+ "/usr/bin/google-chrome-stable",
25
+ "/usr/bin/chromium",
26
+ "/usr/bin/chromium-browser",
27
+ "/snap/bin/chromium",
28
+ ]
29
+ : os === "win32"
30
+ ? [
31
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
32
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
33
+ ]
34
+ : [];
35
+ for (const path of candidates) {
36
+ if (existsSync(path))
37
+ return path;
38
+ }
39
+ return null;
40
+ }
41
+ function findChromeProfileRoot() {
42
+ const os = platform();
43
+ const home = homedir();
44
+ const candidates = os === "darwin"
45
+ ? [
46
+ join(home, "Library", "Application Support", "Google", "Chrome"),
47
+ join(home, "Library", "Application Support", "Chromium"),
48
+ ]
49
+ : os === "linux"
50
+ ? [
51
+ join(home, ".config", "google-chrome"),
52
+ join(home, ".config", "chromium"),
53
+ ]
54
+ : os === "win32"
55
+ ? [join(home, "AppData", "Local", "Google", "Chrome", "User Data")]
56
+ : [];
57
+ for (const path of candidates) {
58
+ if (existsSync(path) && existsSync(join(path, "Local State")))
59
+ return path;
60
+ }
61
+ return null;
62
+ }
63
+ function listProfilesByMtime(profileRoot) {
64
+ try {
65
+ return readdirSync(profileRoot)
66
+ .filter((d) => existsSync(join(profileRoot, d, "Cookies")))
67
+ .map((d) => ({
68
+ name: d,
69
+ mtime: statSync(join(profileRoot, d, "Cookies")).mtimeMs,
70
+ }))
71
+ .sort((a, b) => b.mtime - a.mtime)
72
+ .map((p) => p.name);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ function prepareProfileCopy(profileRoot, profileName) {
79
+ try {
80
+ const tempRoot = mkdtempSync(join(tmpdir(), "standout-chrome-"));
81
+ mkdirSync(join(tempRoot, "Default"), { recursive: true });
82
+ const localState = join(profileRoot, "Local State");
83
+ if (existsSync(localState)) {
84
+ copyFileSync(localState, join(tempRoot, "Local State"));
85
+ }
86
+ const sourceDir = join(profileRoot, profileName);
87
+ for (const name of ["Cookies", "Cookies-journal", "Preferences"]) {
88
+ const src = join(sourceDir, name);
89
+ if (existsSync(src)) {
90
+ copyFileSync(src, join(tempRoot, "Default", name));
91
+ }
92
+ }
93
+ return tempRoot;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function waitForDebugPort(userDataDir, timeoutMs) {
100
+ const deadline = Date.now() + timeoutMs;
101
+ const portFile = join(userDataDir, "DevToolsActivePort");
102
+ while (Date.now() < deadline) {
103
+ if (existsSync(portFile)) {
104
+ try {
105
+ const [port] = readFileSync(portFile, "utf-8").trim().split("\n");
106
+ if (port && /^\d+$/.test(port))
107
+ return parseInt(port, 10);
108
+ }
109
+ catch {
110
+ // keep polling
111
+ }
112
+ }
113
+ await wait(200);
114
+ }
115
+ return null;
116
+ }
117
+ async function spawnChrome(executablePath, userDataDir) {
118
+ const os = platform();
119
+ const args = [
120
+ "--headless=new",
121
+ `--user-data-dir=${userDataDir}`,
122
+ "--remote-debugging-port=0",
123
+ "--no-first-run",
124
+ "--no-default-browser-check",
125
+ "--disable-sync",
126
+ "--disable-features=AutofillServerCommunication",
127
+ ];
128
+ const cleanupFns = [];
129
+ if (os === "darwin") {
130
+ // Use `open -n -a` to force a new Chrome process instance on macOS
131
+ // (direct binary launch gets intercepted by the singleton running Chrome).
132
+ const appBundle = executablePath.replace(/\.app\/Contents\/MacOS\/.*$/, ".app");
133
+ const child = spawn("open", ["-n", "-a", appBundle, "--args", ...args], {
134
+ detached: true,
135
+ stdio: "ignore",
136
+ });
137
+ child.unref();
138
+ cleanupFns.push(() => {
139
+ try {
140
+ spawn("pkill", ["-f", `user-data-dir=${userDataDir}`]);
141
+ }
142
+ catch {
143
+ // skip
144
+ }
145
+ });
146
+ }
147
+ else {
148
+ const child = spawn(executablePath, args, {
149
+ detached: true,
150
+ stdio: "ignore",
151
+ });
152
+ child.unref();
153
+ cleanupFns.push(() => {
154
+ try {
155
+ if (child.pid)
156
+ process.kill(child.pid);
157
+ }
158
+ catch {
159
+ // skip
160
+ }
161
+ });
162
+ }
163
+ const port = await waitForDebugPort(userDataDir, DEBUG_PORT_WAIT_MS);
164
+ return {
165
+ port,
166
+ cleanup: () => cleanupFns.forEach((fn) => fn()),
167
+ };
168
+ }
169
+ async function extractHandleFromProfile(executablePath, profileRoot, profileName) {
170
+ const tempProfile = prepareProfileCopy(profileRoot, profileName);
171
+ if (!tempProfile)
172
+ return null;
173
+ const { port, cleanup } = await spawnChrome(executablePath, tempProfile);
174
+ let browser = null;
175
+ try {
176
+ if (!port)
177
+ return null;
178
+ browser = await chromium.connectOverCDP(`http://localhost:${port}`);
179
+ const contexts = browser.contexts();
180
+ const context = contexts[0] ?? (await browser.newContext());
181
+ const cookies = await context.cookies([
182
+ "https://x.com",
183
+ "https://twitter.com",
184
+ ]);
185
+ const authToken = cookies.find((c) => c.name === "auth_token");
186
+ if (!authToken)
187
+ return null;
188
+ const page = await context.newPage();
189
+ try {
190
+ await page.goto("https://x.com/home", {
191
+ waitUntil: "domcontentloaded",
192
+ timeout: NAV_TIMEOUT_MS,
193
+ });
194
+ const url = page.url();
195
+ if (/\/(login|i\/flow\/login)/.test(url))
196
+ return null;
197
+ try {
198
+ const link = page.locator('[data-testid="AppTabBar_Profile_Link"]');
199
+ await link.first().waitFor({ timeout: 5000 });
200
+ const href = await link.first().getAttribute("href");
201
+ if (href) {
202
+ const handle = href.replace(/^\//, "").trim();
203
+ if (handle && !handle.includes("/"))
204
+ return handle;
205
+ }
206
+ }
207
+ catch {
208
+ // fall through
209
+ }
210
+ try {
211
+ const nameElem = page.locator('[data-testid="UserName"] span:has-text("@")');
212
+ const text = await nameElem.first().innerText({ timeout: 3000 });
213
+ const match = text.match(/@([A-Za-z0-9_]+)/);
214
+ if (match)
215
+ return match[1];
216
+ }
217
+ catch {
218
+ // skip
219
+ }
220
+ return null;
221
+ }
222
+ finally {
223
+ await page.close().catch(() => undefined);
224
+ }
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ finally {
230
+ if (browser) {
231
+ await browser.close().catch(() => undefined);
232
+ }
233
+ cleanup();
234
+ await wait(500);
235
+ try {
236
+ rmSync(tempProfile, { recursive: true, force: true });
237
+ }
238
+ catch {
239
+ // skip
240
+ }
241
+ }
242
+ }
243
+ async function withTimeout(promise, ms) {
244
+ return Promise.race([
245
+ promise,
246
+ new Promise((resolve) => setTimeout(() => resolve(null), ms)),
247
+ ]);
248
+ }
249
+ export async function scrapeTwitterHandle() {
250
+ const executablePath = findChromeExecutable();
251
+ if (!executablePath)
252
+ return null;
253
+ const profileRoot = findChromeProfileRoot();
254
+ if (!profileRoot)
255
+ return null;
256
+ const profiles = listProfilesByMtime(profileRoot);
257
+ if (profiles.length === 0)
258
+ return null;
259
+ return withTimeout((async () => {
260
+ for (const profile of profiles) {
261
+ const handle = await extractHandleFromProfile(executablePath, profileRoot, profile).catch(() => null);
262
+ if (handle)
263
+ return handle;
264
+ }
265
+ return null;
266
+ })(), TOTAL_TIMEOUT_MS);
267
+ }
@@ -0,0 +1,6 @@
1
+ import type { WrappedAggregateView } from "./types.js";
2
+ export declare function aggregateView(args: {
3
+ profile: Record<string, unknown>;
4
+ computed: Record<string, unknown>;
5
+ share_url: string;
6
+ }): WrappedAggregateView;