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.
Files changed (41) hide show
  1. package/.env +0 -0
  2. package/.playwright-mcp/fixed-alignment.png +0 -0
  3. package/.playwright-mcp/fixed-layout.png +0 -0
  4. package/.playwright-mcp/gh-here-home-header-table.png +0 -0
  5. package/.playwright-mcp/gh-here-home.png +0 -0
  6. package/.playwright-mcp/line-selection-multiline.png +0 -0
  7. package/.playwright-mcp/line-selection-test-after.png +0 -0
  8. package/.playwright-mcp/line-selection-test-before.png +0 -0
  9. package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
  10. package/lib/constants.js +25 -15
  11. package/lib/content-search.js +212 -0
  12. package/lib/error-handler.js +39 -28
  13. package/lib/file-utils.js +438 -287
  14. package/lib/git.js +10 -54
  15. package/lib/gitignore.js +70 -41
  16. package/lib/renderers.js +15 -19
  17. package/lib/server.js +70 -193
  18. package/lib/symbol-parser.js +600 -0
  19. package/package.json +1 -1
  20. package/public/app.js +134 -68
  21. package/public/js/constants.js +50 -34
  22. package/public/js/content-search-handler.js +551 -0
  23. package/public/js/file-viewer.js +437 -0
  24. package/public/js/focus-mode.js +280 -0
  25. package/public/js/inline-search.js +659 -0
  26. package/public/js/modal-manager.js +14 -28
  27. package/public/js/symbol-outline.js +454 -0
  28. package/public/js/utils.js +152 -94
  29. package/public/styles.css +2049 -296
  30. package/.claude/settings.local.json +0 -30
  31. package/SAMPLE.md +0 -287
  32. package/lib/validation.js +0 -77
  33. package/public/app.js.backup +0 -1902
  34. package/public/js/draft-manager.js +0 -36
  35. package/public/js/editor-manager.js +0 -159
  36. package/test.js +0 -138
  37. package/tests/draftManager.test.js +0 -241
  38. package/tests/fileTypeDetection.test.js +0 -111
  39. package/tests/httpService.test.js +0 -268
  40. package/tests/languageDetection.test.js +0 -145
  41. 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
- 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}">
@@ -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.5">
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></th>
200
- <th>Name</th>
201
- <th>Size</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="git-status-col"></td>
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.5">
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.5">
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 {