gh-here 3.0.3 → 3.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.
- package/.env +0 -0
- package/.playwright-mcp/fixed-alignment.png +0 -0
- package/.playwright-mcp/fixed-layout.png +0 -0
- package/.playwright-mcp/gh-here-home-header-table.png +0 -0
- package/.playwright-mcp/gh-here-home.png +0 -0
- package/.playwright-mcp/line-selection-multiline.png +0 -0
- package/.playwright-mcp/line-selection-test-after.png +0 -0
- package/.playwright-mcp/line-selection-test-before.png +0 -0
- package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
- package/lib/constants.js +25 -15
- package/lib/content-search.js +212 -0
- package/lib/error-handler.js +39 -28
- package/lib/file-utils.js +438 -287
- package/lib/git.js +10 -54
- package/lib/gitignore.js +70 -41
- package/lib/renderers.js +15 -19
- package/lib/server.js +70 -193
- package/lib/symbol-parser.js +600 -0
- package/package.json +1 -1
- package/public/app.js +134 -68
- package/public/js/constants.js +50 -34
- package/public/js/content-search-handler.js +551 -0
- package/public/js/file-viewer.js +437 -0
- package/public/js/focus-mode.js +280 -0
- package/public/js/inline-search.js +659 -0
- package/public/js/modal-manager.js +14 -28
- package/public/js/symbol-outline.js +454 -0
- package/public/js/utils.js +152 -94
- package/public/styles.css +2049 -296
- package/.claude/settings.local.json +0 -30
- package/SAMPLE.md +0 -287
- package/lib/validation.js +0 -77
- package/public/app.js.backup +0 -1902
- package/public/js/draft-manager.js +0 -36
- package/public/js/editor-manager.js +0 -159
- package/test.js +0 -138
- package/tests/draftManager.test.js +0 -241
- package/tests/fileTypeDetection.test.js +0 -111
- package/tests/httpService.test.js +0 -268
- package/tests/languageDetection.test.js +0 -145
- package/tests/pathUtils.test.js +0 -136
package/lib/git.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
const { exec } = require('child_process');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const octicons = require('@primer/octicons');
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* Git operations module
|
|
8
3
|
* Handles git status, commits, diffs, and branch operations
|
|
4
|
+
* @module git
|
|
9
5
|
*/
|
|
10
6
|
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const octicons = require('@primer/octicons');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
11
12
|
// Check if current directory or any parent is a git repository
|
|
12
13
|
function findGitRepo(dir) {
|
|
13
14
|
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
@@ -130,49 +131,6 @@ function getGitBranch(gitRepoRoot) {
|
|
|
130
131
|
});
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
// Commit operations
|
|
134
|
-
async function commitAllChanges(gitRepoRoot, message) {
|
|
135
|
-
const util = require('util');
|
|
136
|
-
const execAsync = util.promisify(exec);
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
// Add all changes
|
|
140
|
-
await execAsync('git add -A', { cwd: gitRepoRoot });
|
|
141
|
-
|
|
142
|
-
// Commit with message
|
|
143
|
-
const escapedMessage = message.replace(/"/g, '\\"');
|
|
144
|
-
await execAsync(`git commit -m "${escapedMessage}"`, { cwd: gitRepoRoot });
|
|
145
|
-
|
|
146
|
-
return { success: true, message: 'Changes committed successfully' };
|
|
147
|
-
} catch (gitError) {
|
|
148
|
-
throw new Error(gitError.message || 'Git commit failed');
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function commitSelectedFiles(gitRepoRoot, message, files) {
|
|
153
|
-
const util = require('util');
|
|
154
|
-
const execAsync = util.promisify(exec);
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
// Add selected files one by one
|
|
158
|
-
for (const file of files) {
|
|
159
|
-
const escapedFile = file.replace(/"/g, '\\"');
|
|
160
|
-
await execAsync(`git add "${escapedFile}"`, { cwd: gitRepoRoot });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Commit with message
|
|
164
|
-
const escapedMessage = message.replace(/"/g, '\\"');
|
|
165
|
-
await execAsync(`git commit -m "${escapedMessage}"`, { cwd: gitRepoRoot });
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
success: true,
|
|
169
|
-
message: `${files.length} file${files.length === 1 ? '' : 's'} committed successfully`
|
|
170
|
-
};
|
|
171
|
-
} catch (gitError) {
|
|
172
|
-
throw new Error(gitError.message || 'Git commit failed');
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
134
|
// Get git diff for a specific file
|
|
177
135
|
function getGitDiff(gitRepoRoot, filePath, staged = false) {
|
|
178
136
|
return new Promise((resolve, reject) => {
|
|
@@ -197,11 +155,9 @@ function getGitDiff(gitRepoRoot, filePath, staged = false) {
|
|
|
197
155
|
|
|
198
156
|
module.exports = {
|
|
199
157
|
findGitRepo,
|
|
200
|
-
getGitStatusIcon,
|
|
201
|
-
getGitStatusDescription,
|
|
202
|
-
getGitStatus,
|
|
203
158
|
getGitBranch,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
159
|
+
getGitDiff,
|
|
160
|
+
getGitStatus,
|
|
161
|
+
getGitStatusDescription,
|
|
162
|
+
getGitStatusIcon
|
|
207
163
|
};
|
package/lib/gitignore.js
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Gitignore handling module
|
|
6
3
|
* Parses .gitignore files and filters files/directories
|
|
4
|
+
* @module gitignore
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Cache
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
10
14
|
let gitignoreCache = null;
|
|
11
15
|
let gitignoreCacheTime = 0;
|
|
16
|
+
const CACHE_TTL_MS = 5000;
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return { pattern: pattern.slice(0, -1), isDirectory: true };
|
|
29
|
-
}
|
|
30
|
-
return { pattern, isDirectory: false };
|
|
31
|
-
});
|
|
32
|
-
} catch (error) {
|
|
33
|
-
return [];
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Functions
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets cached gitignore rules or parses fresh from disk
|
|
24
|
+
* @param {string} workingDir - Working directory path
|
|
25
|
+
* @returns {Array<{pattern: string, isDirectory: boolean}>} Parsed rules
|
|
26
|
+
*/
|
|
27
|
+
function getGitignoreRules(workingDir) {
|
|
28
|
+
const gitignorePath = path.join(workingDir, '.gitignore');
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
|
|
31
|
+
if (gitignoreCache && (now - gitignoreCacheTime) < CACHE_TTL_MS) {
|
|
32
|
+
return gitignoreCache;
|
|
34
33
|
}
|
|
34
|
+
|
|
35
|
+
gitignoreCache = parseGitignore(gitignorePath);
|
|
36
|
+
gitignoreCacheTime = now;
|
|
37
|
+
return gitignoreCache;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a file path matches any gitignore rules
|
|
42
|
+
* @param {string} filePath - File path to check
|
|
43
|
+
* @param {Array} gitignoreRules - Parsed gitignore rules
|
|
44
|
+
* @param {string} workingDir - Working directory for relative path calculation
|
|
45
|
+
* @param {boolean} [isDirectory=false] - Whether the path is a directory
|
|
46
|
+
* @returns {boolean} True if path should be ignored
|
|
47
|
+
*/
|
|
37
48
|
function isIgnoredByGitignore(filePath, gitignoreRules, workingDir, isDirectory = false) {
|
|
38
|
-
if (!gitignoreRules
|
|
49
|
+
if (!gitignoreRules?.length) {
|
|
39
50
|
return false;
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -45,12 +56,11 @@ function isIgnoredByGitignore(filePath, gitignoreRules, workingDir, isDirectory
|
|
|
45
56
|
for (const rule of gitignoreRules) {
|
|
46
57
|
const { pattern, isDirectory: ruleIsDirectory } = rule;
|
|
47
58
|
|
|
48
|
-
// Skip directory rules for files
|
|
59
|
+
// Skip directory rules for files
|
|
49
60
|
if (ruleIsDirectory && !isDirectory) {
|
|
50
61
|
continue;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
// Simple pattern matching (this is a basic implementation)
|
|
54
64
|
if (pattern.includes('*')) {
|
|
55
65
|
// Wildcard matching
|
|
56
66
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
@@ -70,22 +80,41 @@ function isIgnoredByGitignore(filePath, gitignoreRules, workingDir, isDirectory
|
|
|
70
80
|
return false;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Parses a .gitignore file into structured rules
|
|
85
|
+
* @param {string} gitignorePath - Path to .gitignore file
|
|
86
|
+
* @returns {Array<{pattern: string, isDirectory: boolean}>} Parsed rules
|
|
87
|
+
*/
|
|
88
|
+
function parseGitignore(gitignorePath) {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
95
|
+
return content
|
|
96
|
+
.split('\n')
|
|
97
|
+
.map(line => line.trim())
|
|
98
|
+
.filter(line => line && !line.startsWith('#'))
|
|
99
|
+
.map(pattern => {
|
|
100
|
+
// Convert gitignore patterns to regex-like matching
|
|
101
|
+
if (pattern.endsWith('/')) {
|
|
102
|
+
// Directory pattern
|
|
103
|
+
return { pattern: pattern.slice(0, -1), isDirectory: true };
|
|
104
|
+
}
|
|
105
|
+
return { pattern, isDirectory: false };
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return [];
|
|
80
109
|
}
|
|
81
|
-
|
|
82
|
-
gitignoreCache = parseGitignore(gitignorePath);
|
|
83
|
-
gitignoreCacheTime = now;
|
|
84
|
-
return gitignoreCache;
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Exports (alpha-sorted)
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
87
116
|
module.exports = {
|
|
88
|
-
|
|
117
|
+
getGitignoreRules,
|
|
89
118
|
isIgnoredByGitignore,
|
|
90
|
-
|
|
119
|
+
parseGitignore
|
|
91
120
|
};
|
package/lib/renderers.js
CHANGED
|
@@ -53,13 +53,11 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
53
53
|
const rowStatusClass = statusKey ? ` file-row--${statusKey}` : '';
|
|
54
54
|
return `
|
|
55
55
|
<tr class="file-row${rowStatusClass}" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
|
|
56
|
-
<td class="icon">
|
|
56
|
+
<td class="col-icon">
|
|
57
57
|
${item.isDirectory ? octicons['file-directory-fill'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
|
|
58
|
-
</td>
|
|
59
|
-
<td class="git-status-col">
|
|
60
58
|
${item.gitStatus ? (item.gitStatus.status === '??' ? `<span class="git-status git-status-untracked" title="Untracked file">${require('./git').getGitStatusIcon('??')}</span>` : `<span class="git-status git-status-${item.gitStatus.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(item.gitStatus.status)}">${getGitStatusIcon(item.gitStatus.status)}</span>`) : ''}
|
|
61
59
|
</td>
|
|
62
|
-
<td class="name">
|
|
60
|
+
<td class="col-name">
|
|
63
61
|
<a href="/?path=${encodeURIComponent(item.path)}">${item.name}</a>
|
|
64
62
|
<div class="quick-actions">
|
|
65
63
|
<button class="quick-btn copy-path-btn" title="Copy path" data-path="${item.path}">
|
|
@@ -89,10 +87,10 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
89
87
|
</button>
|
|
90
88
|
</div>
|
|
91
89
|
</td>
|
|
92
|
-
<td class="size">
|
|
90
|
+
<td class="col-size">
|
|
93
91
|
${item.isDirectory ? '-' : formatBytes(item.size)}
|
|
94
92
|
</td>
|
|
95
|
-
<td class="modified">
|
|
93
|
+
<td class="col-modified">
|
|
96
94
|
${item.modified.toLocaleDateString()}
|
|
97
95
|
</td>
|
|
98
96
|
</tr>
|
|
@@ -103,7 +101,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
103
101
|
<html data-theme="dark">
|
|
104
102
|
<head>
|
|
105
103
|
<title>gh-here: ${currentPath || 'Root'}</title>
|
|
106
|
-
<link rel="stylesheet" href="/static/styles.css?v=3.0.
|
|
104
|
+
<link rel="stylesheet" href="/static/styles.css?v=3.0.6">
|
|
107
105
|
<script>
|
|
108
106
|
// Check localStorage and add showGitignored param if needed (before page renders)
|
|
109
107
|
(function() {
|
|
@@ -195,23 +193,21 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
195
193
|
<table class="file-table" id="file-table">
|
|
196
194
|
<thead>
|
|
197
195
|
<tr>
|
|
198
|
-
<th></th>
|
|
199
|
-
<th
|
|
200
|
-
<th>
|
|
201
|
-
<th>
|
|
202
|
-
<th>Modified</th>
|
|
196
|
+
<th class="col-icon"></th>
|
|
197
|
+
<th class="col-name">Name</th>
|
|
198
|
+
<th class="col-size">Size</th>
|
|
199
|
+
<th class="col-modified">Modified</th>
|
|
203
200
|
</tr>
|
|
204
201
|
</thead>
|
|
205
202
|
<tbody>
|
|
206
203
|
${currentPath && currentPath !== '.' ? `
|
|
207
204
|
<tr class="file-row" data-name=".." data-type="dir">
|
|
208
|
-
<td class="icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
|
|
209
|
-
<td class="
|
|
210
|
-
<td class="name">
|
|
205
|
+
<td class="col-icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
|
|
206
|
+
<td class="col-name">
|
|
211
207
|
<a href="/?path=${encodeURIComponent(path.dirname(currentPath))}">.</a>
|
|
212
208
|
</td>
|
|
213
|
-
<td class="size">-</td>
|
|
214
|
-
<td class="modified">-</td>
|
|
209
|
+
<td class="col-size">-</td>
|
|
210
|
+
<td class="col-modified">-</td>
|
|
215
211
|
</tr>
|
|
216
212
|
` : ''}
|
|
217
213
|
${itemsHtml}
|
|
@@ -407,7 +403,7 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
407
403
|
<html data-theme="dark">
|
|
408
404
|
<head>
|
|
409
405
|
<title>gh-here: ${path.basename(filePath)} (diff)</title>
|
|
410
|
-
<link rel="stylesheet" href="/static/styles.css?v=3.0.
|
|
406
|
+
<link rel="stylesheet" href="/static/styles.css?v=3.0.6">
|
|
411
407
|
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
412
408
|
<script>
|
|
413
409
|
// Check localStorage and add showGitignored param if needed (before page renders)
|
|
@@ -787,7 +783,7 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
787
783
|
<html data-theme="dark">
|
|
788
784
|
<head>
|
|
789
785
|
<title>gh-here: ${path.basename(filePath)}</title>
|
|
790
|
-
<link rel="stylesheet" href="/static/styles.css?v=3.0.
|
|
786
|
+
<link rel="stylesheet" href="/static/styles.css?v=3.0.6">
|
|
791
787
|
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
792
788
|
<script>
|
|
793
789
|
// Check localStorage and add showGitignored param if needed (before page renders)
|
package/lib/server.js
CHANGED
|
@@ -2,11 +2,13 @@ const express = require('express');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const { getGitStatus, getGitBranch, commitAllChanges, commitSelectedFiles, getGitDiff } = require('./git');
|
|
6
|
-
const { isIgnoredByGitignore, getGitignoreRules } = require('./gitignore');
|
|
7
|
-
const { renderDirectory, renderFileDiff, renderFile, renderNewFile } = require('./renderers');
|
|
8
|
-
const { isImageFile, isBinaryFile } = require('./file-utils');
|
|
9
5
|
const { buildFileTree } = require('./file-tree-builder');
|
|
6
|
+
const { getGitBranch, getGitDiff, getGitStatus } = require('./git');
|
|
7
|
+
const { getGitignoreRules, isIgnoredByGitignore } = require('./gitignore');
|
|
8
|
+
const { groupSymbolsByKind, parseSymbols } = require('./symbol-parser');
|
|
9
|
+
const { isBinaryFile, isImageFile } = require('./file-utils');
|
|
10
|
+
const { renderDirectory, renderFile, renderFileDiff } = require('./renderers');
|
|
11
|
+
const { searchContent } = require('./content-search');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Express server setup and route handlers
|
|
@@ -97,6 +99,70 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
97
99
|
}
|
|
98
100
|
});
|
|
99
101
|
|
|
102
|
+
// API endpoint for full-text content search
|
|
103
|
+
app.get('/api/search-content', (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
const query = req.query.q || '';
|
|
106
|
+
if (!query.trim()) {
|
|
107
|
+
return res.json({ success: true, results: [], total: 0, query: '' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const regex = req.query.regex === 'true';
|
|
111
|
+
const caseSensitive = req.query.caseSensitive === 'true';
|
|
112
|
+
const maxResults = parseInt(req.query.maxResults || '1000', 10);
|
|
113
|
+
const fileTypes = req.query.fileTypes ? req.query.fileTypes.split(',') : null;
|
|
114
|
+
|
|
115
|
+
const result = searchContent(workingDir, query, {
|
|
116
|
+
regex,
|
|
117
|
+
caseSensitive,
|
|
118
|
+
maxResults,
|
|
119
|
+
fileTypes
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
res.json({ success: true, ...result });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
res.status(400).json({ success: false, error: error.message });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// API endpoint for code symbols (functions, classes, etc.)
|
|
129
|
+
app.get('/api/symbols', (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const filePath = req.query.path;
|
|
132
|
+
if (!filePath) {
|
|
133
|
+
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate path is within working directory
|
|
137
|
+
const fullPath = path.resolve(path.join(workingDir, filePath));
|
|
138
|
+
if (!fullPath.startsWith(workingDir)) {
|
|
139
|
+
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check file exists
|
|
143
|
+
if (!fs.existsSync(fullPath) || fs.statSync(fullPath).isDirectory()) {
|
|
144
|
+
return res.status(404).json({ success: false, error: 'File not found' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read file content
|
|
148
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
149
|
+
|
|
150
|
+
// Parse symbols
|
|
151
|
+
const symbols = parseSymbols(content, filePath);
|
|
152
|
+
const grouped = groupSymbolsByKind(symbols);
|
|
153
|
+
|
|
154
|
+
res.json({
|
|
155
|
+
success: true,
|
|
156
|
+
symbols,
|
|
157
|
+
grouped,
|
|
158
|
+
total: symbols.length,
|
|
159
|
+
path: filePath
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
res.status(500).json({ success: false, error: error.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
100
166
|
// Download route
|
|
101
167
|
app.get('/download', (req, res) => {
|
|
102
168
|
const filePath = req.query.path || '';
|
|
@@ -202,12 +268,6 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
202
268
|
}
|
|
203
269
|
});
|
|
204
270
|
|
|
205
|
-
// Route for creating new files
|
|
206
|
-
app.get('/new', (req, res) => {
|
|
207
|
-
const currentPath = req.query.path || '';
|
|
208
|
-
res.send(renderNewFile(currentPath, workingDir));
|
|
209
|
-
});
|
|
210
|
-
|
|
211
271
|
// API endpoint to get file content for editing
|
|
212
272
|
app.get('/api/file-content', (req, res) => {
|
|
213
273
|
try {
|
|
@@ -237,189 +297,6 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
237
297
|
}
|
|
238
298
|
});
|
|
239
299
|
|
|
240
|
-
// API endpoint to save file changes
|
|
241
|
-
app.post('/api/save-file', express.json(), (req, res) => {
|
|
242
|
-
try {
|
|
243
|
-
const { path: filePath, content } = req.body;
|
|
244
|
-
const fullPath = path.join(workingDir, filePath || '');
|
|
245
|
-
|
|
246
|
-
// Security check - ensure we're not accessing files outside the current directory
|
|
247
|
-
if (!fullPath.startsWith(workingDir)) {
|
|
248
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
252
|
-
res.json({ success: true });
|
|
253
|
-
} catch (error) {
|
|
254
|
-
res.status(500).json({ success: false, error: error.message });
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// API endpoint to create new file
|
|
259
|
-
app.post('/api/create-file', express.json(), (req, res) => {
|
|
260
|
-
try {
|
|
261
|
-
const { path: dirPath, filename } = req.body;
|
|
262
|
-
const fullDirPath = path.join(workingDir, dirPath || '');
|
|
263
|
-
const fullFilePath = path.join(fullDirPath, filename);
|
|
264
|
-
|
|
265
|
-
// Security checks
|
|
266
|
-
if (!fullDirPath.startsWith(workingDir) || !fullFilePath.startsWith(workingDir)) {
|
|
267
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Check if file already exists
|
|
271
|
-
if (fs.existsSync(fullFilePath)) {
|
|
272
|
-
return res.status(400).json({ success: false, error: 'File already exists' });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Create the file with empty content
|
|
276
|
-
fs.writeFileSync(fullFilePath, '', 'utf-8');
|
|
277
|
-
res.json({ success: true });
|
|
278
|
-
} catch (error) {
|
|
279
|
-
res.status(500).json({ success: false, error: error.message });
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// API endpoint to create new folder
|
|
284
|
-
app.post('/api/create-folder', express.json(), (req, res) => {
|
|
285
|
-
try {
|
|
286
|
-
const { path: dirPath, foldername } = req.body;
|
|
287
|
-
const fullDirPath = path.join(workingDir, dirPath || '');
|
|
288
|
-
const fullFolderPath = path.join(fullDirPath, foldername);
|
|
289
|
-
|
|
290
|
-
// Security checks
|
|
291
|
-
if (!fullDirPath.startsWith(workingDir) || !fullFolderPath.startsWith(workingDir)) {
|
|
292
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Check if folder already exists
|
|
296
|
-
if (fs.existsSync(fullFolderPath)) {
|
|
297
|
-
return res.status(400).json({ success: false, error: 'Folder already exists' });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Create the folder
|
|
301
|
-
fs.mkdirSync(fullFolderPath);
|
|
302
|
-
res.json({ success: true });
|
|
303
|
-
} catch (error) {
|
|
304
|
-
res.status(500).json({ success: false, error: error.message });
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// API endpoint to delete file or folder
|
|
309
|
-
app.post('/api/delete', express.json(), (req, res) => {
|
|
310
|
-
try {
|
|
311
|
-
const { path: itemPath } = req.body;
|
|
312
|
-
const fullPath = path.join(workingDir, itemPath);
|
|
313
|
-
|
|
314
|
-
// Security check
|
|
315
|
-
if (!fullPath.startsWith(workingDir)) {
|
|
316
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Check if item exists
|
|
320
|
-
if (!fs.existsSync(fullPath)) {
|
|
321
|
-
return res.status(404).json({ success: false, error: 'Item not found' });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Delete the item
|
|
325
|
-
const stats = fs.statSync(fullPath);
|
|
326
|
-
if (stats.isDirectory()) {
|
|
327
|
-
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
328
|
-
} else {
|
|
329
|
-
fs.unlinkSync(fullPath);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
res.json({ success: true });
|
|
333
|
-
} catch (error) {
|
|
334
|
-
res.status(500).json({ success: false, error: error.message });
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// API endpoint to rename file or folder
|
|
339
|
-
app.post('/api/rename', express.json(), (req, res) => {
|
|
340
|
-
try {
|
|
341
|
-
const { path: oldPath, newName } = req.body;
|
|
342
|
-
const fullOldPath = path.join(workingDir, oldPath);
|
|
343
|
-
const dirPath = path.dirname(fullOldPath);
|
|
344
|
-
const fullNewPath = path.join(dirPath, newName);
|
|
345
|
-
|
|
346
|
-
// Security checks
|
|
347
|
-
if (!fullOldPath.startsWith(workingDir) || !fullNewPath.startsWith(workingDir)) {
|
|
348
|
-
return res.status(403).json({ success: false, error: 'Access denied' });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Check if old item exists
|
|
352
|
-
if (!fs.existsSync(fullOldPath)) {
|
|
353
|
-
return res.status(404).json({ success: false, error: 'Item not found' });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Check if new name already exists
|
|
357
|
-
if (fs.existsSync(fullNewPath)) {
|
|
358
|
-
return res.status(400).json({ success: false, error: 'Name already exists' });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Rename the item
|
|
362
|
-
fs.renameSync(fullOldPath, fullNewPath);
|
|
363
|
-
res.json({ success: true });
|
|
364
|
-
} catch (error) {
|
|
365
|
-
res.status(500).json({ success: false, error: error.message });
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Git commit all endpoint
|
|
370
|
-
app.post('/api/git-commit-all', express.json(), async (req, res) => {
|
|
371
|
-
try {
|
|
372
|
-
if (!isGitRepo) {
|
|
373
|
-
return res.status(404).json({ success: false, error: 'Not a git repository' });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const { message } = req.body;
|
|
377
|
-
if (!message || !message.trim()) {
|
|
378
|
-
return res.status(400).json({ success: false, error: 'Commit message is required' });
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
const result = await commitAllChanges(gitRepoRoot, message);
|
|
383
|
-
res.json(result);
|
|
384
|
-
} catch (gitError) {
|
|
385
|
-
console.error('Git commit error:', gitError);
|
|
386
|
-
res.status(500).json({ success: false, error: gitError.message });
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error('Commit endpoint error:', error);
|
|
390
|
-
res.status(500).json({ success: false, error: error.message });
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// Git commit selected files endpoint
|
|
395
|
-
app.post('/api/git-commit-selected', express.json(), async (req, res) => {
|
|
396
|
-
try {
|
|
397
|
-
if (!isGitRepo) {
|
|
398
|
-
return res.status(404).json({ success: false, error: 'Not a git repository' });
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const { message, files } = req.body;
|
|
402
|
-
if (!message || !message.trim()) {
|
|
403
|
-
return res.status(400).json({ success: false, error: 'Commit message is required' });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!files || files.length === 0) {
|
|
407
|
-
return res.status(400).json({ success: false, error: 'No files selected' });
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const result = await commitSelectedFiles(gitRepoRoot, message, files);
|
|
412
|
-
res.json(result);
|
|
413
|
-
} catch (gitError) {
|
|
414
|
-
console.error('Git commit error:', gitError);
|
|
415
|
-
res.status(500).json({ success: false, error: gitError.message });
|
|
416
|
-
}
|
|
417
|
-
} catch (error) {
|
|
418
|
-
console.error('Commit selected endpoint error:', error);
|
|
419
|
-
res.status(500).json({ success: false, error: error.message });
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
300
|
// Get git changes endpoint (with directory filtering)
|
|
424
301
|
app.get('/api/git-status', async (req, res) => {
|
|
425
302
|
try {
|