@agi-cli/server 0.1.66 → 0.1.67
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/routes/git.ts +132 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agi-cli/sdk": "0.1.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.67",
|
|
33
|
+
"@agi-cli/database": "0.1.67",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/routes/git.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { extname } from 'node:path';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { generateText, resolveModel, type ProviderId } from '@agi-cli/sdk';
|
|
@@ -113,10 +114,13 @@ const gitPushSchema = z.object({
|
|
|
113
114
|
// Types
|
|
114
115
|
export interface GitFile {
|
|
115
116
|
path: string;
|
|
117
|
+
absPath: string; // NEW: Absolute filesystem path
|
|
116
118
|
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked';
|
|
117
119
|
staged: boolean;
|
|
118
120
|
insertions?: number;
|
|
119
121
|
deletions?: number;
|
|
122
|
+
oldPath?: string; // For renamed files
|
|
123
|
+
isNew: boolean; // NEW: True for untracked or newly added files
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
interface GitRoot {
|
|
@@ -149,7 +153,25 @@ async function validateAndGetGitRoot(
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Check if a file is new/untracked (not in git index)
|
|
158
|
+
*/
|
|
159
|
+
async function checkIfNewFile(gitRoot: string, file: string): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
// Check if file exists in git index or committed
|
|
162
|
+
await execFileAsync('git', ['ls-files', '--error-unmatch', file], {
|
|
163
|
+
cwd: gitRoot,
|
|
164
|
+
});
|
|
165
|
+
return false; // File exists in git
|
|
166
|
+
} catch {
|
|
167
|
+
return true; // File is new/untracked
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseGitStatus(
|
|
172
|
+
statusOutput: string,
|
|
173
|
+
gitRoot: string,
|
|
174
|
+
): {
|
|
153
175
|
staged: GitFile[];
|
|
154
176
|
unstaged: GitFile[];
|
|
155
177
|
untracked: GitFile[];
|
|
@@ -160,34 +182,50 @@ function parseGitStatus(statusOutput: string): {
|
|
|
160
182
|
const untracked: GitFile[] = [];
|
|
161
183
|
|
|
162
184
|
for (const line of lines) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
// Porcelain v2 format has different line types
|
|
186
|
+
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
187
|
+
// Regular changed entry: "1 XY sub <mH> <mI> <mW> <hH> <hI> <path>"
|
|
188
|
+
// XY is a 2-character field with staged (X) and unstaged (Y) status
|
|
189
|
+
const parts = line.split(' ');
|
|
190
|
+
if (parts.length < 9) continue;
|
|
191
|
+
|
|
192
|
+
const xy = parts[1]; // e.g., ".M", "M.", "MM", "A.", etc.
|
|
193
|
+
const x = xy[0]; // staged status
|
|
194
|
+
const y = xy[1]; // unstaged status
|
|
195
|
+
const path = parts.slice(8).join(' '); // Path can contain spaces
|
|
196
|
+
const absPath = join(gitRoot, path);
|
|
197
|
+
|
|
198
|
+
// Check if file is staged (X is not '.')
|
|
199
|
+
if (x !== '.') {
|
|
200
|
+
staged.push({
|
|
201
|
+
path,
|
|
202
|
+
absPath,
|
|
203
|
+
status: getStatusFromCodeV2(x),
|
|
204
|
+
staged: true,
|
|
205
|
+
isNew: x === 'A',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
184
208
|
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
// Check if file is unstaged (Y is not '.')
|
|
210
|
+
if (y !== '.') {
|
|
211
|
+
unstaged.push({
|
|
212
|
+
path,
|
|
213
|
+
absPath,
|
|
214
|
+
status: getStatusFromCodeV2(y),
|
|
215
|
+
staged: false,
|
|
216
|
+
isNew: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} else if (line.startsWith('? ')) {
|
|
220
|
+
// Untracked file: "? <path>"
|
|
221
|
+
const path = line.slice(2);
|
|
222
|
+
const absPath = join(gitRoot, path);
|
|
187
223
|
untracked.push({
|
|
188
224
|
path,
|
|
225
|
+
absPath,
|
|
189
226
|
status: 'untracked',
|
|
190
227
|
staged: false,
|
|
228
|
+
isNew: true,
|
|
191
229
|
});
|
|
192
230
|
}
|
|
193
231
|
}
|
|
@@ -210,6 +248,23 @@ function getStatusFromCode(code: string): GitFile['status'] {
|
|
|
210
248
|
}
|
|
211
249
|
}
|
|
212
250
|
|
|
251
|
+
function getStatusFromCodeV2(code: string): GitFile['status'] {
|
|
252
|
+
switch (code) {
|
|
253
|
+
case 'M':
|
|
254
|
+
return 'modified';
|
|
255
|
+
case 'A':
|
|
256
|
+
return 'added';
|
|
257
|
+
case 'D':
|
|
258
|
+
return 'deleted';
|
|
259
|
+
case 'R':
|
|
260
|
+
return 'renamed';
|
|
261
|
+
case 'C':
|
|
262
|
+
return 'modified'; // Copied - treat as modified
|
|
263
|
+
default:
|
|
264
|
+
return 'modified';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
213
268
|
async function getAheadBehind(
|
|
214
269
|
gitRoot: string,
|
|
215
270
|
): Promise<{ ahead: number; behind: number }> {
|
|
@@ -264,11 +319,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
264
319
|
// Get status
|
|
265
320
|
const { stdout: statusOutput } = await execFileAsync(
|
|
266
321
|
'git',
|
|
267
|
-
['status', '--porcelain=
|
|
322
|
+
['status', '--porcelain=v2'],
|
|
268
323
|
{ cwd: gitRoot },
|
|
269
324
|
);
|
|
270
325
|
|
|
271
|
-
const { staged, unstaged, untracked } = parseGitStatus(
|
|
326
|
+
const { staged, unstaged, untracked } = parseGitStatus(
|
|
327
|
+
statusOutput,
|
|
328
|
+
gitRoot,
|
|
329
|
+
);
|
|
272
330
|
|
|
273
331
|
// Get ahead/behind counts
|
|
274
332
|
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
@@ -286,6 +344,8 @@ export function registerGitRoutes(app: Hono) {
|
|
|
286
344
|
branch,
|
|
287
345
|
ahead,
|
|
288
346
|
behind,
|
|
347
|
+
gitRoot, // NEW: Expose git root path
|
|
348
|
+
workingDir: requestedPath, // NEW: Current working directory
|
|
289
349
|
staged,
|
|
290
350
|
unstaged,
|
|
291
351
|
untracked,
|
|
@@ -324,8 +384,46 @@ export function registerGitRoutes(app: Hono) {
|
|
|
324
384
|
}
|
|
325
385
|
|
|
326
386
|
const { gitRoot } = validation;
|
|
387
|
+
const absPath = join(gitRoot, query.file);
|
|
388
|
+
|
|
389
|
+
// Check if file is new/untracked
|
|
390
|
+
const isNewFile = await checkIfNewFile(gitRoot, query.file);
|
|
391
|
+
|
|
392
|
+
// For new files, read and return full content
|
|
393
|
+
if (isNewFile) {
|
|
394
|
+
try {
|
|
395
|
+
const content = await readFile(absPath, 'utf-8');
|
|
396
|
+
const lineCount = content.split('\n').length;
|
|
397
|
+
const language = inferLanguage(query.file);
|
|
398
|
+
|
|
399
|
+
return c.json({
|
|
400
|
+
status: 'ok',
|
|
401
|
+
data: {
|
|
402
|
+
file: query.file,
|
|
403
|
+
absPath,
|
|
404
|
+
diff: '', // Empty diff for new files
|
|
405
|
+
content, // NEW: Full file content
|
|
406
|
+
isNewFile: true, // NEW: Flag indicating this is a new file
|
|
407
|
+
isBinary: false,
|
|
408
|
+
insertions: lineCount,
|
|
409
|
+
deletions: 0,
|
|
410
|
+
language,
|
|
411
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return c.json(
|
|
416
|
+
{
|
|
417
|
+
status: 'error',
|
|
418
|
+
error:
|
|
419
|
+
error instanceof Error ? error.message : 'Failed to read file',
|
|
420
|
+
},
|
|
421
|
+
500,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
327
425
|
|
|
328
|
-
//
|
|
426
|
+
// For existing files, get diff output and stats
|
|
329
427
|
const diffArgs = query.staged
|
|
330
428
|
? ['diff', '--cached', '--', query.file]
|
|
331
429
|
: ['diff', '--', query.file];
|
|
@@ -370,11 +468,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
370
468
|
status: 'ok',
|
|
371
469
|
data: {
|
|
372
470
|
file: query.file,
|
|
471
|
+
absPath, // NEW: Absolute path
|
|
373
472
|
diff: diffText,
|
|
473
|
+
isNewFile: false, // NEW: Not a new file
|
|
474
|
+
isBinary: binary,
|
|
374
475
|
insertions,
|
|
375
476
|
deletions,
|
|
376
477
|
language,
|
|
377
|
-
|
|
478
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
378
479
|
},
|
|
379
480
|
});
|
|
380
481
|
} catch (error) {
|
|
@@ -568,10 +669,10 @@ export function registerGitRoutes(app: Hono) {
|
|
|
568
669
|
// Get file list for context
|
|
569
670
|
const { stdout: statusOutput } = await execFileAsync(
|
|
570
671
|
'git',
|
|
571
|
-
['status', '--porcelain=
|
|
672
|
+
['status', '--porcelain=v2'],
|
|
572
673
|
{ cwd: gitRoot },
|
|
573
674
|
);
|
|
574
|
-
const { staged } = parseGitStatus(statusOutput);
|
|
675
|
+
const { staged } = parseGitStatus(statusOutput, gitRoot);
|
|
575
676
|
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
576
677
|
|
|
577
678
|
// Load config to get provider settings
|