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,230 @@
1
+ import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, cpSync, rmSync, existsSync } from 'fs';
2
+ import { join, relative, extname } from 'path';
3
+ import { createLogger } from './lib/logger.js';
4
+
5
+ const logger = createLogger('SnapshotManager');
6
+
7
+ const IGNORED_DIRS = [
8
+ 'node_modules', '.git', '.svn', '.hg',
9
+ '.idea', '.vscode', 'dist', 'build', '.cache',
10
+ '__pycache__', '.pytest_cache', '.next', '.nuxt',
11
+ '.venv', '.env', '.DS_Store', '.codex-viewer'
12
+ ];
13
+
14
+ const IGNORED_FILES = [
15
+ '.DS_Store', 'Thumbs.db', 'desktop.ini'
16
+ ];
17
+
18
+ const IGNORED_EXTENSIONS = [
19
+ '.log', '.lock'
20
+ ];
21
+
22
+ export class SnapshotManager {
23
+ constructor(snapshotsDir = '.codex-viewer/snapshots') {
24
+ this.snapshotsDir = snapshotsDir;
25
+ }
26
+
27
+ async createSnapshot(projectRoot, taskId) {
28
+ const snapshotPath = join(this.snapshotsDir, taskId);
29
+ const metadataPath = join(snapshotPath, 'snapshot.json');
30
+
31
+ logger.info(`Creating snapshot: ${taskId}`);
32
+
33
+ try {
34
+ if (!existsSync(this.snapshotsDir)) {
35
+ mkdirSync(this.snapshotsDir, { recursive: true });
36
+ }
37
+
38
+ if (existsSync(snapshotPath)) {
39
+ rmSync(snapshotPath, { recursive: true, force: true });
40
+ }
41
+ mkdirSync(snapshotPath, { recursive: true });
42
+
43
+ const files = this.scanProjectFiles(projectRoot);
44
+ const metadata = {
45
+ taskId,
46
+ createdAt: new Date().toISOString(),
47
+ projectRoot,
48
+ filesCount: files.length,
49
+ files: []
50
+ };
51
+
52
+ for (const file of files) {
53
+ const relativePath = relative(projectRoot, file);
54
+ const destPath = join(snapshotPath, relativePath);
55
+ const destDir = join(snapshotPath, relative(projectRoot, join(file, '..')));
56
+
57
+ if (!existsSync(destDir)) {
58
+ mkdirSync(destDir, { recursive: true });
59
+ }
60
+
61
+ try {
62
+ cpSync(file, destPath, { force: true });
63
+ const stats = statSync(file);
64
+ metadata.files.push({
65
+ path: relativePath,
66
+ size: stats.size,
67
+ mtime: stats.mtime.toISOString()
68
+ });
69
+ } catch (err) {
70
+ logger.warn(`Failed to copy file ${file}: ${err.message}`);
71
+ }
72
+ }
73
+
74
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
75
+
76
+ logger.info(`Snapshot created: ${taskId}, ${files.length} files`);
77
+ return { success: true, snapshotPath, filesCount: files.length };
78
+
79
+ } catch (error) {
80
+ logger.error(`Failed to create snapshot: ${error.message}`);
81
+ return { success: false, error: error.message };
82
+ }
83
+ }
84
+
85
+ async restoreSnapshot(taskId) {
86
+ const snapshotPath = join(this.snapshotsDir, taskId);
87
+ const metadataPath = join(snapshotPath, 'snapshot.json');
88
+
89
+ logger.info(`Restoring snapshot: ${taskId}`);
90
+
91
+ try {
92
+ if (!existsSync(snapshotPath)) {
93
+ return { success: false, error: 'Snapshot not found' };
94
+ }
95
+
96
+ if (!existsSync(metadataPath)) {
97
+ return { success: false, error: 'Snapshot metadata not found' };
98
+ }
99
+
100
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
101
+ const projectRoot = metadata.projectRoot;
102
+ let restoredCount = 0;
103
+
104
+ for (const fileInfo of metadata.files) {
105
+ const sourcePath = join(snapshotPath, fileInfo.path);
106
+ const destPath = join(projectRoot, fileInfo.path);
107
+
108
+ if (!existsSync(sourcePath)) {
109
+ continue;
110
+ }
111
+
112
+ const destDir = join(projectRoot, fileInfo.path.replace(/[^/\\]+$/, ''));
113
+ if (!existsSync(destDir)) {
114
+ mkdirSync(destDir, { recursive: true });
115
+ }
116
+
117
+ try {
118
+ cpSync(sourcePath, destPath, { force: true });
119
+ restoredCount++;
120
+ } catch (err) {
121
+ logger.warn(`Failed to restore file ${fileInfo.path}: ${err.message}`);
122
+ }
123
+ }
124
+
125
+ logger.info(`Snapshot restored: ${taskId}, ${restoredCount} files`);
126
+ return { success: true, restoredCount };
127
+
128
+ } catch (error) {
129
+ logger.error(`Failed to restore snapshot: ${error.message}`);
130
+ return { success: false, error: error.message };
131
+ }
132
+ }
133
+
134
+ async deleteSnapshot(taskId) {
135
+ const snapshotPath = join(this.snapshotsDir, taskId);
136
+
137
+ logger.info(`Deleting snapshot: ${taskId}`);
138
+
139
+ try {
140
+ if (existsSync(snapshotPath)) {
141
+ rmSync(snapshotPath, { recursive: true, force: true });
142
+ logger.info(`Snapshot deleted: ${taskId}`);
143
+ return { success: true };
144
+ }
145
+ return { success: false, error: 'Snapshot not found' };
146
+ } catch (error) {
147
+ logger.error(`Failed to delete snapshot: ${error.message}`);
148
+ return { success: false, error: error.message };
149
+ }
150
+ }
151
+
152
+ getSnapshot(taskId) {
153
+ const snapshotPath = join(this.snapshotsDir, taskId);
154
+ const metadataPath = join(snapshotPath, 'snapshot.json');
155
+
156
+ if (!existsSync(metadataPath)) {
157
+ return null;
158
+ }
159
+
160
+ try {
161
+ return JSON.parse(readFileSync(metadataPath, 'utf-8'));
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ listSnapshots() {
168
+ try {
169
+ if (!existsSync(this.snapshotsDir)) {
170
+ return [];
171
+ }
172
+
173
+ const entries = readdirSync(this.snapshotsDir, { withFileTypes: true });
174
+ const snapshots = [];
175
+
176
+ for (const entry of entries) {
177
+ if (entry.isDirectory()) {
178
+ const metadata = this.getSnapshot(entry.name);
179
+ if (metadata) {
180
+ snapshots.push(metadata);
181
+ }
182
+ }
183
+ }
184
+
185
+ return snapshots.sort((a, b) =>
186
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
187
+ );
188
+ } catch (error) {
189
+ logger.error(`Failed to list snapshots: ${error.message}`);
190
+ return [];
191
+ }
192
+ }
193
+
194
+ scanProjectFiles(projectRoot) {
195
+ const files = [];
196
+
197
+ const scan = (dir) => {
198
+ try {
199
+ const entries = readdirSync(dir, { withFileTypes: true });
200
+
201
+ for (const entry of entries) {
202
+ const fullPath = join(dir, entry.name);
203
+
204
+ if (entry.isDirectory()) {
205
+ if (!IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith('.')) {
206
+ scan(fullPath);
207
+ }
208
+ } else if (entry.isFile()) {
209
+ if (IGNORED_FILES.includes(entry.name)) {
210
+ continue;
211
+ }
212
+ if (IGNORED_EXTENSIONS.some(ext => entry.name.endsWith(ext))) {
213
+ continue;
214
+ }
215
+ files.push(fullPath);
216
+ }
217
+ }
218
+ } catch (err) {
219
+ logger.warn(`Failed to scan directory ${dir}: ${err.message}`);
220
+ }
221
+ };
222
+
223
+ scan(projectRoot);
224
+ return files;
225
+ }
226
+ }
227
+
228
+ export function createSnapshotManager(snapshotsDir) {
229
+ return new SnapshotManager(snapshotsDir);
230
+ }
package/src/watcher.js ADDED
@@ -0,0 +1,218 @@
1
+ import chokidar from 'chokidar';
2
+ import { readFileSync, statSync, readdirSync } from 'fs';
3
+ import { diffLines } from 'diff';
4
+ import { extname, basename, join, relative } from 'path';
5
+ import { createLogger } from './lib/logger.js';
6
+
7
+ const logger = createLogger('Watcher');
8
+
9
+ const IGNORED_DIRS = [
10
+ 'node_modules', '.git', '.svn', '.hg',
11
+ '.idea', '.vscode', 'dist', 'build', '.cache',
12
+ '__pycache__', '.pytest_cache', '.next', '.nuxt',
13
+ '.venv', '.env', '.DS_Store'
14
+ ];
15
+
16
+ const IGNORED_FILES = [
17
+ '.DS_Store', 'Thumbs.db', 'desktop.ini'
18
+ ];
19
+
20
+ export function scanDirectory(projectPath, relativeTo = null) {
21
+ const result = [];
22
+
23
+ try {
24
+ const entries = readdirSync(projectPath, { withFileTypes: true });
25
+
26
+ for (const entry of entries) {
27
+ const fullPath = join(projectPath, entry.name);
28
+
29
+ if (entry.isDirectory()) {
30
+ if (IGNORED_DIRS.includes(entry.name)) continue;
31
+
32
+ const children = scanDirectory(fullPath, relativeTo);
33
+ if (children.length > 0 || !IGNORED_DIRS.includes(entry.name)) {
34
+ result.push({
35
+ name: entry.name,
36
+ path: relativeTo ? relative(relativeTo, fullPath) : fullPath,
37
+ type: 'directory',
38
+ children: children
39
+ });
40
+ }
41
+ } else if (entry.isFile()) {
42
+ if (IGNORED_FILES.includes(entry.name)) continue;
43
+ if (entry.name.endsWith('.log')) continue;
44
+
45
+ result.push({
46
+ name: entry.name,
47
+ path: relativeTo ? relative(relativeTo, fullPath) : fullPath,
48
+ type: 'file'
49
+ });
50
+ }
51
+ }
52
+
53
+ result.sort((a, b) => {
54
+ if (a.type !== b.type) {
55
+ return a.type === 'directory' ? -1 : 1;
56
+ }
57
+ return a.name.localeCompare(b.name);
58
+ });
59
+
60
+ } catch (error) {
61
+ logger.error(`Error scanning directory ${projectPath}: ${error.message}`);
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ const CODE_EXTENSIONS = new Set([
68
+ '.js', '.jsx', '.ts', '.tsx', '.json',
69
+ '.py', '.rb', '.go', '.rs', '.java',
70
+ '.c', '.cpp', '.h', '.hpp', '.cs',
71
+ '.php', '.swift', '.kt', '.scala',
72
+ '.html', '.css', '.scss', '.less',
73
+ '.md', '.yaml', '.yml', '.toml',
74
+ '.sh', '.bash', '.zsh', '.ps1',
75
+ '.sql', '.xml', '.vue', '.svelte'
76
+ ]);
77
+
78
+ class FileWatcher {
79
+ constructor(projectPath, wsEmitter) {
80
+ this.projectPath = projectPath;
81
+ this.wsEmitter = wsEmitter;
82
+ this.watcher = null;
83
+ this.fileContents = new Map();
84
+ }
85
+
86
+ async start() {
87
+ const ignored = [
88
+ '**/node_modules/**',
89
+ '**/.git/**',
90
+ '**/dist/**',
91
+ '**/build/**',
92
+ '**/.cache/**',
93
+ '**/__pycache__/**',
94
+ '**/.*/**'
95
+ ].concat(IGNORED_DIRS.map(d => `**/${d}/**`));
96
+
97
+ logger.info(`Starting file watcher on: ${this.projectPath}`);
98
+
99
+ this.watcher = chokidar.watch(this.projectPath, {
100
+ ignored,
101
+ persistent: true,
102
+ ignoreInitial: true,
103
+ awaitWriteFinish: {
104
+ stabilityThreshold: 300,
105
+ pollInterval: 100
106
+ }
107
+ });
108
+
109
+ this.watcher.on('add', (filePath) => this.handleChange(filePath, 'add'));
110
+ this.watcher.on('change', (filePath) => this.handleChange(filePath, 'change'));
111
+ this.watcher.on('unlink', (filePath) => this.handleUnlink(filePath));
112
+ this.watcher.on('error', (error) => logger.errorWithStack('Watcher error:', error));
113
+
114
+ logger.info('File watcher started successfully');
115
+
116
+ return this.projectPath;
117
+ }
118
+
119
+ handleChange(filePath, eventType) {
120
+ try {
121
+ if (!this.isCodeFile(filePath)) return;
122
+
123
+ const stats = statSync(filePath);
124
+ if (stats.size > 5 * 1024 * 1024) {
125
+ logger.warn(`Skipping large file: ${filePath}`);
126
+ return;
127
+ }
128
+
129
+ const newContent = readFileSync(filePath, 'utf-8');
130
+ const oldContent = this.fileContents.get(filePath) || '';
131
+
132
+ this.fileContents.set(filePath, newContent);
133
+
134
+ if (eventType === 'add') {
135
+ logger.info(`File added: ${filePath}`);
136
+ const lines = newContent.split('\n');
137
+ const diff = lines.map(line => ({ content: line, added: true }));
138
+ this.emitFileChange(filePath, newContent, diff);
139
+ } else {
140
+ logger.info(`File changed: ${filePath}`);
141
+ const changes = diffLines(oldContent, newContent);
142
+ const diff = [];
143
+
144
+ for (const change of changes) {
145
+ const lines = change.value.split('\n').filter(l => l !== '');
146
+ for (const line of lines) {
147
+ diff.push({
148
+ content: line,
149
+ added: change.added,
150
+ removed: change.removed
151
+ });
152
+ }
153
+ }
154
+
155
+ this.emitFileChange(filePath, newContent, diff);
156
+ }
157
+ } catch (error) {
158
+ logger.errorWithStack(`Error handling file change: ${filePath}`, error);
159
+ }
160
+ }
161
+
162
+ handleUnlink(filePath) {
163
+ this.fileContents.delete(filePath);
164
+ logger.info(`File deleted: ${filePath}`);
165
+ this.wsEmitter({
166
+ type: 'file_delete',
167
+ data: {
168
+ path: filePath,
169
+ timestamp: new Date().toISOString()
170
+ }
171
+ });
172
+ }
173
+
174
+ emitFileChange(filePath, newContent, diff) {
175
+ this.wsEmitter({
176
+ type: 'file_change',
177
+ data: {
178
+ path: filePath,
179
+ fileName: basename(filePath),
180
+ extension: extname(filePath),
181
+ newContent,
182
+ diff,
183
+ timestamp: new Date().toISOString()
184
+ }
185
+ });
186
+ }
187
+
188
+ isCodeFile(filePath) {
189
+ const ext = extname(filePath).toLowerCase();
190
+ return CODE_EXTENSIONS.has(ext);
191
+ }
192
+
193
+ readFile(filePath) {
194
+ try {
195
+ const stats = statSync(filePath);
196
+ if (stats.size > 5 * 1024 * 1024) {
197
+ return { error: 'File too large' };
198
+ }
199
+ const content = readFileSync(filePath, 'utf-8');
200
+ return { content, path: filePath };
201
+ } catch (error) {
202
+ return { error: error.message };
203
+ }
204
+ }
205
+
206
+ stop() {
207
+ if (this.watcher) {
208
+ this.watcher.close();
209
+ logger.info('File watcher stopped');
210
+ }
211
+ }
212
+ }
213
+
214
+ export function createFileWatcher(projectPath, wsEmitter) {
215
+ return new FileWatcher(projectPath, wsEmitter);
216
+ }
217
+
218
+ export { FileWatcher };
package/vite.config.js ADDED
@@ -0,0 +1,39 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { resolve } from 'path';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ base: './',
8
+ root: 'src',
9
+ build: {
10
+ outDir: '../dist',
11
+ emptyOutDir: true,
12
+ rollupOptions: {
13
+ input: {
14
+ main: resolve(__dirname, 'src/index.html'),
15
+ },
16
+ },
17
+ },
18
+ server: {
19
+ port: 5173,
20
+ strictPort: true,
21
+ proxy: {
22
+ '/ws': {
23
+ target: 'http://localhost:5174',
24
+ ws: true,
25
+ },
26
+ '/api': {
27
+ target: 'http://localhost:5174',
28
+ },
29
+ '/lib/xterm': {
30
+ target: 'http://localhost:5174',
31
+ },
32
+ },
33
+ },
34
+ resolve: {
35
+ alias: {
36
+ '@': resolve(__dirname, 'src'),
37
+ },
38
+ },
39
+ });