gh-here 3.0.3 → 3.2.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.
Files changed (47) hide show
  1. package/.env +0 -0
  2. package/lib/constants.js +21 -16
  3. package/lib/content-search.js +212 -0
  4. package/lib/error-handler.js +39 -28
  5. package/lib/file-utils.js +438 -287
  6. package/lib/git.js +11 -55
  7. package/lib/gitignore.js +70 -41
  8. package/lib/renderers.js +17 -33
  9. package/lib/server.js +73 -196
  10. package/lib/symbol-parser.js +600 -0
  11. package/package.json +1 -1
  12. package/public/app.js +135 -68
  13. package/public/css/components/buttons.css +423 -0
  14. package/public/css/components/forms.css +171 -0
  15. package/public/css/components/modals.css +286 -0
  16. package/public/css/components/notifications.css +36 -0
  17. package/public/css/file-table.css +318 -0
  18. package/public/css/file-tree.css +269 -0
  19. package/public/css/file-viewer.css +1259 -0
  20. package/public/css/layout.css +372 -0
  21. package/public/css/main.css +35 -0
  22. package/public/css/reset.css +64 -0
  23. package/public/css/search.css +694 -0
  24. package/public/css/symbol-outline.css +279 -0
  25. package/public/css/variables.css +135 -0
  26. package/public/js/constants.js +50 -34
  27. package/public/js/content-search-handler.js +551 -0
  28. package/public/js/file-viewer.js +437 -0
  29. package/public/js/focus-mode.js +280 -0
  30. package/public/js/inline-search.js +659 -0
  31. package/public/js/modal-manager.js +14 -28
  32. package/public/js/symbol-outline.js +454 -0
  33. package/public/js/utils.js +152 -94
  34. package/.claude/settings.local.json +0 -30
  35. package/SAMPLE.md +0 -287
  36. package/lib/validation.js +0 -77
  37. package/public/app.js.backup +0 -1902
  38. package/public/highlight.css +0 -121
  39. package/public/js/draft-manager.js +0 -36
  40. package/public/js/editor-manager.js +0 -159
  41. package/public/styles.css +0 -2727
  42. package/test.js +0 -138
  43. package/tests/draftManager.test.js +0 -241
  44. package/tests/fileTypeDetection.test.js +0 -111
  45. package/tests/httpService.test.js +0 -268
  46. package/tests/languageDetection.test.js +0 -145
  47. 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
- * Handles git status, commits, diffs, and branch operations
3
+ * Handles git status, diffs, and branch operations (read-only)
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
- commitAllChanges,
205
- commitSelectedFiles,
206
- getGitDiff
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
- // Cache for gitignore rules
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
- function parseGitignore(gitignorePath) {
14
- try {
15
- if (!fs.existsSync(gitignorePath)) {
16
- return [];
17
- }
18
-
19
- const content = fs.readFileSync(gitignorePath, 'utf8');
20
- return content
21
- .split('\n')
22
- .map(line => line.trim())
23
- .filter(line => line && !line.startsWith('#'))
24
- .map(pattern => {
25
- // Convert gitignore patterns to regex-like matching
26
- if (pattern.endsWith('/')) {
27
- // Directory pattern
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 || gitignoreRules.length === 0) {
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 and vice versa (unless rule applies to both)
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
- function getGitignoreRules(workingDir) {
74
- const gitignorePath = path.join(workingDir, '.gitignore');
75
- const now = Date.now();
76
-
77
- // Cache for 5 seconds to avoid excessive file reads
78
- if (gitignoreCache && (now - gitignoreCacheTime) < 5000) {
79
- return gitignoreCache;
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
- parseGitignore,
117
+ getGitignoreRules,
89
118
  isIgnoredByGitignore,
90
- getGitignoreRules
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}">
@@ -75,24 +73,12 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
75
73
  ${octicons.download.toSVG({ class: 'quick-icon' })}
76
74
  </a>
77
75
  ` : ''}
78
- ${!item.isDirectory ? `
79
- <button class="quick-btn edit-file-btn" title="Edit file" data-path="${item.path}">
80
- ${octicons.pencil.toSVG({ class: 'quick-icon' })}
81
- </button>
82
- ` : `
83
- <button class="quick-btn rename-btn" title="Rename" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
84
- ${octicons.pencil.toSVG({ class: 'quick-icon' })}
85
- </button>
86
- `}
87
- <button class="quick-btn delete-btn" title="Delete" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
88
- ${octicons.trash.toSVG({ class: 'quick-icon' })}
89
- </button>
90
76
  </div>
91
77
  </td>
92
- <td class="size">
78
+ <td class="col-size">
93
79
  ${item.isDirectory ? '-' : formatBytes(item.size)}
94
80
  </td>
95
- <td class="modified">
81
+ <td class="col-modified">
96
82
  ${item.modified.toLocaleDateString()}
97
83
  </td>
98
84
  </tr>
@@ -103,7 +89,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
103
89
  <html data-theme="dark">
104
90
  <head>
105
91
  <title>gh-here: ${currentPath || 'Root'}</title>
106
- <link rel="stylesheet" href="/static/styles.css?v=3.0.5">
92
+ <link rel="stylesheet" href="/static/css/main.css?v=3.2.0">
107
93
  <script>
108
94
  // Check localStorage and add showGitignored param if needed (before page renders)
109
95
  (function() {
@@ -195,23 +181,21 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
195
181
  <table class="file-table" id="file-table">
196
182
  <thead>
197
183
  <tr>
198
- <th></th>
199
- <th></th>
200
- <th>Name</th>
201
- <th>Size</th>
202
- <th>Modified</th>
184
+ <th class="col-icon"></th>
185
+ <th class="col-name">Name</th>
186
+ <th class="col-size">Size</th>
187
+ <th class="col-modified">Modified</th>
203
188
  </tr>
204
189
  </thead>
205
190
  <tbody>
206
191
  ${currentPath && currentPath !== '.' ? `
207
192
  <tr class="file-row" data-name=".." data-type="dir">
208
- <td class="icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
209
- <td class="git-status-col"></td>
210
- <td class="name">
193
+ <td class="col-icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
194
+ <td class="col-name">
211
195
  <a href="/?path=${encodeURIComponent(path.dirname(currentPath))}">.</a>
212
196
  </td>
213
- <td class="size">-</td>
214
- <td class="modified">-</td>
197
+ <td class="col-size">-</td>
198
+ <td class="col-modified">-</td>
215
199
  </tr>
216
200
  ` : ''}
217
201
  ${itemsHtml}
@@ -407,8 +391,8 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
407
391
  <html data-theme="dark">
408
392
  <head>
409
393
  <title>gh-here: ${path.basename(filePath)} (diff)</title>
410
- <link rel="stylesheet" href="/static/styles.css?v=3.0.5">
411
- <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
394
+ <link rel="stylesheet" href="/static/css/main.css?v=3.2.0">
395
+
412
396
  <script>
413
397
  // Check localStorage and add showGitignored param if needed (before page renders)
414
398
  (function() {
@@ -787,8 +771,8 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
787
771
  <html data-theme="dark">
788
772
  <head>
789
773
  <title>gh-here: ${path.basename(filePath)}</title>
790
- <link rel="stylesheet" href="/static/styles.css?v=3.0.5">
791
- <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
774
+ <link rel="stylesheet" href="/static/css/main.css?v=3.2.0">
775
+
792
776
  <script>
793
777
  // Check localStorage and add showGitignored param if needed (before page renders)
794
778
  (function() {
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,13 +268,7 @@ 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
- // API endpoint to get file content for editing
271
+ // API endpoint to get raw file content (for copy raw feature)
212
272
  app.get('/api/file-content', (req, res) => {
213
273
  try {
214
274
  const currentPath = req.query.path || '';
@@ -219,9 +279,9 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
219
279
  return res.status(403).send('Access denied');
220
280
  }
221
281
 
222
- // Check if it's a binary file - prevent editing
282
+ // Skip binary files
223
283
  if (isBinaryFile(fullPath)) {
224
- return res.status(400).json({ error: 'Cannot edit binary files' });
284
+ return res.status(400).json({ error: 'Cannot read binary files as text' });
225
285
  }
226
286
 
227
287
  const content = fs.readFileSync(fullPath, 'utf-8');
@@ -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 {