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.
- package/dist/__tests__/auth.test.js +49 -0
- package/dist/__tests__/watcher.test.js +58 -0
- package/dist/claude/chatService.js +175 -0
- package/dist/claude/sessionBrowser.js +743 -0
- package/dist/claude/watcher.js +242 -0
- package/dist/config.js +44 -0
- package/dist/files/browser.js +227 -0
- package/dist/files/reader.js +159 -0
- package/dist/files/search.js +124 -0
- package/dist/git/gitHandler.js +177 -0
- package/dist/git/gitService.js +299 -0
- package/dist/git/index.js +8 -0
- package/dist/http/server.js +96 -0
- package/dist/index.js +77 -0
- package/dist/ssh/index.js +9 -0
- package/dist/ssh/sshHandler.js +205 -0
- package/dist/ssh/sshManager.js +329 -0
- package/dist/terminal/index.js +11 -0
- package/dist/terminal/localTerminalHandler.js +176 -0
- package/dist/terminal/localTerminalManager.js +497 -0
- package/dist/terminal/terminalWebSocket.js +136 -0
- package/dist/types.js +2 -0
- package/dist/utils/logger.js +42 -0
- package/dist/websocket/auth.js +18 -0
- package/dist/websocket/server.js +631 -0
- package/package.json +66 -0
- package/web-dist/assets/highlight-l0sNRNKZ.js +1 -0
- package/web-dist/assets/index-C8TJGN-T.css +41 -0
- package/web-dist/assets/index-DjLLxjMD.js +39 -0
- package/web-dist/assets/markdown-C_j0ZeeY.js +51 -0
- package/web-dist/assets/react-vendor-CqP5oCk4.js +9 -0
- package/web-dist/assets/xterm-BCk906R6.js +9 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/index.html +23 -0
- package/web-dist/manifest.json +24 -0
- 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; } });
|