gh-here 1.1.0 → 2.0.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.
- package/.channels_cache_v2.json +10882 -0
- package/.claude/settings.local.json +10 -17
- package/.users_cache.json +16187 -0
- package/bin/gh-here.js +8 -9
- package/blog-post.md +100 -0
- package/lib/file-utils.js +45 -1
- package/lib/renderers.js +64 -13
- package/lib/server.js +25 -5
- package/package.json +1 -1
- package/public/styles.css +137 -10
- package/tests/fileTypeDetection.test.js +111 -0
package/bin/gh-here.js
CHANGED
|
@@ -9,7 +9,8 @@ const { setupRoutes } = require('../lib/server');
|
|
|
9
9
|
|
|
10
10
|
// Parse command line arguments
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
|
-
const
|
|
12
|
+
const noOpen = args.includes('--no-open');
|
|
13
|
+
const openBrowser = !noOpen; // Default is to open browser
|
|
13
14
|
const helpRequested = args.includes('--help') || args.includes('-h');
|
|
14
15
|
|
|
15
16
|
// Check for port specification
|
|
@@ -37,17 +38,17 @@ gh-here - GitHub-like local file browser
|
|
|
37
38
|
Usage: npx gh-here [options]
|
|
38
39
|
|
|
39
40
|
Options:
|
|
40
|
-
--open
|
|
41
|
+
--no-open Do not open browser automatically
|
|
41
42
|
--browser=<name> Specify browser (safari, chrome, firefox, arc)
|
|
42
43
|
--port=<number> Specify port number (default: 5555)
|
|
43
44
|
--help, -h Show this help message
|
|
44
45
|
|
|
45
46
|
Examples:
|
|
46
|
-
npx gh-here Start server
|
|
47
|
-
npx gh-here --open
|
|
48
|
-
npx gh-here --port=8080 Start server on port 8080
|
|
49
|
-
npx gh-here --
|
|
50
|
-
npx gh-here --
|
|
47
|
+
npx gh-here Start server and open browser
|
|
48
|
+
npx gh-here --no-open Start server without opening browser
|
|
49
|
+
npx gh-here --port=8080 Start server on port 8080 and open browser
|
|
50
|
+
npx gh-here --browser=safari Start server and open in Safari
|
|
51
|
+
npx gh-here --browser=arc Start server and open in Arc
|
|
51
52
|
`);
|
|
52
53
|
process.exit(0);
|
|
53
54
|
}
|
|
@@ -181,8 +182,6 @@ async function startServer() {
|
|
|
181
182
|
if (openBrowser) {
|
|
182
183
|
console.log(`🌍 Opening browser...`);
|
|
183
184
|
setTimeout(() => openBrowserToUrl(url), 1000);
|
|
184
|
-
} else {
|
|
185
|
-
console.log(`💡 Tip: Use --open flag to launch browser automatically`);
|
|
186
185
|
}
|
|
187
186
|
});
|
|
188
187
|
} catch (error) {
|
package/blog-post.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Why gh-here is the Perfect Tool for Terminal-Heavy Developers
|
|
2
|
+
|
|
3
|
+
If you're like most developers in 2025, you probably spend a lot of time in the terminal. You're using Claude Code to generate and refactor code, running commands left and right, and iterating quickly on projects that haven't even been pushed to GitHub yet. But sometimes you need to step back and get a bird's-eye view of your codebase—and that's where `gh-here` shines.
|
|
4
|
+
|
|
5
|
+
## The Gap Between Terminal and IDE
|
|
6
|
+
|
|
7
|
+
Picture this scenario: You're deep in a terminal session, using Claude Code to implement a new feature. You've got files scattered across multiple directories, you're testing things with curl, and you're in that flow state where opening a heavy IDE would just break your momentum. But you need to:
|
|
8
|
+
|
|
9
|
+
- Quickly browse through your project structure
|
|
10
|
+
- Check what files you've modified
|
|
11
|
+
- Preview a README or markdown file
|
|
12
|
+
- Navigate between related files without losing context
|
|
13
|
+
- See language distribution across your codebase
|
|
14
|
+
|
|
15
|
+
This is the exact gap that `gh-here` fills.
|
|
16
|
+
|
|
17
|
+
## Built for Modern Development Workflows
|
|
18
|
+
|
|
19
|
+
### 1. **Perfect for AI-Assisted Development**
|
|
20
|
+
When you're working with Claude Code or GitHub Copilot, you're often generating and modifying files rapidly. `gh-here` gives you an instant, GitHub-like interface to review what's been changed without committing to version control or opening a full IDE.
|
|
21
|
+
|
|
22
|
+
### 2. **Ideal for Pre-Push Workflows**
|
|
23
|
+
Not everything needs to hit GitHub immediately. Whether you're experimenting with a proof of concept or working on a feature branch, `gh-here` lets you navigate and review your local changes in a familiar, web-based interface.
|
|
24
|
+
|
|
25
|
+
### 3. **Terminal-Native but GUI-Friendly**
|
|
26
|
+
Launch it with a single command (`gh-here`) from any directory, and instantly get a clean, GitHub-inspired interface in your browser. No configuration, no setup—just point and browse.
|
|
27
|
+
|
|
28
|
+
## Real-World Use Cases
|
|
29
|
+
|
|
30
|
+
**Scenario 1: Code Review Before Push**
|
|
31
|
+
```bash
|
|
32
|
+
$ claude "implement user authentication system"
|
|
33
|
+
# ... lots of generated files and changes
|
|
34
|
+
$ gh-here
|
|
35
|
+
# Browse through generated files, check git status, review changes
|
|
36
|
+
$ git add . && git commit -m "Add user authentication"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Scenario 2: Project Exploration**
|
|
40
|
+
```bash
|
|
41
|
+
$ git clone some-interesting-repo
|
|
42
|
+
$ cd some-interesting-repo
|
|
43
|
+
$ gh-here
|
|
44
|
+
# Instantly get an overview: languages used, file structure, README preview
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Scenario 3: Documentation Review**
|
|
48
|
+
```bash
|
|
49
|
+
$ claude "update all the documentation"
|
|
50
|
+
$ gh-here
|
|
51
|
+
# Preview all the markdown files in GitHub-style rendering
|
|
52
|
+
# Check if everything looks good before committing
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why Not Just Use GitHub or an IDE?
|
|
56
|
+
|
|
57
|
+
**GitHub**: Your code isn't pushed yet, or you're working on experimental changes you don't want in version control.
|
|
58
|
+
|
|
59
|
+
**IDE**: Too heavy for quick browsing, breaks your terminal flow, takes time to open and index.
|
|
60
|
+
|
|
61
|
+
**File Explorer**: No syntax highlighting, no git integration, no markdown rendering, no language stats.
|
|
62
|
+
|
|
63
|
+
**Terminal**: Great for editing, terrible for browsing and getting visual context.
|
|
64
|
+
|
|
65
|
+
## The Sweet Spot
|
|
66
|
+
|
|
67
|
+
`gh-here` hits the perfect sweet spot:
|
|
68
|
+
- **Lightweight**: Starts instantly, no heavy indexing
|
|
69
|
+
- **Familiar**: GitHub-style interface that every developer knows
|
|
70
|
+
- **Integrated**: Shows git status, handles various file types, renders markdown
|
|
71
|
+
- **Flexible**: Works with any directory, whether it's a git repo or not
|
|
72
|
+
- **Terminal-friendly**: Launch with one command, works alongside your existing workflow
|
|
73
|
+
|
|
74
|
+
## Features That Just Make Sense
|
|
75
|
+
|
|
76
|
+
- **Language statistics**: Instantly see what technologies your project uses
|
|
77
|
+
- **Git integration**: Visual git status indicators, diff viewing
|
|
78
|
+
- **File type support**: Syntax highlighting, image viewing, markdown rendering
|
|
79
|
+
- **Search functionality**: Quick file search with keyboard shortcuts
|
|
80
|
+
- **Breadcrumb navigation**: Always know where you are in your project
|
|
81
|
+
|
|
82
|
+
## Perfect for 2025 Development
|
|
83
|
+
|
|
84
|
+
As development workflows become more AI-assisted and terminal-centric, tools like `gh-here` become essential. You're not always in an IDE, you're not always ready to push to GitHub, but you always need to understand your codebase structure and changes.
|
|
85
|
+
|
|
86
|
+
It's the missing link between your terminal workflow and the visual context you need to stay productive.
|
|
87
|
+
|
|
88
|
+
## Get Started
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install -g gh-here
|
|
92
|
+
cd your-project
|
|
93
|
+
gh-here
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That's it. Your local codebase is now browsable in a clean, GitHub-style interface at `localhost:5556`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
*Built for developers who live in the terminal but occasionally need a better view of their code.*
|
package/lib/file-utils.js
CHANGED
|
@@ -256,9 +256,53 @@ function formatBytes(bytes) {
|
|
|
256
256
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* File type detection functions
|
|
261
|
+
*/
|
|
262
|
+
function isImageFile(filePathOrExt) {
|
|
263
|
+
const ext = getExtension(filePathOrExt);
|
|
264
|
+
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'];
|
|
265
|
+
return imageExtensions.includes(ext);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isBinaryFile(filePathOrExt) {
|
|
269
|
+
const ext = getExtension(filePathOrExt);
|
|
270
|
+
const binaryExtensions = [
|
|
271
|
+
// Images (handled separately)
|
|
272
|
+
'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico',
|
|
273
|
+
// Archives
|
|
274
|
+
'zip', 'tar', 'gz', 'rar', '7z',
|
|
275
|
+
// Executables
|
|
276
|
+
'exe', 'bin', 'app', 'deb', 'rpm',
|
|
277
|
+
// Documents
|
|
278
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
|
|
279
|
+
// Media
|
|
280
|
+
'mp4', 'mov', 'avi', 'mkv', 'mp3', 'wav', 'flac',
|
|
281
|
+
// Other
|
|
282
|
+
'class', 'so', 'dll', 'dylib'
|
|
283
|
+
];
|
|
284
|
+
return binaryExtensions.includes(ext);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isTextFile(filePathOrExt) {
|
|
288
|
+
return !isBinaryFile(filePathOrExt);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getExtension(filePathOrExt) {
|
|
292
|
+
// If it's already just an extension (no dots), return as-is
|
|
293
|
+
if (!filePathOrExt.includes('.') && !filePathOrExt.includes('/') && !filePathOrExt.includes('\\')) {
|
|
294
|
+
return filePathOrExt.toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
// Extract extension from file path
|
|
297
|
+
return path.extname(filePathOrExt).toLowerCase().slice(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
259
300
|
module.exports = {
|
|
260
301
|
getFileIcon,
|
|
261
302
|
getLanguageFromExtension,
|
|
262
303
|
getLanguageColor,
|
|
263
|
-
formatBytes
|
|
304
|
+
formatBytes,
|
|
305
|
+
isImageFile,
|
|
306
|
+
isBinaryFile,
|
|
307
|
+
isTextFile
|
|
264
308
|
};
|
package/lib/renderers.js
CHANGED
|
@@ -5,7 +5,7 @@ const marked = require('marked');
|
|
|
5
5
|
const octicons = require('@primer/octicons');
|
|
6
6
|
const { exec } = require('child_process');
|
|
7
7
|
|
|
8
|
-
const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes } = require('./file-utils');
|
|
8
|
+
const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes, isImageFile, isBinaryFile, isTextFile } = require('./file-utils');
|
|
9
9
|
const { getGitStatusIcon, getGitStatusDescription } = require('./git');
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,7 +18,8 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
18
18
|
const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch, workingDirName);
|
|
19
19
|
const readmeFile = findReadmeFile(items);
|
|
20
20
|
const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
|
|
21
|
-
|
|
21
|
+
// Only show language stats on root/top-level directory
|
|
22
|
+
const languageStats = (!currentPath || currentPath === '.') ? generateLanguageStats(items) : '';
|
|
22
23
|
|
|
23
24
|
const itemsHtml = items.map(item => `
|
|
24
25
|
<tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
|
|
@@ -113,6 +114,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
113
114
|
${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
|
|
114
115
|
</button>
|
|
115
116
|
` : ''}
|
|
117
|
+
${languageStats}
|
|
116
118
|
</div>
|
|
117
119
|
<div class="repo-controls-right">
|
|
118
120
|
<div class="search-container">
|
|
@@ -130,7 +132,6 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
130
132
|
<main>
|
|
131
133
|
<div class="repo-canvas">
|
|
132
134
|
<div class="repo-canvas-content">
|
|
133
|
-
${languageStats}
|
|
134
135
|
<div class="file-table-container">
|
|
135
136
|
<table class="file-table" id="file-table">
|
|
136
137
|
<thead>
|
|
@@ -242,8 +243,9 @@ function generateLanguageStats(items) {
|
|
|
242
243
|
`;
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
|
|
246
|
-
const
|
|
246
|
+
async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir = null) {
|
|
247
|
+
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
248
|
+
const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
|
|
247
249
|
|
|
248
250
|
// Get git diff for the file
|
|
249
251
|
return new Promise((resolve, reject) => {
|
|
@@ -369,8 +371,10 @@ function renderRawDiff(diffOutput, ext) {
|
|
|
369
371
|
return `<div class="raw-diff-container">${linesHtml}</div>`;
|
|
370
372
|
}
|
|
371
373
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
|
|
375
|
+
async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir = null) {
|
|
376
|
+
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
377
|
+
const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
|
|
374
378
|
let displayContent;
|
|
375
379
|
let viewToggle = '';
|
|
376
380
|
|
|
@@ -378,7 +382,49 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
378
382
|
const absolutePath = path.resolve(path.join(workingDir, filePath));
|
|
379
383
|
const hasGitChanges = gitStatus && gitStatus[absolutePath];
|
|
380
384
|
|
|
381
|
-
|
|
385
|
+
// Determine file category and handle accordingly
|
|
386
|
+
if (isImageFile(ext)) {
|
|
387
|
+
// Handle image files
|
|
388
|
+
const imageUrl = `/download?path=${encodeURIComponent(filePath)}`;
|
|
389
|
+
displayContent = `
|
|
390
|
+
<div class="image-container">
|
|
391
|
+
<img src="${imageUrl}" alt="${path.basename(filePath)}" class="image-display">
|
|
392
|
+
<div class="image-info">
|
|
393
|
+
<span class="image-filename">${path.basename(filePath)}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
// Add diff view for images with git changes
|
|
399
|
+
if (hasGitChanges) {
|
|
400
|
+
const currentParams = new URLSearchParams({ path: filePath });
|
|
401
|
+
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
402
|
+
|
|
403
|
+
viewToggle = `
|
|
404
|
+
<div class="view-toggle">
|
|
405
|
+
<a href="/?path=${encodeURIComponent(filePath)}" class="view-btn active">
|
|
406
|
+
${octicons.eye.toSVG({ class: 'view-icon' })} View
|
|
407
|
+
</a>
|
|
408
|
+
<a href="${diffUrl}" class="view-btn">
|
|
409
|
+
${octicons.diff.toSVG({ class: 'view-icon' })} Diff
|
|
410
|
+
</a>
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
} else if (isBinaryFile(ext) && !isImageFile(ext)) {
|
|
415
|
+
// Handle other binary files - download only
|
|
416
|
+
displayContent = `
|
|
417
|
+
<div class="binary-file-container">
|
|
418
|
+
<div class="binary-file-info">
|
|
419
|
+
<h3>Binary File</h3>
|
|
420
|
+
<p>This is a binary file that cannot be displayed in the browser.</p>
|
|
421
|
+
<a href="/download?path=${encodeURIComponent(filePath)}" class="btn btn-primary" download="${path.basename(filePath)}">
|
|
422
|
+
${octicons.download.toSVG({ class: 'download-icon' })} Download File
|
|
423
|
+
</a>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
`;
|
|
427
|
+
} else if (ext === 'md') {
|
|
382
428
|
if (viewMode === 'raw') {
|
|
383
429
|
const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
|
|
384
430
|
|
|
@@ -468,9 +514,11 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
468
514
|
<div id="filename-input-container" class="filename-input-container" style="display: none;">
|
|
469
515
|
<input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
|
|
470
516
|
</div>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
517
|
+
${isTextFile(ext) ? `
|
|
518
|
+
<button id="edit-btn" class="edit-btn" aria-label="Edit file">
|
|
519
|
+
${octicons.pencil.toSVG({ class: 'edit-icon' })}
|
|
520
|
+
</button>
|
|
521
|
+
` : ''}
|
|
474
522
|
${viewToggle}
|
|
475
523
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
476
524
|
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
@@ -484,6 +532,7 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
484
532
|
<div class="file-content">
|
|
485
533
|
${displayContent}
|
|
486
534
|
</div>
|
|
535
|
+
${isTextFile(ext) ? `
|
|
487
536
|
<div id="editor-container" class="editor-container" style="display: none;">
|
|
488
537
|
<div class="editor-header">
|
|
489
538
|
<div class="editor-title">Edit ${path.basename(filePath)}</div>
|
|
@@ -497,6 +546,7 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
497
546
|
<div id="file-editor" class="monaco-editor"></div>
|
|
498
547
|
</div>
|
|
499
548
|
</div>
|
|
549
|
+
` : ''}
|
|
500
550
|
</div>
|
|
501
551
|
</main>
|
|
502
552
|
</body>
|
|
@@ -504,8 +554,9 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
504
554
|
`;
|
|
505
555
|
}
|
|
506
556
|
|
|
507
|
-
function renderNewFile(currentPath) {
|
|
508
|
-
const
|
|
557
|
+
function renderNewFile(currentPath, workingDir = null) {
|
|
558
|
+
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
559
|
+
const breadcrumbs = generateBreadcrumbs(currentPath, null, workingDirName);
|
|
509
560
|
|
|
510
561
|
return `
|
|
511
562
|
<!DOCTYPE html>
|
package/lib/server.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { getGitStatus, getGitBranch, commitAllChanges, commitSelectedFiles, getGitDiff } = require('./git');
|
|
6
6
|
const { isIgnoredByGitignore, getGitignoreRules } = require('./gitignore');
|
|
7
7
|
const { renderDirectory, renderFileDiff, renderFile, renderNewFile } = require('./renderers');
|
|
8
|
+
const { isImageFile, isBinaryFile } = require('./file-utils');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Express server setup and route handlers
|
|
@@ -16,6 +17,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
16
17
|
app.use('/static', express.static(path.join(__dirname, '..', 'public')));
|
|
17
18
|
app.use('/octicons', express.static(path.join(__dirname, '..', 'node_modules', '@primer', 'octicons', 'build')));
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
// Download route
|
|
20
22
|
app.get('/download', (req, res) => {
|
|
21
23
|
const filePath = req.query.path || '';
|
|
@@ -25,8 +27,16 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
25
27
|
const stats = fs.statSync(fullPath);
|
|
26
28
|
if (stats.isFile()) {
|
|
27
29
|
const fileName = path.basename(fullPath);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
// For images, serve inline for viewing, otherwise force download
|
|
32
|
+
if (isImageFile(fullPath)) {
|
|
33
|
+
// Serve image inline
|
|
34
|
+
res.sendFile(fullPath);
|
|
35
|
+
} else {
|
|
36
|
+
// Force download for non-images
|
|
37
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
38
|
+
res.sendFile(fullPath);
|
|
39
|
+
}
|
|
30
40
|
} else {
|
|
31
41
|
res.status(400).send('Cannot download directories');
|
|
32
42
|
}
|
|
@@ -84,16 +94,21 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
84
94
|
|
|
85
95
|
res.send(renderDirectory(currentPath, items, showGitignored, gitBranch, gitStatus, workingDir));
|
|
86
96
|
} else {
|
|
87
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
88
97
|
const ext = path.extname(fullPath).slice(1);
|
|
89
98
|
const viewMode = req.query.view || 'rendered';
|
|
90
99
|
|
|
100
|
+
// For image files, don't try to read as text
|
|
101
|
+
let content = '';
|
|
102
|
+
if (!isImageFile(fullPath)) {
|
|
103
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
if (viewMode === 'diff' && isGitRepo) {
|
|
92
107
|
// Check if file has git status
|
|
93
108
|
const absolutePath = path.resolve(fullPath);
|
|
94
109
|
const gitInfo = gitStatus[absolutePath];
|
|
95
110
|
if (gitInfo) {
|
|
96
|
-
const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot);
|
|
111
|
+
const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot, workingDir);
|
|
97
112
|
return res.send(diffHtml);
|
|
98
113
|
}
|
|
99
114
|
}
|
|
@@ -108,7 +123,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
108
123
|
// Route for creating new files
|
|
109
124
|
app.get('/new', (req, res) => {
|
|
110
125
|
const currentPath = req.query.path || '';
|
|
111
|
-
res.send(renderNewFile(currentPath));
|
|
126
|
+
res.send(renderNewFile(currentPath, workingDir));
|
|
112
127
|
});
|
|
113
128
|
|
|
114
129
|
// API endpoint to get file content for editing
|
|
@@ -122,6 +137,11 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
122
137
|
return res.status(403).send('Access denied');
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
// Check if it's a binary file - prevent editing
|
|
141
|
+
if (isBinaryFile(fullPath)) {
|
|
142
|
+
return res.status(400).json({ error: 'Cannot edit binary files' });
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
126
146
|
res.send(content);
|
|
127
147
|
} catch (error) {
|
package/package.json
CHANGED
package/public/styles.css
CHANGED
|
@@ -499,29 +499,54 @@ main {
|
|
|
499
499
|
|
|
500
500
|
/* Old search styles removed - using new GitHub-style search */
|
|
501
501
|
|
|
502
|
-
/* Language stats */
|
|
502
|
+
/* Language stats in control bar */
|
|
503
503
|
.language-stats {
|
|
504
504
|
display: flex;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
505
|
+
align-items: center;
|
|
506
|
+
gap: 8px;
|
|
507
|
+
margin-left: 16px;
|
|
508
|
+
max-width: 300px;
|
|
509
|
+
overflow: hidden;
|
|
510
|
+
position: relative;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.language-stats:hover {
|
|
514
|
+
overflow: visible;
|
|
515
|
+
z-index: 10;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.language-stats:hover::before {
|
|
519
|
+
content: '';
|
|
520
|
+
position: absolute;
|
|
521
|
+
top: -4px;
|
|
522
|
+
left: -8px;
|
|
523
|
+
right: -8px;
|
|
524
|
+
bottom: -4px;
|
|
525
|
+
background: var(--bg-secondary);
|
|
526
|
+
border: 1px solid var(--border-primary);
|
|
527
|
+
border-radius: 6px;
|
|
528
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
529
|
+
z-index: -1;
|
|
530
|
+
pointer-events: none;
|
|
510
531
|
}
|
|
511
532
|
|
|
512
533
|
.lang-stat {
|
|
513
534
|
display: flex;
|
|
514
535
|
align-items: center;
|
|
515
|
-
gap:
|
|
516
|
-
font-size:
|
|
536
|
+
gap: 4px;
|
|
537
|
+
font-size: 11px;
|
|
517
538
|
color: var(--text-secondary);
|
|
539
|
+
white-space: nowrap;
|
|
540
|
+
flex-shrink: 0;
|
|
541
|
+
padding: 2px 0;
|
|
518
542
|
}
|
|
519
543
|
|
|
520
544
|
.lang-dot {
|
|
521
|
-
width:
|
|
522
|
-
height:
|
|
545
|
+
width: 8px;
|
|
546
|
+
height: 8px;
|
|
523
547
|
border-radius: 50%;
|
|
524
548
|
display: inline-block;
|
|
549
|
+
flex-shrink: 0;
|
|
525
550
|
}
|
|
526
551
|
|
|
527
552
|
.lang-name {
|
|
@@ -722,6 +747,83 @@ main {
|
|
|
722
747
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
723
748
|
}
|
|
724
749
|
|
|
750
|
+
/* Image viewing styles */
|
|
751
|
+
.image-container {
|
|
752
|
+
padding: 20px;
|
|
753
|
+
text-align: center;
|
|
754
|
+
background-color: var(--bg-primary);
|
|
755
|
+
border-radius: 6px;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.image-display {
|
|
759
|
+
max-width: 100%;
|
|
760
|
+
max-height: 80vh;
|
|
761
|
+
height: auto;
|
|
762
|
+
border-radius: 6px;
|
|
763
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
764
|
+
background: var(--bg-secondary);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
[data-theme="light"] .image-display {
|
|
768
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.image-info {
|
|
772
|
+
margin-top: 16px;
|
|
773
|
+
padding-top: 16px;
|
|
774
|
+
border-top: 1px solid var(--border-primary);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.image-filename {
|
|
778
|
+
font-size: 14px;
|
|
779
|
+
font-weight: 600;
|
|
780
|
+
color: var(--text-primary);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Binary file viewing styles */
|
|
784
|
+
.binary-file-container {
|
|
785
|
+
padding: 40px 20px;
|
|
786
|
+
text-align: center;
|
|
787
|
+
background-color: var(--bg-primary);
|
|
788
|
+
border-radius: 6px;
|
|
789
|
+
border: 1px solid var(--border-primary);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.binary-file-info h3 {
|
|
793
|
+
font-size: 18px;
|
|
794
|
+
font-weight: 600;
|
|
795
|
+
color: var(--text-primary);
|
|
796
|
+
margin-bottom: 12px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.binary-file-info p {
|
|
800
|
+
font-size: 14px;
|
|
801
|
+
color: var(--text-secondary);
|
|
802
|
+
margin-bottom: 20px;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.binary-file-info .btn {
|
|
806
|
+
display: inline-flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
gap: 8px;
|
|
809
|
+
padding: 8px 16px;
|
|
810
|
+
border-radius: 6px;
|
|
811
|
+
font-size: 14px;
|
|
812
|
+
font-weight: 500;
|
|
813
|
+
text-decoration: none;
|
|
814
|
+
border: 1px solid transparent;
|
|
815
|
+
cursor: pointer;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.binary-file-info .btn-primary {
|
|
819
|
+
background-color: var(--link-color);
|
|
820
|
+
color: white;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.binary-file-info .btn-primary:hover {
|
|
824
|
+
opacity: 0.9;
|
|
825
|
+
}
|
|
826
|
+
|
|
725
827
|
/* Line numbers for code viewing */
|
|
726
828
|
.with-line-numbers {
|
|
727
829
|
counter-reset: line;
|
|
@@ -1933,6 +2035,31 @@ main {
|
|
|
1933
2035
|
height: 14px;
|
|
1934
2036
|
}
|
|
1935
2037
|
|
|
2038
|
+
/* General modal overlay */
|
|
2039
|
+
.modal-overlay {
|
|
2040
|
+
position: fixed;
|
|
2041
|
+
top: 0;
|
|
2042
|
+
left: 0;
|
|
2043
|
+
right: 0;
|
|
2044
|
+
bottom: 0;
|
|
2045
|
+
background: rgba(0, 0, 0, 0.7);
|
|
2046
|
+
display: flex;
|
|
2047
|
+
align-items: center;
|
|
2048
|
+
justify-content: center;
|
|
2049
|
+
z-index: 1000;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
.modal-content {
|
|
2053
|
+
background: var(--bg-primary);
|
|
2054
|
+
border: 1px solid var(--border-primary);
|
|
2055
|
+
border-radius: 8px;
|
|
2056
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
|
2057
|
+
padding: 20px;
|
|
2058
|
+
max-width: 90vw;
|
|
2059
|
+
max-height: 90vh;
|
|
2060
|
+
overflow-y: auto;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1936
2063
|
/* Clean commit modal */
|
|
1937
2064
|
.commit-modal-overlay {
|
|
1938
2065
|
position: fixed;
|