@agi-cli/server 0.1.61 → 0.1.63
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/package.json +3 -3
- package/src/openapi/spec.ts +47 -0
- package/src/routes/git.ts +514 -426
- package/src/runtime/cache-optimizer.ts +51 -29
- package/src/runtime/db-operations.ts +48 -43
- package/src/runtime/runner.ts +248 -99
- package/src/runtime/stream-handlers.ts +209 -175
package/src/routes/git.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
3
|
import { extname } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { generateText, resolveModel } from '@agi-cli/sdk';
|
|
6
|
+
import { generateText, resolveModel, type ProviderId } from '@agi-cli/sdk';
|
|
7
7
|
import { loadConfig } from '@agi-cli/sdk';
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -22,6 +22,71 @@ const gitDiffSchema = z.object({
|
|
|
22
22
|
.transform((val) => val === 'true'),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
const LANGUAGE_MAP: Record<string, string> = {
|
|
26
|
+
js: 'javascript',
|
|
27
|
+
jsx: 'jsx',
|
|
28
|
+
ts: 'typescript',
|
|
29
|
+
tsx: 'tsx',
|
|
30
|
+
py: 'python',
|
|
31
|
+
rb: 'ruby',
|
|
32
|
+
go: 'go',
|
|
33
|
+
rs: 'rust',
|
|
34
|
+
java: 'java',
|
|
35
|
+
c: 'c',
|
|
36
|
+
cpp: 'cpp',
|
|
37
|
+
h: 'c',
|
|
38
|
+
hpp: 'cpp',
|
|
39
|
+
cs: 'csharp',
|
|
40
|
+
php: 'php',
|
|
41
|
+
sh: 'bash',
|
|
42
|
+
bash: 'bash',
|
|
43
|
+
zsh: 'bash',
|
|
44
|
+
sql: 'sql',
|
|
45
|
+
json: 'json',
|
|
46
|
+
yaml: 'yaml',
|
|
47
|
+
yml: 'yaml',
|
|
48
|
+
xml: 'xml',
|
|
49
|
+
html: 'html',
|
|
50
|
+
css: 'css',
|
|
51
|
+
scss: 'scss',
|
|
52
|
+
md: 'markdown',
|
|
53
|
+
txt: 'plaintext',
|
|
54
|
+
svelte: 'svelte',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function inferLanguage(filePath: string): string {
|
|
58
|
+
const extension = extname(filePath).toLowerCase().replace('.', '');
|
|
59
|
+
if (!extension) {
|
|
60
|
+
return 'plaintext';
|
|
61
|
+
}
|
|
62
|
+
return LANGUAGE_MAP[extension] ?? 'plaintext';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summarizeDiff(diff: string): {
|
|
66
|
+
insertions: number;
|
|
67
|
+
deletions: number;
|
|
68
|
+
binary: boolean;
|
|
69
|
+
} {
|
|
70
|
+
let insertions = 0;
|
|
71
|
+
let deletions = 0;
|
|
72
|
+
let binary = false;
|
|
73
|
+
|
|
74
|
+
for (const line of diff.split('\n')) {
|
|
75
|
+
if (line.startsWith('Binary files ') || line.includes('GIT binary patch')) {
|
|
76
|
+
binary = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
81
|
+
insertions++;
|
|
82
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
83
|
+
deletions++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { insertions, deletions, binary };
|
|
88
|
+
}
|
|
89
|
+
|
|
25
90
|
const gitStageSchema = z.object({
|
|
26
91
|
project: z.string().optional(),
|
|
27
92
|
files: z.array(z.string()),
|
|
@@ -41,6 +106,10 @@ const gitGenerateCommitMessageSchema = z.object({
|
|
|
41
106
|
project: z.string().optional(),
|
|
42
107
|
});
|
|
43
108
|
|
|
109
|
+
const gitPushSchema = z.object({
|
|
110
|
+
project: z.string().optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
44
113
|
// Types
|
|
45
114
|
export interface GitFile {
|
|
46
115
|
path: string;
|
|
@@ -48,144 +117,75 @@ export interface GitFile {
|
|
|
48
117
|
staged: boolean;
|
|
49
118
|
insertions?: number;
|
|
50
119
|
deletions?: number;
|
|
51
|
-
oldPath?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface GitStatus {
|
|
55
|
-
branch: string;
|
|
56
|
-
ahead: number;
|
|
57
|
-
behind: number;
|
|
58
|
-
staged: GitFile[];
|
|
59
|
-
unstaged: GitFile[];
|
|
60
|
-
untracked: GitFile[];
|
|
61
|
-
hasChanges: boolean;
|
|
62
120
|
}
|
|
63
121
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'.ts': 'typescript',
|
|
67
|
-
'.tsx': 'typescript',
|
|
68
|
-
'.js': 'javascript',
|
|
69
|
-
'.jsx': 'javascript',
|
|
70
|
-
'.py': 'python',
|
|
71
|
-
'.java': 'java',
|
|
72
|
-
'.c': 'c',
|
|
73
|
-
'.cpp': 'cpp',
|
|
74
|
-
'.go': 'go',
|
|
75
|
-
'.rs': 'rust',
|
|
76
|
-
'.rb': 'ruby',
|
|
77
|
-
'.php': 'php',
|
|
78
|
-
'.css': 'css',
|
|
79
|
-
'.html': 'html',
|
|
80
|
-
'.json': 'json',
|
|
81
|
-
'.xml': 'xml',
|
|
82
|
-
'.yaml': 'yaml',
|
|
83
|
-
'.yml': 'yaml',
|
|
84
|
-
'.md': 'markdown',
|
|
85
|
-
'.sh': 'bash',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
function detectLanguage(filePath: string): string {
|
|
89
|
-
const ext = extname(filePath).toLowerCase();
|
|
90
|
-
return languageMap[ext] || 'plaintext';
|
|
122
|
+
interface GitRoot {
|
|
123
|
+
gitRoot: string;
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: path });
|
|
97
|
-
return true;
|
|
98
|
-
} catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
126
|
+
interface GitError {
|
|
127
|
+
error: string;
|
|
128
|
+
code?: string;
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
// Helper
|
|
104
|
-
async function
|
|
131
|
+
// Helper functions
|
|
132
|
+
async function validateAndGetGitRoot(
|
|
133
|
+
requestedPath: string,
|
|
134
|
+
): Promise<GitRoot | GitError> {
|
|
105
135
|
try {
|
|
106
|
-
const { stdout } = await execFileAsync(
|
|
136
|
+
const { stdout: gitRoot } = await execFileAsync(
|
|
107
137
|
'git',
|
|
108
138
|
['rev-parse', '--show-toplevel'],
|
|
109
|
-
{
|
|
139
|
+
{
|
|
140
|
+
cwd: requestedPath,
|
|
141
|
+
},
|
|
110
142
|
);
|
|
111
|
-
return
|
|
143
|
+
return { gitRoot: gitRoot.trim() };
|
|
112
144
|
} catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Helper to validate git repo and get root
|
|
118
|
-
async function validateAndGetGitRoot(
|
|
119
|
-
path: string,
|
|
120
|
-
): Promise<{ gitRoot: string } | { error: string; code: string }> {
|
|
121
|
-
if (!(await isGitRepository(path))) {
|
|
122
|
-
return { error: 'Not a git repository', code: 'NOT_A_GIT_REPO' };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const gitRoot = await findGitRoot(path);
|
|
126
|
-
if (!gitRoot) {
|
|
127
145
|
return {
|
|
128
|
-
error: '
|
|
129
|
-
code: '
|
|
146
|
+
error: 'Not a git repository',
|
|
147
|
+
code: 'NOT_A_GIT_REPO',
|
|
130
148
|
};
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
return { gitRoot };
|
|
134
150
|
}
|
|
135
151
|
|
|
136
|
-
|
|
137
|
-
function parseGitStatus(porcelainOutput: string): {
|
|
152
|
+
function parseGitStatus(statusOutput: string): {
|
|
138
153
|
staged: GitFile[];
|
|
139
154
|
unstaged: GitFile[];
|
|
140
155
|
untracked: GitFile[];
|
|
141
156
|
} {
|
|
157
|
+
const lines = statusOutput.trim().split('\n').filter(Boolean);
|
|
142
158
|
const staged: GitFile[] = [];
|
|
143
159
|
const unstaged: GitFile[] = [];
|
|
144
160
|
const untracked: GitFile[] = [];
|
|
145
161
|
|
|
146
|
-
const lines = porcelainOutput.split('\n').filter((line) => line.trim());
|
|
147
|
-
|
|
148
162
|
for (const line of lines) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const unstagedStatus = line[1];
|
|
153
|
-
const filePath = line.slice(3);
|
|
154
|
-
|
|
155
|
-
// Parse staged files
|
|
156
|
-
if (stagedStatus !== ' ' && stagedStatus !== '?') {
|
|
157
|
-
let status: GitFile['status'] = 'modified';
|
|
158
|
-
if (stagedStatus === 'A') status = 'added';
|
|
159
|
-
else if (stagedStatus === 'D') status = 'deleted';
|
|
160
|
-
else if (stagedStatus === 'R') status = 'renamed';
|
|
161
|
-
else if (stagedStatus === 'M') status = 'modified';
|
|
163
|
+
const x = line[0]; // staged status
|
|
164
|
+
const y = line[1]; // unstaged status
|
|
165
|
+
const path = line.slice(3).trim();
|
|
162
166
|
|
|
167
|
+
// Check if file is staged (X is not space or ?)
|
|
168
|
+
if (x !== ' ' && x !== '?') {
|
|
163
169
|
staged.push({
|
|
164
|
-
path
|
|
165
|
-
status,
|
|
170
|
+
path,
|
|
171
|
+
status: getStatusFromCode(x),
|
|
166
172
|
staged: true,
|
|
167
173
|
});
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
// and additional working directory changes
|
|
173
|
-
if (unstagedStatus !== ' ' && unstagedStatus !== '?') {
|
|
174
|
-
let status: GitFile['status'] = 'modified';
|
|
175
|
-
if (unstagedStatus === 'M') status = 'modified';
|
|
176
|
-
else if (unstagedStatus === 'D') status = 'deleted';
|
|
177
|
-
|
|
176
|
+
// Check if file is unstaged (Y is not space)
|
|
177
|
+
if (y !== ' ' && y !== '?') {
|
|
178
178
|
unstaged.push({
|
|
179
|
-
path
|
|
180
|
-
status,
|
|
179
|
+
path,
|
|
180
|
+
status: getStatusFromCode(y),
|
|
181
181
|
staged: false,
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
//
|
|
186
|
-
if (
|
|
185
|
+
// Check if file is untracked
|
|
186
|
+
if (x === '?' && y === '?') {
|
|
187
187
|
untracked.push({
|
|
188
|
-
path
|
|
188
|
+
path,
|
|
189
189
|
status: 'untracked',
|
|
190
190
|
staged: false,
|
|
191
191
|
});
|
|
@@ -195,63 +195,54 @@ function parseGitStatus(porcelainOutput: string): {
|
|
|
195
195
|
return { staged, unstaged, untracked };
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const filePath = parts[2];
|
|
211
|
-
|
|
212
|
-
const file = files.find((f) => f.path === filePath);
|
|
213
|
-
if (file) {
|
|
214
|
-
file.insertions = Number.isNaN(insertions) ? 0 : insertions;
|
|
215
|
-
file.deletions = Number.isNaN(deletions) ? 0 : deletions;
|
|
216
|
-
}
|
|
198
|
+
function getStatusFromCode(code: string): GitFile['status'] {
|
|
199
|
+
switch (code) {
|
|
200
|
+
case 'M':
|
|
201
|
+
return 'modified';
|
|
202
|
+
case 'A':
|
|
203
|
+
return 'added';
|
|
204
|
+
case 'D':
|
|
205
|
+
return 'deleted';
|
|
206
|
+
case 'R':
|
|
207
|
+
return 'renamed';
|
|
208
|
+
default:
|
|
209
|
+
return 'modified';
|
|
217
210
|
}
|
|
218
211
|
}
|
|
219
212
|
|
|
220
|
-
async function
|
|
213
|
+
async function getAheadBehind(
|
|
214
|
+
gitRoot: string,
|
|
215
|
+
): Promise<{ ahead: number; behind: number }> {
|
|
221
216
|
try {
|
|
222
217
|
const { stdout } = await execFileAsync(
|
|
223
218
|
'git',
|
|
224
|
-
['
|
|
225
|
-
{ cwd },
|
|
219
|
+
['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'],
|
|
220
|
+
{ cwd: gitRoot },
|
|
226
221
|
);
|
|
227
|
-
|
|
222
|
+
const [ahead, behind] = stdout.trim().split(/\s+/).map(Number);
|
|
223
|
+
return { ahead: ahead || 0, behind: behind || 0 };
|
|
228
224
|
} catch {
|
|
229
|
-
return
|
|
225
|
+
return { ahead: 0, behind: 0 };
|
|
230
226
|
}
|
|
231
227
|
}
|
|
232
228
|
|
|
233
|
-
async function
|
|
234
|
-
cwd: string,
|
|
235
|
-
): Promise<{ ahead: number; behind: number }> {
|
|
229
|
+
async function getCurrentBranch(gitRoot: string): Promise<string> {
|
|
236
230
|
try {
|
|
237
231
|
const { stdout } = await execFileAsync(
|
|
238
232
|
'git',
|
|
239
|
-
['
|
|
240
|
-
{
|
|
233
|
+
['branch', '--show-current'],
|
|
234
|
+
{
|
|
235
|
+
cwd: gitRoot,
|
|
236
|
+
},
|
|
241
237
|
);
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
ahead: Number.parseInt(parts[0], 10) || 0,
|
|
245
|
-
behind: Number.parseInt(parts[1], 10) || 0,
|
|
246
|
-
};
|
|
238
|
+
return stdout.trim();
|
|
247
239
|
} catch {
|
|
248
|
-
return
|
|
240
|
+
return 'unknown';
|
|
249
241
|
}
|
|
250
242
|
}
|
|
251
243
|
|
|
252
|
-
// Route handlers
|
|
253
244
|
export function registerGitRoutes(app: Hono) {
|
|
254
|
-
// GET /v1/git/status - Get
|
|
245
|
+
// GET /v1/git/status - Get git status
|
|
255
246
|
app.get('/v1/git/status', async (c) => {
|
|
256
247
|
try {
|
|
257
248
|
const query = gitStatusSchema.parse({
|
|
@@ -270,7 +261,7 @@ export function registerGitRoutes(app: Hono) {
|
|
|
270
261
|
|
|
271
262
|
const { gitRoot } = validation;
|
|
272
263
|
|
|
273
|
-
// Get
|
|
264
|
+
// Get status
|
|
274
265
|
const { stdout: statusOutput } = await execFileAsync(
|
|
275
266
|
'git',
|
|
276
267
|
['status', '--porcelain=v1'],
|
|
@@ -279,67 +270,41 @@ export function registerGitRoutes(app: Hono) {
|
|
|
279
270
|
|
|
280
271
|
const { staged, unstaged, untracked } = parseGitStatus(statusOutput);
|
|
281
272
|
|
|
282
|
-
// Get
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const { stdout: stagedNumstat } = await execFileAsync(
|
|
286
|
-
'git',
|
|
287
|
-
['diff', '--cached', '--numstat'],
|
|
288
|
-
{ cwd: gitRoot },
|
|
289
|
-
);
|
|
290
|
-
await parseNumstat(stagedNumstat, staged);
|
|
291
|
-
} catch {
|
|
292
|
-
// Ignore numstat errors
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Get stats for unstaged files
|
|
297
|
-
if (unstaged.length > 0) {
|
|
298
|
-
try {
|
|
299
|
-
const { stdout: unstagedNumstat } = await execFileAsync(
|
|
300
|
-
'git',
|
|
301
|
-
['diff', '--numstat'],
|
|
302
|
-
{ cwd: gitRoot },
|
|
303
|
-
);
|
|
304
|
-
await parseNumstat(unstagedNumstat, unstaged);
|
|
305
|
-
} catch {
|
|
306
|
-
// Ignore numstat errors
|
|
307
|
-
}
|
|
308
|
-
}
|
|
273
|
+
// Get ahead/behind counts
|
|
274
|
+
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
309
275
|
|
|
310
|
-
// Get branch
|
|
276
|
+
// Get current branch
|
|
311
277
|
const branch = await getCurrentBranch(gitRoot);
|
|
312
|
-
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
313
278
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
behind,
|
|
318
|
-
staged,
|
|
319
|
-
unstaged,
|
|
320
|
-
untracked,
|
|
321
|
-
hasChanges:
|
|
322
|
-
staged.length > 0 || unstaged.length > 0 || untracked.length > 0,
|
|
323
|
-
};
|
|
279
|
+
// Calculate hasChanges
|
|
280
|
+
const hasChanges =
|
|
281
|
+
staged.length > 0 || unstaged.length > 0 || untracked.length > 0;
|
|
324
282
|
|
|
325
283
|
return c.json({
|
|
326
284
|
status: 'ok',
|
|
327
|
-
data:
|
|
285
|
+
data: {
|
|
286
|
+
branch,
|
|
287
|
+
ahead,
|
|
288
|
+
behind,
|
|
289
|
+
staged,
|
|
290
|
+
unstaged,
|
|
291
|
+
untracked,
|
|
292
|
+
hasChanges,
|
|
293
|
+
},
|
|
328
294
|
});
|
|
329
295
|
} catch (error) {
|
|
330
|
-
const errorMessage =
|
|
331
|
-
error instanceof Error ? error.message : 'Failed to get git status';
|
|
332
296
|
return c.json(
|
|
333
297
|
{
|
|
334
298
|
status: 'error',
|
|
335
|
-
error:
|
|
299
|
+
error:
|
|
300
|
+
error instanceof Error ? error.message : 'Failed to get status',
|
|
336
301
|
},
|
|
337
302
|
500,
|
|
338
303
|
);
|
|
339
304
|
}
|
|
340
305
|
});
|
|
341
306
|
|
|
342
|
-
// GET /v1/git/diff - Get diff
|
|
307
|
+
// GET /v1/git/diff - Get file diff
|
|
343
308
|
app.get('/v1/git/diff', async (c) => {
|
|
344
309
|
try {
|
|
345
310
|
const query = gitDiffSchema.parse({
|
|
@@ -359,187 +324,105 @@ export function registerGitRoutes(app: Hono) {
|
|
|
359
324
|
}
|
|
360
325
|
|
|
361
326
|
const { gitRoot } = validation;
|
|
362
|
-
const file = query.file;
|
|
363
|
-
const staged = query.staged;
|
|
364
327
|
|
|
365
|
-
//
|
|
366
|
-
const
|
|
367
|
-
'
|
|
368
|
-
['
|
|
369
|
-
|
|
370
|
-
|
|
328
|
+
// Get diff output and stats for the requested file
|
|
329
|
+
const diffArgs = query.staged
|
|
330
|
+
? ['diff', '--cached', '--', query.file]
|
|
331
|
+
: ['diff', '--', query.file];
|
|
332
|
+
const numstatArgs = query.staged
|
|
333
|
+
? ['diff', '--cached', '--numstat', '--', query.file]
|
|
334
|
+
: ['diff', '--numstat', '--', query.file];
|
|
371
335
|
|
|
372
|
-
const
|
|
336
|
+
const [{ stdout: diffOutput }, { stdout: numstatOutput }] =
|
|
337
|
+
await Promise.all([
|
|
338
|
+
execFileAsync('git', diffArgs, { cwd: gitRoot }),
|
|
339
|
+
execFileAsync('git', numstatArgs, { cwd: gitRoot }),
|
|
340
|
+
]);
|
|
373
341
|
|
|
374
|
-
let diffOutput = '';
|
|
375
342
|
let insertions = 0;
|
|
376
343
|
let deletions = 0;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// Create a diff-like output showing all lines as additions
|
|
388
|
-
diffOutput = `diff --git a/${file} b/${file}\n`;
|
|
389
|
-
diffOutput += `new file mode 100644\n`;
|
|
390
|
-
diffOutput += `--- /dev/null\n`;
|
|
391
|
-
diffOutput += `+++ b/${file}\n`;
|
|
392
|
-
diffOutput += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
393
|
-
diffOutput += lines.map((line) => `+${line}`).join('\n');
|
|
394
|
-
|
|
395
|
-
insertions = lines.length;
|
|
396
|
-
deletions = 0;
|
|
397
|
-
} catch (err) {
|
|
398
|
-
diffOutput = `Error reading file: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
399
|
-
}
|
|
400
|
-
} else {
|
|
401
|
-
// For tracked files, use git diff
|
|
402
|
-
const args = ['diff'];
|
|
403
|
-
if (staged) {
|
|
404
|
-
args.push('--cached');
|
|
405
|
-
}
|
|
406
|
-
args.push('--', file);
|
|
407
|
-
|
|
408
|
-
const { stdout: gitDiff } = await execFileAsync('git', args, {
|
|
409
|
-
cwd: gitRoot,
|
|
410
|
-
});
|
|
411
|
-
diffOutput = gitDiff;
|
|
412
|
-
|
|
413
|
-
// Get stats
|
|
414
|
-
const numstatArgs = ['diff', '--numstat'];
|
|
415
|
-
if (staged) {
|
|
416
|
-
numstatArgs.push('--cached');
|
|
344
|
+
let binary = false;
|
|
345
|
+
|
|
346
|
+
const numstatLine = numstatOutput.trim().split('\n').find(Boolean);
|
|
347
|
+
if (numstatLine) {
|
|
348
|
+
const [rawInsertions, rawDeletions] = numstatLine.split('\t');
|
|
349
|
+
if (rawInsertions === '-' || rawDeletions === '-') {
|
|
350
|
+
binary = true;
|
|
351
|
+
} else {
|
|
352
|
+
insertions = Number.parseInt(rawInsertions, 10) || 0;
|
|
353
|
+
deletions = Number.parseInt(rawDeletions, 10) || 0;
|
|
417
354
|
}
|
|
418
|
-
|
|
355
|
+
}
|
|
419
356
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (parts.length >= 2) {
|
|
428
|
-
insertions = Number.parseInt(parts[0], 10) || 0;
|
|
429
|
-
deletions = Number.parseInt(parts[1], 10) || 0;
|
|
430
|
-
}
|
|
431
|
-
} catch {
|
|
432
|
-
// Ignore numstat errors
|
|
357
|
+
const diffText = diffOutput ?? '';
|
|
358
|
+
if (!binary) {
|
|
359
|
+
const summary = summarizeDiff(diffText);
|
|
360
|
+
binary = summary.binary;
|
|
361
|
+
if (insertions === 0 && deletions === 0) {
|
|
362
|
+
insertions = summary.insertions;
|
|
363
|
+
deletions = summary.deletions;
|
|
433
364
|
}
|
|
434
365
|
}
|
|
435
366
|
|
|
436
|
-
|
|
437
|
-
const isBinary = diffOutput.includes('Binary files');
|
|
367
|
+
const language = inferLanguage(query.file);
|
|
438
368
|
|
|
439
369
|
return c.json({
|
|
440
370
|
status: 'ok',
|
|
441
371
|
data: {
|
|
442
|
-
file,
|
|
443
|
-
diff:
|
|
372
|
+
file: query.file,
|
|
373
|
+
diff: diffText,
|
|
444
374
|
insertions,
|
|
445
375
|
deletions,
|
|
446
|
-
language
|
|
447
|
-
binary
|
|
376
|
+
language,
|
|
377
|
+
binary,
|
|
448
378
|
},
|
|
449
379
|
});
|
|
450
380
|
} catch (error) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
381
|
+
return c.json(
|
|
382
|
+
{
|
|
383
|
+
status: 'error',
|
|
384
|
+
error: error instanceof Error ? error.message : 'Failed to get diff',
|
|
385
|
+
},
|
|
386
|
+
500,
|
|
387
|
+
);
|
|
454
388
|
}
|
|
455
389
|
});
|
|
456
390
|
|
|
457
|
-
// POST /v1/git/
|
|
458
|
-
app.post('/v1/git/
|
|
391
|
+
// POST /v1/git/stage - Stage files
|
|
392
|
+
app.post('/v1/git/stage', async (c) => {
|
|
459
393
|
try {
|
|
460
394
|
const body = await c.req.json();
|
|
461
|
-
const { project } =
|
|
395
|
+
const { files, project } = gitStageSchema.parse(body);
|
|
396
|
+
|
|
462
397
|
const requestedPath = project || process.cwd();
|
|
463
|
-
const gitRoot = await findGitRoot(requestedPath);
|
|
464
398
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
399
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
400
|
+
if ('error' in validation) {
|
|
401
|
+
return c.json(
|
|
402
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
403
|
+
400,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { gitRoot } = validation;
|
|
471
408
|
|
|
472
|
-
if (
|
|
409
|
+
if (files.length === 0) {
|
|
473
410
|
return c.json(
|
|
474
411
|
{
|
|
475
412
|
status: 'error',
|
|
476
|
-
error: 'No
|
|
413
|
+
error: 'No files specified',
|
|
477
414
|
},
|
|
478
415
|
400,
|
|
479
416
|
);
|
|
480
417
|
}
|
|
481
418
|
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
'git',
|
|
485
|
-
['diff', '--cached'],
|
|
486
|
-
{ cwd: gitRoot },
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
// Limit diff size to avoid token limits (keep first 8000 chars)
|
|
490
|
-
const limitedDiff =
|
|
491
|
-
stagedDiff.length > 8000
|
|
492
|
-
? `${stagedDiff.slice(0, 8000)}\n\n... (diff truncated due to size)`
|
|
493
|
-
: stagedDiff;
|
|
494
|
-
|
|
495
|
-
// Generate commit message using AI
|
|
496
|
-
const prompt = `Based on the following git diff of staged changes, generate a clear and concise commit message following these guidelines:
|
|
497
|
-
|
|
498
|
-
1. Use conventional commit format: <type>(<scope>): <subject>
|
|
499
|
-
2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
|
500
|
-
3. Keep the subject line under 72 characters
|
|
501
|
-
4. Use imperative mood ("add" not "added" or "adds")
|
|
502
|
-
5. Don't end the subject line with a period
|
|
503
|
-
6. If there are multiple significant changes, focus on the most important one
|
|
504
|
-
7. Be specific about what changed
|
|
505
|
-
|
|
506
|
-
Git diff of staged changes:
|
|
507
|
-
\`\`\`diff
|
|
508
|
-
${limitedDiff}
|
|
509
|
-
\`\`\`
|
|
510
|
-
|
|
511
|
-
Generate only the commit message, nothing else.`;
|
|
512
|
-
|
|
513
|
-
// Load config to get default provider/model
|
|
514
|
-
const cfg = await loadConfig(gitRoot);
|
|
515
|
-
|
|
516
|
-
// Resolve the model using SDK - this doesn't create any session
|
|
517
|
-
const model = await resolveModel(
|
|
518
|
-
cfg.defaults.provider,
|
|
519
|
-
cfg.defaults.model,
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// Generate text directly - no session involved
|
|
523
|
-
const result = await generateText({
|
|
524
|
-
model: model as Parameters<typeof generateText>[0]['model'],
|
|
525
|
-
prompt,
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// Extract and clean up the message
|
|
529
|
-
let message = result.text || '';
|
|
530
|
-
|
|
531
|
-
// Clean up the message (remove markdown code blocks if present)
|
|
532
|
-
if (typeof message === 'string') {
|
|
533
|
-
message = message
|
|
534
|
-
.replace(/^```(?:text|commit)?\n?/gm, '')
|
|
535
|
-
.replace(/```$/gm, '')
|
|
536
|
-
.trim();
|
|
537
|
-
}
|
|
419
|
+
// Stage files
|
|
420
|
+
await execFileAsync('git', ['add', ...files], { cwd: gitRoot });
|
|
538
421
|
|
|
539
422
|
return c.json({
|
|
540
423
|
status: 'ok',
|
|
541
424
|
data: {
|
|
542
|
-
|
|
425
|
+
staged: files,
|
|
543
426
|
},
|
|
544
427
|
});
|
|
545
428
|
} catch (error) {
|
|
@@ -547,32 +430,50 @@ Generate only the commit message, nothing else.`;
|
|
|
547
430
|
{
|
|
548
431
|
status: 'error',
|
|
549
432
|
error:
|
|
550
|
-
error instanceof Error
|
|
551
|
-
? error.message
|
|
552
|
-
: 'Failed to generate commit message',
|
|
433
|
+
error instanceof Error ? error.message : 'Failed to stage files',
|
|
553
434
|
},
|
|
554
435
|
500,
|
|
555
436
|
);
|
|
556
437
|
}
|
|
557
438
|
});
|
|
558
439
|
|
|
559
|
-
// POST /v1/git/
|
|
560
|
-
app.post('/v1/git/
|
|
440
|
+
// POST /v1/git/unstage - Unstage files
|
|
441
|
+
app.post('/v1/git/unstage', async (c) => {
|
|
561
442
|
try {
|
|
562
443
|
const body = await c.req.json();
|
|
563
|
-
const {
|
|
444
|
+
const { files, project } = gitUnstageSchema.parse(body);
|
|
564
445
|
|
|
565
446
|
const requestedPath = project || process.cwd();
|
|
566
|
-
const gitRoot = await findGitRoot(requestedPath);
|
|
567
447
|
|
|
568
|
-
|
|
569
|
-
|
|
448
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
449
|
+
if ('error' in validation) {
|
|
450
|
+
return c.json(
|
|
451
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
452
|
+
400,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const { gitRoot } = validation;
|
|
457
|
+
|
|
458
|
+
if (files.length === 0) {
|
|
459
|
+
return c.json(
|
|
460
|
+
{
|
|
461
|
+
status: 'error',
|
|
462
|
+
error: 'No files specified',
|
|
463
|
+
},
|
|
464
|
+
400,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Unstage files
|
|
469
|
+
await execFileAsync('git', ['reset', 'HEAD', '--', ...files], {
|
|
470
|
+
cwd: gitRoot,
|
|
471
|
+
});
|
|
570
472
|
|
|
571
473
|
return c.json({
|
|
572
474
|
status: 'ok',
|
|
573
475
|
data: {
|
|
574
|
-
|
|
575
|
-
failed: [],
|
|
476
|
+
unstaged: files,
|
|
576
477
|
},
|
|
577
478
|
});
|
|
578
479
|
} catch (error) {
|
|
@@ -580,129 +481,161 @@ Generate only the commit message, nothing else.`;
|
|
|
580
481
|
{
|
|
581
482
|
status: 'error',
|
|
582
483
|
error:
|
|
583
|
-
error instanceof Error ? error.message : 'Failed to
|
|
484
|
+
error instanceof Error ? error.message : 'Failed to unstage files',
|
|
584
485
|
},
|
|
585
486
|
500,
|
|
586
487
|
);
|
|
587
488
|
}
|
|
588
489
|
});
|
|
589
490
|
|
|
590
|
-
// POST /v1/git/
|
|
591
|
-
app.post('/v1/git/
|
|
491
|
+
// POST /v1/git/commit - Commit staged changes
|
|
492
|
+
app.post('/v1/git/commit', async (c) => {
|
|
592
493
|
try {
|
|
593
494
|
const body = await c.req.json();
|
|
594
|
-
const {
|
|
495
|
+
const { message, project } = gitCommitSchema.parse(body);
|
|
595
496
|
|
|
596
497
|
const requestedPath = project || process.cwd();
|
|
597
|
-
const gitRoot = await findGitRoot(requestedPath);
|
|
598
498
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// Fallback to older git reset HEAD
|
|
606
|
-
await execFileAsync('git', ['reset', 'HEAD', ...files], {
|
|
607
|
-
cwd: gitRoot,
|
|
608
|
-
});
|
|
499
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
500
|
+
if ('error' in validation) {
|
|
501
|
+
return c.json(
|
|
502
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
503
|
+
400,
|
|
504
|
+
);
|
|
609
505
|
}
|
|
610
506
|
|
|
507
|
+
const { gitRoot } = validation;
|
|
508
|
+
|
|
509
|
+
// Commit changes
|
|
510
|
+
const { stdout } = await execFileAsync('git', ['commit', '-m', message], {
|
|
511
|
+
cwd: gitRoot,
|
|
512
|
+
});
|
|
513
|
+
|
|
611
514
|
return c.json({
|
|
612
515
|
status: 'ok',
|
|
613
516
|
data: {
|
|
614
|
-
|
|
615
|
-
failed: [],
|
|
517
|
+
message: stdout.trim(),
|
|
616
518
|
},
|
|
617
519
|
});
|
|
618
520
|
} catch (error) {
|
|
619
521
|
return c.json(
|
|
620
522
|
{
|
|
621
523
|
status: 'error',
|
|
622
|
-
error:
|
|
623
|
-
error instanceof Error ? error.message : 'Failed to unstage files',
|
|
524
|
+
error: error instanceof Error ? error.message : 'Failed to commit',
|
|
624
525
|
},
|
|
625
526
|
500,
|
|
626
527
|
);
|
|
627
528
|
}
|
|
628
529
|
});
|
|
629
530
|
|
|
630
|
-
// POST /v1/git/commit -
|
|
631
|
-
app.post('/v1/git/commit', async (c) => {
|
|
531
|
+
// POST /v1/git/generate-commit-message - Generate commit message from staged changes
|
|
532
|
+
app.post('/v1/git/generate-commit-message', async (c) => {
|
|
632
533
|
try {
|
|
633
534
|
const body = await c.req.json();
|
|
634
|
-
const { project
|
|
535
|
+
const { project } = gitGenerateCommitMessageSchema.parse(body);
|
|
635
536
|
|
|
636
537
|
const requestedPath = project || process.cwd();
|
|
637
|
-
const gitRoot = await findGitRoot(requestedPath);
|
|
638
538
|
|
|
639
|
-
|
|
640
|
-
|
|
539
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
540
|
+
if ('error' in validation) {
|
|
541
|
+
return c.json(
|
|
542
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
543
|
+
400,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { gitRoot } = validation;
|
|
548
|
+
|
|
549
|
+
// Get staged diff
|
|
550
|
+
const { stdout: diff } = await execFileAsync(
|
|
641
551
|
'git',
|
|
642
|
-
['diff', '--cached'
|
|
643
|
-
{
|
|
552
|
+
['diff', '--cached'],
|
|
553
|
+
{
|
|
554
|
+
cwd: gitRoot,
|
|
555
|
+
},
|
|
644
556
|
);
|
|
645
557
|
|
|
646
|
-
if (!
|
|
558
|
+
if (!diff.trim()) {
|
|
647
559
|
return c.json(
|
|
648
560
|
{
|
|
649
561
|
status: 'error',
|
|
650
|
-
error: 'No staged changes to
|
|
562
|
+
error: 'No staged changes to generate message from',
|
|
651
563
|
},
|
|
652
564
|
400,
|
|
653
565
|
);
|
|
654
566
|
}
|
|
655
567
|
|
|
656
|
-
//
|
|
657
|
-
const { stdout:
|
|
568
|
+
// Get file list for context
|
|
569
|
+
const { stdout: statusOutput } = await execFileAsync(
|
|
658
570
|
'git',
|
|
659
|
-
['
|
|
571
|
+
['status', '--porcelain=v1'],
|
|
660
572
|
{ cwd: gitRoot },
|
|
661
573
|
);
|
|
574
|
+
const { staged } = parseGitStatus(statusOutput);
|
|
575
|
+
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
662
576
|
|
|
663
|
-
//
|
|
664
|
-
const
|
|
665
|
-
const hash = hashMatch ? hashMatch[1] : '';
|
|
577
|
+
// Load config to get provider settings
|
|
578
|
+
const config = await loadConfig();
|
|
666
579
|
|
|
667
|
-
//
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
580
|
+
// Use a simple model for quick commit message generation
|
|
581
|
+
const provider = config.defaults?.provider || 'anthropic';
|
|
582
|
+
const model = await resolveModel(
|
|
583
|
+
provider as ProviderId,
|
|
584
|
+
config.defaults?.model,
|
|
585
|
+
undefined,
|
|
672
586
|
);
|
|
673
587
|
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
588
|
+
// Generate commit message using AI
|
|
589
|
+
const prompt = `Generate a concise, conventional commit message for these git changes.
|
|
590
|
+
|
|
591
|
+
Staged files:
|
|
592
|
+
${fileList}
|
|
593
|
+
|
|
594
|
+
Diff (first 2000 chars):
|
|
595
|
+
${diff.slice(0, 2000)}
|
|
596
|
+
|
|
597
|
+
Guidelines:
|
|
598
|
+
- Use conventional commits format (feat:, fix:, docs:, etc.)
|
|
599
|
+
- Keep the first line under 72 characters
|
|
600
|
+
- Be specific but concise
|
|
601
|
+
- Focus on what changed and why, not how
|
|
602
|
+
- Do not include any markdown formatting or code blocks
|
|
603
|
+
- Return ONLY the commit message text, nothing else
|
|
604
|
+
|
|
605
|
+
Commit message:`;
|
|
606
|
+
|
|
607
|
+
const { text } = await generateText({
|
|
608
|
+
provider: provider as ProviderId,
|
|
609
|
+
model: model.id,
|
|
610
|
+
systemPrompt:
|
|
611
|
+
'You are a helpful assistant that generates git commit messages.',
|
|
612
|
+
prompt,
|
|
613
|
+
maxTokens: 200,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const message = text.trim();
|
|
677
617
|
|
|
678
618
|
return c.json({
|
|
679
619
|
status: 'ok',
|
|
680
620
|
data: {
|
|
681
|
-
|
|
682
|
-
message: message.split('\n')[0],
|
|
683
|
-
filesChanged: filesChangedMatch
|
|
684
|
-
? Number.parseInt(filesChangedMatch[1], 10)
|
|
685
|
-
: 0,
|
|
686
|
-
insertions: insertionsMatch
|
|
687
|
-
? Number.parseInt(insertionsMatch[1], 10)
|
|
688
|
-
: 0,
|
|
689
|
-
deletions: deletionsMatch
|
|
690
|
-
? Number.parseInt(deletionsMatch[1], 10)
|
|
691
|
-
: 0,
|
|
621
|
+
message,
|
|
692
622
|
},
|
|
693
623
|
});
|
|
694
624
|
} catch (error) {
|
|
695
625
|
return c.json(
|
|
696
626
|
{
|
|
697
627
|
status: 'error',
|
|
698
|
-
error:
|
|
628
|
+
error:
|
|
629
|
+
error instanceof Error
|
|
630
|
+
? error.message
|
|
631
|
+
: 'Failed to generate commit message',
|
|
699
632
|
},
|
|
700
633
|
500,
|
|
701
634
|
);
|
|
702
635
|
}
|
|
703
636
|
});
|
|
704
637
|
|
|
705
|
-
// GET /v1/git/branch - Get branch
|
|
638
|
+
// GET /v1/git/branch - Get branch info
|
|
706
639
|
app.get('/v1/git/branch', async (c) => {
|
|
707
640
|
try {
|
|
708
641
|
const query = gitStatusSchema.parse({
|
|
@@ -721,56 +654,211 @@ Generate only the commit message, nothing else.`;
|
|
|
721
654
|
|
|
722
655
|
const { gitRoot } = validation;
|
|
723
656
|
|
|
657
|
+
// Get current branch
|
|
724
658
|
const branch = await getCurrentBranch(gitRoot);
|
|
659
|
+
|
|
660
|
+
// Get ahead/behind counts
|
|
725
661
|
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
726
662
|
|
|
727
|
-
// Get
|
|
728
|
-
let allBranches: string[] = [];
|
|
663
|
+
// Get remote info
|
|
729
664
|
try {
|
|
730
|
-
const { stdout:
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
665
|
+
const { stdout: remotes } = await execFileAsync('git', ['remote'], {
|
|
666
|
+
cwd: gitRoot,
|
|
667
|
+
});
|
|
668
|
+
const remoteList = remotes.trim().split('\n').filter(Boolean);
|
|
669
|
+
|
|
670
|
+
return c.json({
|
|
671
|
+
status: 'ok',
|
|
672
|
+
data: {
|
|
673
|
+
branch,
|
|
674
|
+
ahead,
|
|
675
|
+
behind,
|
|
676
|
+
remotes: remoteList,
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
} catch {
|
|
680
|
+
return c.json({
|
|
681
|
+
status: 'ok',
|
|
682
|
+
data: {
|
|
683
|
+
branch,
|
|
684
|
+
ahead,
|
|
685
|
+
behind,
|
|
686
|
+
remotes: [],
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
return c.json(
|
|
692
|
+
{
|
|
693
|
+
status: 'error',
|
|
694
|
+
error:
|
|
695
|
+
error instanceof Error
|
|
696
|
+
? error.message
|
|
697
|
+
: 'Failed to get branch info',
|
|
698
|
+
},
|
|
699
|
+
500,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// POST /v1/git/push - Push commits to remote
|
|
705
|
+
app.post('/v1/git/push', async (c) => {
|
|
706
|
+
try {
|
|
707
|
+
// Parse JSON body, defaulting to empty object if parsing fails
|
|
708
|
+
let body = {};
|
|
709
|
+
try {
|
|
710
|
+
body = await c.req.json();
|
|
711
|
+
} catch (jsonError) {
|
|
712
|
+
// If JSON parsing fails (e.g., empty body), use empty object
|
|
713
|
+
console.warn(
|
|
714
|
+
'Failed to parse JSON body for git push, using empty object:',
|
|
715
|
+
jsonError,
|
|
734
716
|
);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const { project } = gitPushSchema.parse(body);
|
|
720
|
+
|
|
721
|
+
const requestedPath = project || process.cwd();
|
|
722
|
+
|
|
723
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
724
|
+
if ('error' in validation) {
|
|
725
|
+
return c.json(
|
|
726
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
727
|
+
400,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const { gitRoot } = validation;
|
|
732
|
+
|
|
733
|
+
// Check if there's a remote configured
|
|
734
|
+
try {
|
|
735
|
+
const { stdout: remotes } = await execFileAsync('git', ['remote'], {
|
|
736
|
+
cwd: gitRoot,
|
|
737
|
+
});
|
|
738
|
+
if (!remotes.trim()) {
|
|
739
|
+
return c.json(
|
|
740
|
+
{ status: 'error', error: 'No remote repository configured' },
|
|
741
|
+
400,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
739
744
|
} catch {
|
|
740
|
-
|
|
745
|
+
return c.json(
|
|
746
|
+
{ status: 'error', error: 'No remote repository configured' },
|
|
747
|
+
400,
|
|
748
|
+
);
|
|
741
749
|
}
|
|
742
750
|
|
|
743
|
-
// Get
|
|
744
|
-
|
|
751
|
+
// Get current branch and check for upstream
|
|
752
|
+
const branch = await getCurrentBranch(gitRoot);
|
|
753
|
+
let hasUpstream = false;
|
|
745
754
|
try {
|
|
746
|
-
|
|
755
|
+
await execFileAsync(
|
|
747
756
|
'git',
|
|
748
|
-
['rev-parse', '--abbrev-ref', '
|
|
749
|
-
{
|
|
757
|
+
['rev-parse', '--abbrev-ref', '@{upstream}'],
|
|
758
|
+
{
|
|
759
|
+
cwd: gitRoot,
|
|
760
|
+
},
|
|
750
761
|
);
|
|
751
|
-
|
|
762
|
+
hasUpstream = true;
|
|
752
763
|
} catch {
|
|
753
|
-
// No upstream
|
|
764
|
+
// No upstream set
|
|
754
765
|
}
|
|
755
766
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
767
|
+
// Push to remote - with proper error handling
|
|
768
|
+
try {
|
|
769
|
+
let pushOutput: string;
|
|
770
|
+
let pushError: string;
|
|
771
|
+
|
|
772
|
+
if (hasUpstream) {
|
|
773
|
+
// Push to existing upstream
|
|
774
|
+
const result = await execFileAsync('git', ['push'], { cwd: gitRoot });
|
|
775
|
+
pushOutput = result.stdout;
|
|
776
|
+
pushError = result.stderr;
|
|
777
|
+
} else {
|
|
778
|
+
// Set upstream and push
|
|
779
|
+
const result = await execFileAsync(
|
|
780
|
+
'git',
|
|
781
|
+
['push', '--set-upstream', 'origin', branch],
|
|
782
|
+
{ cwd: gitRoot },
|
|
783
|
+
);
|
|
784
|
+
pushOutput = result.stdout;
|
|
785
|
+
pushError = result.stderr;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return c.json({
|
|
789
|
+
status: 'ok',
|
|
790
|
+
data: {
|
|
791
|
+
output: pushOutput.trim() || pushError.trim(),
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
} catch (pushErr: unknown) {
|
|
795
|
+
// Handle specific git push errors
|
|
796
|
+
const error = pushErr as {
|
|
797
|
+
message?: string;
|
|
798
|
+
stderr?: string;
|
|
799
|
+
code?: number;
|
|
800
|
+
};
|
|
801
|
+
const errorMessage = error.stderr || error.message || 'Failed to push';
|
|
802
|
+
|
|
803
|
+
// Check for common error patterns
|
|
804
|
+
if (
|
|
805
|
+
errorMessage.includes('failed to push') ||
|
|
806
|
+
errorMessage.includes('rejected')
|
|
807
|
+
) {
|
|
808
|
+
return c.json(
|
|
809
|
+
{
|
|
810
|
+
status: 'error',
|
|
811
|
+
error: 'Push rejected. Try pulling changes first with: git pull',
|
|
812
|
+
details: errorMessage,
|
|
813
|
+
},
|
|
814
|
+
400,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (
|
|
819
|
+
errorMessage.includes('Permission denied') ||
|
|
820
|
+
errorMessage.includes('authentication') ||
|
|
821
|
+
errorMessage.includes('could not read')
|
|
822
|
+
) {
|
|
823
|
+
return c.json(
|
|
824
|
+
{
|
|
825
|
+
status: 'error',
|
|
826
|
+
error: 'Authentication failed. Check your git credentials',
|
|
827
|
+
details: errorMessage,
|
|
828
|
+
},
|
|
829
|
+
401,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (
|
|
834
|
+
errorMessage.includes('Could not resolve host') ||
|
|
835
|
+
errorMessage.includes('network')
|
|
836
|
+
) {
|
|
837
|
+
return c.json(
|
|
838
|
+
{
|
|
839
|
+
status: 'error',
|
|
840
|
+
error: 'Network error. Check your internet connection',
|
|
841
|
+
details: errorMessage,
|
|
842
|
+
},
|
|
843
|
+
503,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Generic push error
|
|
848
|
+
return c.json(
|
|
849
|
+
{
|
|
850
|
+
status: 'error',
|
|
851
|
+
error: 'Failed to push commits',
|
|
852
|
+
details: errorMessage,
|
|
853
|
+
},
|
|
854
|
+
500,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
766
857
|
} catch (error) {
|
|
767
858
|
return c.json(
|
|
768
859
|
{
|
|
769
860
|
status: 'error',
|
|
770
|
-
error:
|
|
771
|
-
error instanceof Error
|
|
772
|
-
? error.message
|
|
773
|
-
: 'Failed to get branch info',
|
|
861
|
+
error: error instanceof Error ? error.message : 'Failed to push',
|
|
774
862
|
},
|
|
775
863
|
500,
|
|
776
864
|
);
|