codex-lens 0.1.0 → 0.1.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/build.js CHANGED
@@ -18,9 +18,12 @@ async function buildAll() {
18
18
  'src/proxy.js',
19
19
  'src/aggregator.js',
20
20
  'src/watcher.js',
21
+ 'src/pty-manager.js',
22
+ 'src/snapshot-manager.js',
21
23
  'src/lib/sse-parser.js',
22
24
  'src/lib/diff-builder.js',
23
25
  'src/lib/log-manager.js',
26
+ 'src/lib/logger.js',
24
27
  ];
25
28
 
26
29
  for (const file of backendFiles) {
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { appendFileSync, existsSync, mkdirSync } from "fs";
3
+ import { dirname, join, resolve } from "path";
4
+ import { fileURLToPath } from "url";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const LOG_DIR = resolve(__dirname, "../../logs");
8
+ const LEVELS = {
9
+ DEBUG: 0,
10
+ INFO: 1,
11
+ WARN: 2,
12
+ ERROR: 3
13
+ };
14
+ let sharedLogFile = null;
15
+ let sharedInitDone = false;
16
+ class Logger {
17
+ constructor(moduleName, level = "INFO") {
18
+ this.moduleName = moduleName;
19
+ this.level = LEVELS[level] ?? LEVELS.INFO;
20
+ }
21
+ init() {
22
+ if (!sharedInitDone) {
23
+ if (!existsSync(LOG_DIR)) {
24
+ mkdirSync(LOG_DIR, { recursive: true });
25
+ }
26
+ const now = /* @__PURE__ */ new Date();
27
+ const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
28
+ sharedLogFile = join(LOG_DIR, `${timestamp}.txt`);
29
+ appendFileSync(sharedLogFile, `
30
+ ========== Session Started: ${now.toISOString()} ==========
31
+ `);
32
+ sharedInitDone = true;
33
+ }
34
+ }
35
+ _format(level, message) {
36
+ const now = /* @__PURE__ */ new Date();
37
+ const timestamp = now.toISOString();
38
+ return `[${timestamp}] [${level}] [${this.moduleName}] ${message}`;
39
+ }
40
+ _write(level, message) {
41
+ if (LEVELS[level] < this.level) return;
42
+ const formatted = this._format(level, message);
43
+ console.log(formatted);
44
+ if (sharedLogFile) {
45
+ try {
46
+ appendFileSync(sharedLogFile, formatted + "\n");
47
+ } catch (e) {
48
+ console.error("Failed to write to log file:", e);
49
+ }
50
+ }
51
+ }
52
+ debug(message) {
53
+ this._write("DEBUG", message);
54
+ }
55
+ info(message) {
56
+ this._write("INFO", message);
57
+ }
58
+ warn(message) {
59
+ this._write("WARN", message);
60
+ }
61
+ error(message) {
62
+ this._write("ERROR", message);
63
+ }
64
+ errorWithStack(message, error) {
65
+ const stack = error?.stack || error?.message || error || "";
66
+ this._write("ERROR", `${message}
67
+ ${stack}`);
68
+ }
69
+ }
70
+ function createLogger(moduleName, level = "INFO") {
71
+ const logger = new Logger(moduleName, level);
72
+ logger.init();
73
+ return logger;
74
+ }
75
+ export {
76
+ createLogger
77
+ };
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { join, dirname } from "node:path";
4
+ import { platform, arch } from "node:os";
5
+ import { chmodSync, statSync } from "node:fs";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ let ptyProcess = null;
9
+ let dataListeners = [];
10
+ let exitListeners = [];
11
+ let lastExitCode = null;
12
+ let outputBuffer = "";
13
+ let lastPtyCols = 120;
14
+ let lastPtyRows = 30;
15
+ const MAX_BUFFER = 2e5;
16
+ let batchBuffer = "";
17
+ let batchScheduled = false;
18
+ async function getPty() {
19
+ const ptyMod = await import("node-pty");
20
+ return ptyMod.default || ptyMod;
21
+ }
22
+ function findSafeSliceStart(buf, rawStart) {
23
+ const scanLimit = Math.min(rawStart + 64, buf.length);
24
+ let i = rawStart;
25
+ while (i < scanLimit) {
26
+ const ch = buf.charCodeAt(i);
27
+ if (ch === 27) {
28
+ let j = i + 1;
29
+ while (j < scanLimit && !(buf.charCodeAt(j) >= 64 && buf.charCodeAt(j) <= 126 && j > i + 1)) {
30
+ j++;
31
+ }
32
+ if (j < scanLimit) {
33
+ return j + 1;
34
+ }
35
+ i = j;
36
+ continue;
37
+ }
38
+ if (ch >= 32 && ch <= 63) {
39
+ i++;
40
+ continue;
41
+ }
42
+ break;
43
+ }
44
+ return i < buf.length ? i : rawStart;
45
+ }
46
+ function flushBatch() {
47
+ batchScheduled = false;
48
+ if (!batchBuffer) return;
49
+ const chunk = batchBuffer;
50
+ batchBuffer = "";
51
+ for (const cb of dataListeners) {
52
+ try {
53
+ cb(chunk);
54
+ } catch {
55
+ }
56
+ }
57
+ }
58
+ function fixSpawnHelperPermissions() {
59
+ try {
60
+ const os = platform();
61
+ const cpu = arch();
62
+ const helperPath = join(__dirname, "node_modules", "node-pty", "prebuilds", `${os}-${cpu}`, "spawn-helper");
63
+ const stat = statSync(helperPath);
64
+ if (!(stat.mode & 73)) {
65
+ chmodSync(helperPath, stat.mode | 493);
66
+ }
67
+ } catch {
68
+ }
69
+ }
70
+ async function spawnCodex(codexBinary, projectRoot, proxyPort) {
71
+ if (ptyProcess) {
72
+ killPty();
73
+ }
74
+ const pty = await getPty();
75
+ fixSpawnHelperPermissions();
76
+ const shell = platform() === "win32" ? "powershell.exe" : "bash";
77
+ const args = platform() === "win32" ? ["-NoExit", "-Command", `Set-Location "${projectRoot}"; & "${codexBinary}"`] : [];
78
+ const env = { ...process.env };
79
+ env.OPENAI_BASE_URL = `http://127.0.0.1:${proxyPort}`;
80
+ if (platform() === "win32") {
81
+ env.WINPTY = "1";
82
+ }
83
+ lastExitCode = null;
84
+ outputBuffer = "";
85
+ ptyProcess = pty.spawn(shell, args, {
86
+ name: "xterm-256color",
87
+ cols: lastPtyCols,
88
+ rows: lastPtyRows,
89
+ cwd: projectRoot,
90
+ env,
91
+ useConpty: false
92
+ });
93
+ ptyProcess.onData((data) => {
94
+ outputBuffer += data;
95
+ if (outputBuffer.length > MAX_BUFFER) {
96
+ const rawStart = outputBuffer.length - MAX_BUFFER;
97
+ const safeStart = findSafeSliceStart(outputBuffer, rawStart);
98
+ outputBuffer = outputBuffer.slice(safeStart);
99
+ }
100
+ batchBuffer += data;
101
+ if (!batchScheduled) {
102
+ batchScheduled = true;
103
+ setImmediate(flushBatch);
104
+ }
105
+ });
106
+ ptyProcess.onExit(({ exitCode }) => {
107
+ flushBatch();
108
+ lastExitCode = exitCode;
109
+ ptyProcess = null;
110
+ for (const cb of exitListeners) {
111
+ try {
112
+ cb(exitCode);
113
+ } catch {
114
+ }
115
+ }
116
+ });
117
+ return ptyProcess;
118
+ }
119
+ function writeToPty(data) {
120
+ if (ptyProcess) {
121
+ ptyProcess.write(data);
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+ function resizePty(cols, rows) {
127
+ lastPtyCols = cols;
128
+ lastPtyRows = rows;
129
+ if (ptyProcess) {
130
+ try {
131
+ ptyProcess.resize(cols, rows);
132
+ } catch {
133
+ }
134
+ }
135
+ }
136
+ function killPty() {
137
+ if (ptyProcess) {
138
+ flushBatch();
139
+ batchBuffer = "";
140
+ batchScheduled = false;
141
+ try {
142
+ ptyProcess.kill();
143
+ } catch {
144
+ }
145
+ ptyProcess = null;
146
+ }
147
+ }
148
+ function onPtyData(cb) {
149
+ dataListeners.push(cb);
150
+ return () => {
151
+ dataListeners = dataListeners.filter((l) => l !== cb);
152
+ };
153
+ }
154
+ function onPtyExit(cb) {
155
+ exitListeners.push(cb);
156
+ return () => {
157
+ exitListeners = exitListeners.filter((l) => l !== cb);
158
+ };
159
+ }
160
+ function getPtyPid() {
161
+ return ptyProcess ? ptyProcess.pid : null;
162
+ }
163
+ function getPtyState() {
164
+ return {
165
+ running: !!ptyProcess,
166
+ exitCode: lastExitCode
167
+ };
168
+ }
169
+ function getOutputBuffer() {
170
+ return outputBuffer;
171
+ }
172
+ export {
173
+ getOutputBuffer,
174
+ getPtyPid,
175
+ getPtyState,
176
+ killPty,
177
+ onPtyData,
178
+ onPtyExit,
179
+ resizePty,
180
+ spawnCodex,
181
+ writeToPty
182
+ };
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, cpSync, rmSync, existsSync } from "fs";
3
+ import { join, relative, extname } from "path";
4
+ import { createLogger } from "./lib/logger.js";
5
+ const logger = createLogger("SnapshotManager");
6
+ const IGNORED_DIRS = [
7
+ "node_modules",
8
+ ".git",
9
+ ".svn",
10
+ ".hg",
11
+ ".idea",
12
+ ".vscode",
13
+ "dist",
14
+ "build",
15
+ ".cache",
16
+ "__pycache__",
17
+ ".pytest_cache",
18
+ ".next",
19
+ ".nuxt",
20
+ ".venv",
21
+ ".env",
22
+ ".DS_Store",
23
+ ".codex-viewer"
24
+ ];
25
+ const IGNORED_FILES = [
26
+ ".DS_Store",
27
+ "Thumbs.db",
28
+ "desktop.ini"
29
+ ];
30
+ const IGNORED_EXTENSIONS = [
31
+ ".log",
32
+ ".lock"
33
+ ];
34
+ class SnapshotManager {
35
+ constructor(snapshotsDir = ".codex-viewer/snapshots") {
36
+ this.snapshotsDir = snapshotsDir;
37
+ }
38
+ async createSnapshot(projectRoot, taskId) {
39
+ const snapshotPath = join(this.snapshotsDir, taskId);
40
+ const metadataPath = join(snapshotPath, "snapshot.json");
41
+ logger.info(`Creating snapshot: ${taskId}`);
42
+ try {
43
+ if (!existsSync(this.snapshotsDir)) {
44
+ mkdirSync(this.snapshotsDir, { recursive: true });
45
+ }
46
+ if (existsSync(snapshotPath)) {
47
+ rmSync(snapshotPath, { recursive: true, force: true });
48
+ }
49
+ mkdirSync(snapshotPath, { recursive: true });
50
+ const files = this.scanProjectFiles(projectRoot);
51
+ const metadata = {
52
+ taskId,
53
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
54
+ projectRoot,
55
+ filesCount: files.length,
56
+ files: []
57
+ };
58
+ for (const file of files) {
59
+ const relativePath = relative(projectRoot, file);
60
+ const destPath = join(snapshotPath, relativePath);
61
+ const destDir = join(snapshotPath, relative(projectRoot, join(file, "..")));
62
+ if (!existsSync(destDir)) {
63
+ mkdirSync(destDir, { recursive: true });
64
+ }
65
+ try {
66
+ cpSync(file, destPath, { force: true });
67
+ const stats = statSync(file);
68
+ metadata.files.push({
69
+ path: relativePath,
70
+ size: stats.size,
71
+ mtime: stats.mtime.toISOString()
72
+ });
73
+ } catch (err) {
74
+ logger.warn(`Failed to copy file ${file}: ${err.message}`);
75
+ }
76
+ }
77
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
78
+ logger.info(`Snapshot created: ${taskId}, ${files.length} files`);
79
+ return { success: true, snapshotPath, filesCount: files.length };
80
+ } catch (error) {
81
+ logger.error(`Failed to create snapshot: ${error.message}`);
82
+ return { success: false, error: error.message };
83
+ }
84
+ }
85
+ async restoreSnapshot(taskId) {
86
+ const snapshotPath = join(this.snapshotsDir, taskId);
87
+ const metadataPath = join(snapshotPath, "snapshot.json");
88
+ logger.info(`Restoring snapshot: ${taskId}`);
89
+ try {
90
+ if (!existsSync(snapshotPath)) {
91
+ return { success: false, error: "Snapshot not found" };
92
+ }
93
+ if (!existsSync(metadataPath)) {
94
+ return { success: false, error: "Snapshot metadata not found" };
95
+ }
96
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
97
+ const projectRoot = metadata.projectRoot;
98
+ let restoredCount = 0;
99
+ for (const fileInfo of metadata.files) {
100
+ const sourcePath = join(snapshotPath, fileInfo.path);
101
+ const destPath = join(projectRoot, fileInfo.path);
102
+ if (!existsSync(sourcePath)) {
103
+ continue;
104
+ }
105
+ const destDir = join(projectRoot, fileInfo.path.replace(/[^/\\]+$/, ""));
106
+ if (!existsSync(destDir)) {
107
+ mkdirSync(destDir, { recursive: true });
108
+ }
109
+ try {
110
+ cpSync(sourcePath, destPath, { force: true });
111
+ restoredCount++;
112
+ } catch (err) {
113
+ logger.warn(`Failed to restore file ${fileInfo.path}: ${err.message}`);
114
+ }
115
+ }
116
+ logger.info(`Snapshot restored: ${taskId}, ${restoredCount} files`);
117
+ return { success: true, restoredCount };
118
+ } catch (error) {
119
+ logger.error(`Failed to restore snapshot: ${error.message}`);
120
+ return { success: false, error: error.message };
121
+ }
122
+ }
123
+ async deleteSnapshot(taskId) {
124
+ const snapshotPath = join(this.snapshotsDir, taskId);
125
+ logger.info(`Deleting snapshot: ${taskId}`);
126
+ try {
127
+ if (existsSync(snapshotPath)) {
128
+ rmSync(snapshotPath, { recursive: true, force: true });
129
+ logger.info(`Snapshot deleted: ${taskId}`);
130
+ return { success: true };
131
+ }
132
+ return { success: false, error: "Snapshot not found" };
133
+ } catch (error) {
134
+ logger.error(`Failed to delete snapshot: ${error.message}`);
135
+ return { success: false, error: error.message };
136
+ }
137
+ }
138
+ getSnapshot(taskId) {
139
+ const snapshotPath = join(this.snapshotsDir, taskId);
140
+ const metadataPath = join(snapshotPath, "snapshot.json");
141
+ if (!existsSync(metadataPath)) {
142
+ return null;
143
+ }
144
+ try {
145
+ return JSON.parse(readFileSync(metadataPath, "utf-8"));
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ listSnapshots() {
151
+ try {
152
+ if (!existsSync(this.snapshotsDir)) {
153
+ return [];
154
+ }
155
+ const entries = readdirSync(this.snapshotsDir, { withFileTypes: true });
156
+ const snapshots = [];
157
+ for (const entry of entries) {
158
+ if (entry.isDirectory()) {
159
+ const metadata = this.getSnapshot(entry.name);
160
+ if (metadata) {
161
+ snapshots.push(metadata);
162
+ }
163
+ }
164
+ }
165
+ return snapshots.sort(
166
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
167
+ );
168
+ } catch (error) {
169
+ logger.error(`Failed to list snapshots: ${error.message}`);
170
+ return [];
171
+ }
172
+ }
173
+ scanProjectFiles(projectRoot) {
174
+ const files = [];
175
+ const scan = (dir) => {
176
+ try {
177
+ const entries = readdirSync(dir, { withFileTypes: true });
178
+ for (const entry of entries) {
179
+ const fullPath = join(dir, entry.name);
180
+ if (entry.isDirectory()) {
181
+ if (!IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith(".")) {
182
+ scan(fullPath);
183
+ }
184
+ } else if (entry.isFile()) {
185
+ if (IGNORED_FILES.includes(entry.name)) {
186
+ continue;
187
+ }
188
+ if (IGNORED_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
189
+ continue;
190
+ }
191
+ files.push(fullPath);
192
+ }
193
+ }
194
+ } catch (err) {
195
+ logger.warn(`Failed to scan directory ${dir}: ${err.message}`);
196
+ }
197
+ };
198
+ scan(projectRoot);
199
+ return files;
200
+ }
201
+ }
202
+ function createSnapshotManager(snapshotsDir) {
203
+ return new SnapshotManager(snapshotsDir);
204
+ }
205
+ export {
206
+ SnapshotManager,
207
+ createSnapshotManager
208
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-lens",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A visualization tool for Codex that monitors API requests and file system changes with task snapshot rollback",
5
5
  "license": "MIT",
6
6
  "type": "module",