gh-here 1.0.2 → 1.0.5

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.
@@ -0,0 +1,264 @@
1
+ const path = require('path');
2
+ const octicons = require('@primer/octicons');
3
+
4
+ /**
5
+ * File utilities module
6
+ * Handles file icons, language detection, and formatting
7
+ */
8
+
9
+ function getFileIcon(filename) {
10
+ const ext = path.extname(filename).toLowerCase();
11
+ const name = filename.toLowerCase();
12
+
13
+ try {
14
+ // Configuration files
15
+ if (name === 'package.json' || name === 'composer.json') {
16
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
17
+ }
18
+ if (name === 'tsconfig.json' || name === 'jsconfig.json') {
19
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
20
+ }
21
+ if (name === '.eslintrc' || name === '.eslintrc.json' || name === '.eslintrc.js' || name === '.eslintrc.yml') {
22
+ return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
23
+ }
24
+ if (name === '.prettierrc' || name === 'prettier.config.js' || name === '.prettierrc.json') {
25
+ return octicons.gear?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
26
+ }
27
+ if (name === 'webpack.config.js' || name === 'vite.config.js' || name === 'rollup.config.js' || name === 'next.config.js' || name === 'nuxt.config.js' || name === 'svelte.config.js') {
28
+ return octicons.gear?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
29
+ }
30
+ if (name === 'tailwind.config.js' || name === 'postcss.config.js' || name === 'babel.config.js' || name === '.babelrc') {
31
+ return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
32
+ }
33
+
34
+ // Docker files
35
+ if (name === 'dockerfile' || name === 'dockerfile.dev' || name === '.dockerignore') {
36
+ return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
37
+ }
38
+ if (name === 'docker-compose.yml' || name === 'docker-compose.yaml') {
39
+ return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
40
+ }
41
+
42
+ // Git files
43
+ if (name === '.gitignore' || name === '.gitattributes' || name === '.gitmodules') {
44
+ return octicons['git-branch']?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
45
+ }
46
+
47
+ // Documentation
48
+ if (name.startsWith('readme') || name === 'changelog.md' || name === 'history.md') {
49
+ return octicons.book.toSVG({ class: 'octicon-file text-blue' });
50
+ }
51
+ if (name === 'license' || name === 'license.txt' || name === 'license.md') {
52
+ return octicons.law?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
53
+ }
54
+
55
+ // Build files
56
+ if (name === 'makefile' || name === 'makefile.am' || name === 'cmakelists.txt') {
57
+ return octicons.tools?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
58
+ }
59
+ if (name.endsWith('.lock') || name === 'yarn.lock' || name === 'package-lock.json' || name === 'pipfile.lock') {
60
+ return octicons.lock?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
61
+ }
62
+
63
+ // CI/CD files
64
+ if (name === '.travis.yml' || name === '.circleci' || name.startsWith('.github')) {
65
+ return octicons.gear?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
66
+ }
67
+
68
+ // Environment files
69
+ if (name === '.env' || name === '.env.local' || name.startsWith('.env.')) {
70
+ return octicons.key?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
71
+ }
72
+
73
+ // Extension-based icons
74
+ switch (ext) {
75
+ case '.js':
76
+ case '.mjs':
77
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
78
+ case '.jsx':
79
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
80
+ case '.ts':
81
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
82
+ case '.tsx':
83
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
84
+ case '.vue':
85
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
86
+ case '.svelte':
87
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
88
+ case '.py':
89
+ case '.pyx':
90
+ case '.pyi':
91
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
92
+ case '.java':
93
+ case '.class':
94
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
95
+ case '.c':
96
+ case '.h':
97
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
98
+ case '.cpp':
99
+ case '.cxx':
100
+ case '.cc':
101
+ case '.hpp':
102
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
103
+ case '.cs':
104
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
105
+ case '.go':
106
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
107
+ case '.rs':
108
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
109
+ case '.php':
110
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
111
+ case '.rb':
112
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
113
+ case '.swift':
114
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
115
+ case '.kt':
116
+ case '.kts':
117
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
118
+ case '.dart':
119
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
120
+ case '.scala':
121
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
122
+ case '.clj':
123
+ case '.cljs':
124
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
125
+ case '.hs':
126
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
127
+ case '.elm':
128
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
129
+ case '.r':
130
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
131
+ case '.html':
132
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
133
+ case '.css':
134
+ case '.scss':
135
+ case '.sass':
136
+ case '.less':
137
+ return octicons.paintbrush?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
138
+ case '.json':
139
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
140
+ case '.xml':
141
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
142
+ case '.yml':
143
+ case '.yaml':
144
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
145
+ case '.md':
146
+ case '.markdown':
147
+ return octicons.book.toSVG({ class: 'octicon-file text-blue' });
148
+ case '.txt':
149
+ return octicons['file-text']?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
150
+ case '.pdf':
151
+ return octicons['file-binary']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
152
+ case '.png':
153
+ case '.jpg':
154
+ case '.jpeg':
155
+ case '.gif':
156
+ case '.svg':
157
+ case '.webp':
158
+ return octicons['file-media']?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
159
+ case '.mp4':
160
+ case '.mov':
161
+ case '.avi':
162
+ case '.mkv':
163
+ return octicons['device-camera-video']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
164
+ case '.mp3':
165
+ case '.wav':
166
+ case '.flac':
167
+ return octicons.unmute?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
168
+ case '.zip':
169
+ case '.tar':
170
+ case '.gz':
171
+ case '.rar':
172
+ case '.7z':
173
+ return octicons['file-zip']?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
174
+ case '.sh':
175
+ case '.bash':
176
+ case '.zsh':
177
+ case '.fish':
178
+ return octicons.terminal?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
179
+ case '.sql':
180
+ return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
181
+ default:
182
+ return octicons.file.toSVG({ class: 'octicon-file text-gray' });
183
+ }
184
+ } catch (error) {
185
+ return octicons.file.toSVG({ class: 'octicon-file text-gray' });
186
+ }
187
+ }
188
+
189
+ function getLanguageFromExtension(ext) {
190
+ const langMap = {
191
+ 'js': 'javascript',
192
+ 'jsx': 'javascript',
193
+ 'ts': 'typescript',
194
+ 'tsx': 'typescript',
195
+ 'py': 'python',
196
+ 'java': 'java',
197
+ 'html': 'html',
198
+ 'css': 'css',
199
+ 'scss': 'scss',
200
+ 'sass': 'sass',
201
+ 'json': 'json',
202
+ 'xml': 'xml',
203
+ 'yaml': 'yaml',
204
+ 'yml': 'yaml',
205
+ 'sh': 'bash',
206
+ 'bash': 'bash',
207
+ 'zsh': 'bash',
208
+ 'go': 'go',
209
+ 'rs': 'rust',
210
+ 'cpp': 'cpp',
211
+ 'cxx': 'cpp',
212
+ 'cc': 'cpp',
213
+ 'c': 'c',
214
+ 'h': 'c',
215
+ 'hpp': 'cpp',
216
+ 'php': 'php',
217
+ 'rb': 'ruby',
218
+ 'swift': 'swift',
219
+ 'kt': 'kotlin',
220
+ 'dart': 'dart',
221
+ 'r': 'r',
222
+ 'sql': 'sql',
223
+ 'dockerfile': 'dockerfile',
224
+ 'md': 'markdown',
225
+ 'markdown': 'markdown',
226
+ 'vue': 'vue',
227
+ 'svelte': 'svelte'
228
+ };
229
+ return langMap[ext];
230
+ }
231
+
232
+ function getLanguageColor(language) {
233
+ const colors = {
234
+ javascript: '#f1e05a',
235
+ typescript: '#2b7489',
236
+ python: '#3572A5',
237
+ java: '#b07219',
238
+ html: '#e34c26',
239
+ css: '#563d7c',
240
+ json: '#292929',
241
+ markdown: '#083fa1',
242
+ go: '#00ADD8',
243
+ rust: '#dea584',
244
+ php: '#4F5D95',
245
+ ruby: '#701516',
246
+ other: '#cccccc'
247
+ };
248
+ return colors[language] || colors.other;
249
+ }
250
+
251
+ function formatBytes(bytes) {
252
+ if (bytes === 0) return '0 B';
253
+ const k = 1024;
254
+ const sizes = ['B', 'KB', 'MB', 'GB'];
255
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
256
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
257
+ }
258
+
259
+ module.exports = {
260
+ getFileIcon,
261
+ getLanguageFromExtension,
262
+ getLanguageColor,
263
+ formatBytes
264
+ };
package/lib/git.js ADDED
@@ -0,0 +1,207 @@
1
+ const { exec } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const octicons = require('@primer/octicons');
5
+
6
+ /**
7
+ * Git operations module
8
+ * Handles git status, commits, diffs, and branch operations
9
+ */
10
+
11
+ // Check if current directory or any parent is a git repository
12
+ function findGitRepo(dir) {
13
+ if (fs.existsSync(path.join(dir, '.git'))) {
14
+ return dir;
15
+ }
16
+ const parentDir = path.dirname(dir);
17
+ if (parentDir === dir) {
18
+ return null; // Reached root directory
19
+ }
20
+ return findGitRepo(parentDir);
21
+ }
22
+
23
+ // Git status icon helpers
24
+ function getGitStatusIcon(status) {
25
+ switch (status.trim()) {
26
+ case 'M': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
27
+ case 'A': return octicons['plus'].toSVG({ class: 'git-status-icon' });
28
+ case 'D': return octicons['dash'].toSVG({ class: 'git-status-icon' });
29
+ case 'R': return octicons['arrow-right'].toSVG({ class: 'git-status-icon' });
30
+ case '??': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
31
+ case 'MM':
32
+ case 'AM':
33
+ case 'AD': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
34
+ default: return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
35
+ }
36
+ }
37
+
38
+ function getGitStatusDescription(status) {
39
+ switch (status.trim()) {
40
+ case 'M': return 'Modified';
41
+ case 'A': return 'Added';
42
+ case 'D': return 'Deleted';
43
+ case 'R': return 'Renamed';
44
+ case '??': return 'Untracked';
45
+ case 'MM': return 'Modified (staged and unstaged)';
46
+ case 'AM': return 'Added (modified)';
47
+ case 'AD': return 'Added (deleted)';
48
+ default: return `Git status: ${status}`;
49
+ }
50
+ }
51
+
52
+ // Get git status for files
53
+ function getGitStatus(gitRepoRoot) {
54
+ return new Promise((resolve) => {
55
+ if (!gitRepoRoot) {
56
+ resolve({});
57
+ return;
58
+ }
59
+
60
+ exec('git status --porcelain', { cwd: gitRepoRoot }, (error, stdout) => {
61
+ if (error) {
62
+ resolve({});
63
+ return;
64
+ }
65
+
66
+ const statusMap = {};
67
+ const lines = stdout.trim().split('\n').filter(line => line);
68
+
69
+ for (const line of lines) {
70
+ const status = line.substring(0, 2);
71
+ // Git status format: XY filename - find first non-space character after position 2
72
+ const filePath = line.substring(2).replace(/^\s+/, '');
73
+ const absolutePath = path.resolve(gitRepoRoot, filePath);
74
+ statusMap[absolutePath] = {
75
+ status: status.trim(),
76
+ staged: status[0] !== ' ' && status[0] !== '?',
77
+ modified: status[1] !== ' ',
78
+ untracked: status === '??'
79
+ };
80
+
81
+ // If this is an untracked directory, mark all files within it as untracked too
82
+ if (status === '??' && fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
83
+ try {
84
+ const markFilesInDirectory = (dirPath) => {
85
+ const entries = fs.readdirSync(dirPath);
86
+ for (const entry of entries) {
87
+ const entryPath = path.join(dirPath, entry);
88
+ const entryStat = fs.statSync(entryPath);
89
+
90
+ statusMap[entryPath] = {
91
+ status: '??',
92
+ staged: false,
93
+ modified: false,
94
+ untracked: true
95
+ };
96
+
97
+ // Recursively handle subdirectories
98
+ if (entryStat.isDirectory()) {
99
+ markFilesInDirectory(entryPath);
100
+ }
101
+ }
102
+ };
103
+ markFilesInDirectory(absolutePath);
104
+ } catch (err) {
105
+ // Ignore errors reading directory contents
106
+ }
107
+ }
108
+ }
109
+
110
+ resolve(statusMap);
111
+ });
112
+ });
113
+ }
114
+
115
+ // Get git branch info
116
+ function getGitBranch(gitRepoRoot) {
117
+ return new Promise((resolve) => {
118
+ if (!gitRepoRoot) {
119
+ resolve(null);
120
+ return;
121
+ }
122
+
123
+ exec('git branch --show-current', { cwd: gitRepoRoot }, (error, stdout) => {
124
+ if (error) {
125
+ resolve('main');
126
+ return;
127
+ }
128
+ resolve(stdout.trim() || 'main');
129
+ });
130
+ });
131
+ }
132
+
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
+ // Get git diff for a specific file
177
+ function getGitDiff(gitRepoRoot, filePath, staged = false) {
178
+ return new Promise((resolve, reject) => {
179
+ const diffCommand = staged ?
180
+ `git diff --cached "${filePath}"` :
181
+ `git diff "${filePath}"`;
182
+
183
+ exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout, stderr) => {
184
+ if (error) {
185
+ return reject(new Error(error.message));
186
+ }
187
+
188
+ resolve({
189
+ success: true,
190
+ diff: stdout,
191
+ staged: staged,
192
+ filePath: filePath
193
+ });
194
+ });
195
+ });
196
+ }
197
+
198
+ module.exports = {
199
+ findGitRepo,
200
+ getGitStatusIcon,
201
+ getGitStatusDescription,
202
+ getGitStatus,
203
+ getGitBranch,
204
+ commitAllChanges,
205
+ commitSelectedFiles,
206
+ getGitDiff
207
+ };
@@ -0,0 +1,91 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Gitignore handling module
6
+ * Parses .gitignore files and filters files/directories
7
+ */
8
+
9
+ // Cache for gitignore rules
10
+ let gitignoreCache = null;
11
+ let gitignoreCacheTime = 0;
12
+
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 [];
34
+ }
35
+ }
36
+
37
+ function isIgnoredByGitignore(filePath, gitignoreRules, workingDir, isDirectory = false) {
38
+ if (!gitignoreRules || gitignoreRules.length === 0) {
39
+ return false;
40
+ }
41
+
42
+ const relativePath = path.relative(workingDir, filePath).replace(/\\/g, '/');
43
+ const pathParts = relativePath.split('/');
44
+
45
+ for (const rule of gitignoreRules) {
46
+ const { pattern, isDirectory: ruleIsDirectory } = rule;
47
+
48
+ // Skip directory rules for files and vice versa (unless rule applies to both)
49
+ if (ruleIsDirectory && !isDirectory) {
50
+ continue;
51
+ }
52
+
53
+ // Simple pattern matching (this is a basic implementation)
54
+ if (pattern.includes('*')) {
55
+ // Wildcard matching
56
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
57
+ if (regex.test(relativePath) || pathParts.some(part => regex.test(part))) {
58
+ return true;
59
+ }
60
+ } else {
61
+ // Exact matching
62
+ if (relativePath === pattern ||
63
+ relativePath.startsWith(pattern + '/') ||
64
+ pathParts.includes(pattern)) {
65
+ return true;
66
+ }
67
+ }
68
+ }
69
+
70
+ return false;
71
+ }
72
+
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;
80
+ }
81
+
82
+ gitignoreCache = parseGitignore(gitignorePath);
83
+ gitignoreCacheTime = now;
84
+ return gitignoreCache;
85
+ }
86
+
87
+ module.exports = {
88
+ parseGitignore,
89
+ isIgnoredByGitignore,
90
+ getGitignoreRules
91
+ };