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/.env ADDED
File without changes
package/lib/constants.js CHANGED
@@ -1,38 +1,43 @@
1
1
  /**
2
2
  * Backend constants and configuration
3
+ * @module constants
3
4
  */
4
5
 
5
6
  module.exports = {
7
+ /**
8
+ * Standard HTTP status codes
9
+ */
6
10
  HTTP_STATUS: {
7
- OK: 200,
8
11
  BAD_REQUEST: 400,
12
+ CONFLICT: 409,
9
13
  FORBIDDEN: 403,
14
+ INTERNAL_ERROR: 500,
10
15
  NOT_FOUND: 404,
11
- CONFLICT: 409,
12
- INTERNAL_ERROR: 500
16
+ OK: 200
13
17
  },
14
18
 
19
+ /**
20
+ * User-facing error messages (alpha-sorted)
21
+ */
15
22
  ERROR_MESSAGES: {
16
- NOT_GIT_REPO: 'Not a git repository',
17
- COMMIT_MESSAGE_REQUIRED: 'Commit message is required',
18
- NO_FILES_SELECTED: 'No files selected',
19
- FILE_PATH_REQUIRED: 'File path is required',
20
23
  ACCESS_DENIED: 'Access denied',
24
+ CANNOT_DOWNLOAD_DIRECTORIES: 'Cannot download directories',
21
25
  FILE_NOT_FOUND: 'File not found',
22
- ITEM_NOT_FOUND: 'Item not found',
23
- ITEM_ALREADY_EXISTS: 'Item already exists',
24
- CANNOT_EDIT_BINARY: 'Cannot edit binary files',
25
- CANNOT_DOWNLOAD_DIRECTORIES: 'Cannot download directories'
26
+ FILE_PATH_REQUIRED: 'File path is required',
27
+ NOT_GIT_REPO: 'Not a git repository'
26
28
  },
27
29
 
30
+ /**
31
+ * Git status code to human-readable status mapping
32
+ */
28
33
  GIT_STATUS_MAP: {
34
+ '??': 'untracked',
29
35
  'A': 'added',
30
- 'M': 'modified',
36
+ 'AD': 'mixed',
37
+ 'AM': 'mixed',
31
38
  'D': 'deleted',
32
- 'R': 'renamed',
33
- '??': 'untracked',
39
+ 'M': 'modified',
34
40
  'MM': 'mixed',
35
- 'AM': 'mixed',
36
- 'AD': 'mixed'
41
+ 'R': 'renamed'
37
42
  }
38
43
  };
@@ -0,0 +1,212 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { isIgnoredByGitignore, getGitignoreRules } = require('./gitignore');
4
+ const { isTextFile } = require('./file-utils');
5
+
6
+ /**
7
+ * Full-text content search across codebase
8
+ *
9
+ * @param {string} workingDir - Root directory to search
10
+ * @param {string} query - Search query
11
+ * @param {Object} options - Search options
12
+ * @param {boolean} options.regex - Use regex pattern
13
+ * @param {boolean} options.caseSensitive - Case sensitive search
14
+ * @param {number} options.maxResults - Maximum number of results
15
+ * @param {string[]} options.fileTypes - Filter by file extensions
16
+ * @returns {Object} Search results
17
+ */
18
+
19
+ const DEFAULT_MAX_RESULTS = 1000;
20
+ const MAX_MATCHES_PER_FILE = 10;
21
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
22
+
23
+ function searchContent(workingDir, query, options = {}) {
24
+ const {
25
+ regex = false,
26
+ caseSensitive = false,
27
+ maxResults = DEFAULT_MAX_RESULTS,
28
+ fileTypes = null
29
+ } = options;
30
+
31
+ if (!query || typeof query !== 'string' || !query.trim()) {
32
+ return {
33
+ results: [],
34
+ total: 0,
35
+ query: ''
36
+ };
37
+ }
38
+
39
+ const gitignoreRules = getGitignoreRules(workingDir);
40
+ const results = [];
41
+ let searchPattern;
42
+
43
+ // Validate and create search pattern
44
+ try {
45
+ if (regex) {
46
+ searchPattern = new RegExp(query, caseSensitive ? 'g' : 'gi');
47
+ } else {
48
+ // Escape special regex characters for literal search
49
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ searchPattern = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
51
+ }
52
+ } catch (error) {
53
+ throw new Error(`Invalid regular expression: ${error.message}`);
54
+ }
55
+
56
+ /**
57
+ * Search a single file for matches
58
+ */
59
+ function searchFile(filePath, relativePath) {
60
+ // Early return if we've reached max results
61
+ if (results.length >= maxResults) {
62
+ return false; // Signal to stop searching
63
+ }
64
+
65
+ try {
66
+ const stats = fs.statSync(filePath);
67
+
68
+ // Skip files that are too large
69
+ if (stats.size > MAX_FILE_SIZE) {
70
+ return true; // Continue searching other files
71
+ }
72
+
73
+ // Skip binary files
74
+ const ext = path.extname(filePath).slice(1);
75
+ if (!isTextFile(ext)) {
76
+ return true;
77
+ }
78
+
79
+ // Skip gitignored files
80
+ if (isIgnoredByGitignore(filePath, gitignoreRules, workingDir, false)) {
81
+ return true;
82
+ }
83
+
84
+ // Filter by file type if specified
85
+ if (fileTypes && Array.isArray(fileTypes) && fileTypes.length > 0) {
86
+ const extLower = ext.toLowerCase();
87
+ if (!fileTypes.some(type => type.toLowerCase() === extLower)) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ // Read file content
93
+ const content = fs.readFileSync(filePath, 'utf8');
94
+ const lines = content.split('\n');
95
+ const fileMatches = [];
96
+
97
+ // Search each line
98
+ lines.forEach((line, lineIndex) => {
99
+ if (results.length >= maxResults) {
100
+ return;
101
+ }
102
+
103
+ let matches;
104
+ try {
105
+ matches = [...line.matchAll(searchPattern)];
106
+ } catch (error) {
107
+ // Skip lines with regex errors
108
+ return;
109
+ }
110
+
111
+ if (matches.length > 0) {
112
+ matches.forEach(match => {
113
+ if (match.index !== undefined && match[0]) {
114
+ fileMatches.push({
115
+ line: lineIndex + 1,
116
+ column: match.index + 1,
117
+ text: line.trim().substring(0, 200), // Limit preview length
118
+ match: match[0]
119
+ });
120
+ }
121
+ });
122
+ }
123
+ });
124
+
125
+ // Add file to results if it has matches
126
+ if (fileMatches.length > 0) {
127
+ results.push({
128
+ path: relativePath,
129
+ matches: fileMatches.slice(0, MAX_MATCHES_PER_FILE),
130
+ matchCount: fileMatches.length
131
+ });
132
+ }
133
+
134
+ return true; // Continue searching
135
+ } catch (error) {
136
+ // Skip files we can't read (permissions, etc.)
137
+ return true;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Recursively search a directory
143
+ */
144
+ function searchDirectory(dir, relativePath = '') {
145
+ // Early return if we've reached max results
146
+ if (results.length >= maxResults) {
147
+ return;
148
+ }
149
+
150
+ let entries;
151
+ try {
152
+ entries = fs.readdirSync(dir);
153
+ } catch (error) {
154
+ // Skip directories we can't access
155
+ return;
156
+ }
157
+
158
+ for (const entry of entries) {
159
+ if (results.length >= maxResults) {
160
+ break;
161
+ }
162
+
163
+ const fullPath = path.join(dir, entry);
164
+ const relPath = path.join(relativePath, entry).replace(/\\/g, '/');
165
+
166
+ try {
167
+ const stats = fs.statSync(fullPath);
168
+
169
+ if (stats.isDirectory()) {
170
+ // Skip gitignored directories
171
+ if (!isIgnoredByGitignore(fullPath, gitignoreRules, workingDir, true)) {
172
+ searchDirectory(fullPath, relPath);
173
+ }
174
+ } else if (stats.isFile()) {
175
+ const shouldContinue = searchFile(fullPath, relPath);
176
+ if (!shouldContinue) {
177
+ // Reached max results, stop searching
178
+ break;
179
+ }
180
+ }
181
+ } catch (error) {
182
+ // Skip files/dirs we can't access
183
+ continue;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Start search
189
+ try {
190
+ searchDirectory(workingDir);
191
+ } catch (error) {
192
+ throw new Error(`Search failed: ${error.message}`);
193
+ }
194
+
195
+ // Sort results by match count (descending), then by path (ascending)
196
+ results.sort((a, b) => {
197
+ if (b.matchCount !== a.matchCount) {
198
+ return b.matchCount - a.matchCount;
199
+ }
200
+ return a.path.localeCompare(b.path);
201
+ });
202
+
203
+ return {
204
+ results: results.slice(0, maxResults),
205
+ total: results.length,
206
+ query: query.trim()
207
+ };
208
+ }
209
+
210
+ module.exports = {
211
+ searchContent
212
+ };
@@ -1,32 +1,14 @@
1
1
  /**
2
- * Centralized error handling
2
+ * Centralized error handling utilities
3
+ * @module error-handler
3
4
  */
4
5
 
5
- const { HTTP_STATUS, ERROR_MESSAGES } = require('./constants');
6
+ const { HTTP_STATUS } = require('./constants');
6
7
 
7
8
  /**
8
- * Formats error response
9
- */
10
- function formatErrorResponse(error, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
11
- return {
12
- success: false,
13
- error: error.message || error,
14
- statusCode
15
- };
16
- }
17
-
18
- /**
19
- * Sends error response
20
- */
21
- function sendError(res, message, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
22
- res.status(statusCode).json({
23
- success: false,
24
- error: message
25
- });
26
- }
27
-
28
- /**
29
- * Handles async route errors
9
+ * Wraps async route handlers to catch errors
10
+ * @param {Function} fn - Async route handler function
11
+ * @returns {Function} Express middleware function
30
12
  */
31
13
  function asyncHandler(fn) {
32
14
  return (req, res, next) => {
@@ -38,7 +20,23 @@ function asyncHandler(fn) {
38
20
  }
39
21
 
40
22
  /**
41
- * Logs error with context
23
+ * Formats an error into a standard response object
24
+ * @param {Error|string} error - Error object or message
25
+ * @param {number} [statusCode=500] - HTTP status code
26
+ * @returns {{success: boolean, error: string, statusCode: number}}
27
+ */
28
+ function formatErrorResponse(error, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
29
+ return {
30
+ success: false,
31
+ error: error.message || error,
32
+ statusCode
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Logs error with context information
38
+ * @param {string} context - Context identifier for the error
39
+ * @param {Error|string} error - Error object or message
42
40
  */
43
41
  function logError(context, error) {
44
42
  console.error(`[${context}] Error:`, error.message || error);
@@ -47,9 +45,22 @@ function logError(context, error) {
47
45
  }
48
46
  }
49
47
 
48
+ /**
49
+ * Sends a JSON error response
50
+ * @param {Object} res - Express response object
51
+ * @param {string} message - Error message
52
+ * @param {number} [statusCode=500] - HTTP status code
53
+ */
54
+ function sendError(res, message, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
55
+ res.status(statusCode).json({
56
+ success: false,
57
+ error: message
58
+ });
59
+ }
60
+
50
61
  module.exports = {
51
- formatErrorResponse,
52
- sendError,
53
62
  asyncHandler,
54
- logError
63
+ formatErrorResponse,
64
+ logError,
65
+ sendError
55
66
  };