@apmantza/greedysearch-pi 1.8.2 → 1.8.4

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,366 +1,366 @@
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 --kill — stop and restore original DevToolsActivePort
14
- // node launch.mjs --status — check if running
15
- //
16
- // Environment:
17
- // GREEDY_SEARCH_VISIBLE=1 — Show Chrome window instead of minimizing
18
- // CHROME_PATH — Path to Chrome executable
19
-
20
- import { execSync, spawn } from "node:child_process";
21
- import {
22
- existsSync,
23
- mkdirSync,
24
- readFileSync,
25
- unlinkSync,
26
- writeFileSync,
27
- } from "node:fs";
28
- import http from "node:http";
29
- import { platform, tmpdir } from "node:os";
30
- import { join } from "node:path";
31
-
32
- const PORT = 9222;
33
- const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
34
- const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
35
- const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
36
-
37
- function findChrome() {
38
- const os = platform();
39
- const candidates =
40
- os === "win32"
41
- ? [
42
- "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
43
- "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
44
- ]
45
- : os === "darwin"
46
- ? [
47
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
48
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
49
- ]
50
- : [
51
- "/usr/bin/google-chrome",
52
- "/usr/bin/google-chrome-stable",
53
- "/usr/bin/chromium-browser",
54
- "/usr/bin/chromium",
55
- "/snap/bin/chromium",
56
- ];
57
- return candidates.find(existsSync) || null;
58
- }
59
-
60
- const CHROME_FLAGS = [
61
- `--remote-debugging-port=${PORT}`,
62
- "--disable-features=DevToolsPrivacyUI",
63
- "--no-first-run",
64
- "--no-default-browser-check",
65
- "--disable-default-apps",
66
- `--user-data-dir=${PROFILE_DIR}`,
67
- "--profile-directory=Default",
68
- "about:blank",
69
- ];
70
-
71
- const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
72
-
73
- // ---------------------------------------------------------------------------
74
- // CDP Window Minimization
75
- // ---------------------------------------------------------------------------
76
-
77
- async function minimizeViaCDP() {
78
- if (isVisible()) return;
79
- console.log("[minimize] Starting...");
80
-
81
- // Wait for Chrome to be ready
82
- await new Promise((r) => setTimeout(r, 1000));
83
-
84
- try {
85
- // Get browser WebSocket URL
86
- console.log("[minimize] Getting version info...");
87
- const version = await new Promise((resolve, reject) => {
88
- http
89
- .get(`http://localhost:${PORT}/json/version`, (res) => {
90
- let body = "";
91
- res.on("data", (d) => (body += d));
92
- res.on("end", () => resolve(JSON.parse(body)));
93
- })
94
- .on("error", reject);
95
- });
96
-
97
- const wsUrl = version.webSocketDebuggerUrl;
98
- console.log("[minimize] WebSocket URL:", wsUrl.slice(0, 40) + "...");
99
-
100
- const WebSocket = globalThis.WebSocket;
101
- if (!WebSocket) {
102
- console.log("[minimize] WebSocket not available");
103
- return;
104
- }
105
-
106
- const ws = new WebSocket(wsUrl);
107
- let requestId = 0;
108
- const pending = new Map();
109
-
110
- ws.onopen = () => {
111
- console.log("[minimize] Connected, getting targets...");
112
- // Step 1: Get targets
113
- const id = ++requestId;
114
- pending.set(id, {
115
- resolve: (result) => {
116
- const targets = result.targetInfos || [];
117
- console.log(`[minimize] Found ${targets.length} targets`);
118
- const pageTarget = targets.find((t) => t.type === "page");
119
- if (!pageTarget) {
120
- console.log("[minimize] No page target, closing");
121
- ws.close();
122
- return;
123
- }
124
-
125
- console.log(`[minimize] Using target: ${pageTarget.targetId}`);
126
- // Step 2: Get windowId for target
127
- const winId = ++requestId;
128
- pending.set(winId, {
129
- resolve: (winResult) => {
130
- const windowId = winResult.windowId;
131
- console.log(
132
- `[minimize] Got windowId: ${windowId}, minimizing...`,
133
- );
134
- // Step 3: Minimize window
135
- const minId = ++requestId;
136
- pending.set(minId, {
137
- resolve: () =>
138
- console.log("[minimize] Window minimized successfully"),
139
- reject: (err) =>
140
- console.log("[minimize] Minimize failed:", err),
141
- });
142
- ws.send(
143
- JSON.stringify({
144
- id: minId,
145
- method: "Browser.setWindowBounds",
146
- params: { windowId, bounds: { windowState: "minimized" } },
147
- }),
148
- );
149
- setTimeout(() => ws.close(), 500);
150
- },
151
- reject: () => ws.close(),
152
- });
153
- ws.send(
154
- JSON.stringify({
155
- id: winId,
156
- method: "Browser.getWindowForTarget",
157
- params: { targetId: pageTarget.targetId },
158
- }),
159
- );
160
- },
161
- reject: () => ws.close(),
162
- });
163
- ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
164
- };
165
-
166
- ws.onmessage = (event) => {
167
- const msg = JSON.parse(event.data);
168
- if (msg.id && pending.has(msg.id)) {
169
- const { resolve, reject } = pending.get(msg.id);
170
- pending.delete(msg.id);
171
- if (msg.error) reject?.(msg.error);
172
- else resolve?.(msg.result);
173
- }
174
- };
175
-
176
- setTimeout(() => ws.close(), 5000);
177
- } catch {
178
- // Best-effort
179
- }
180
- }
181
-
182
- // ---------------------------------------------------------------------------
183
- // Chrome process management
184
- // ---------------------------------------------------------------------------
185
-
186
- function isRunning() {
187
- if (!existsSync(PID_FILE)) return false;
188
- const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
189
- if (!pid) return false;
190
- try {
191
- process.kill(pid, 0);
192
- return pid;
193
- } catch {
194
- return false;
195
- }
196
- }
197
-
198
- function getPortPid(port) {
199
- try {
200
- const os = platform();
201
- if (os === "win32") {
202
- const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
203
- const regex = new RegExp(
204
- `TCP\\s+[^\\s]*:${port}\\s+[^\\s]*:0\\s+LISTENING\\s+(\\d+)`,
205
- "i",
206
- );
207
- const match = out.match(regex);
208
- return match ? parseInt(match[1], 10) : null;
209
- }
210
- const out = execSync(
211
- `lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\\K\\d+'`,
212
- {
213
- encoding: "utf8",
214
- },
215
- ).trim();
216
- return out ? parseInt(out.split("\n")[0], 10) : null;
217
- } catch {
218
- return null;
219
- }
220
- }
221
-
222
- function killProcess(pid) {
223
- try {
224
- if (platform() === "win32") {
225
- execSync(`taskkill //F //PID ${pid}`, { stdio: "ignore" });
226
- } else {
227
- process.kill(pid, "SIGTERM");
228
- }
229
- return true;
230
- } catch {
231
- return false;
232
- }
233
- }
234
-
235
- function cleanupGhostChrome() {
236
- const portPid = getPortPid(PORT);
237
- if (!portPid) return;
238
-
239
- const trackedPid = isRunning();
240
- if (trackedPid && portPid === trackedPid) return;
241
-
242
- console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
243
- killProcess(portPid);
244
- try {
245
- unlinkSync(PID_FILE);
246
- } catch {}
247
- try {
248
- unlinkSync(ACTIVE_PORT);
249
- } catch {}
250
- }
251
-
252
- function httpGet(url, timeoutMs = 1000) {
253
- return new Promise((resolve) => {
254
- const req = http.get(url, (res) => {
255
- let body = "";
256
- res.on("data", (d) => (body += d));
257
- res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
258
- });
259
- req.on("error", () => resolve({ ok: false }));
260
- req.setTimeout(timeoutMs, () => {
261
- req.destroy();
262
- resolve({ ok: false });
263
- });
264
- });
265
- }
266
-
267
- async function writePortFile(timeoutMs = 15000) {
268
- const deadline = Date.now() + timeoutMs;
269
- while (Date.now() < deadline) {
270
- const { ok, body } = await httpGet(
271
- `http://localhost:${PORT}/json/version`,
272
- 1500,
273
- );
274
- if (ok) {
275
- try {
276
- const { webSocketDebuggerUrl } = JSON.parse(body);
277
- const wsPath = new URL(webSocketDebuggerUrl).pathname;
278
- writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
279
- return true;
280
- } catch {
281
- /* ignore */
282
- }
283
- }
284
- await new Promise((r) => setTimeout(r, 400));
285
- }
286
- return false;
287
- }
288
-
289
- // ---------------------------------------------------------------------------
290
- // Main
291
- // ---------------------------------------------------------------------------
292
-
293
- async function main() {
294
- const arg = process.argv[2];
295
-
296
- cleanupGhostChrome();
297
-
298
- if (arg === "--kill") {
299
- const pid = isRunning();
300
- if (pid) {
301
- const ok = killProcess(pid);
302
- console.log(
303
- ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
304
- );
305
- } else {
306
- console.log("GreedySearch Chrome is not running.");
307
- }
308
- return;
309
- }
310
-
311
- if (arg === "--status") {
312
- const pid = isRunning();
313
- if (pid) {
314
- console.log(`Running — pid ${pid}, port ${PORT}`);
315
- } else {
316
- console.log("Not running.");
317
- }
318
- return;
319
- }
320
-
321
- const existing = isRunning();
322
- if (existing) {
323
- const ready = await writePortFile(5000);
324
- if (ready) {
325
- console.log(`GreedySearch Chrome already running (pid ${existing}).`);
326
- return;
327
- }
328
- console.log(`Stale PID ${existing} — launching fresh.`);
329
- try {
330
- unlinkSync(PID_FILE);
331
- } catch {}
332
- }
333
-
334
- const CHROME_EXE = process.env.CHROME_PATH || findChrome();
335
- if (!CHROME_EXE) {
336
- console.error("Chrome not found. Set CHROME_PATH env var.");
337
- process.exit(1);
338
- }
339
-
340
- mkdirSync(PROFILE_DIR, { recursive: true });
341
-
342
- console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
343
- if (!isVisible()) {
344
- console.log("Window will be minimized");
345
- }
346
-
347
- const proc = spawn(CHROME_EXE, CHROME_FLAGS, {
348
- detached: true,
349
- stdio: "ignore",
350
- });
351
- proc.unref();
352
- writeFileSync(PID_FILE, String(proc.pid));
353
-
354
- const portFileReady = await writePortFile();
355
- if (!portFileReady) {
356
- console.error("Chrome did not become ready within 15s.");
357
- process.exit(1);
358
- }
359
-
360
- // Minimize window via CDP
361
- await minimizeViaCDP();
362
-
363
- console.log("Ready.");
364
- }
365
-
366
- 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 --kill — stop and restore original DevToolsActivePort
14
+ // node launch.mjs --status — check if running
15
+ //
16
+ // Environment:
17
+ // GREEDY_SEARCH_VISIBLE=1 — Show Chrome window instead of minimizing
18
+ // CHROME_PATH — Path to Chrome executable
19
+
20
+ import { execSync, spawn } from "node:child_process";
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readFileSync,
25
+ unlinkSync,
26
+ writeFileSync,
27
+ } from "node:fs";
28
+ import http from "node:http";
29
+ import { platform, tmpdir } from "node:os";
30
+ import { join } from "node:path";
31
+
32
+ const PORT = 9222;
33
+ const PROFILE_DIR = join(tmpdir(), "greedysearch-chrome-profile");
34
+ const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
35
+ const PID_FILE = join(tmpdir(), "greedysearch-chrome.pid");
36
+
37
+ function findChrome() {
38
+ const os = platform();
39
+ const candidates =
40
+ os === "win32"
41
+ ? [
42
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
43
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
44
+ ]
45
+ : os === "darwin"
46
+ ? [
47
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
48
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
49
+ ]
50
+ : [
51
+ "/usr/bin/google-chrome",
52
+ "/usr/bin/google-chrome-stable",
53
+ "/usr/bin/chromium-browser",
54
+ "/usr/bin/chromium",
55
+ "/snap/bin/chromium",
56
+ ];
57
+ return candidates.find(existsSync) || null;
58
+ }
59
+
60
+ const CHROME_FLAGS = [
61
+ `--remote-debugging-port=${PORT}`,
62
+ "--disable-features=DevToolsPrivacyUI",
63
+ "--no-first-run",
64
+ "--no-default-browser-check",
65
+ "--disable-default-apps",
66
+ `--user-data-dir=${PROFILE_DIR}`,
67
+ "--profile-directory=Default",
68
+ "about:blank",
69
+ ];
70
+
71
+ const isVisible = () => process.env.GREEDY_SEARCH_VISIBLE === "1";
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // CDP Window Minimization
75
+ // ---------------------------------------------------------------------------
76
+
77
+ async function minimizeViaCDP() {
78
+ if (isVisible()) return;
79
+ console.log("[minimize] Starting...");
80
+
81
+ // Wait for Chrome to be ready
82
+ await new Promise((r) => setTimeout(r, 1000));
83
+
84
+ try {
85
+ // Get browser WebSocket URL
86
+ console.log("[minimize] Getting version info...");
87
+ const version = await new Promise((resolve, reject) => {
88
+ http
89
+ .get(`http://localhost:${PORT}/json/version`, (res) => {
90
+ let body = "";
91
+ res.on("data", (d) => (body += d));
92
+ res.on("end", () => resolve(JSON.parse(body)));
93
+ })
94
+ .on("error", reject);
95
+ });
96
+
97
+ const wsUrl = version.webSocketDebuggerUrl;
98
+ console.log("[minimize] WebSocket URL:", wsUrl.slice(0, 40) + "...");
99
+
100
+ const WebSocket = globalThis.WebSocket;
101
+ if (!WebSocket) {
102
+ console.log("[minimize] WebSocket not available");
103
+ return;
104
+ }
105
+
106
+ const ws = new WebSocket(wsUrl);
107
+ let requestId = 0;
108
+ const pending = new Map();
109
+
110
+ ws.onopen = () => {
111
+ console.log("[minimize] Connected, getting targets...");
112
+ // Step 1: Get targets
113
+ const id = ++requestId;
114
+ pending.set(id, {
115
+ resolve: (result) => {
116
+ const targets = result.targetInfos || [];
117
+ console.log(`[minimize] Found ${targets.length} targets`);
118
+ const pageTarget = targets.find((t) => t.type === "page");
119
+ if (!pageTarget) {
120
+ console.log("[minimize] No page target, closing");
121
+ ws.close();
122
+ return;
123
+ }
124
+
125
+ console.log(`[minimize] Using target: ${pageTarget.targetId}`);
126
+ // Step 2: Get windowId for target
127
+ const winId = ++requestId;
128
+ pending.set(winId, {
129
+ resolve: (winResult) => {
130
+ const windowId = winResult.windowId;
131
+ console.log(
132
+ `[minimize] Got windowId: ${windowId}, minimizing...`,
133
+ );
134
+ // Step 3: Minimize window
135
+ const minId = ++requestId;
136
+ pending.set(minId, {
137
+ resolve: () =>
138
+ console.log("[minimize] Window minimized successfully"),
139
+ reject: (err) =>
140
+ console.log("[minimize] Minimize failed:", err),
141
+ });
142
+ ws.send(
143
+ JSON.stringify({
144
+ id: minId,
145
+ method: "Browser.setWindowBounds",
146
+ params: { windowId, bounds: { windowState: "minimized" } },
147
+ }),
148
+ );
149
+ setTimeout(() => ws.close(), 500);
150
+ },
151
+ reject: () => ws.close(),
152
+ });
153
+ ws.send(
154
+ JSON.stringify({
155
+ id: winId,
156
+ method: "Browser.getWindowForTarget",
157
+ params: { targetId: pageTarget.targetId },
158
+ }),
159
+ );
160
+ },
161
+ reject: () => ws.close(),
162
+ });
163
+ ws.send(JSON.stringify({ id, method: "Target.getTargets", params: {} }));
164
+ };
165
+
166
+ ws.onmessage = (event) => {
167
+ const msg = JSON.parse(event.data);
168
+ if (msg.id && pending.has(msg.id)) {
169
+ const { resolve, reject } = pending.get(msg.id);
170
+ pending.delete(msg.id);
171
+ if (msg.error) reject?.(msg.error);
172
+ else resolve?.(msg.result);
173
+ }
174
+ };
175
+
176
+ setTimeout(() => ws.close(), 5000);
177
+ } catch {
178
+ // Best-effort
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Chrome process management
184
+ // ---------------------------------------------------------------------------
185
+
186
+ function isRunning() {
187
+ if (!existsSync(PID_FILE)) return false;
188
+ const pid = parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
189
+ if (!pid) return false;
190
+ try {
191
+ process.kill(pid, 0);
192
+ return pid;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function getPortPid(port) {
199
+ try {
200
+ const os = platform();
201
+ if (os === "win32") {
202
+ const out = execSync(`netstat -ano -p TCP 2>nul`, { encoding: "utf8" });
203
+ const regex = new RegExp(
204
+ `TCP\\s+[^\\s]*:${port}\\s+[^\\s]*:0\\s+LISTENING\\s+(\\d+)`,
205
+ "i",
206
+ );
207
+ const match = out.match(regex);
208
+ return match ? parseInt(match[1], 10) : null;
209
+ }
210
+ const out = execSync(
211
+ `lsof -i :${port} -t 2>/dev/null || ss -tlnp 2>/dev/null | grep :${port} | grep -oP 'pid=\\K\\d+'`,
212
+ {
213
+ encoding: "utf8",
214
+ },
215
+ ).trim();
216
+ return out ? parseInt(out.split("\n")[0], 10) : null;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ function killProcess(pid) {
223
+ try {
224
+ if (platform() === "win32") {
225
+ execSync(`taskkill //F //PID ${pid}`, { stdio: "ignore" });
226
+ } else {
227
+ process.kill(pid, "SIGTERM");
228
+ }
229
+ return true;
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+
235
+ function cleanupGhostChrome() {
236
+ const portPid = getPortPid(PORT);
237
+ if (!portPid) return;
238
+
239
+ const trackedPid = isRunning();
240
+ if (trackedPid && portPid === trackedPid) return;
241
+
242
+ console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
243
+ killProcess(portPid);
244
+ try {
245
+ unlinkSync(PID_FILE);
246
+ } catch {}
247
+ try {
248
+ unlinkSync(ACTIVE_PORT);
249
+ } catch {}
250
+ }
251
+
252
+ function httpGet(url, timeoutMs = 1000) {
253
+ return new Promise((resolve) => {
254
+ const req = http.get(url, (res) => {
255
+ let body = "";
256
+ res.on("data", (d) => (body += d));
257
+ res.on("end", () => resolve({ ok: res.statusCode === 200, body }));
258
+ });
259
+ req.on("error", () => resolve({ ok: false }));
260
+ req.setTimeout(timeoutMs, () => {
261
+ req.destroy();
262
+ resolve({ ok: false });
263
+ });
264
+ });
265
+ }
266
+
267
+ async function writePortFile(timeoutMs = 15000) {
268
+ const deadline = Date.now() + timeoutMs;
269
+ while (Date.now() < deadline) {
270
+ const { ok, body } = await httpGet(
271
+ `http://localhost:${PORT}/json/version`,
272
+ 1500,
273
+ );
274
+ if (ok) {
275
+ try {
276
+ const { webSocketDebuggerUrl } = JSON.parse(body);
277
+ const wsPath = new URL(webSocketDebuggerUrl).pathname;
278
+ writeFileSync(ACTIVE_PORT, `${PORT}\n${wsPath}`, "utf8");
279
+ return true;
280
+ } catch {
281
+ /* ignore */
282
+ }
283
+ }
284
+ await new Promise((r) => setTimeout(r, 400));
285
+ }
286
+ return false;
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Main
291
+ // ---------------------------------------------------------------------------
292
+
293
+ async function main() {
294
+ const arg = process.argv[2];
295
+
296
+ cleanupGhostChrome();
297
+
298
+ if (arg === "--kill") {
299
+ const pid = isRunning();
300
+ if (pid) {
301
+ const ok = killProcess(pid);
302
+ console.log(
303
+ ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
304
+ );
305
+ } else {
306
+ console.log("GreedySearch Chrome is not running.");
307
+ }
308
+ return;
309
+ }
310
+
311
+ if (arg === "--status") {
312
+ const pid = isRunning();
313
+ if (pid) {
314
+ console.log(`Running — pid ${pid}, port ${PORT}`);
315
+ } else {
316
+ console.log("Not running.");
317
+ }
318
+ return;
319
+ }
320
+
321
+ const existing = isRunning();
322
+ if (existing) {
323
+ const ready = await writePortFile(5000);
324
+ if (ready) {
325
+ console.log(`GreedySearch Chrome already running (pid ${existing}).`);
326
+ return;
327
+ }
328
+ console.log(`Stale PID ${existing} — launching fresh.`);
329
+ try {
330
+ unlinkSync(PID_FILE);
331
+ } catch {}
332
+ }
333
+
334
+ const CHROME_EXE = process.env.CHROME_PATH || findChrome();
335
+ if (!CHROME_EXE) {
336
+ console.error("Chrome not found. Set CHROME_PATH env var.");
337
+ process.exit(1);
338
+ }
339
+
340
+ mkdirSync(PROFILE_DIR, { recursive: true });
341
+
342
+ console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
343
+ if (!isVisible()) {
344
+ console.log("Window will be minimized");
345
+ }
346
+
347
+ const proc = spawn(CHROME_EXE, CHROME_FLAGS, {
348
+ detached: true,
349
+ stdio: "ignore",
350
+ });
351
+ proc.unref();
352
+ writeFileSync(PID_FILE, String(proc.pid));
353
+
354
+ const portFileReady = await writePortFile();
355
+ if (!portFileReady) {
356
+ console.error("Chrome did not become ready within 15s.");
357
+ process.exit(1);
358
+ }
359
+
360
+ // Minimize window via CDP
361
+ await minimizeViaCDP();
362
+
363
+ console.log("Ready.");
364
+ }
365
+
366
+ main();