@apmantza/greedysearch-pi 1.9.0 → 1.9.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/launch.mjs CHANGED
@@ -1,417 +1,442 @@
1
- #!/usr/bin/env node
2
- // launch.mjs — start a dedicated Chrome instance for GreedySearch
3
- //
4
- // This Chrome instance uses --disable-features=DevToolsPrivacyUI which suppresses
5
- // the "Allow remote debugging?" dialog entirely. It runs on port 9222 so it doesn't
6
- // conflict with your main Chrome session (which may use port 9223).
7
- //
8
- // search.mjs passes CDP_PROFILE_DIR so cdp.mjs targets this dedicated Chrome
9
- // without ever touching the user's main Chrome DevToolsActivePort file.
10
- //
11
- // Usage:
12
- // node launch.mjs — launch (or report if already running)
13
- // node launch.mjs --headless — launch in headless mode (no GUI window)
14
- // node launch.mjs --kill — stop and restore original DevToolsActivePort
15
- // node launch.mjs --status — check if running
16
- //
17
- // Environment:
18
- // GREEDY_SEARCH_VISIBLE=1 — Show Chrome window (disables headless mode)
19
- // CHROME_PATH — Path to Chrome executable
20
-
21
- import { execSync, spawn } from "node:child_process";
22
- import {
23
- existsSync,
24
- mkdirSync,
25
- readFileSync,
26
- unlinkSync,
27
- writeFileSync,
28
- } from "node:fs";
29
- import http from "node:http";
30
- import { platform, tmpdir } from "node:os";
31
- import { join } from "node:path";
32
-
33
- const PORT = 9222;
34
- const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
35
- const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
36
- const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
37
- const MODE_FILE = join(tmpdir(), "greedysearch-chrome-mode");
38
-
39
- function findChrome() {
40
- const os = platform();
41
- const candidates =
42
- os === "win32"
43
- ? [
44
- "C:/Program Files/Google/Chrome/Application/chrome.exe",
45
- "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
46
- ]
47
- : os === "darwin"
48
- ? [
49
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
50
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
51
- ]
52
- : [
53
- "/usr/bin/google-chrome",
54
- "/usr/bin/google-chrome-stable",
55
- "/usr/bin/chromium-browser",
56
- "/usr/bin/chromium",
57
- "/snap/bin/chromium",
58
- ];
59
- return candidates.find(existsSync) || null;
60
- }
61
-
62
- const isHeadless = () => process.env.GREEDY_SEARCH_VISIBLE !== "1";
63
-
64
- const BASE_CHROME_FLAGS = [
65
- `--remote-debugging-port=${PORT}`,
66
- "--disable-features=DevToolsPrivacyUI",
67
- "--no-first-run",
68
- "--no-default-browser-check",
69
- "--disable-default-apps",
70
- // Anti-detection: suppress the AutomationControlled flag that exposes CDP usage.
71
- // Must be set for BOTH headless and visible — Cloudflare / DataDome detect it.
72
- "--disable-blink-features=AutomationControlled",
73
- `--user-data-dir=${PROFILE_DIR}`,
74
- "--profile-directory=Default",
75
- "--window-size=1920,1080",
76
- ];
77
-
78
- function buildChromeFlags() {
79
- const flags = [...BASE_CHROME_FLAGS];
80
- if (isHeadless()) {
81
- flags.push("--headless=new");
82
- flags.push("--disable-gpu");
83
- flags.push(
84
- "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
85
- );
86
- if (platform() === "win32") flags.push("--disable-software-rasterizer");
87
- }
88
- flags.push("about:blank");
89
- return flags;
90
- }
91
-
92
- const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
93
-
94
- /** Check if the running Chrome was launched headless from the mode marker file */
95
- function isModeFileHeadless() {
96
- try {
97
- if (!existsSync(MODE_FILE)) return true; // default: assume headless
98
- return readFileSync(MODE_FILE, "utf8").trim() === "headless";
99
- } catch {
100
- return true;
101
- }
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // CDP Window Minimization
106
- // ---------------------------------------------------------------------------
107
-
108
- async function minimizeViaCDP() {
109
- if (isVisible()) return;
110
-
111
- // Wait for Chrome to be ready
112
- await new Promise((r) => setTimeout(r, 1000));
113
-
114
- try {
115
- // Get browser WebSocket URL
116
- const version = await new Promise((resolve, reject) => {
117
- http
118
- .get(`http://localhost:${PORT}/json/version`, (res) => {
119
- let body = "";
120
- res.on("data", (d) => (body += d));
121
- res.on("end", () => resolve(JSON.parse(body)));
122
- })
123
- .on("error", reject);
124
- });
125
-
126
- const wsUrl = version.webSocketDebuggerUrl;
127
-
128
- const WebSocket = globalThis.WebSocket;
129
- if (!WebSocket) return;
130
-
131
- const ws = new WebSocket(wsUrl);
132
- let requestId = 0;
133
- const pending = new Map();
134
-
135
- ws.onopen = () => {
136
- // Step 1: Get targets
137
- const id = ++requestId;
138
- pending.set(id, {
139
- resolve: (result) => {
140
- const targets = result.targetInfos || [];
141
- const pageTarget = targets.find((t) => t.type === "page");
142
- if (!pageTarget) {
143
- ws.close();
144
- return;
145
- }
146
-
147
- // Step 2: Get windowId for target
148
- const winId = ++requestId;
149
- pending.set(winId, {
150
- resolve: (winResult) => {
151
- const windowId = winResult.windowId;
152
- // Step 3: Minimize window
153
- const minId = ++requestId;
154
- pending.set(minId, { resolve: () => {}, reject: () => {} });
155
- ws.send(
156
- JSON.stringify({
157
- id: minId,
158
- method: "Browser.setWindowBounds",
159
- params: { windowId, bounds: { windowState: "minimized" } },
160
- }),
161
- );
162
- setTimeout(() => ws.close(), 500);
163
- },
164
- reject: () => ws.close(),
165
- });
166
- ws.send(
167
- JSON.stringify({
168
- id: winId,
169
- method: "Browser.getWindowForTarget",
170
- params: { targetId: pageTarget.targetId },
171
- }),
172
- );
173
- },
174
- reject: () => ws.close(),
175
- });
176
- ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
177
- };
178
-
179
- ws.onmessage = (event) => {
180
- const msg = JSON.parse(event.data);
181
- if (msg.id && pending.has(msg.id)) {
182
- const { resolve, reject } = pending.get(msg.id);
183
- pending.delete(msg.id);
184
- if (msg.error) reject?.(msg.error);
185
- else resolve?.(msg.result);
186
- }
187
- };
188
-
189
- setTimeout(() => ws.close(), 5000);
190
- } catch {
191
- // Best-effort
192
- }
193
- }
194
-
195
- // ---------------------------------------------------------------------------
196
- // Chrome process management
197
- // ---------------------------------------------------------------------------
198
-
199
- function isRunning() {
200
- if (!existsSync(PID_FILE)) return false;
201
- const pid = Number.parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
202
- if (!pid) return false;
203
- try {
204
- process.kill(pid, 0);
205
- return pid;
206
- } catch {
207
- return false;
208
- }
209
- }
210
-
211
- function getPortPid(port) {
212
- try {
213
- const os = platform();
214
- if (os === "win32") {
215
- const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
216
- const regex = new RegExp(
217
- String.raw`TCP\s+[^\s]*:${port}\s+[^\s]*:0\s+LISTENING\s+(\d+)`,
218
- "i",
219
- );
220
- const match = out.match(regex);
221
- return match ? Number.parseInt(match[1], 10) : null;
222
- }
223
- const out = execSync(
224
- String.raw`lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\K\d+'`,
225
- {
226
- encoding: "utf8",
227
- },
228
- ).trim();
229
- return out ? Number.parseInt(out.split("\n")[0], 10) : null;
230
- } catch {
231
- return null;
232
- }
233
- }
234
-
235
- function killProcess(pid) {
236
- try {
237
- if (platform() === "win32") {
238
- execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" });
239
- } else {
240
- process.kill(pid, "SIGTERM");
241
- }
242
- return true;
243
- } catch {
244
- return false;
245
- }
246
- }
247
-
248
- function cleanupGhostChrome() {
249
- const portPid = getPortPid(PORT);
250
- if (!portPid) return;
251
-
252
- const trackedPid = isRunning();
253
- if (trackedPid && portPid === trackedPid) return;
254
-
255
- console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
256
- killProcess(portPid);
257
- try {
258
- unlinkSync(PID_FILE);
259
- } catch {}
260
- try {
261
- unlinkSync(ACTIVE_PORT);
262
- } catch {}
263
- try {
264
- unlinkSync(MODE_FILE);
265
- } catch {}
266
- }
267
-
268
- function httpGet(url, timeoutMs = 1000) {
269
- return new Promise((resolve) => {
270
- const req = http.get(url, (res) => {
271
- let body = "";
272
- res.on("data", (d) => (body += d));
273
- res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
274
- });
275
- req.on("error", () => resolve({ ok: false }));
276
- req.setTimeout(timeoutMs, () => {
277
- req.destroy();
278
- resolve({ ok: false });
279
- });
280
- });
281
- }
282
-
283
- async function writePortFile(timeoutMs = 15000) {
284
- const deadline = Date.now() + timeoutMs;
285
- while (Date.now() < deadline) {
286
- const { ok, body } = await httpGet(
287
- `http://localhost:${PORT}/json/version`,
288
- 1500,
289
- );
290
- if (ok) {
291
- try {
292
- const { webSocketDebuggerUrl } = JSON.parse(body);
293
- const wsPath = new URL(webSocketDebuggerUrl).pathname;
294
- writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
295
- return true;
296
- } catch {
297
- /* ignore */
298
- }
299
- }
300
- await new Promise((r) => setTimeout(r, 400));
301
- }
302
- return false;
303
- }
304
-
305
- // ---------------------------------------------------------------------------
306
- // Main
307
- // ---------------------------------------------------------------------------
308
-
309
- async function main() {
310
- const arg = process.argv[2];
311
-
312
- cleanupGhostChrome();
313
-
314
- if (arg === "--kill") {
315
- const pid = isRunning() || getPortPid(PORT);
316
- if (pid) {
317
- const ok = killProcess(pid);
318
- console.log(
319
- ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
320
- );
321
- } else {
322
- console.log("GreedySearch Chrome is not running.");
323
- }
324
- try {
325
- unlinkSync(PID_FILE);
326
- } catch {}
327
- try {
328
- unlinkSync(ACTIVE_PORT);
329
- } catch {}
330
- try {
331
- unlinkSync(MODE_FILE);
332
- } catch {}
333
- return;
334
- }
335
-
336
- if (arg === "--status") {
337
- const pid = isRunning();
338
- if (pid) {
339
- console.log(`Running pid ${pid}, port ${PORT}`);
340
- } else {
341
- console.log("Not running.");
342
- }
343
- return;
344
- }
345
-
346
- const existing = isRunning();
347
- if (existing) {
348
- // Mode check: if caller wants visible but Chrome is headless, kill and relaunch
349
- const isWantingVisible =
350
- process.env.GREEDY_SEARCH_VISIBLE === "1" &&
351
- !process.argv.includes("--headless");
352
- if (isWantingVisible && isModeFileHeadless()) {
353
- console.log(
354
- `Headless Chrome running (pid ${existing}) but visible requested — killing...`,
355
- );
356
- killProcess(existing);
357
- try {
358
- unlinkSync(PID_FILE);
359
- } catch {}
360
- try {
361
- unlinkSync(MODE_FILE);
362
- } catch {}
363
- // Fall through to fresh launch below
364
- } else {
365
- const ready = await writePortFile(5000);
366
- if (ready) {
367
- console.log(`GreedySearch Chrome already running (pid ${existing}).`);
368
- return;
369
- }
370
- console.log(`Stale PID ${existing} — launching fresh.`);
371
- try {
372
- unlinkSync(PID_FILE);
373
- } catch {}
374
- }
375
- }
376
-
377
- const CHROME_EXE = process.env.CHROME_PATH || findChrome();
378
- if (!CHROME_EXE) {
379
- console.error("Chrome not found. Set CHROME_PATH env var.");
380
- process.exit(1);
381
- }
382
-
383
- mkdirSync(PROFILE_DIR, { recursive: true });
384
-
385
- console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
386
- if (isHeadless()) {
387
- console.log("Headless mode — no window will be shown");
388
- } else if (!isVisible()) {
389
- console.log("Window will be minimized");
390
- }
391
-
392
- const proc = spawn(CHROME_EXE, buildChromeFlags(), {
393
- detached: true,
394
- stdio: "ignore",
395
- });
396
- proc.unref();
397
- writeFileSync(PID_FILE, String(proc.pid));
398
- // Write mode marker so ensureChrome() can detect headless vs visible
399
- writeFileSync(MODE_FILE, isHeadless() ? "headless" : "visible", "utf8");
400
-
401
- const portFileReady = await writePortFile();
402
- if (!portFileReady) {
403
- console.error("Chrome did not become ready within 15s.");
404
- process.exit(1);
405
- }
406
-
407
- if (isHeadless()) {
408
- // No window to minimize in headless mode
409
- console.log("Ready (headless).");
410
- } else {
411
- // Minimize window via CDP
412
- await minimizeViaCDP();
413
- console.log("Ready.");
414
- }
415
- }
416
-
417
- main();
1
+ #!/usr/bin/env node
2
+ // launch.mjs — start a dedicated Chrome instance for GreedySearch
3
+ //
4
+ // This Chrome instance uses --disable-features=DevToolsPrivacyUI which suppresses
5
+ // the "Allow remote debugging?" dialog entirely. It runs on port 9222 so it doesn't
6
+ // conflict with your main Chrome session (which may use port 9223).
7
+ //
8
+ // search.mjs passes CDP_PROFILE_DIR so cdp.mjs targets this dedicated Chrome
9
+ // without ever touching the user's main Chrome DevToolsActivePort file.
10
+ //
11
+ // Usage:
12
+ // node launch.mjs — launch (or report if already running)
13
+ // node launch.mjs --headless — launch in headless mode (no GUI window)
14
+ // node launch.mjs --kill — stop and restore original DevToolsActivePort
15
+ // node launch.mjs --status — check if running
16
+ //
17
+ // Environment:
18
+ // GREEDY_SEARCH_VISIBLE=1 — Show Chrome window (disables headless mode)
19
+ // CHROME_PATH — Path to Chrome executable
20
+
21
+ import { execSync, spawn } from "node:child_process";
22
+ import {
23
+ existsSync,
24
+ mkdirSync,
25
+ readdirSync,
26
+ readFileSync,
27
+ unlinkSync,
28
+ writeFileSync,
29
+ } from "node:fs";
30
+ import http from "node:http";
31
+ import { platform, tmpdir } from "node:os";
32
+ import { join } from "node:path";
33
+
34
+ const PORT = 9222;
35
+ const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
36
+ const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
37
+ const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
38
+ const MODE_FILE = join(tmpdir(), "greedysearch-chrome-mode");
39
+
40
+ function findChrome() {
41
+ const os = platform();
42
+ const candidates =
43
+ os === "win32"
44
+ ? [
45
+ "C:/Program Files/Google/Chrome/Application/chrome.exe",
46
+ "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
47
+ ]
48
+ : os === "darwin"
49
+ ? [
50
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
51
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
52
+ ]
53
+ : [
54
+ "/usr/bin/google-chrome",
55
+ "/usr/bin/google-chrome-stable",
56
+ "/usr/bin/chromium-browser",
57
+ "/usr/bin/chromium",
58
+ "/snap/bin/chromium",
59
+ ];
60
+ return candidates.find(existsSync) || null;
61
+ }
62
+
63
+ const isHeadless = () => process.env.GREEDY_SEARCH_VISIBLE !== "1";
64
+
65
+ const BASE_CHROME_FLAGS = [
66
+ `--remote-debugging-port=${PORT}`,
67
+ "--disable-features=DevToolsPrivacyUI",
68
+ "--no-first-run",
69
+ "--no-default-browser-check",
70
+ "--disable-default-apps",
71
+ // Anti-detection: suppress the AutomationControlled flag that exposes CDP usage.
72
+ // Must be set for BOTH headless and visible — Cloudflare / DataDome detect it.
73
+ "--disable-blink-features=AutomationControlled",
74
+ `--user-data-dir=${PROFILE_DIR}`,
75
+ "--profile-directory=Default",
76
+ "--window-size=1920,1080",
77
+ "--lang=en-US",
78
+ "--force-color-profile=srgb",
79
+ ];
80
+
81
+ function getChromeVersion(chromePath) {
82
+ // Primary: versioned sub-directory inside the Chrome Application folder.
83
+ // Chrome always creates one (e.g. "148.0.7778.168") — works on all platforms,
84
+ // avoids launching the GUI process just to read a version string.
85
+ try {
86
+ const appDir = join(chromePath, "..");
87
+ const entries = readdirSync(appDir);
88
+ const ver = entries.find((e) =>
89
+ /^\d{1,10}\.\d{1,10}\.\d{1,10}\.\d{1,10}$/.test(e),
90
+ );
91
+ if (ver) return ver.split(".")[0];
92
+ } catch {}
93
+
94
+ // Fallback: `chrome --version` works on macOS/Linux where Chrome is a CLI process.
95
+ try {
96
+ const out = execSync(`"${chromePath}" --version`, {
97
+ encoding: "utf8",
98
+ timeout: 5000,
99
+ }).trim();
100
+ const m = out.match(/(\d{1,10})\.\d{1,10}\.\d{1,10}/);
101
+ if (m) return m[1];
102
+ } catch {}
103
+
104
+ return null;
105
+ }
106
+
107
+ function buildChromeFlags(chromePath) {
108
+ const flags = [...BASE_CHROME_FLAGS];
109
+ if (isHeadless()) {
110
+ flags.push("--headless=new");
111
+ const major = getChromeVersion(chromePath) || "136";
112
+ flags.push(
113
+ `--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`,
114
+ );
115
+ }
116
+ flags.push("about:blank");
117
+ return flags;
118
+ }
119
+
120
+ const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
121
+
122
+ /** Check if the running Chrome was launched headless from the mode marker file */
123
+ function isModeFileHeadless() {
124
+ try {
125
+ if (!existsSync(MODE_FILE)) return true; // default: assume headless
126
+ return readFileSync(MODE_FILE, "utf8").trim() === "headless";
127
+ } catch {
128
+ return true;
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // CDP Window Minimization
134
+ // ---------------------------------------------------------------------------
135
+
136
+ async function minimizeViaCDP() {
137
+ if (isHeadless()) return;
138
+
139
+ try {
140
+ // Get browser WebSocket URL
141
+ const version = await new Promise((resolve, reject) => {
142
+ http
143
+ .get(`http://localhost:${PORT}/json/version`, (res) => {
144
+ let body = "";
145
+ res.on("data", (d) => (body += d));
146
+ res.on("end", () => resolve(JSON.parse(body)));
147
+ })
148
+ .on("error", reject);
149
+ });
150
+
151
+ const wsPath = new URL(version.webSocketDebuggerUrl).pathname;
152
+
153
+ const WebSocket = globalThis.WebSocket;
154
+ if (!WebSocket) return;
155
+
156
+ const ws = new WebSocket(`ws://localhost:${PORT}${wsPath}`);
157
+ let requestId = 0;
158
+ const pending = new Map();
159
+
160
+ ws.onopen = () => {
161
+ // Step 1: Get targets
162
+ const id = ++requestId;
163
+ pending.set(id, {
164
+ resolve: (result) => {
165
+ const targets = result.targetInfos || [];
166
+ const pageTarget = targets.find((t) => t.type === "page");
167
+ if (!pageTarget) {
168
+ ws.close();
169
+ return;
170
+ }
171
+
172
+ // Step 2: Get windowId for target
173
+ const winId = ++requestId;
174
+ pending.set(winId, {
175
+ resolve: (winResult) => {
176
+ const windowId = winResult.windowId;
177
+ // Step 3: Minimize window
178
+ const minId = ++requestId;
179
+ pending.set(minId, { resolve: () => {}, reject: () => {} });
180
+ ws.send(
181
+ JSON.stringify({
182
+ id: minId,
183
+ method: "Browser.setWindowBounds",
184
+ params: { windowId, bounds: { windowState: "minimized" } },
185
+ }),
186
+ );
187
+ setTimeout(() => ws.close(), 500);
188
+ },
189
+ reject: () => ws.close(),
190
+ });
191
+ ws.send(
192
+ JSON.stringify({
193
+ id: winId,
194
+ method: "Browser.getWindowForTarget",
195
+ params: { targetId: pageTarget.targetId },
196
+ }),
197
+ );
198
+ },
199
+ reject: () => ws.close(),
200
+ });
201
+ ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
202
+ };
203
+
204
+ ws.onmessage = (event) => {
205
+ const msg = JSON.parse(event.data);
206
+ if (msg.id && pending.has(msg.id)) {
207
+ const { resolve, reject } = pending.get(msg.id);
208
+ pending.delete(msg.id);
209
+ if (msg.error) reject?.(msg.error);
210
+ else resolve?.(msg.result);
211
+ }
212
+ };
213
+
214
+ setTimeout(() => ws.close(), 5000);
215
+ } catch {
216
+ // Best-effort
217
+ }
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Chrome process management
222
+ // ---------------------------------------------------------------------------
223
+
224
+ function isRunning() {
225
+ if (!existsSync(PID_FILE)) return false;
226
+ const pid = Number.parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
227
+ if (!pid) return false;
228
+ try {
229
+ process.kill(pid, 0);
230
+ return pid;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ function getPortPid(port) {
237
+ try {
238
+ const os = platform();
239
+ if (os === "win32") {
240
+ const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
241
+ const regex = new RegExp(
242
+ String.raw`TCP\s+[^\s]*:${port}\s+[^\s]*:0\s+LISTENING\s+(\d+)`,
243
+ "i",
244
+ );
245
+ const match = out.match(regex);
246
+ return match ? Number.parseInt(match[1], 10) : null;
247
+ }
248
+ const out = execSync(
249
+ String.raw`lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\K\d+'`,
250
+ {
251
+ encoding: "utf8",
252
+ },
253
+ ).trim();
254
+ return out ? Number.parseInt(out.split("\n")[0], 10) : null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+
260
+ function killProcess(pid) {
261
+ try {
262
+ if (platform() === "win32") {
263
+ execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" });
264
+ } else {
265
+ process.kill(pid, "SIGTERM");
266
+ }
267
+ return true;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
273
+ function cleanupGhostChrome() {
274
+ const portPid = getPortPid(PORT);
275
+ if (!portPid) return;
276
+
277
+ const trackedPid = isRunning();
278
+ if (trackedPid && portPid === trackedPid) return;
279
+
280
+ console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
281
+ killProcess(portPid);
282
+ try {
283
+ unlinkSync(PID_FILE);
284
+ } catch {}
285
+ try {
286
+ unlinkSync(ACTIVE_PORT);
287
+ } catch {}
288
+ try {
289
+ unlinkSync(MODE_FILE);
290
+ } catch {}
291
+ }
292
+
293
+ function httpGet(url, timeoutMs = 1000) {
294
+ return new Promise((resolve) => {
295
+ const req = http.get(url, (res) => {
296
+ let body = "";
297
+ res.on("data", (d) => (body += d));
298
+ res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
299
+ });
300
+ req.on("error", () => resolve({ ok: false }));
301
+ req.setTimeout(timeoutMs, () => {
302
+ req.destroy();
303
+ resolve({ ok: false });
304
+ });
305
+ });
306
+ }
307
+
308
+ async function writePortFile(timeoutMs = 15000) {
309
+ const deadline = Date.now() + timeoutMs;
310
+ while (Date.now() < deadline) {
311
+ const { ok, body } = await httpGet(
312
+ `http://localhost:${PORT}/json/version`,
313
+ 1500,
314
+ );
315
+ if (ok) {
316
+ try {
317
+ const { webSocketDebuggerUrl } = JSON.parse(body);
318
+ const wsPath = new URL(webSocketDebuggerUrl).pathname;
319
+ writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
320
+ return true;
321
+ } catch {
322
+ /* ignore */
323
+ }
324
+ }
325
+ await new Promise((r) => setTimeout(r, 400));
326
+ }
327
+ return false;
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Main
332
+ // ---------------------------------------------------------------------------
333
+
334
+ async function main() {
335
+ const arg = process.argv[2];
336
+
337
+ cleanupGhostChrome();
338
+
339
+ if (arg === "--kill") {
340
+ const pid = isRunning() || getPortPid(PORT);
341
+ if (pid) {
342
+ const ok = killProcess(pid);
343
+ console.log(
344
+ ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
345
+ );
346
+ } else {
347
+ console.log("GreedySearch Chrome is not running.");
348
+ }
349
+ try {
350
+ unlinkSync(PID_FILE);
351
+ } catch {}
352
+ try {
353
+ unlinkSync(ACTIVE_PORT);
354
+ } catch {}
355
+ try {
356
+ unlinkSync(MODE_FILE);
357
+ } catch {}
358
+ return;
359
+ }
360
+
361
+ if (arg === "--status") {
362
+ const pid = isRunning();
363
+ if (pid) {
364
+ console.log(`Running — pid ${pid}, port ${PORT}`);
365
+ } else {
366
+ console.log("Not running.");
367
+ }
368
+ return;
369
+ }
370
+
371
+ const existing = isRunning();
372
+ if (existing) {
373
+ // Mode check: if caller wants visible but Chrome is headless, kill and relaunch
374
+ const isWantingVisible =
375
+ process.env.GREEDY_SEARCH_VISIBLE === "1" &&
376
+ !process.argv.includes("--headless");
377
+ if (isWantingVisible && isModeFileHeadless()) {
378
+ console.log(
379
+ `Headless Chrome running (pid ${existing}) but visible requested — killing...`,
380
+ );
381
+ killProcess(existing);
382
+ try {
383
+ unlinkSync(PID_FILE);
384
+ } catch {}
385
+ try {
386
+ unlinkSync(MODE_FILE);
387
+ } catch {}
388
+ // Fall through to fresh launch below
389
+ } else {
390
+ const ready = await writePortFile(5000);
391
+ if (ready) {
392
+ console.log(`GreedySearch Chrome already running (pid ${existing}).`);
393
+ return;
394
+ }
395
+ console.log(`Stale PID ${existing} — launching fresh.`);
396
+ try {
397
+ unlinkSync(PID_FILE);
398
+ } catch {}
399
+ }
400
+ }
401
+
402
+ const CHROME_EXE = process.env.CHROME_PATH || findChrome();
403
+ if (!CHROME_EXE) {
404
+ console.error("Chrome not found. Set CHROME_PATH env var.");
405
+ process.exit(1);
406
+ }
407
+
408
+ mkdirSync(PROFILE_DIR, { recursive: true });
409
+
410
+ console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
411
+ if (isHeadless()) {
412
+ console.log("Headless mode — no window will be shown");
413
+ } else if (!isVisible()) {
414
+ console.log("Window will be minimized");
415
+ }
416
+
417
+ const proc = spawn(CHROME_EXE, buildChromeFlags(CHROME_EXE), {
418
+ detached: true,
419
+ stdio: "ignore",
420
+ });
421
+ proc.unref();
422
+ writeFileSync(PID_FILE, String(proc.pid));
423
+ // Write mode marker so ensureChrome() can detect headless vs visible
424
+ writeFileSync(MODE_FILE, isHeadless() ? "headless" : "visible", "utf8");
425
+
426
+ const portFileReady = await writePortFile();
427
+ if (!portFileReady) {
428
+ console.error("Chrome did not become ready within 15s.");
429
+ process.exit(1);
430
+ }
431
+
432
+ if (isHeadless()) {
433
+ // No window to minimize in headless mode
434
+ console.log("Ready (headless).");
435
+ } else {
436
+ // Minimize window via CDP
437
+ await minimizeViaCDP();
438
+ console.log("Ready.");
439
+ }
440
+ }
441
+
442
+ main();