@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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. 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.66",
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.66",
33
- "@agi-cli/database": "0.1.66",
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
- function parseGitStatus(statusOutput: string): {
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
- const x = line[0]; // staged status
164
- const y = line[1]; // unstaged status
165
- const path = line.slice(3).trim();
166
-
167
- // Check if file is staged (X is not space or ?)
168
- if (x !== ' ' && x !== '?') {
169
- staged.push({
170
- path,
171
- status: getStatusFromCode(x),
172
- staged: true,
173
- });
174
- }
175
-
176
- // Check if file is unstaged (Y is not space)
177
- if (y !== ' ' && y !== '?') {
178
- unstaged.push({
179
- path,
180
- status: getStatusFromCode(y),
181
- staged: false,
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
- // Check if file is untracked
186
- if (x === '?' && y === '?') {
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=v1'],
322
+ ['status', '--porcelain=v2'],
268
323
  { cwd: gitRoot },
269
324
  );
270
325
 
271
- const { staged, unstaged, untracked } = parseGitStatus(statusOutput);
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
- // Get diff output and stats for the requested file
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
- binary,
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=v1'],
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