gh-here 1.0.2 → 1.0.5
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/.claude/settings.local.json +16 -10
- package/README.md +6 -3
- package/bin/gh-here.js +49 -1258
- package/lib/file-utils.js +264 -0
- package/lib/git.js +207 -0
- package/lib/gitignore.js +91 -0
- package/lib/renderers.js +569 -0
- package/lib/server.js +391 -0
- package/package.json +1 -1
- package/public/app.js +692 -129
- package/public/styles.css +414 -44
- package/tests/draftManager.test.js +241 -0
- package/tests/httpService.test.js +268 -0
- package/tests/languageDetection.test.js +145 -0
- package/tests/pathUtils.test.js +136 -0
package/bin/gh-here.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const express = require('express');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const hljs = require('highlight.js');
|
|
7
|
-
const marked = require('marked');
|
|
8
|
-
const octicons = require('@primer/octicons');
|
|
9
4
|
const { exec } = require('child_process');
|
|
10
5
|
|
|
6
|
+
// Import our modularized components
|
|
7
|
+
const { findGitRepo } = require('../lib/git');
|
|
8
|
+
const { setupRoutes } = require('../lib/server');
|
|
9
|
+
|
|
11
10
|
// Parse command line arguments
|
|
12
11
|
const args = process.argv.slice(2);
|
|
13
12
|
const openBrowser = args.includes('--open') || args.includes('-o');
|
|
14
13
|
const helpRequested = args.includes('--help') || args.includes('-h');
|
|
15
14
|
|
|
15
|
+
// Check for port specification
|
|
16
|
+
let specifiedPort = null;
|
|
17
|
+
const portArg = args.find(arg => arg.startsWith('--port='));
|
|
18
|
+
if (portArg) {
|
|
19
|
+
specifiedPort = parseInt(portArg.split('=')[1]);
|
|
20
|
+
if (isNaN(specifiedPort) || specifiedPort < 1 || specifiedPort > 65535) {
|
|
21
|
+
console.error('❌ Invalid port number. Port must be between 1 and 65535.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
// Check for browser specification
|
|
17
27
|
let specificBrowser = null;
|
|
18
28
|
const browserArg = args.find(arg => arg.startsWith('--browser='));
|
|
@@ -29,11 +39,13 @@ Usage: npx gh-here [options]
|
|
|
29
39
|
Options:
|
|
30
40
|
--open, -o Open browser automatically
|
|
31
41
|
--browser=<name> Specify browser (safari, chrome, firefox, arc)
|
|
42
|
+
--port=<number> Specify port number (default: 5555)
|
|
32
43
|
--help, -h Show this help message
|
|
33
44
|
|
|
34
45
|
Examples:
|
|
35
|
-
npx gh-here Start server on available
|
|
46
|
+
npx gh-here Start server on port 5555 (or next available)
|
|
36
47
|
npx gh-here --open Start server and open browser
|
|
48
|
+
npx gh-here --port=8080 Start server on port 8080
|
|
37
49
|
npx gh-here --open --browser=safari Start server and open in Safari
|
|
38
50
|
npx gh-here --open --browser=arc Start server and open in Arc
|
|
39
51
|
`);
|
|
@@ -44,187 +56,14 @@ const app = express();
|
|
|
44
56
|
const workingDir = process.cwd();
|
|
45
57
|
|
|
46
58
|
// Git repository detection
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Check if current directory or any parent is a git repository
|
|
51
|
-
function findGitRepo(dir) {
|
|
52
|
-
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
53
|
-
return dir;
|
|
54
|
-
}
|
|
55
|
-
const parentDir = path.dirname(dir);
|
|
56
|
-
if (parentDir === dir) {
|
|
57
|
-
return null; // Reached root directory
|
|
58
|
-
}
|
|
59
|
-
return findGitRepo(parentDir);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Initialize git detection
|
|
63
|
-
gitRepoRoot = findGitRepo(workingDir);
|
|
64
|
-
isGitRepo = !!gitRepoRoot;
|
|
65
|
-
|
|
66
|
-
// Git status icon and description helpers
|
|
67
|
-
function getGitStatusIcon(status) {
|
|
68
|
-
switch (status.trim()) {
|
|
69
|
-
case 'M': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
70
|
-
case 'A': return octicons['plus'].toSVG({ class: 'git-status-icon' });
|
|
71
|
-
case 'D': return octicons['dash'].toSVG({ class: 'git-status-icon' });
|
|
72
|
-
case 'R': return octicons['arrow-right'].toSVG({ class: 'git-status-icon' });
|
|
73
|
-
case '??': return octicons['question'].toSVG({ class: 'git-status-icon' });
|
|
74
|
-
case 'MM':
|
|
75
|
-
case 'AM':
|
|
76
|
-
case 'AD': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
77
|
-
default: return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getGitStatusDescription(status) {
|
|
82
|
-
switch (status.trim()) {
|
|
83
|
-
case 'M': return 'Modified';
|
|
84
|
-
case 'A': return 'Added';
|
|
85
|
-
case 'D': return 'Deleted';
|
|
86
|
-
case 'R': return 'Renamed';
|
|
87
|
-
case '??': return 'Untracked';
|
|
88
|
-
case 'MM': return 'Modified (staged and unstaged)';
|
|
89
|
-
case 'AM': return 'Added (modified)';
|
|
90
|
-
case 'AD': return 'Added (deleted)';
|
|
91
|
-
default: return `Git status: ${status}`;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Get git status for files
|
|
96
|
-
function getGitStatus() {
|
|
97
|
-
return new Promise((resolve) => {
|
|
98
|
-
if (!isGitRepo) {
|
|
99
|
-
resolve({});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
exec('git status --porcelain', { cwd: gitRepoRoot }, (error, stdout) => {
|
|
104
|
-
if (error) {
|
|
105
|
-
resolve({});
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const statusMap = {};
|
|
110
|
-
const lines = stdout.trim().split('\n').filter(line => line);
|
|
111
|
-
|
|
112
|
-
for (const line of lines) {
|
|
113
|
-
const status = line.substring(0, 2);
|
|
114
|
-
const filePath = line.substring(3);
|
|
115
|
-
const absolutePath = path.resolve(gitRepoRoot, filePath);
|
|
116
|
-
statusMap[absolutePath] = {
|
|
117
|
-
status: status.trim(),
|
|
118
|
-
staged: status[0] !== ' ' && status[0] !== '?',
|
|
119
|
-
modified: status[1] !== ' ',
|
|
120
|
-
untracked: status === '??'
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
resolve(statusMap);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Get git branch info
|
|
130
|
-
function getGitBranch() {
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
if (!isGitRepo) {
|
|
133
|
-
resolve(null);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
exec('git branch --show-current', { cwd: gitRepoRoot }, (error, stdout) => {
|
|
138
|
-
if (error) {
|
|
139
|
-
resolve('main');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
resolve(stdout.trim() || 'main');
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// .gitignore parsing functionality
|
|
148
|
-
function parseGitignore(gitignorePath) {
|
|
149
|
-
try {
|
|
150
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
155
|
-
return content
|
|
156
|
-
.split('\n')
|
|
157
|
-
.map(line => line.trim())
|
|
158
|
-
.filter(line => line && !line.startsWith('#'))
|
|
159
|
-
.map(pattern => {
|
|
160
|
-
// Convert gitignore patterns to regex-like matching
|
|
161
|
-
if (pattern.endsWith('/')) {
|
|
162
|
-
// Directory pattern
|
|
163
|
-
return { pattern: pattern.slice(0, -1), isDirectory: true };
|
|
164
|
-
}
|
|
165
|
-
return { pattern, isDirectory: false };
|
|
166
|
-
});
|
|
167
|
-
} catch (error) {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
}
|
|
59
|
+
const gitRepoRoot = findGitRepo(workingDir);
|
|
60
|
+
const isGitRepo = !!gitRepoRoot;
|
|
171
61
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const relativePath = path.relative(workingDir, filePath).replace(/\\/g, '/');
|
|
178
|
-
const pathParts = relativePath.split('/');
|
|
179
|
-
|
|
180
|
-
for (const rule of gitignoreRules) {
|
|
181
|
-
const { pattern, isDirectory: ruleIsDirectory } = rule;
|
|
182
|
-
|
|
183
|
-
// Skip directory rules for files and vice versa (unless rule applies to both)
|
|
184
|
-
if (ruleIsDirectory && !isDirectory) {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Simple pattern matching (this is a basic implementation)
|
|
189
|
-
if (pattern.includes('*')) {
|
|
190
|
-
// Wildcard matching
|
|
191
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
192
|
-
if (regex.test(relativePath) || pathParts.some(part => regex.test(part))) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
// Exact matching
|
|
197
|
-
if (relativePath === pattern ||
|
|
198
|
-
relativePath.startsWith(pattern + '/') ||
|
|
199
|
-
pathParts.includes(pattern)) {
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Cache for gitignore rules
|
|
209
|
-
let gitignoreCache = null;
|
|
210
|
-
let gitignoreCacheTime = 0;
|
|
211
|
-
|
|
212
|
-
function getGitignoreRules() {
|
|
213
|
-
const gitignorePath = path.join(workingDir, '.gitignore');
|
|
214
|
-
const now = Date.now();
|
|
215
|
-
|
|
216
|
-
// Cache for 5 seconds to avoid excessive file reads
|
|
217
|
-
if (gitignoreCache && (now - gitignoreCacheTime) < 5000) {
|
|
218
|
-
return gitignoreCache;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
gitignoreCache = parseGitignore(gitignorePath);
|
|
222
|
-
gitignoreCacheTime = now;
|
|
223
|
-
return gitignoreCache;
|
|
224
|
-
}
|
|
62
|
+
// Setup all routes
|
|
63
|
+
setupRoutes(app, workingDir, isGitRepo, gitRepoRoot);
|
|
225
64
|
|
|
226
65
|
// Function to find an available port
|
|
227
|
-
async function findAvailablePort(startPort =
|
|
66
|
+
async function findAvailablePort(startPort = 5555) {
|
|
228
67
|
const net = require('net');
|
|
229
68
|
|
|
230
69
|
return new Promise((resolve, reject) => {
|
|
@@ -246,1079 +85,6 @@ async function findAvailablePort(startPort = 3000) {
|
|
|
246
85
|
});
|
|
247
86
|
}
|
|
248
87
|
|
|
249
|
-
app.use('/static', express.static(path.join(__dirname, '..', 'public')));
|
|
250
|
-
app.use('/octicons', express.static(path.join(__dirname, '..', 'node_modules', '@primer', 'octicons', 'build')));
|
|
251
|
-
|
|
252
|
-
// Download route
|
|
253
|
-
app.get('/download', (req, res) => {
|
|
254
|
-
const filePath = req.query.path || '';
|
|
255
|
-
const fullPath = path.join(workingDir, filePath);
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const stats = fs.statSync(fullPath);
|
|
259
|
-
if (stats.isFile()) {
|
|
260
|
-
const fileName = path.basename(fullPath);
|
|
261
|
-
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
262
|
-
res.sendFile(fullPath);
|
|
263
|
-
} else {
|
|
264
|
-
res.status(400).send('Cannot download directories');
|
|
265
|
-
}
|
|
266
|
-
} catch (error) {
|
|
267
|
-
res.status(404).send('File not found');
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
app.get('/', async (req, res) => {
|
|
272
|
-
const currentPath = req.query.path || '';
|
|
273
|
-
const showGitignored = req.query.gitignore === 'false'; // Default to hiding gitignored files
|
|
274
|
-
const fullPath = path.join(workingDir, currentPath);
|
|
275
|
-
|
|
276
|
-
// Get git status and branch info
|
|
277
|
-
const gitStatus = await getGitStatus();
|
|
278
|
-
const gitBranch = await getGitBranch();
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
const stats = fs.statSync(fullPath);
|
|
282
|
-
|
|
283
|
-
if (stats.isDirectory()) {
|
|
284
|
-
const gitignoreRules = getGitignoreRules();
|
|
285
|
-
|
|
286
|
-
let items = fs.readdirSync(fullPath).map(item => {
|
|
287
|
-
const itemPath = path.join(fullPath, item);
|
|
288
|
-
const itemStats = fs.statSync(itemPath);
|
|
289
|
-
const absoluteItemPath = path.resolve(itemPath);
|
|
290
|
-
const gitInfo = gitStatus[absoluteItemPath] || null;
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
name: item,
|
|
294
|
-
path: path.join(currentPath, item).replace(/\\/g, '/'),
|
|
295
|
-
isDirectory: itemStats.isDirectory(),
|
|
296
|
-
size: itemStats.size,
|
|
297
|
-
modified: itemStats.mtime,
|
|
298
|
-
gitStatus: gitInfo
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Filter out gitignored files unless explicitly requested to show them
|
|
303
|
-
if (!showGitignored) {
|
|
304
|
-
items = items.filter(item => {
|
|
305
|
-
const itemFullPath = path.join(fullPath, item.name);
|
|
306
|
-
return !isIgnoredByGitignore(itemFullPath, gitignoreRules, item.isDirectory);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Sort items (directories first, then alphabetically)
|
|
311
|
-
items.sort((a, b) => {
|
|
312
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
313
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
314
|
-
return a.name.localeCompare(b.name);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
res.send(renderDirectory(currentPath, items, showGitignored, gitBranch));
|
|
318
|
-
} else {
|
|
319
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
320
|
-
const ext = path.extname(fullPath).slice(1);
|
|
321
|
-
const viewMode = req.query.view || 'rendered';
|
|
322
|
-
|
|
323
|
-
if (viewMode === 'diff' && isGitRepo) {
|
|
324
|
-
// Check if file has git status
|
|
325
|
-
const absolutePath = path.resolve(fullPath);
|
|
326
|
-
const gitInfo = gitStatus[absolutePath];
|
|
327
|
-
if (gitInfo) {
|
|
328
|
-
const diffHtml = await renderFileDiff(currentPath, ext, gitInfo);
|
|
329
|
-
return res.send(diffHtml);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
res.send(await renderFile(currentPath, content, ext, viewMode, gitStatus));
|
|
334
|
-
}
|
|
335
|
-
} catch (error) {
|
|
336
|
-
res.status(404).send(`<h1>File not found</h1><p>${error.message}</p>`);
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Route for creating new files
|
|
341
|
-
app.get('/new', (req, res) => {
|
|
342
|
-
const currentPath = req.query.path || '';
|
|
343
|
-
res.send(renderNewFile(currentPath));
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// API endpoint to get file content for editing
|
|
347
|
-
app.get('/api/file-content', (req, res) => {
|
|
348
|
-
try {
|
|
349
|
-
const currentPath = req.query.path || '';
|
|
350
|
-
const fullPath = path.join(process.cwd(), currentPath);
|
|
351
|
-
|
|
352
|
-
// Security check - ensure we're not accessing files outside the current directory
|
|
353
|
-
if (!fullPath.startsWith(process.cwd())) {
|
|
354
|
-
return res.status(403).send('Access denied');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
358
|
-
res.send(content);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
res.status(404).send(`File not found: ${error.message}`);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// API endpoint to save file changes
|
|
365
|
-
app.post('/api/save-file', express.json(), (req, res) => {
|
|
366
|
-
try {
|
|
367
|
-
const { path: filePath, content } = req.body;
|
|
368
|
-
const fullPath = path.join(process.cwd(), filePath || '');
|
|
369
|
-
|
|
370
|
-
// Security check - ensure we're not accessing files outside the current directory
|
|
371
|
-
if (!fullPath.startsWith(process.cwd())) {
|
|
372
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
376
|
-
res.json({ success: true });
|
|
377
|
-
} catch (error) {
|
|
378
|
-
res.status(500).json({ success: false, error: error.message });
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// API endpoint to create new file
|
|
383
|
-
app.post('/api/create-file', express.json(), (req, res) => {
|
|
384
|
-
try {
|
|
385
|
-
const { path: dirPath, filename } = req.body;
|
|
386
|
-
const fullDirPath = path.join(process.cwd(), dirPath || '');
|
|
387
|
-
const fullFilePath = path.join(fullDirPath, filename);
|
|
388
|
-
|
|
389
|
-
// Security checks
|
|
390
|
-
if (!fullDirPath.startsWith(process.cwd()) || !fullFilePath.startsWith(process.cwd())) {
|
|
391
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Check if file already exists
|
|
395
|
-
if (fs.existsSync(fullFilePath)) {
|
|
396
|
-
return res.status(400).json({ success: false, error: 'File already exists' });
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Create the file with empty content
|
|
400
|
-
fs.writeFileSync(fullFilePath, '', 'utf-8');
|
|
401
|
-
res.json({ success: true });
|
|
402
|
-
} catch (error) {
|
|
403
|
-
res.status(500).json({ success: false, error: error.message });
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// API endpoint to create new folder
|
|
408
|
-
app.post('/api/create-folder', express.json(), (req, res) => {
|
|
409
|
-
try {
|
|
410
|
-
const { path: dirPath, foldername } = req.body;
|
|
411
|
-
const fullDirPath = path.join(process.cwd(), dirPath || '');
|
|
412
|
-
const fullFolderPath = path.join(fullDirPath, foldername);
|
|
413
|
-
|
|
414
|
-
// Security checks
|
|
415
|
-
if (!fullDirPath.startsWith(process.cwd()) || !fullFolderPath.startsWith(process.cwd())) {
|
|
416
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Check if folder already exists
|
|
420
|
-
if (fs.existsSync(fullFolderPath)) {
|
|
421
|
-
return res.status(400).json({ success: false, error: 'Folder already exists' });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Create the folder
|
|
425
|
-
fs.mkdirSync(fullFolderPath);
|
|
426
|
-
res.json({ success: true });
|
|
427
|
-
} catch (error) {
|
|
428
|
-
res.status(500).json({ success: false, error: error.message });
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// API endpoint to delete file or folder
|
|
433
|
-
app.post('/api/delete', express.json(), (req, res) => {
|
|
434
|
-
try {
|
|
435
|
-
const { path: itemPath } = req.body;
|
|
436
|
-
const fullPath = path.join(process.cwd(), itemPath);
|
|
437
|
-
|
|
438
|
-
// Security check
|
|
439
|
-
if (!fullPath.startsWith(process.cwd())) {
|
|
440
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Check if item exists
|
|
444
|
-
if (!fs.existsSync(fullPath)) {
|
|
445
|
-
return res.status(404).json({ success: false, error: 'Item not found' });
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Delete the item
|
|
449
|
-
const stats = fs.statSync(fullPath);
|
|
450
|
-
if (stats.isDirectory()) {
|
|
451
|
-
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
452
|
-
} else {
|
|
453
|
-
fs.unlinkSync(fullPath);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
res.json({ success: true });
|
|
457
|
-
} catch (error) {
|
|
458
|
-
res.status(500).json({ success: false, error: error.message });
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// API endpoint to rename file or folder
|
|
463
|
-
app.post('/api/rename', express.json(), (req, res) => {
|
|
464
|
-
try {
|
|
465
|
-
const { path: oldPath, newName } = req.body;
|
|
466
|
-
const fullOldPath = path.join(process.cwd(), oldPath);
|
|
467
|
-
const dirPath = path.dirname(fullOldPath);
|
|
468
|
-
const fullNewPath = path.join(dirPath, newName);
|
|
469
|
-
|
|
470
|
-
// Security checks
|
|
471
|
-
if (!fullOldPath.startsWith(process.cwd()) || !fullNewPath.startsWith(process.cwd())) {
|
|
472
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Check if old item exists
|
|
476
|
-
if (!fs.existsSync(fullOldPath)) {
|
|
477
|
-
return res.status(404).json({ success: false, error: 'Item not found' });
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Check if new name already exists
|
|
481
|
-
if (fs.existsSync(fullNewPath)) {
|
|
482
|
-
return res.status(400).json({ success: false, error: 'Name already exists' });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Rename the item
|
|
486
|
-
fs.renameSync(fullOldPath, fullNewPath);
|
|
487
|
-
res.json({ success: true });
|
|
488
|
-
} catch (error) {
|
|
489
|
-
res.status(500).json({ success: false, error: error.message });
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Git diff endpoint
|
|
494
|
-
app.get('/api/git-diff', async (req, res) => {
|
|
495
|
-
try {
|
|
496
|
-
if (!isGitRepo) {
|
|
497
|
-
return res.status(404).json({ success: false, error: 'Not a git repository' });
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const filePath = req.query.path;
|
|
501
|
-
const staged = req.query.staged === 'true';
|
|
502
|
-
|
|
503
|
-
if (!filePath) {
|
|
504
|
-
return res.status(400).json({ success: false, error: 'File path is required' });
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Get the diff for the specific file
|
|
508
|
-
const diffCommand = staged ?
|
|
509
|
-
`git diff --cached "${filePath}"` :
|
|
510
|
-
`git diff "${filePath}"`;
|
|
511
|
-
|
|
512
|
-
exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout, stderr) => {
|
|
513
|
-
if (error) {
|
|
514
|
-
return res.status(500).json({ success: false, error: error.message });
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
res.json({
|
|
518
|
-
success: true,
|
|
519
|
-
diff: stdout,
|
|
520
|
-
staged: staged,
|
|
521
|
-
filePath: filePath
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
} catch (error) {
|
|
525
|
-
res.status(500).json({ success: false, error: error.message });
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
function renderDirectory(currentPath, items, showGitignored = false, gitBranch = null) {
|
|
531
|
-
const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch);
|
|
532
|
-
const readmeFile = findReadmeFile(items);
|
|
533
|
-
const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
|
|
534
|
-
const languageStats = generateLanguageStats(items);
|
|
535
|
-
|
|
536
|
-
const itemsHtml = items.map(item => `
|
|
537
|
-
<tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
|
|
538
|
-
<td class="icon">
|
|
539
|
-
${item.isDirectory ? octicons['file-directory'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
|
|
540
|
-
</td>
|
|
541
|
-
<td class="name">
|
|
542
|
-
<a href="/?path=${encodeURIComponent(item.path)}">${item.name}</a>
|
|
543
|
-
${item.gitStatus ? `<span class="git-status git-status-${item.gitStatus.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(item.gitStatus.status)}">${getGitStatusIcon(item.gitStatus.status)}</span>` : ''}
|
|
544
|
-
<div class="quick-actions">
|
|
545
|
-
<button class="quick-btn copy-path-btn" title="Copy path" data-path="${item.path}">
|
|
546
|
-
${octicons.copy.toSVG({ class: 'quick-icon' })}
|
|
547
|
-
</button>
|
|
548
|
-
${!item.isDirectory && item.gitStatus ? `
|
|
549
|
-
<button class="quick-btn diff-btn" title="Show diff" data-path="${item.path}">
|
|
550
|
-
${octicons.diff.toSVG({ class: 'quick-icon' })}
|
|
551
|
-
</button>
|
|
552
|
-
` : ''}
|
|
553
|
-
${!item.isDirectory ? `
|
|
554
|
-
<a class="quick-btn download-btn" href="/download?path=${encodeURIComponent(item.path)}" title="Download" download="${item.name}">
|
|
555
|
-
${octicons.download.toSVG({ class: 'quick-icon' })}
|
|
556
|
-
</a>
|
|
557
|
-
` : ''}
|
|
558
|
-
${!item.isDirectory ? `
|
|
559
|
-
<button class="quick-btn edit-file-btn" title="Edit file" data-path="${item.path}">
|
|
560
|
-
${octicons.pencil.toSVG({ class: 'quick-icon' })}
|
|
561
|
-
</button>
|
|
562
|
-
` : `
|
|
563
|
-
<button class="quick-btn rename-btn" title="Rename" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
|
|
564
|
-
${octicons.pencil.toSVG({ class: 'quick-icon' })}
|
|
565
|
-
</button>
|
|
566
|
-
`}
|
|
567
|
-
<button class="quick-btn delete-btn" title="Delete" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
|
|
568
|
-
${octicons.trash.toSVG({ class: 'quick-icon' })}
|
|
569
|
-
</button>
|
|
570
|
-
</div>
|
|
571
|
-
</td>
|
|
572
|
-
<td class="size">
|
|
573
|
-
${item.isDirectory ? '-' : formatBytes(item.size)}
|
|
574
|
-
</td>
|
|
575
|
-
<td class="modified">
|
|
576
|
-
${item.modified.toLocaleDateString()}
|
|
577
|
-
</td>
|
|
578
|
-
</tr>
|
|
579
|
-
`).join('');
|
|
580
|
-
|
|
581
|
-
return `
|
|
582
|
-
<!DOCTYPE html>
|
|
583
|
-
<html data-theme="dark">
|
|
584
|
-
<head>
|
|
585
|
-
<title>gh-here: ${currentPath || 'Root'}</title>
|
|
586
|
-
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
587
|
-
<script src="/static/app.js"></script>
|
|
588
|
-
</head>
|
|
589
|
-
<body>
|
|
590
|
-
<header>
|
|
591
|
-
<div class="header-content">
|
|
592
|
-
<div class="header-left">
|
|
593
|
-
<h1 class="header-path">${breadcrumbs}</h1>
|
|
594
|
-
</div>
|
|
595
|
-
<div class="header-right">
|
|
596
|
-
<div class="search-container">
|
|
597
|
-
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
598
|
-
<input type="text" id="file-search" placeholder="Find files..." class="search-input">
|
|
599
|
-
</div>
|
|
600
|
-
<button id="gitignore-toggle" class="gitignore-toggle ${showGitignored ? 'showing-ignored' : ''}" aria-label="Toggle .gitignore filtering" title="${showGitignored ? 'Hide' : 'Show'} gitignored files">
|
|
601
|
-
${octicons.eye.toSVG({ class: 'gitignore-icon' })}
|
|
602
|
-
</button>
|
|
603
|
-
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
604
|
-
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
605
|
-
</button>
|
|
606
|
-
</div>
|
|
607
|
-
</div>
|
|
608
|
-
</header>
|
|
609
|
-
<main>
|
|
610
|
-
${languageStats}
|
|
611
|
-
<div class="directory-actions">
|
|
612
|
-
<button id="new-file-btn" class="btn btn-secondary">
|
|
613
|
-
${octicons['file-added'].toSVG({ class: 'btn-icon' })} New file
|
|
614
|
-
</button>
|
|
615
|
-
</div>
|
|
616
|
-
<div class="file-table-container">
|
|
617
|
-
<table class="file-table" id="file-table">
|
|
618
|
-
<thead>
|
|
619
|
-
<tr>
|
|
620
|
-
<th></th>
|
|
621
|
-
<th>Name</th>
|
|
622
|
-
<th>Size</th>
|
|
623
|
-
<th>Modified</th>
|
|
624
|
-
</tr>
|
|
625
|
-
</thead>
|
|
626
|
-
<tbody>
|
|
627
|
-
${currentPath && currentPath !== '.' ? `
|
|
628
|
-
<tr class="file-row" data-name=".." data-type="dir">
|
|
629
|
-
<td class="icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
|
|
630
|
-
<td class="name">
|
|
631
|
-
<a href="/?path=${encodeURIComponent(path.dirname(currentPath))}">..</a>
|
|
632
|
-
</td>
|
|
633
|
-
<td class="size">-</td>
|
|
634
|
-
<td class="modified">-</td>
|
|
635
|
-
</tr>
|
|
636
|
-
` : ''}
|
|
637
|
-
${itemsHtml}
|
|
638
|
-
</tbody>
|
|
639
|
-
</table>
|
|
640
|
-
</div>
|
|
641
|
-
${readmePreview}
|
|
642
|
-
</main>
|
|
643
|
-
</body>
|
|
644
|
-
</html>
|
|
645
|
-
`;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function findReadmeFile(items) {
|
|
649
|
-
const readmeNames = ['README.md', 'readme.md', 'README.rst', 'readme.rst', 'README.txt', 'readme.txt', 'README'];
|
|
650
|
-
return items.find(item => !item.isDirectory && readmeNames.includes(item.name));
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function generateReadmePreview(currentPath, readmeFile) {
|
|
654
|
-
try {
|
|
655
|
-
const readmePath = path.join(workingDir, currentPath, readmeFile.name);
|
|
656
|
-
const content = fs.readFileSync(readmePath, 'utf8');
|
|
657
|
-
const ext = path.extname(readmeFile.name).slice(1).toLowerCase();
|
|
658
|
-
|
|
659
|
-
let renderedContent;
|
|
660
|
-
if (ext === 'md' || ext === '') {
|
|
661
|
-
renderedContent = `<div class="markdown">${marked.parse(content)}</div>`;
|
|
662
|
-
} else {
|
|
663
|
-
const highlighted = hljs.highlightAuto(content).value;
|
|
664
|
-
renderedContent = `<pre><code class="hljs">${highlighted}</code></pre>`;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
return `
|
|
668
|
-
<div class="readme-section">
|
|
669
|
-
<div class="readme-header">
|
|
670
|
-
<h2>
|
|
671
|
-
${octicons.book.toSVG({ class: 'readme-icon' })}
|
|
672
|
-
${readmeFile.name}
|
|
673
|
-
</h2>
|
|
674
|
-
</div>
|
|
675
|
-
<div class="readme-content">
|
|
676
|
-
${renderedContent}
|
|
677
|
-
</div>
|
|
678
|
-
</div>
|
|
679
|
-
`;
|
|
680
|
-
} catch (error) {
|
|
681
|
-
return '';
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function generateLanguageStats(items) {
|
|
686
|
-
const languages = {};
|
|
687
|
-
let totalFiles = 0;
|
|
688
|
-
|
|
689
|
-
items.forEach(item => {
|
|
690
|
-
if (!item.isDirectory) {
|
|
691
|
-
const ext = path.extname(item.name).slice(1).toLowerCase();
|
|
692
|
-
const lang = getLanguageFromExtension(ext) || 'other';
|
|
693
|
-
languages[lang] = (languages[lang] || 0) + 1;
|
|
694
|
-
totalFiles++;
|
|
695
|
-
}
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
if (totalFiles === 0) return '';
|
|
699
|
-
|
|
700
|
-
const sortedLangs = Object.entries(languages)
|
|
701
|
-
.sort(([,a], [,b]) => b - a)
|
|
702
|
-
.slice(0, 5);
|
|
703
|
-
|
|
704
|
-
const statsHtml = sortedLangs.map(([lang, count]) => {
|
|
705
|
-
const percentage = ((count / totalFiles) * 100).toFixed(1);
|
|
706
|
-
const color = getLanguageColor(lang);
|
|
707
|
-
return `
|
|
708
|
-
<div class="lang-stat">
|
|
709
|
-
<span class="lang-dot" style="background-color: ${color}"></span>
|
|
710
|
-
<span class="lang-name">${lang}</span>
|
|
711
|
-
<span class="lang-percent">${percentage}%</span>
|
|
712
|
-
</div>
|
|
713
|
-
`;
|
|
714
|
-
}).join('');
|
|
715
|
-
|
|
716
|
-
return `
|
|
717
|
-
<div class="language-stats">
|
|
718
|
-
${statsHtml}
|
|
719
|
-
</div>
|
|
720
|
-
`;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function getLanguageColor(language) {
|
|
724
|
-
const colors = {
|
|
725
|
-
javascript: '#f1e05a',
|
|
726
|
-
typescript: '#2b7489',
|
|
727
|
-
python: '#3572A5',
|
|
728
|
-
java: '#b07219',
|
|
729
|
-
html: '#e34c26',
|
|
730
|
-
css: '#563d7c',
|
|
731
|
-
json: '#292929',
|
|
732
|
-
markdown: '#083fa1',
|
|
733
|
-
go: '#00ADD8',
|
|
734
|
-
rust: '#dea584',
|
|
735
|
-
php: '#4F5D95',
|
|
736
|
-
ruby: '#701516',
|
|
737
|
-
other: '#cccccc'
|
|
738
|
-
};
|
|
739
|
-
return colors[language] || colors.other;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function renderFileDiff(filePath, ext, gitInfo) {
|
|
743
|
-
const breadcrumbs = generateBreadcrumbs(filePath);
|
|
744
|
-
|
|
745
|
-
// Get git diff for the file
|
|
746
|
-
return new Promise((resolve, reject) => {
|
|
747
|
-
const diffCommand = gitInfo.staged ?
|
|
748
|
-
`git diff --cached "${filePath}"` :
|
|
749
|
-
`git diff "${filePath}"`;
|
|
750
|
-
|
|
751
|
-
exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout) => {
|
|
752
|
-
if (error) {
|
|
753
|
-
return reject(error);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const diffContent = renderRawDiff(stdout, ext);
|
|
757
|
-
const currentParams = new URLSearchParams({ path: filePath });
|
|
758
|
-
const viewUrl = `/?${currentParams.toString()}&view=rendered`;
|
|
759
|
-
const rawUrl = `/?${currentParams.toString()}&view=raw`;
|
|
760
|
-
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
761
|
-
|
|
762
|
-
const viewToggle = `
|
|
763
|
-
<div class="view-toggle">
|
|
764
|
-
<a href="${viewUrl}" class="view-btn">
|
|
765
|
-
${octicons.eye.toSVG({ class: 'view-icon' })} View
|
|
766
|
-
</a>
|
|
767
|
-
${ext === 'md' ? `
|
|
768
|
-
<a href="${rawUrl}" class="view-btn">
|
|
769
|
-
${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
|
|
770
|
-
</a>
|
|
771
|
-
` : ''}
|
|
772
|
-
<a href="${diffUrl}" class="view-btn active">
|
|
773
|
-
${octicons.diff.toSVG({ class: 'view-icon' })} Diff
|
|
774
|
-
</a>
|
|
775
|
-
</div>
|
|
776
|
-
`;
|
|
777
|
-
|
|
778
|
-
const html = `
|
|
779
|
-
<!DOCTYPE html>
|
|
780
|
-
<html data-theme="dark">
|
|
781
|
-
<head>
|
|
782
|
-
<title>gh-here: ${path.basename(filePath)} (diff)</title>
|
|
783
|
-
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
784
|
-
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
785
|
-
<script src="/static/app.js"></script>
|
|
786
|
-
</head>
|
|
787
|
-
<body>
|
|
788
|
-
<header>
|
|
789
|
-
<div class="header-content">
|
|
790
|
-
<div class="header-left">
|
|
791
|
-
<h1 class="header-path">${breadcrumbs}</h1>
|
|
792
|
-
</div>
|
|
793
|
-
<div class="header-right">
|
|
794
|
-
${viewToggle}
|
|
795
|
-
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
796
|
-
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
797
|
-
</button>
|
|
798
|
-
</div>
|
|
799
|
-
</div>
|
|
800
|
-
</header>
|
|
801
|
-
<main>
|
|
802
|
-
<div class="diff-container">
|
|
803
|
-
<div class="diff-content">
|
|
804
|
-
${diffContent}
|
|
805
|
-
</div>
|
|
806
|
-
</div>
|
|
807
|
-
</main>
|
|
808
|
-
</body>
|
|
809
|
-
</html>
|
|
810
|
-
`;
|
|
811
|
-
|
|
812
|
-
resolve(html);
|
|
813
|
-
});
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function renderRawDiff(diffOutput, ext) {
|
|
818
|
-
if (!diffOutput.trim()) {
|
|
819
|
-
return '<div class="no-changes">No changes to display</div>';
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const language = getLanguageFromExtension(ext);
|
|
823
|
-
|
|
824
|
-
// Apply syntax highlighting to the entire diff
|
|
825
|
-
let highlighted;
|
|
826
|
-
try {
|
|
827
|
-
// Use diff language for syntax highlighting if available, otherwise use the file's language
|
|
828
|
-
highlighted = hljs.highlight(diffOutput, { language: 'diff' }).value;
|
|
829
|
-
} catch {
|
|
830
|
-
// Fallback to plain text if diff highlighting fails
|
|
831
|
-
highlighted = diffOutput.replace(/&/g, '&')
|
|
832
|
-
.replace(/</g, '<')
|
|
833
|
-
.replace(/>/g, '>');
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Split into lines and add line numbers
|
|
837
|
-
const lines = highlighted.split('\n');
|
|
838
|
-
let lineNumber = 1;
|
|
839
|
-
|
|
840
|
-
const linesHtml = lines.map(line => {
|
|
841
|
-
// Determine line type based on first character
|
|
842
|
-
let lineType = 'context';
|
|
843
|
-
let displayLine = line;
|
|
844
|
-
|
|
845
|
-
if (line.startsWith('<span class="hljs-deletion">-') || line.startsWith('-')) {
|
|
846
|
-
lineType = 'removed';
|
|
847
|
-
} else if (line.startsWith('<span class="hljs-addition">+') || line.startsWith('+')) {
|
|
848
|
-
lineType = 'added';
|
|
849
|
-
} else if (line.startsWith('@@') || line.includes('hljs-meta')) {
|
|
850
|
-
lineType = 'hunk';
|
|
851
|
-
} else if (line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('---') || line.startsWith('+++')) {
|
|
852
|
-
lineType = 'header';
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const currentLineNumber = (lineType === 'context' || lineType === 'removed' || lineType === 'added') ? lineNumber++ : '';
|
|
856
|
-
|
|
857
|
-
return `<div class="diff-line diff-line-${lineType}">
|
|
858
|
-
<span class="diff-line-number">${currentLineNumber}</span>
|
|
859
|
-
<span class="diff-line-content">${displayLine}</span>
|
|
860
|
-
</div>`;
|
|
861
|
-
}).join('');
|
|
862
|
-
|
|
863
|
-
return `<div class="raw-diff-container">${linesHtml}</div>`;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null) {
|
|
867
|
-
const breadcrumbs = generateBreadcrumbs(filePath);
|
|
868
|
-
let displayContent;
|
|
869
|
-
let viewToggle = '';
|
|
870
|
-
|
|
871
|
-
// Check if file has git changes
|
|
872
|
-
const absolutePath = path.resolve(path.join(workingDir, filePath));
|
|
873
|
-
const hasGitChanges = gitStatus && gitStatus[absolutePath];
|
|
874
|
-
|
|
875
|
-
if (ext === 'md') {
|
|
876
|
-
if (viewMode === 'raw') {
|
|
877
|
-
const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
|
|
878
|
-
|
|
879
|
-
// Add line numbers for raw markdown view
|
|
880
|
-
const lines = highlighted.split('\n');
|
|
881
|
-
const numberedLines = lines.map((line, index) => {
|
|
882
|
-
const lineNum = index + 1;
|
|
883
|
-
return `<span class="line-container" data-line="${lineNum}"><a class="line-number" href="#L${lineNum}" id="L${lineNum}">${lineNum}</a><span class="line-content">${line}</span></span>`;
|
|
884
|
-
}).join('');
|
|
885
|
-
|
|
886
|
-
displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
|
|
887
|
-
} else {
|
|
888
|
-
displayContent = `<div class="markdown">${marked.parse(content)}</div>`;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const currentParams = new URLSearchParams({ path: filePath });
|
|
892
|
-
const rawUrl = `/?${currentParams.toString()}&view=raw`;
|
|
893
|
-
const renderedUrl = `/?${currentParams.toString()}&view=rendered`;
|
|
894
|
-
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
895
|
-
|
|
896
|
-
viewToggle = `
|
|
897
|
-
<div class="view-toggle">
|
|
898
|
-
<a href="${renderedUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
|
|
899
|
-
${octicons.eye.toSVG({ class: 'view-icon' })} View
|
|
900
|
-
</a>
|
|
901
|
-
<a href="${rawUrl}" class="view-btn ${viewMode === 'raw' ? 'active' : ''}">
|
|
902
|
-
${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
|
|
903
|
-
</a>
|
|
904
|
-
${hasGitChanges ? `
|
|
905
|
-
<a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
|
|
906
|
-
${octicons.diff.toSVG({ class: 'view-icon' })} Diff
|
|
907
|
-
</a>
|
|
908
|
-
` : ''}
|
|
909
|
-
</div>
|
|
910
|
-
`;
|
|
911
|
-
} else {
|
|
912
|
-
const language = getLanguageFromExtension(ext);
|
|
913
|
-
const highlighted = language ?
|
|
914
|
-
hljs.highlight(content, { language }).value :
|
|
915
|
-
hljs.highlightAuto(content).value;
|
|
916
|
-
|
|
917
|
-
// Add line numbers with clickable links
|
|
918
|
-
const lines = highlighted.split('\n');
|
|
919
|
-
const numberedLines = lines.map((line, index) => {
|
|
920
|
-
const lineNum = index + 1;
|
|
921
|
-
return `<span class="line-container" data-line="${lineNum}"><a class="line-number" href="#L${lineNum}" id="L${lineNum}">${lineNum}</a><span class="line-content">${line}</span></span>`;
|
|
922
|
-
}).join('');
|
|
923
|
-
|
|
924
|
-
displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
|
|
925
|
-
|
|
926
|
-
// Add view toggle for non-markdown files with git changes
|
|
927
|
-
if (hasGitChanges) {
|
|
928
|
-
const currentParams = new URLSearchParams({ path: filePath });
|
|
929
|
-
const viewUrl = `/?${currentParams.toString()}&view=rendered`;
|
|
930
|
-
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
931
|
-
|
|
932
|
-
viewToggle = `
|
|
933
|
-
<div class="view-toggle">
|
|
934
|
-
<a href="${viewUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
|
|
935
|
-
${octicons.eye.toSVG({ class: 'view-icon' })} View
|
|
936
|
-
</a>
|
|
937
|
-
<a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
|
|
938
|
-
${octicons.diff.toSVG({ class: 'view-icon' })} Diff
|
|
939
|
-
</a>
|
|
940
|
-
</div>
|
|
941
|
-
`;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
return `
|
|
946
|
-
<!DOCTYPE html>
|
|
947
|
-
<html data-theme="dark">
|
|
948
|
-
<head>
|
|
949
|
-
<title>gh-here: ${path.basename(filePath)}</title>
|
|
950
|
-
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
951
|
-
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
952
|
-
<script src="/static/app.js"></script>
|
|
953
|
-
</head>
|
|
954
|
-
<body>
|
|
955
|
-
<header>
|
|
956
|
-
<div class="header-content">
|
|
957
|
-
<div class="header-left">
|
|
958
|
-
<h1 class="header-path">
|
|
959
|
-
${breadcrumbs}
|
|
960
|
-
${hasGitChanges ? `<span class="git-status git-status-${hasGitChanges.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(hasGitChanges.status)}">${getGitStatusIcon(hasGitChanges.status)}</span>` : ''}
|
|
961
|
-
</h1>
|
|
962
|
-
</div>
|
|
963
|
-
<div class="header-right">
|
|
964
|
-
<div id="filename-input-container" class="filename-input-container" style="display: none;">
|
|
965
|
-
<input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
|
|
966
|
-
</div>
|
|
967
|
-
<button id="edit-btn" class="edit-btn" aria-label="Edit file">
|
|
968
|
-
${octicons.pencil.toSVG({ class: 'edit-icon' })}
|
|
969
|
-
</button>
|
|
970
|
-
${viewToggle}
|
|
971
|
-
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
972
|
-
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
973
|
-
</button>
|
|
974
|
-
</div>
|
|
975
|
-
</div>
|
|
976
|
-
</header>
|
|
977
|
-
<main>
|
|
978
|
-
<div class="file-content">
|
|
979
|
-
${displayContent}
|
|
980
|
-
</div>
|
|
981
|
-
<div id="editor-container" class="editor-container" style="display: none;">
|
|
982
|
-
<div class="editor-header">
|
|
983
|
-
<div class="editor-title">Edit ${path.basename(filePath)}</div>
|
|
984
|
-
<div class="editor-actions">
|
|
985
|
-
<button id="cancel-btn" class="btn btn-secondary">Cancel</button>
|
|
986
|
-
<button id="save-btn" class="btn btn-primary">Save</button>
|
|
987
|
-
</div>
|
|
988
|
-
</div>
|
|
989
|
-
<div class="editor-with-line-numbers">
|
|
990
|
-
<div class="editor-line-numbers" id="editor-line-numbers">1</div>
|
|
991
|
-
<textarea id="file-editor" class="file-editor"></textarea>
|
|
992
|
-
</div>
|
|
993
|
-
</div>
|
|
994
|
-
</main>
|
|
995
|
-
</body>
|
|
996
|
-
</html>
|
|
997
|
-
`;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function renderNewFile(currentPath) {
|
|
1001
|
-
const breadcrumbs = generateBreadcrumbs(currentPath);
|
|
1002
|
-
|
|
1003
|
-
return `
|
|
1004
|
-
<!DOCTYPE html>
|
|
1005
|
-
<html data-theme="dark">
|
|
1006
|
-
<head>
|
|
1007
|
-
<title>gh-here: Create new file</title>
|
|
1008
|
-
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
1009
|
-
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
1010
|
-
<script src="/static/app.js"></script>
|
|
1011
|
-
</head>
|
|
1012
|
-
<body>
|
|
1013
|
-
<header>
|
|
1014
|
-
<div class="header-content">
|
|
1015
|
-
<div class="header-left">
|
|
1016
|
-
<h1 class="header-path">${breadcrumbs}</h1>
|
|
1017
|
-
</div>
|
|
1018
|
-
<div class="header-right">
|
|
1019
|
-
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
1020
|
-
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
1021
|
-
</button>
|
|
1022
|
-
</div>
|
|
1023
|
-
</div>
|
|
1024
|
-
</header>
|
|
1025
|
-
<main>
|
|
1026
|
-
<div class="new-file-container">
|
|
1027
|
-
<div class="new-file-header">
|
|
1028
|
-
<div class="filename-section">
|
|
1029
|
-
<span class="filename-label">Name your file...</span>
|
|
1030
|
-
<input type="text" id="new-filename-input" class="new-filename-input" placeholder="README.md" autofocus>
|
|
1031
|
-
</div>
|
|
1032
|
-
<div class="new-file-actions">
|
|
1033
|
-
<button id="cancel-new-file" class="btn btn-secondary">Cancel</button>
|
|
1034
|
-
<button id="create-new-file" class="btn btn-primary">Create file</button>
|
|
1035
|
-
</div>
|
|
1036
|
-
</div>
|
|
1037
|
-
<div class="new-file-editor">
|
|
1038
|
-
<div class="editor-with-line-numbers">
|
|
1039
|
-
<div class="editor-line-numbers" id="new-file-line-numbers">1</div>
|
|
1040
|
-
<textarea id="new-file-content" class="file-editor" placeholder="Enter file contents here..."></textarea>
|
|
1041
|
-
</div>
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
</main>
|
|
1045
|
-
</body>
|
|
1046
|
-
</html>
|
|
1047
|
-
`;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function generateBreadcrumbs(currentPath, gitBranch = null) {
|
|
1051
|
-
// At root, show gh-here branding with git branch if available
|
|
1052
|
-
if (!currentPath || currentPath === '.') {
|
|
1053
|
-
const gitBranchDisplay = gitBranch ? `<span class="git-branch">${octicons['git-branch'].toSVG({ class: 'octicon-branch' })} ${gitBranch}</span>` : '';
|
|
1054
|
-
return `${octicons.home.toSVG({ class: 'octicon-home' })} gh-here ${gitBranchDisplay}`;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// In subdirectories, show clickable path
|
|
1058
|
-
const parts = currentPath.split('/').filter(p => p && p !== '.');
|
|
1059
|
-
let breadcrumbs = `
|
|
1060
|
-
<div class="breadcrumb-item">
|
|
1061
|
-
<a href="/">${octicons.home.toSVG({ class: 'octicon-home' })}</a>
|
|
1062
|
-
</div>
|
|
1063
|
-
`;
|
|
1064
|
-
let buildPath = '';
|
|
1065
|
-
|
|
1066
|
-
parts.forEach((part, index) => {
|
|
1067
|
-
buildPath += (buildPath ? '/' : '') + part;
|
|
1068
|
-
breadcrumbs += `
|
|
1069
|
-
<span class="breadcrumb-separator">/</span>
|
|
1070
|
-
<div class="breadcrumb-item">
|
|
1071
|
-
<a href="/?path=${encodeURIComponent(buildPath)}">
|
|
1072
|
-
<span>${part}</span>
|
|
1073
|
-
</a>
|
|
1074
|
-
</div>
|
|
1075
|
-
`;
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
return breadcrumbs;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function getFileIcon(filename) {
|
|
1082
|
-
const ext = path.extname(filename).toLowerCase();
|
|
1083
|
-
const name = filename.toLowerCase();
|
|
1084
|
-
|
|
1085
|
-
try {
|
|
1086
|
-
// Configuration files
|
|
1087
|
-
if (name === 'package.json' || name === 'composer.json') {
|
|
1088
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
1089
|
-
}
|
|
1090
|
-
if (name === 'tsconfig.json' || name === 'jsconfig.json') {
|
|
1091
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1092
|
-
}
|
|
1093
|
-
if (name === '.eslintrc' || name === '.eslintrc.json' || name === '.eslintrc.js' || name === '.eslintrc.yml') {
|
|
1094
|
-
return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
1095
|
-
}
|
|
1096
|
-
if (name === '.prettierrc' || name === 'prettier.config.js' || name === '.prettierrc.json') {
|
|
1097
|
-
return octicons.gear?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
1098
|
-
}
|
|
1099
|
-
if (name === 'webpack.config.js' || name === 'vite.config.js' || name === 'rollup.config.js' || name === 'next.config.js' || name === 'nuxt.config.js' || name === 'svelte.config.js') {
|
|
1100
|
-
return octicons.gear?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
|
|
1101
|
-
}
|
|
1102
|
-
if (name === 'tailwind.config.js' || name === 'postcss.config.js' || name === 'babel.config.js' || name === '.babelrc') {
|
|
1103
|
-
return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// Docker files
|
|
1107
|
-
if (name === 'dockerfile' || name === 'dockerfile.dev' || name === '.dockerignore') {
|
|
1108
|
-
return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
1109
|
-
}
|
|
1110
|
-
if (name === 'docker-compose.yml' || name === 'docker-compose.yaml') {
|
|
1111
|
-
return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
// Git files
|
|
1115
|
-
if (name === '.gitignore' || name === '.gitattributes' || name === '.gitmodules') {
|
|
1116
|
-
return octicons['git-branch']?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Documentation
|
|
1120
|
-
if (name.startsWith('readme') || name === 'changelog.md' || name === 'history.md') {
|
|
1121
|
-
return octicons.book.toSVG({ class: 'octicon-file text-blue' });
|
|
1122
|
-
}
|
|
1123
|
-
if (name === 'license' || name === 'license.txt' || name === 'license.md') {
|
|
1124
|
-
return octicons.law?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Build files
|
|
1128
|
-
if (name === 'makefile' || name === 'makefile.am' || name === 'cmakelists.txt') {
|
|
1129
|
-
return octicons.tools?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
1130
|
-
}
|
|
1131
|
-
if (name.endsWith('.lock') || name === 'yarn.lock' || name === 'package-lock.json' || name === 'pipfile.lock') {
|
|
1132
|
-
return octicons.lock?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// CI/CD files
|
|
1136
|
-
if (name === '.travis.yml' || name === '.circleci' || name.startsWith('.github')) {
|
|
1137
|
-
return octicons.gear?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Environment files
|
|
1141
|
-
if (name === '.env' || name === '.env.local' || name.startsWith('.env.')) {
|
|
1142
|
-
return octicons.key?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// Extension-based icons
|
|
1146
|
-
switch (ext) {
|
|
1147
|
-
case '.js':
|
|
1148
|
-
case '.mjs':
|
|
1149
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
|
|
1150
|
-
case '.jsx':
|
|
1151
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1152
|
-
case '.ts':
|
|
1153
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1154
|
-
case '.tsx':
|
|
1155
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1156
|
-
case '.vue':
|
|
1157
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
1158
|
-
case '.svelte':
|
|
1159
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1160
|
-
case '.py':
|
|
1161
|
-
case '.pyx':
|
|
1162
|
-
case '.pyi':
|
|
1163
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1164
|
-
case '.java':
|
|
1165
|
-
case '.class':
|
|
1166
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
1167
|
-
case '.c':
|
|
1168
|
-
case '.h':
|
|
1169
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1170
|
-
case '.cpp':
|
|
1171
|
-
case '.cxx':
|
|
1172
|
-
case '.cc':
|
|
1173
|
-
case '.hpp':
|
|
1174
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1175
|
-
case '.cs':
|
|
1176
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
1177
|
-
case '.go':
|
|
1178
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1179
|
-
case '.rs':
|
|
1180
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1181
|
-
case '.php':
|
|
1182
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
1183
|
-
case '.rb':
|
|
1184
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
1185
|
-
case '.swift':
|
|
1186
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1187
|
-
case '.kt':
|
|
1188
|
-
case '.kts':
|
|
1189
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
1190
|
-
case '.dart':
|
|
1191
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1192
|
-
case '.scala':
|
|
1193
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
|
|
1194
|
-
case '.clj':
|
|
1195
|
-
case '.cljs':
|
|
1196
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
|
|
1197
|
-
case '.hs':
|
|
1198
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
1199
|
-
case '.elm':
|
|
1200
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1201
|
-
case '.r':
|
|
1202
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
|
|
1203
|
-
case '.html':
|
|
1204
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1205
|
-
case '.css':
|
|
1206
|
-
case '.scss':
|
|
1207
|
-
case '.sass':
|
|
1208
|
-
case '.less':
|
|
1209
|
-
return octicons.paintbrush?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
1210
|
-
case '.json':
|
|
1211
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
|
|
1212
|
-
case '.xml':
|
|
1213
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1214
|
-
case '.yml':
|
|
1215
|
-
case '.yaml':
|
|
1216
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
|
|
1217
|
-
case '.md':
|
|
1218
|
-
case '.markdown':
|
|
1219
|
-
return octicons.book.toSVG({ class: 'octicon-file text-blue' });
|
|
1220
|
-
case '.txt':
|
|
1221
|
-
return octicons['file-text']?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
1222
|
-
case '.pdf':
|
|
1223
|
-
return octicons['file-binary']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
|
|
1224
|
-
case '.png':
|
|
1225
|
-
case '.jpg':
|
|
1226
|
-
case '.jpeg':
|
|
1227
|
-
case '.gif':
|
|
1228
|
-
case '.svg':
|
|
1229
|
-
case '.webp':
|
|
1230
|
-
return octicons['file-media']?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
1231
|
-
case '.mp4':
|
|
1232
|
-
case '.mov':
|
|
1233
|
-
case '.avi':
|
|
1234
|
-
case '.mkv':
|
|
1235
|
-
return octicons['device-camera-video']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
|
|
1236
|
-
case '.mp3':
|
|
1237
|
-
case '.wav':
|
|
1238
|
-
case '.flac':
|
|
1239
|
-
return octicons.unmute?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
|
|
1240
|
-
case '.zip':
|
|
1241
|
-
case '.tar':
|
|
1242
|
-
case '.gz':
|
|
1243
|
-
case '.rar':
|
|
1244
|
-
case '.7z':
|
|
1245
|
-
return octicons['file-zip']?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
|
|
1246
|
-
case '.sh':
|
|
1247
|
-
case '.bash':
|
|
1248
|
-
case '.zsh':
|
|
1249
|
-
case '.fish':
|
|
1250
|
-
return octicons.terminal?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
|
|
1251
|
-
case '.sql':
|
|
1252
|
-
return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
|
|
1253
|
-
default:
|
|
1254
|
-
return octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
1255
|
-
}
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
return octicons.file.toSVG({ class: 'octicon-file text-gray' });
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function getColorForExtension(ext) {
|
|
1262
|
-
const colorMap = {
|
|
1263
|
-
'.js': 'yellow', '.jsx': 'yellow', '.ts': 'blue', '.tsx': 'blue',
|
|
1264
|
-
'.py': 'green', '.java': 'red', '.go': 'blue', '.rs': 'orange',
|
|
1265
|
-
'.html': 'orange', '.css': 'purple', '.json': 'yellow',
|
|
1266
|
-
'.md': 'blue', '.txt': 'gray', '.sh': 'green'
|
|
1267
|
-
};
|
|
1268
|
-
return colorMap[ext] || 'gray';
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function getLanguageFromExtension(ext) {
|
|
1272
|
-
const langMap = {
|
|
1273
|
-
'js': 'javascript',
|
|
1274
|
-
'jsx': 'javascript',
|
|
1275
|
-
'ts': 'typescript',
|
|
1276
|
-
'tsx': 'typescript',
|
|
1277
|
-
'py': 'python',
|
|
1278
|
-
'java': 'java',
|
|
1279
|
-
'html': 'html',
|
|
1280
|
-
'css': 'css',
|
|
1281
|
-
'scss': 'scss',
|
|
1282
|
-
'sass': 'sass',
|
|
1283
|
-
'json': 'json',
|
|
1284
|
-
'xml': 'xml',
|
|
1285
|
-
'yaml': 'yaml',
|
|
1286
|
-
'yml': 'yaml',
|
|
1287
|
-
'sh': 'bash',
|
|
1288
|
-
'bash': 'bash',
|
|
1289
|
-
'zsh': 'bash',
|
|
1290
|
-
'go': 'go',
|
|
1291
|
-
'rs': 'rust',
|
|
1292
|
-
'cpp': 'cpp',
|
|
1293
|
-
'cxx': 'cpp',
|
|
1294
|
-
'cc': 'cpp',
|
|
1295
|
-
'c': 'c',
|
|
1296
|
-
'h': 'c',
|
|
1297
|
-
'hpp': 'cpp',
|
|
1298
|
-
'php': 'php',
|
|
1299
|
-
'rb': 'ruby',
|
|
1300
|
-
'swift': 'swift',
|
|
1301
|
-
'kt': 'kotlin',
|
|
1302
|
-
'dart': 'dart',
|
|
1303
|
-
'r': 'r',
|
|
1304
|
-
'sql': 'sql',
|
|
1305
|
-
'dockerfile': 'dockerfile',
|
|
1306
|
-
'md': 'markdown',
|
|
1307
|
-
'markdown': 'markdown',
|
|
1308
|
-
'vue': 'vue',
|
|
1309
|
-
'svelte': 'svelte'
|
|
1310
|
-
};
|
|
1311
|
-
return langMap[ext];
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function formatBytes(bytes) {
|
|
1315
|
-
if (bytes === 0) return '0 B';
|
|
1316
|
-
const k = 1024;
|
|
1317
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1318
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1319
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
88
|
// Function to open browser
|
|
1323
89
|
function openBrowserToUrl(url) {
|
|
1324
90
|
let command;
|
|
@@ -1380,7 +146,32 @@ function openBrowserToUrl(url) {
|
|
|
1380
146
|
// Start server with automatic port selection
|
|
1381
147
|
async function startServer() {
|
|
1382
148
|
try {
|
|
1383
|
-
|
|
149
|
+
let port;
|
|
150
|
+
if (specifiedPort) {
|
|
151
|
+
// If user specified a port, try only that port
|
|
152
|
+
const net = require('net');
|
|
153
|
+
const server = net.createServer();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
server.listen(specifiedPort, () => {
|
|
158
|
+
server.close(() => resolve());
|
|
159
|
+
});
|
|
160
|
+
server.on('error', reject);
|
|
161
|
+
});
|
|
162
|
+
port = specifiedPort;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.code === 'EADDRINUSE') {
|
|
165
|
+
console.error(`❌ Port ${specifiedPort} is already in use. Please choose a different port.`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
} else {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Find available port starting from 5555
|
|
173
|
+
port = await findAvailablePort(5555);
|
|
174
|
+
}
|
|
1384
175
|
const url = `http://localhost:${port}`;
|
|
1385
176
|
|
|
1386
177
|
app.listen(port, () => {
|