codex-lens 0.1.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,576 @@
1
+ import { createServer } from 'http';
2
+ import { createHash, createHash as cryptoCreateHash } from 'crypto';
3
+ import { WebSocketServer } from 'ws';
4
+ import express from 'express';
5
+ import { createProxyServer } from './proxy.js';
6
+ import { FileWatcher, scanDirectory } from './watcher.js';
7
+ import { createLogger } from './lib/logger.js';
8
+ import { spawnCodex, writeToPty, resizePty, killPty, onPtyData, onPtyExit, getPtyState } from './pty-manager.js';
9
+ import { SnapshotManager } from './snapshot-manager.js';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { readFileSync, existsSync } from 'fs';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ const HTTP_PORT = 5174;
17
+ const NPM_PACKAGE_NAME = 'codex-lens';
18
+
19
+ const logger = createLogger('Aggregator');
20
+
21
+ let latestVersion = null;
22
+ let currentVersion = null;
23
+
24
+ async function fetchLatestVersion() {
25
+ try {
26
+ const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`);
27
+ if (response.ok) {
28
+ const data = await response.json();
29
+ latestVersion = data.version;
30
+ logger.info(`Latest ${NPM_PACKAGE_NAME} version: ${latestVersion}`);
31
+ }
32
+ } catch (error) {
33
+ logger.warn(`Failed to fetch latest version: ${error.message}`);
34
+ }
35
+ }
36
+
37
+ function getCurrentVersion() {
38
+ if (currentVersion) return currentVersion;
39
+ try {
40
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
41
+ currentVersion = packageJson.version;
42
+ } catch {
43
+ currentVersion = '0.0.0';
44
+ }
45
+ return currentVersion;
46
+ }
47
+
48
+ class Aggregator {
49
+ constructor(codexBinary, projectRoot) {
50
+ this.codexBinary = codexBinary;
51
+ this.projectRoot = projectRoot;
52
+ this.clients = new Set();
53
+ this.terminalClients = new Set();
54
+ this.httpServer = null;
55
+ this.wss = null;
56
+ this.proxyServer = null;
57
+ this.fileWatcher = null;
58
+ this.ptyProcess = null;
59
+ this.snapshotManager = new SnapshotManager();
60
+ this.currentTaskId = null;
61
+ this.taskStatus = 'idle';
62
+ }
63
+
64
+ async start(proxyPort) {
65
+ await fetchLatestVersion();
66
+
67
+ const app = express();
68
+ const publicPath = path.join(__dirname, 'public');
69
+ app.use(express.static(publicPath));
70
+
71
+ app.use((req, res, next) => {
72
+ if (req.url === '/' || req.url === '/index.html') {
73
+ res.send(this.getIndexHtml());
74
+ } else if (req.url === '/api/status') {
75
+ const current = getCurrentVersion();
76
+ res.json({
77
+ status: 'running',
78
+ clients: this.clients.size,
79
+ codexRunning: !!this.ptyProcess,
80
+ version: current,
81
+ latestVersion: latestVersion,
82
+ hasUpdate: latestVersion && latestVersion !== current
83
+ });
84
+ } else {
85
+ next();
86
+ }
87
+ });
88
+
89
+ await new Promise((resolve) => {
90
+ this.httpServer = createServer(app);
91
+
92
+ this.httpServer.on('upgrade', (req, socket, head) => {
93
+ const pathname = new URL(req.url, `http://${req.headers.host}`).pathname;
94
+ logger.info(`Upgrade: ${pathname}`);
95
+ if (pathname === '/ws/terminal') {
96
+ this.handleTerminalUpgrade(req, socket, head);
97
+ } else if (pathname === '/ws') {
98
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
99
+ this.wss.emit('connection', ws, req);
100
+ });
101
+ } else {
102
+ socket.destroy();
103
+ }
104
+ });
105
+
106
+ this.httpServer.listen(HTTP_PORT, () => {
107
+ logger.info(`HTTP server started on port ${HTTP_PORT}`);
108
+ resolve();
109
+ });
110
+ });
111
+
112
+ this.wss = new WebSocketServer({ noServer: true });
113
+ this.setupMainWebSocket();
114
+
115
+ this.proxyServer = createProxyServer((event) => this.broadcast(event), proxyPort);
116
+ await this.proxyServer.start();
117
+
118
+ this.fileWatcher = new FileWatcher(this.projectRoot, (event) => this.broadcast(event));
119
+ await this.fileWatcher.start();
120
+
121
+ await this.startCodex(proxyPort);
122
+
123
+ return { httpPort: HTTP_PORT, proxyPort };
124
+ }
125
+
126
+ setupMainWebSocket() {
127
+ this.wss.on('connection', (ws) => {
128
+ logger.info('API client connected');
129
+ this.clients.add(ws);
130
+
131
+ const fileTree = scanDirectory(this.projectRoot);
132
+ logger.info(`Sending file_tree with ${fileTree.length} top-level items`);
133
+ ws.send(JSON.stringify({ type: 'file_tree', data: fileTree }));
134
+
135
+ ws.on('close', () => {
136
+ logger.info('API client disconnected');
137
+ this.clients.delete(ws);
138
+ });
139
+
140
+ ws.on('message', (message) => {
141
+ try {
142
+ const data = JSON.parse(message.toString());
143
+ this.handleClientMessage(data, ws);
144
+ } catch (e) {
145
+ logger.error(`Invalid message: ${e.message}`);
146
+ }
147
+ });
148
+ });
149
+ }
150
+
151
+ async startCodex(proxyPort) {
152
+ this.ptyProcess = await spawnCodex(this.codexBinary, this.projectRoot, proxyPort);
153
+ logger.info(`Codex PTY spawned with PID: ${this.ptyProcess.pid}`);
154
+
155
+ onPtyData((data) => {
156
+ this.broadcastToTerminal({ type: 'data', data });
157
+ });
158
+
159
+ onPtyExit((exitCode) => {
160
+ logger.info(`Codex PTY exited with code: ${exitCode}`);
161
+ this.broadcastToTerminal({ type: 'exit', exitCode });
162
+ });
163
+ }
164
+
165
+ handleTerminalUpgrade(req, socket, head) {
166
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
167
+ logger.info('Terminal WebSocket connected');
168
+ this.handleTerminalConnection(ws);
169
+ });
170
+ }
171
+
172
+ handleTerminalConnection(ws) {
173
+ this.terminalClients.add(ws);
174
+ logger.info(`Terminal client connected. Total: ${this.terminalClients.size}`);
175
+
176
+ ws.on('message', (raw) => {
177
+ try {
178
+ const msg = JSON.parse(raw.toString());
179
+ if (msg.type === 'input') {
180
+ writeToPty(msg.data);
181
+ } else if (msg.type === 'resize') {
182
+ resizePty(msg.cols || 120, msg.rows || 30);
183
+ }
184
+ } catch (e) {
185
+ logger.error(`Terminal message error: ${e.message}`);
186
+ }
187
+ });
188
+
189
+ ws.on('close', () => {
190
+ this.terminalClients.delete(ws);
191
+ logger.info(`Terminal client disconnected. Total: ${this.terminalClients.size}`);
192
+ });
193
+
194
+ const state = getPtyState();
195
+ ws.send(JSON.stringify({ type: 'state', ...state }));
196
+ }
197
+
198
+ broadcastToTerminal(event) {
199
+ const message = JSON.stringify(event);
200
+ for (const client of this.terminalClients) {
201
+ if (client.readyState === WebSocket.OPEN) {
202
+ client.send(message);
203
+ }
204
+ }
205
+ }
206
+
207
+ handleClientMessage(data, ws) {
208
+ if (data.type === 'user_message') {
209
+ writeToPty(data.data + '\r');
210
+ } else if (data.type === 'open_file') {
211
+ const result = this.fileWatcher.readFile(data.data);
212
+ if (result.error) {
213
+ ws.send(JSON.stringify({ type: 'error', message: result.error }));
214
+ } else {
215
+ const extension = path.extname(result.path);
216
+ ws.send(JSON.stringify({
217
+ type: 'file_content',
218
+ data: {
219
+ path: result.path,
220
+ content: result.content,
221
+ extension
222
+ }
223
+ }));
224
+ }
225
+ } else if (data.type === 'start_task') {
226
+ this.handleStartTask(ws);
227
+ } else if (data.type === 'rollback_task') {
228
+ this.handleRollbackTask(ws);
229
+ } else if (data.type === 'complete_task') {
230
+ this.handleCompleteTask(ws);
231
+ } else if (data.type === 'get_task_status') {
232
+ ws.send(JSON.stringify({
233
+ type: 'task_status',
234
+ data: {
235
+ status: this.taskStatus,
236
+ taskId: this.currentTaskId
237
+ }
238
+ }));
239
+ }
240
+ }
241
+
242
+ async handleStartTask(ws) {
243
+ if (this.taskStatus === 'running') {
244
+ ws.send(JSON.stringify({ type: 'error', message: 'Task already running' }));
245
+ return;
246
+ }
247
+
248
+ this.currentTaskId = Date.now().toString();
249
+ this.taskStatus = 'running';
250
+
251
+ const result = await this.snapshotManager.createSnapshot(this.projectRoot, this.currentTaskId);
252
+
253
+ if (result.success) {
254
+ logger.info(`Task started: ${this.currentTaskId}, ${result.filesCount} files snapshotted`);
255
+ this.broadcast({
256
+ type: 'task_status',
257
+ data: {
258
+ status: this.taskStatus,
259
+ taskId: this.currentTaskId,
260
+ filesCount: result.filesCount
261
+ }
262
+ });
263
+ ws.send(JSON.stringify({
264
+ type: 'task_started',
265
+ data: {
266
+ taskId: this.currentTaskId,
267
+ filesCount: result.filesCount
268
+ }
269
+ }));
270
+ } else {
271
+ this.taskStatus = 'idle';
272
+ this.currentTaskId = null;
273
+ ws.send(JSON.stringify({ type: 'error', message: 'Failed to create snapshot: ' + result.error }));
274
+ }
275
+ }
276
+
277
+ async handleRollbackTask(ws) {
278
+ if (!this.currentTaskId || this.taskStatus !== 'running') {
279
+ ws.send(JSON.stringify({ type: 'error', message: 'No active task to rollback' }));
280
+ return;
281
+ }
282
+
283
+ const taskId = this.currentTaskId;
284
+ const result = await this.snapshotManager.restoreSnapshot(taskId);
285
+
286
+ if (result.success) {
287
+ await this.snapshotManager.deleteSnapshot(taskId);
288
+ logger.info(`Task rolled back: ${taskId}, ${result.restoredCount} files restored`);
289
+
290
+ this.taskStatus = 'idle';
291
+ this.currentTaskId = null;
292
+
293
+ this.broadcast({
294
+ type: 'task_status',
295
+ data: {
296
+ status: this.taskStatus,
297
+ taskId: null
298
+ }
299
+ });
300
+
301
+ ws.send(JSON.stringify({
302
+ type: 'task_rolled_back',
303
+ data: {
304
+ restoredCount: result.restoredCount
305
+ }
306
+ }));
307
+ } else {
308
+ ws.send(JSON.stringify({ type: 'error', message: 'Failed to rollback: ' + result.error }));
309
+ }
310
+ }
311
+
312
+ async handleCompleteTask(ws) {
313
+ if (!this.currentTaskId || this.taskStatus !== 'running') {
314
+ ws.send(JSON.stringify({ type: 'error', message: 'No active task to complete' }));
315
+ return;
316
+ }
317
+
318
+ const taskId = this.currentTaskId;
319
+ await this.snapshotManager.deleteSnapshot(taskId);
320
+
321
+ logger.info(`Task completed: ${taskId}`);
322
+
323
+ this.taskStatus = 'idle';
324
+ this.currentTaskId = null;
325
+
326
+ this.broadcast({
327
+ type: 'task_status',
328
+ data: {
329
+ status: this.taskStatus,
330
+ taskId: null
331
+ }
332
+ });
333
+
334
+ ws.send(JSON.stringify({
335
+ type: 'task_completed',
336
+ data: {}
337
+ }));
338
+ }
339
+
340
+ broadcast(event) {
341
+ const message = JSON.stringify(event);
342
+ for (const client of this.clients) {
343
+ if (client.readyState === WebSocket.OPEN) {
344
+ client.send(message);
345
+ }
346
+ }
347
+ }
348
+
349
+ getIndexHtml() {
350
+ return `<!DOCTYPE html>
351
+ <html lang="zh-CN">
352
+ <head>
353
+ <meta charset="UTF-8">
354
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
355
+ <title>Codex Viewer</title>
356
+ <link rel="stylesheet" href="/lib/xterm/xterm.css">
357
+ <style>
358
+ :root {
359
+ --bg-primary: #1e1e1e;
360
+ --bg-secondary: #252526;
361
+ --bg-tertiary: #2d2d30;
362
+ --text-primary: #cccccc;
363
+ --text-secondary: #858585;
364
+ --border-color: #3c3c3c;
365
+ --accent-color: #007acc;
366
+ }
367
+ * { margin: 0; padding: 0; box-sizing: border-box; }
368
+ html, body, #root { height: 100%; width: 100%; overflow: hidden; }
369
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg-primary); color: var(--text-primary); }
370
+ .app { display: flex; height: 100vh; }
371
+ .panel { display: flex; flex-direction: column; border-right: 1px solid var(--border-color); overflow: hidden; }
372
+ .panel:last-child { border-right: none; }
373
+ .panel-header { padding: 8px 12px; background: var(--bg-tertiary); border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 12px; text-transform: uppercase; color: var(--text-secondary); }
374
+ .panel-content { flex: 1; overflow: auto; padding: 8px; }
375
+ .file-tree .item { padding: 4px 8px; cursor: pointer; border-radius: 3px; display: flex; align-items: center; gap: 6px; }
376
+ .file-tree .item:hover { background: var(--bg-tertiary); }
377
+ .diff-line { font-family: 'SF Mono', Consolas, monospace; font-size: 13px; padding: 2px 12px; white-space: pre; }
378
+ .diff-line.added { background: #2d4a2d; color: #89d185; }
379
+ .diff-line.removed { background: #5a2d2d; color: #f48771; }
380
+ .diff-line::before { display: inline-block; width: 16px; margin-right: 8px; text-align: center; font-weight: bold; }
381
+ .diff-line.added::before { content: '+'; }
382
+ .diff-line.removed::before { content: '-'; }
383
+ .terminal-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
384
+ .terminal-container { flex: 1; padding: 8px; overflow: hidden; background: #1e1e1e; }
385
+ .terminal-toolbar { display: flex; gap: 4px; padding: 8px; background: #252526; border-top: 1px solid var(--border-color); flex-wrap: wrap; }
386
+ .terminal-btn { padding: 4px 8px; background: #2d2d30; border: 1px solid #3c3c3c; border-radius: 3px; color: #cccccc; cursor: pointer; font-size: 12px; }
387
+ .terminal-btn:hover { background: #3c3c3c; }
388
+ .left-panel { width: 250px; min-width: 200px; }
389
+ .middle-panel { flex: 1; min-width: 400px; }
390
+ .right-panel { width: 45%; min-width: 350px; }
391
+ .loading { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); }
392
+ </style>
393
+ </head>
394
+ <body>
395
+ <div id="root">
396
+ <div class="app">
397
+ <div class="panel left-panel">
398
+ <div class="panel-header">File Explorer</div>
399
+ <div class="panel-content" id="fileTree">
400
+ <div class="loading">Waiting for file changes...</div>
401
+ </div>
402
+ </div>
403
+ <div class="panel middle-panel">
404
+ <div class="panel-header" id="fileHeader">File Content</div>
405
+ <div class="panel-content" id="codeContent">
406
+ <div class="loading">Select a file to view diff...</div>
407
+ </div>
408
+ </div>
409
+ <div class="panel right-panel">
410
+ <div class="panel-header">Terminal</div>
411
+ <div class="terminal-wrapper">
412
+ <div class="terminal-container" id="terminalContainer"></div>
413
+ <div class="terminal-toolbar">
414
+ <button class="terminal-btn" onclick="sendKey('\\x1b[A')">Up</button>
415
+ <button class="terminal-btn" onclick="sendKey('\\x1b[B')">Down</button>
416
+ <button class="terminal-btn" onclick="sendKey('\\x1b[D')">Left</button>
417
+ <button class="terminal-btn" onclick="sendKey('\\x1b[C')">Right</button>
418
+ <button class="terminal-btn" onclick="sendKey('\\r')">Enter</button>
419
+ <button class="terminal-btn" onclick="sendKey('\\t')">Tab</button>
420
+ <button class="terminal-btn" onclick="sendKey('\\x03')">Ctrl+C</button>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ <script src="/lib/xterm/xterm.js"></script>
427
+ <script src="/lib/xterm/addon-fit.js"></script>
428
+ <script src="/lib/xterm/addon-web-links.js"></script>
429
+ <script>
430
+ const term = new Terminal({
431
+ cursorBlink: true,
432
+ fontSize: 14,
433
+ fontFamily: '"SF Mono", "Fira Code", Consolas, monospace',
434
+ theme: {
435
+ background: '#1e1e1e',
436
+ foreground: '#cccccc',
437
+ cursor: '#ffffff',
438
+ selectionBackground: '#264f78',
439
+ },
440
+ scrollback: 10000,
441
+ });
442
+
443
+ const fitAddon = new FitAddon();
444
+ term.loadAddon(fitAddon);
445
+ term.loadAddon(new WebLinksAddon());
446
+ term.open(document.getElementById('terminalContainer'));
447
+ term.write('Connecting to Codex...\r\n');
448
+
449
+ const wsHost = window.location.hostname;
450
+ let terminalWs = null;
451
+
452
+ function connectTerminal() {
453
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
454
+ terminalWs = new WebSocket('ws://' + wsHost + ':' + port + '/ws/terminal');
455
+
456
+ terminalWs.onopen = () => {
457
+ term.write('\r\n\x1b[32mConnected to Codex!\x1b[0m\r\n');
458
+ };
459
+
460
+ terminalWs.onclose = () => {
461
+ term.write('\r\n\x1b[33mDisconnected, reconnecting...\x1b[0m\r\n');
462
+ setTimeout(connectTerminal, 3000);
463
+ };
464
+
465
+ terminalWs.onerror = (e) => {
466
+ term.write('\r\n\x1b[31mWebSocket Error\x1b[0m\r\n');
467
+ };
468
+
469
+ terminalWs.onmessage = (event) => {
470
+ const msg = JSON.parse(event.data);
471
+ if (msg.type === 'data') {
472
+ term.write(msg.data);
473
+ } else if (msg.type === 'exit') {
474
+ term.write('\r\n\x1b[31m[Process exited with code ' + msg.exitCode + ']\x1b[0m\r\n');
475
+ }
476
+ };
477
+ }
478
+
479
+ connectTerminal();
480
+
481
+ term.onData((data) => {
482
+ if (terminalWs && terminalWs.readyState === WebSocket.OPEN) {
483
+ terminalWs.send(JSON.stringify({ type: 'input', data }));
484
+ }
485
+ });
486
+
487
+ window.sendKey = function(seq) {
488
+ if (terminalWs && terminalWs.readyState === WebSocket.OPEN) {
489
+ terminalWs.send(JSON.stringify({ type: 'input', data: seq }));
490
+ }
491
+ term.write(seq);
492
+ };
493
+
494
+ const resizeObserver = new ResizeObserver(() => {
495
+ try {
496
+ fitAddon.fit();
497
+ const dims = fitAddon.proposeDimensions();
498
+ if (dims && terminalWs && terminalWs.readyState === WebSocket.OPEN) {
499
+ terminalWs.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
500
+ }
501
+ } catch (e) {}
502
+ });
503
+ resizeObserver.observe(document.getElementById('terminalContainer'));
504
+
505
+ let apiWs = null;
506
+ const fileTree = document.getElementById('fileTree');
507
+ const codeContent = document.getElementById('codeContent');
508
+ let recentChanges = [];
509
+
510
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
511
+ apiWs = new WebSocket('ws://' + wsHost + ':' + port + '/ws');
512
+
513
+ apiWs.onopen = () => console.log('[API] Connected');
514
+ apiWs.onclose = () => {
515
+ setTimeout(() => { apiWs = new WebSocket('ws://' + wsHost + ':' + port + '/ws'); }, 3000);
516
+ };
517
+
518
+ apiWs.onmessage = (event) => {
519
+ const msg = JSON.parse(event.data);
520
+ if (msg.type === 'file_change') {
521
+ recentChanges.unshift({ path: msg.data.path, time: Date.now(), diff: msg.data.diff, content: msg.data.newContent });
522
+ if (recentChanges.length > 10) recentChanges.pop();
523
+ renderRecentChanges();
524
+ }
525
+ };
526
+
527
+ function renderRecentChanges() {
528
+ let html = '<div style="padding:8px;font-size:11px;color:var(--text-secondary);border-bottom:1px solid var(--border-color)">RECENT CHANGES</div>';
529
+ recentChanges.forEach((c, i) => {
530
+ const fileName = c.path.split(/[/\\\\]/).pop();
531
+ const time = new Date(c.time).toLocaleTimeString();
532
+ html += '<div class="item" onclick="openFile(' + i + ')">' +
533
+ '<span>📝</span><span style="flex:1">' + fileName + '</span><span style="font-size:10px;color:#858585">' + time + '</span>' +
534
+ '</div>';
535
+ });
536
+ if (recentChanges.length === 0) {
537
+ html += '<div style="padding:20px;color:#858585;text-align:center">No changes yet</div>';
538
+ }
539
+ fileTree.innerHTML = html;
540
+ }
541
+
542
+ window.openFile = function(index) {
543
+ const change = recentChanges[index];
544
+ if (change.diff && change.diff.some(d => d.added || d.removed)) {
545
+ codeContent.innerHTML = change.diff.map(line =>
546
+ '<div class="diff-line ' + (line.added ? 'added' : line.removed ? 'removed' : '') + '">' +
547
+ escapeHtml(line.content) + '</div>'
548
+ ).join('');
549
+ } else {
550
+ codeContent.innerHTML = '<pre style="padding:8px;white-space:pre-wrap">' + escapeHtml(change.content) + '</pre>';
551
+ }
552
+ };
553
+
554
+ function escapeHtml(str) {
555
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
556
+ }
557
+
558
+ renderRecentChanges();
559
+ </script>
560
+ </body>
561
+ </html>`;
562
+ }
563
+
564
+ stop() {
565
+ if (this.fileWatcher) this.fileWatcher.stop();
566
+ if (this.proxyServer) this.proxyServer.stop();
567
+ killPty();
568
+ if (this.wss) this.wss.close();
569
+ if (this.httpServer) this.httpServer.close();
570
+ logger.info('All services stopped');
571
+ }
572
+ }
573
+
574
+ export function createAggregator(codexBinary, projectRoot) {
575
+ return new Aggregator(codexBinary, projectRoot);
576
+ }
package/src/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ import { spawn } from 'child_process';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { existsSync } from 'fs';
5
+ import { createAggregator } from './aggregator.js';
6
+ import { createLogger } from './lib/logger.js';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const PROXY_PORT = 8080;
10
+
11
+ const logger = createLogger('CLI');
12
+
13
+ async function findCodexBinary() {
14
+ const { execSync } = await import('child_process');
15
+
16
+ logger.debug('Searching for Codex binary...');
17
+
18
+ const candidates = [
19
+ 'D:\\Software\\npm\\codex.cmd',
20
+ 'C:\\Program Files\\nodejs\\codex.cmd',
21
+ join(process.env.APPDATA || '', 'npm', 'codex.cmd'),
22
+ ];
23
+
24
+ for (const candidate of candidates) {
25
+ if (existsSync(candidate)) {
26
+ logger.info(`Found Codex at: ${candidate}`);
27
+ return candidate;
28
+ }
29
+ }
30
+
31
+ try {
32
+ const result = execSync('where codex', { encoding: 'utf-8' });
33
+ const lines = result.trim().split('\n');
34
+ if (lines.length > 0) {
35
+ logger.info(`Found Codex in PATH: ${lines[0].trim()}`);
36
+ return lines[0].trim();
37
+ }
38
+ } catch {}
39
+
40
+ logger.warn('Codex not found, using "codex" command');
41
+ return 'codex';
42
+ }
43
+
44
+ function findProjectRoot() {
45
+ let cwd = process.cwd();
46
+
47
+ while (cwd !== dirname(cwd)) {
48
+ if (existsSync(join(cwd, 'package.json')) ||
49
+ existsSync(join(cwd, '.git')) ||
50
+ existsSync(join(cwd, 'src'))) {
51
+ return cwd;
52
+ }
53
+ cwd = dirname(cwd);
54
+ }
55
+
56
+ return process.cwd();
57
+ }
58
+
59
+ async function main() {
60
+ logger.info('========================================');
61
+ logger.info(' Codex Viewer Starting');
62
+ logger.info('========================================');
63
+
64
+ const projectRoot = findProjectRoot();
65
+ logger.info(`Project root: ${projectRoot}`);
66
+
67
+ const codexBinary = await findCodexBinary();
68
+
69
+ const aggregator = createAggregator(codexBinary, projectRoot);
70
+ await aggregator.start(PROXY_PORT);
71
+
72
+ logger.info('All services started');
73
+ logger.info('Open http://localhost:5173 in browser');
74
+ logger.info('Press Ctrl+C to stop');
75
+
76
+ process.on('SIGINT', () => {
77
+ logger.info('Received SIGINT, shutting down...');
78
+ aggregator.stop();
79
+ process.exit(0);
80
+ });
81
+
82
+ process.on('SIGTERM', () => {
83
+ logger.info('Received SIGTERM, shutting down...');
84
+ aggregator.stop();
85
+ process.exit(0);
86
+ });
87
+ }
88
+
89
+ main().catch((error) => {
90
+ logger.errorWithStack('Fatal error:', error);
91
+ process.exit(1);
92
+ });