gh-here 3.0.2 → 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 (42) 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 +207 -73
  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/navigation.js +5 -0
  28. package/public/js/symbol-outline.js +454 -0
  29. package/public/js/utils.js +152 -94
  30. package/public/styles.css +2049 -296
  31. package/.claude/settings.local.json +0 -30
  32. package/SAMPLE.md +0 -287
  33. package/lib/validation.js +0 -77
  34. package/public/app.js.backup +0 -1902
  35. package/public/js/draft-manager.js +0 -36
  36. package/public/js/editor-manager.js +0 -159
  37. package/test.js +0 -138
  38. package/tests/draftManager.test.js +0 -241
  39. package/tests/fileTypeDetection.test.js +0 -111
  40. package/tests/httpService.test.js +0 -268
  41. package/tests/languageDetection.test.js +0 -145
  42. package/tests/pathUtils.test.js +0 -136
package/.env ADDED
File without changes
Binary file
Binary file
package/lib/constants.js CHANGED
@@ -1,38 +1,48 @@
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',
25
+ CANNOT_EDIT_BINARY: 'Cannot edit binary files',
26
+ COMMIT_MESSAGE_REQUIRED: 'Commit message is required',
21
27
  FILE_NOT_FOUND: 'File not found',
22
- ITEM_NOT_FOUND: 'Item not found',
28
+ FILE_PATH_REQUIRED: 'File path is required',
23
29
  ITEM_ALREADY_EXISTS: 'Item already exists',
24
- CANNOT_EDIT_BINARY: 'Cannot edit binary files',
25
- CANNOT_DOWNLOAD_DIRECTORIES: 'Cannot download directories'
30
+ ITEM_NOT_FOUND: 'Item not found',
31
+ NO_FILES_SELECTED: 'No files selected',
32
+ NOT_GIT_REPO: 'Not a git repository'
26
33
  },
27
34
 
35
+ /**
36
+ * Git status code to human-readable status mapping
37
+ */
28
38
  GIT_STATUS_MAP: {
39
+ '??': 'untracked',
29
40
  'A': 'added',
30
- 'M': 'modified',
41
+ 'AD': 'mixed',
42
+ 'AM': 'mixed',
31
43
  'D': 'deleted',
32
- 'R': 'renamed',
33
- '??': 'untracked',
44
+ 'M': 'modified',
34
45
  'MM': 'mixed',
35
- 'AM': 'mixed',
36
- 'AD': 'mixed'
46
+ 'R': 'renamed'
37
47
  }
38
48
  };
@@ -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
  };