orc-server 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.
Files changed (37) hide show
  1. package/dist/__tests__/auth.test.js +49 -0
  2. package/dist/__tests__/watcher.test.js +58 -0
  3. package/dist/claude/chatService.js +175 -0
  4. package/dist/claude/sessionBrowser.js +743 -0
  5. package/dist/claude/watcher.js +242 -0
  6. package/dist/config.js +44 -0
  7. package/dist/files/browser.js +227 -0
  8. package/dist/files/reader.js +159 -0
  9. package/dist/files/search.js +124 -0
  10. package/dist/git/gitHandler.js +177 -0
  11. package/dist/git/gitService.js +299 -0
  12. package/dist/git/index.js +8 -0
  13. package/dist/http/server.js +96 -0
  14. package/dist/index.js +77 -0
  15. package/dist/ssh/index.js +9 -0
  16. package/dist/ssh/sshHandler.js +205 -0
  17. package/dist/ssh/sshManager.js +329 -0
  18. package/dist/terminal/index.js +11 -0
  19. package/dist/terminal/localTerminalHandler.js +176 -0
  20. package/dist/terminal/localTerminalManager.js +497 -0
  21. package/dist/terminal/terminalWebSocket.js +136 -0
  22. package/dist/types.js +2 -0
  23. package/dist/utils/logger.js +42 -0
  24. package/dist/websocket/auth.js +18 -0
  25. package/dist/websocket/server.js +631 -0
  26. package/package.json +66 -0
  27. package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
  28. package/web-dist/assets/index-C8TJGN-T.css +41 -0
  29. package/web-dist/assets/index-DjLLxjMD.js +39 -0
  30. package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
  31. package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
  32. package/web-dist/assets/xterm-BCk906R6.js +9 -0
  33. package/web-dist/icon-192.png +0 -0
  34. package/web-dist/icon-512.png +0 -0
  35. package/web-dist/index.html +23 -0
  36. package/web-dist/manifest.json +24 -0
  37. package/web-dist/sw.js +35 -0
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.searchService = exports.SearchService = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const util_1 = require("util");
6
+ const config_1 = require("../config");
7
+ const logger_1 = require("../utils/logger");
8
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
9
+ class SearchService {
10
+ /**
11
+ * Search for text in files using ripgrep
12
+ */
13
+ async search(query, searchPath, options = {}) {
14
+ if (!query || query.trim().length === 0) {
15
+ throw new Error('Search query cannot be empty');
16
+ }
17
+ try {
18
+ const args = this.buildRipgrepArgs(query, options);
19
+ const command = `rg ${args.join(' ')} "${query}" "${searchPath}"`;
20
+ logger_1.logger.debug(`Executing search: ${command}`);
21
+ const { stdout, stderr } = await execAsync(command, {
22
+ timeout: config_1.CONFIG.searchTimeout,
23
+ maxBuffer: 10 * 1024 * 1024, // 10MB
24
+ });
25
+ if (stderr && !stderr.includes('No such file or directory')) {
26
+ logger_1.logger.warn('Ripgrep stderr:', stderr);
27
+ }
28
+ return this.parseRipgrepOutput(stdout, options.maxResults);
29
+ }
30
+ catch (error) {
31
+ // ripgrep returns exit code 1 when no matches found
32
+ if (error.code === 1) {
33
+ return [];
34
+ }
35
+ // ripgrep not installed
36
+ if (error.code === 127 || error.message.includes('command not found')) {
37
+ logger_1.logger.error('ripgrep not installed');
38
+ throw new Error('ripgrep is not installed. Please install it: https://github.com/BurntSushi/ripgrep');
39
+ }
40
+ // Timeout
41
+ if (error.killed) {
42
+ logger_1.logger.error('Search timeout');
43
+ throw new Error('Search timeout exceeded');
44
+ }
45
+ logger_1.logger.error('Search failed:', error);
46
+ throw new Error(`Search failed: ${error.message}`);
47
+ }
48
+ }
49
+ /**
50
+ * Build ripgrep command arguments
51
+ */
52
+ buildRipgrepArgs(query, options) {
53
+ const args = [
54
+ '--json', // JSON output for easier parsing
55
+ '--line-number',
56
+ '--column',
57
+ '--no-heading',
58
+ '--with-filename',
59
+ ];
60
+ // Case sensitivity
61
+ if (!options.caseSensitive) {
62
+ args.push('--ignore-case');
63
+ }
64
+ // Regex mode
65
+ if (!options.regex) {
66
+ args.push('--fixed-strings');
67
+ }
68
+ // File type filter
69
+ if (options.fileType) {
70
+ args.push(`--type=${options.fileType}`);
71
+ }
72
+ // Max results
73
+ if (options.maxResults) {
74
+ args.push(`--max-count=${options.maxResults}`);
75
+ }
76
+ return args;
77
+ }
78
+ /**
79
+ * Parse ripgrep JSON output
80
+ */
81
+ parseRipgrepOutput(output, maxResults) {
82
+ const results = [];
83
+ const lines = output.split('\n').filter(l => l.trim());
84
+ for (const line of lines) {
85
+ try {
86
+ const json = JSON.parse(line);
87
+ // Only process match entries
88
+ if (json.type === 'match') {
89
+ const data = json.data;
90
+ const result = {
91
+ file: data.path.text,
92
+ line: data.line_number,
93
+ column: data.submatches[0]?.start || 0,
94
+ content: data.lines.text.trim(),
95
+ match: data.submatches[0]?.match?.text || '',
96
+ };
97
+ results.push(result);
98
+ // Stop if we've reached max results
99
+ if (maxResults && results.length >= maxResults) {
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ catch (error) {
105
+ logger_1.logger.warn('Failed to parse ripgrep line:', line, error);
106
+ }
107
+ }
108
+ return results;
109
+ }
110
+ /**
111
+ * Check if ripgrep is installed
112
+ */
113
+ async isRipgrepInstalled() {
114
+ try {
115
+ await execAsync('rg --version');
116
+ return true;
117
+ }
118
+ catch (error) {
119
+ return false;
120
+ }
121
+ }
122
+ }
123
+ exports.SearchService = SearchService;
124
+ exports.searchService = new SearchService();
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHandler = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const gitService_1 = require("./gitService");
6
+ /**
7
+ * Handles Git-related WebSocket messages.
8
+ * Routes git_* messages to GitService methods.
9
+ */
10
+ class GitHandler {
11
+ constructor(sendFn) {
12
+ this.sendFn = sendFn;
13
+ }
14
+ canHandle(messageType) {
15
+ return messageType.startsWith('git_');
16
+ }
17
+ async handle(ws, clientId, message) {
18
+ switch (message.type) {
19
+ case 'git_status':
20
+ await this.handleStatus(ws, message);
21
+ break;
22
+ case 'git_file_diff':
23
+ await this.handleFileDiff(ws, message);
24
+ break;
25
+ case 'git_check_repo':
26
+ await this.handleCheckRepo(ws, message);
27
+ break;
28
+ case 'git_commit':
29
+ await this.handleCommit(ws, message);
30
+ break;
31
+ case 'git_stage':
32
+ await this.handleStage(ws, message);
33
+ break;
34
+ case 'git_unstage':
35
+ await this.handleUnstage(ws, message);
36
+ break;
37
+ case 'git_discard':
38
+ await this.handleDiscard(ws, message);
39
+ break;
40
+ default:
41
+ logger_1.logger.warn(`Unknown git message type: ${message.type}`);
42
+ }
43
+ }
44
+ async handleCheckRepo(ws, message) {
45
+ try {
46
+ const workingDir = message.path || process.cwd();
47
+ const isGitRepo = await gitService_1.gitService.isGitRepo(workingDir);
48
+ this.sendFn(ws, {
49
+ type: 'git_check_repo_response',
50
+ data: { isGitRepo },
51
+ });
52
+ }
53
+ catch (error) {
54
+ logger_1.logger.error('Git check repo error:', error);
55
+ this.sendFn(ws, {
56
+ type: 'git_check_repo_response',
57
+ data: { isGitRepo: false },
58
+ error: error.message,
59
+ });
60
+ }
61
+ }
62
+ async handleStatus(ws, message) {
63
+ try {
64
+ const workingDir = message.path || process.cwd();
65
+ const files = await gitService_1.gitService.getStatus(workingDir);
66
+ this.sendFn(ws, {
67
+ type: 'git_status_response',
68
+ data: { files },
69
+ });
70
+ }
71
+ catch (error) {
72
+ logger_1.logger.error('Git status error:', error);
73
+ this.sendFn(ws, {
74
+ type: 'error',
75
+ error: `Failed to get git status: ${error.message}`,
76
+ });
77
+ }
78
+ }
79
+ async handleCommit(ws, message) {
80
+ try {
81
+ const workingDir = message.path || process.cwd();
82
+ const { commitMessage, mode } = message;
83
+ const result = await gitService_1.gitService.commit(workingDir, commitMessage || '', mode || 'commit');
84
+ const files = await gitService_1.gitService.getStatus(workingDir);
85
+ this.sendFn(ws, {
86
+ type: 'git_commit_response',
87
+ data: { success: true, output: result, files },
88
+ });
89
+ }
90
+ catch (error) {
91
+ logger_1.logger.error('Git commit error:', error);
92
+ this.sendFn(ws, {
93
+ type: 'git_commit_response',
94
+ data: { success: false, output: error.message },
95
+ });
96
+ }
97
+ }
98
+ async handleStage(ws, message) {
99
+ try {
100
+ const workingDir = message.path || process.cwd();
101
+ const { filePath } = message;
102
+ if (!filePath) {
103
+ this.sendFn(ws, { type: 'error', error: 'File path is required for git stage' });
104
+ return;
105
+ }
106
+ await gitService_1.gitService.stageFile(workingDir, filePath);
107
+ const files = await gitService_1.gitService.getStatus(workingDir);
108
+ this.sendFn(ws, { type: 'git_status_response', data: { files } });
109
+ }
110
+ catch (error) {
111
+ logger_1.logger.error('Git stage error:', error);
112
+ this.sendFn(ws, { type: 'error', error: `Failed to stage file: ${error.message}` });
113
+ }
114
+ }
115
+ async handleUnstage(ws, message) {
116
+ try {
117
+ const workingDir = message.path || process.cwd();
118
+ const { filePath } = message;
119
+ if (!filePath) {
120
+ this.sendFn(ws, { type: 'error', error: 'File path is required for git unstage' });
121
+ return;
122
+ }
123
+ await gitService_1.gitService.unstageFile(workingDir, filePath);
124
+ const files = await gitService_1.gitService.getStatus(workingDir);
125
+ this.sendFn(ws, { type: 'git_status_response', data: { files } });
126
+ }
127
+ catch (error) {
128
+ logger_1.logger.error('Git unstage error:', error);
129
+ this.sendFn(ws, { type: 'error', error: `Failed to unstage file: ${error.message}` });
130
+ }
131
+ }
132
+ async handleDiscard(ws, message) {
133
+ try {
134
+ const workingDir = message.path || process.cwd();
135
+ const { filePath, fileStatus } = message;
136
+ if (!filePath) {
137
+ this.sendFn(ws, { type: 'error', error: 'File path is required for git discard' });
138
+ return;
139
+ }
140
+ await gitService_1.gitService.discardFile(workingDir, filePath, fileStatus || 'modified');
141
+ const files = await gitService_1.gitService.getStatus(workingDir);
142
+ this.sendFn(ws, { type: 'git_status_response', data: { files } });
143
+ }
144
+ catch (error) {
145
+ logger_1.logger.error('Git discard error:', error);
146
+ this.sendFn(ws, { type: 'error', error: `Failed to discard changes: ${error.message}` });
147
+ }
148
+ }
149
+ async handleFileDiff(ws, message) {
150
+ try {
151
+ const workingDir = message.path || process.cwd();
152
+ const { filePath, staged } = message;
153
+ logger_1.logger.info(`Git diff request: filePath=${filePath}, staged=${staged}, workingDir=${workingDir}`);
154
+ if (!filePath) {
155
+ this.sendFn(ws, {
156
+ type: 'error',
157
+ error: 'File path is required for git diff',
158
+ });
159
+ return;
160
+ }
161
+ const diff = await gitService_1.gitService.getFileDiff(workingDir, filePath, staged);
162
+ logger_1.logger.info(`Git diff result: ${diff.length} bytes`);
163
+ this.sendFn(ws, {
164
+ type: 'git_file_diff_response',
165
+ data: { filePath, diff },
166
+ });
167
+ }
168
+ catch (error) {
169
+ logger_1.logger.error('Git diff error:', error);
170
+ this.sendFn(ws, {
171
+ type: 'error',
172
+ error: `Failed to get diff: ${error.message}`,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ exports.GitHandler = GitHandler;
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.gitService = exports.GitService = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const promises_1 = require("fs/promises");
10
+ const path_1 = __importDefault(require("path"));
11
+ const logger_1 = require("../utils/logger");
12
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
13
+ /**
14
+ * Git service for executing git commands safely.
15
+ */
16
+ class GitService {
17
+ /**
18
+ * Check if a directory is inside a git repository.
19
+ */
20
+ async isGitRepo(workingDir) {
21
+ try {
22
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
23
+ cwd: workingDir,
24
+ });
25
+ return stdout.trim() === 'true';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Get the root directory of the git repository.
33
+ */
34
+ async getRepoRoot(workingDir) {
35
+ try {
36
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
37
+ cwd: workingDir,
38
+ });
39
+ return stdout.trim();
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Get git status for all changed files.
47
+ * Uses `git status --porcelain` for machine-parseable output.
48
+ */
49
+ async getStatus(workingDir) {
50
+ try {
51
+ // Use repo root so paths in output are consistent
52
+ const repoRoot = await this.getRepoRoot(workingDir);
53
+ const cwd = repoRoot || workingDir;
54
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-uall'], { cwd, maxBuffer: 10 * 1024 * 1024 });
55
+ if (!stdout.trim()) {
56
+ return [];
57
+ }
58
+ const files = [];
59
+ const lines = stdout.split('\n').filter(line => line.length > 0);
60
+ for (const line of lines) {
61
+ const parsed = this.parseStatusLine(line);
62
+ if (parsed) {
63
+ files.push(parsed);
64
+ }
65
+ }
66
+ return files;
67
+ }
68
+ catch (error) {
69
+ logger_1.logger.error('Failed to get git status:', error);
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * Parse a single line of git status --porcelain output.
75
+ * Format: XY PATH or XY ORIG_PATH -> PATH (for renames)
76
+ */
77
+ parseStatusLine(line) {
78
+ if (line.length < 3)
79
+ return null;
80
+ const indexStatus = line[0];
81
+ const workTreeStatus = line[1];
82
+ const filePath = line.slice(3);
83
+ // Handle renames: "R old -> new"
84
+ if (indexStatus === 'R' || workTreeStatus === 'R') {
85
+ const parts = filePath.split(' -> ');
86
+ if (parts.length === 2) {
87
+ return {
88
+ path: parts[1],
89
+ status: 'renamed',
90
+ staged: indexStatus === 'R',
91
+ oldPath: parts[0],
92
+ };
93
+ }
94
+ }
95
+ // Determine status based on index and work tree status
96
+ let status;
97
+ let staged = false;
98
+ // Prioritize staged changes for display
99
+ if (indexStatus === 'A') {
100
+ status = 'added';
101
+ staged = true;
102
+ }
103
+ else if (indexStatus === 'D') {
104
+ status = 'deleted';
105
+ staged = true;
106
+ }
107
+ else if (indexStatus === 'M') {
108
+ status = 'modified';
109
+ staged = true;
110
+ }
111
+ else if (workTreeStatus === 'M') {
112
+ status = 'modified';
113
+ staged = false;
114
+ }
115
+ else if (workTreeStatus === 'D') {
116
+ status = 'deleted';
117
+ staged = false;
118
+ }
119
+ else if (workTreeStatus === '?' || indexStatus === '?') {
120
+ status = 'untracked';
121
+ staged = false;
122
+ }
123
+ else if (workTreeStatus === 'A') {
124
+ status = 'added';
125
+ staged = false;
126
+ }
127
+ else {
128
+ // Fallback
129
+ status = 'modified';
130
+ staged = indexStatus !== ' ' && indexStatus !== '?';
131
+ }
132
+ return { path: filePath, status, staged };
133
+ }
134
+ /**
135
+ * Get diff for a specific file.
136
+ * Returns unified diff format.
137
+ *
138
+ * Strategy:
139
+ * - Untracked files (??) → generate synthetic diff from file content
140
+ * - New files (A) → use git diff --cached (compare with empty)
141
+ * - Other files → use git diff HEAD (shows all changes vs last commit)
142
+ */
143
+ async getFileDiff(workingDir, filePath, staged = false) {
144
+ try {
145
+ // Get the repo root to ensure correct path handling
146
+ const repoRoot = await this.getRepoRoot(workingDir);
147
+ const cwd = repoRoot || workingDir;
148
+ logger_1.logger.info(`getFileDiff: cwd=${cwd}, filePath=${filePath}`);
149
+ // Get the actual status of this file
150
+ const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain', '--', filePath], { cwd });
151
+ const statusLine = statusOut.trim().split('\n')[0] || '';
152
+ const indexStatus = statusLine[0] || ' ';
153
+ const workTreeStatus = statusLine[1] || ' ';
154
+ logger_1.logger.info(`getFileDiff: status line="${statusLine}", index=${indexStatus}, worktree=${workTreeStatus}`);
155
+ // Case 1: Untracked file - generate diff from file content
156
+ if (indexStatus === '?' || workTreeStatus === '?') {
157
+ logger_1.logger.info('getFileDiff: untracked file, generating synthetic diff');
158
+ return this.getUntrackedFileDiff(cwd, filePath);
159
+ }
160
+ // Case 2: Newly added file (only in index, not in HEAD)
161
+ if (indexStatus === 'A') {
162
+ logger_1.logger.info('getFileDiff: new file, using --cached');
163
+ const { stdout } = await execFileAsync('git', ['diff', '--cached', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
164
+ if (stdout.trim()) {
165
+ return stdout;
166
+ }
167
+ // Fallback: if --cached returns empty (shouldn't happen), show file content
168
+ return this.getUntrackedFileDiff(cwd, filePath);
169
+ }
170
+ // Case 3: Modified/Deleted files - use git diff HEAD to show all changes
171
+ // This captures both staged and unstaged changes relative to last commit
172
+ logger_1.logger.info('getFileDiff: tracked file, using diff HEAD');
173
+ const { stdout: headDiff } = await execFileAsync('git', ['diff', 'HEAD', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
174
+ if (headDiff.trim()) {
175
+ logger_1.logger.info(`getFileDiff: HEAD diff length=${headDiff.length}`);
176
+ return headDiff;
177
+ }
178
+ // Fallback: try regular diff (in case HEAD doesn't exist)
179
+ logger_1.logger.info('getFileDiff: HEAD diff empty, trying regular diff');
180
+ const { stdout: regularDiff } = await execFileAsync('git', ['diff', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
181
+ if (regularDiff.trim()) {
182
+ return regularDiff;
183
+ }
184
+ // Last resort: try --cached
185
+ logger_1.logger.info('getFileDiff: regular diff empty, trying --cached');
186
+ const { stdout: cachedDiff } = await execFileAsync('git', ['diff', '--cached', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
187
+ return cachedDiff;
188
+ }
189
+ catch (error) {
190
+ logger_1.logger.error('Failed to get file diff:', error);
191
+ throw error;
192
+ }
193
+ }
194
+ /**
195
+ * Generate a diff-like output for an untracked file.
196
+ */
197
+ async getUntrackedFileDiff(workingDir, filePath) {
198
+ try {
199
+ const fullPath = path_1.default.join(workingDir, filePath);
200
+ // Check if file is binary
201
+ const isBinary = await this.isBinaryFile(fullPath);
202
+ if (isBinary) {
203
+ return `diff --git a/${filePath} b/${filePath}\nnew file mode 100644\nBinary file`;
204
+ }
205
+ // Read file content
206
+ const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
207
+ const lines = content.split('\n');
208
+ const diffLines = [
209
+ `diff --git a/${filePath} b/${filePath}`,
210
+ 'new file mode 100644',
211
+ '--- /dev/null',
212
+ `+++ b/${filePath}`,
213
+ `@@ -0,0 +1,${lines.length} @@`,
214
+ ...lines.map(line => `+${line}`),
215
+ ];
216
+ return diffLines.join('\n');
217
+ }
218
+ catch (error) {
219
+ logger_1.logger.error('Failed to generate untracked file diff:', error);
220
+ throw error;
221
+ }
222
+ }
223
+ /**
224
+ * Commit staged changes.
225
+ * mode: 'commit' | 'amend' | 'push' | 'sync'
226
+ * For 'amend', message can be empty to reuse the last commit message (--no-edit).
227
+ * 'push' = commit then push.
228
+ * 'sync' = commit, pull --rebase, push.
229
+ */
230
+ async commit(workingDir, message, mode) {
231
+ const repoRoot = await this.getRepoRoot(workingDir);
232
+ const cwd = repoRoot || workingDir;
233
+ // Build commit args
234
+ if (mode === 'amend') {
235
+ const args = message.trim()
236
+ ? ['commit', '--amend', '-m', message]
237
+ : ['commit', '--amend', '--no-edit'];
238
+ const { stdout } = await execFileAsync('git', args, { cwd });
239
+ return stdout.trim();
240
+ }
241
+ if (!message.trim()) {
242
+ throw new Error('Commit message is required');
243
+ }
244
+ const { stdout: commitOut } = await execFileAsync('git', ['commit', '-m', message], { cwd });
245
+ if (mode === 'push') {
246
+ await execFileAsync('git', ['push'], { cwd });
247
+ return commitOut.trim() + '\nPushed successfully.';
248
+ }
249
+ if (mode === 'sync') {
250
+ await execFileAsync('git', ['pull', '--rebase'], { cwd });
251
+ await execFileAsync('git', ['push'], { cwd });
252
+ return commitOut.trim() + '\nSynced successfully.';
253
+ }
254
+ return commitOut.trim();
255
+ }
256
+ /**
257
+ * Stage a file (git add).
258
+ */
259
+ async stageFile(workingDir, filePath) {
260
+ const repoRoot = await this.getRepoRoot(workingDir);
261
+ const cwd = repoRoot || workingDir;
262
+ await execFileAsync('git', ['add', '--', filePath], { cwd });
263
+ }
264
+ /**
265
+ * Unstage a file (git restore --staged).
266
+ */
267
+ async unstageFile(workingDir, filePath) {
268
+ const repoRoot = await this.getRepoRoot(workingDir);
269
+ const cwd = repoRoot || workingDir;
270
+ await execFileAsync('git', ['restore', '--staged', '--', filePath], { cwd });
271
+ }
272
+ /**
273
+ * Discard unstaged changes for a file (git restore or git clean for untracked).
274
+ */
275
+ async discardFile(workingDir, filePath, status) {
276
+ const repoRoot = await this.getRepoRoot(workingDir);
277
+ const cwd = repoRoot || workingDir;
278
+ if (status === 'untracked') {
279
+ await execFileAsync('git', ['clean', '-f', '--', filePath], { cwd });
280
+ }
281
+ else {
282
+ await execFileAsync('git', ['restore', '--', filePath], { cwd });
283
+ }
284
+ }
285
+ /**
286
+ * Check if a file is binary by looking at the first few bytes.
287
+ */
288
+ async isBinaryFile(filePath) {
289
+ try {
290
+ const { stdout } = await execFileAsync('file', ['--mime', filePath]);
291
+ return stdout.includes('charset=binary');
292
+ }
293
+ catch {
294
+ return false;
295
+ }
296
+ }
297
+ }
298
+ exports.GitService = GitService;
299
+ exports.gitService = new GitService();
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHandler = exports.gitService = exports.GitService = void 0;
4
+ var gitService_1 = require("./gitService");
5
+ Object.defineProperty(exports, "GitService", { enumerable: true, get: function () { return gitService_1.GitService; } });
6
+ Object.defineProperty(exports, "gitService", { enumerable: true, get: function () { return gitService_1.gitService; } });
7
+ var gitHandler_1 = require("./gitHandler");
8
+ Object.defineProperty(exports, "GitHandler", { enumerable: true, get: function () { return gitHandler_1.GitHandler; } });