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/.env
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
FILE_PATH_REQUIRED: 'File path is required',
|
|
23
29
|
ITEM_ALREADY_EXISTS: 'Item already exists',
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
'
|
|
41
|
+
'AD': 'mixed',
|
|
42
|
+
'AM': 'mixed',
|
|
31
43
|
'D': 'deleted',
|
|
32
|
-
'
|
|
33
|
-
'??': 'untracked',
|
|
44
|
+
'M': 'modified',
|
|
34
45
|
'MM': 'mixed',
|
|
35
|
-
'
|
|
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
|
+
};
|
package/lib/error-handler.js
CHANGED
|
@@ -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
|
|
6
|
+
const { HTTP_STATUS } = require('./constants');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
63
|
+
formatErrorResponse,
|
|
64
|
+
logError,
|
|
65
|
+
sendError
|
|
55
66
|
};
|