@thegitai/cli 1.0.0-preview.36 → 1.0.0-preview.37

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.
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, lstatSync, readFileSync } from 'node:fs';
3
+ import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { BINARY_ARTIFACT_EXTENSIONS } from './artifact-policy.js';
6
6
  import { resolveProjectPath } from './patcher.js';
@@ -30,13 +30,33 @@ function snapshotEncoding(filePath, content) {
30
30
  return 'base64';
31
31
  return 'utf8';
32
32
  }
33
+ const MAX_SNAPSHOT_READ_BYTES = MAX_STORED_CONTENT_CHARS;
34
+ const SNAPSHOT_HASH_CHUNK_BYTES = 1024 * 1024;
35
+ function hashFileInChunks(absPath) {
36
+ const hash = createHash('sha256');
37
+ const chunk = Buffer.allocUnsafe(SNAPSHOT_HASH_CHUNK_BYTES);
38
+ const fd = openSync(absPath, 'r');
39
+ try {
40
+ for (;;) {
41
+ const read = readSync(fd, chunk, 0, chunk.length, null);
42
+ if (read <= 0)
43
+ break;
44
+ hash.update(chunk.subarray(0, read));
45
+ }
46
+ }
47
+ finally {
48
+ closeSync(fd);
49
+ }
50
+ return `sha256:${hash.digest('hex')}`;
51
+ }
33
52
  export function readFileEditSnapshot(rootDir, filePath) {
34
53
  try {
35
54
  const absPath = resolveProjectPath(rootDir, filePath);
36
55
  if (!existsSync(absPath)) {
37
56
  return { exists: false, content: null, contentEncoding: 'utf8', hash: null };
38
57
  }
39
- if (lstatSync(absPath).isSymbolicLink()) {
58
+ const stat = lstatSync(absPath);
59
+ if (stat.isSymbolicLink()) {
40
60
  return {
41
61
  exists: true,
42
62
  content: null,
@@ -45,6 +65,17 @@ export function readFileEditSnapshot(rootDir, filePath) {
45
65
  error: `Refusing to snapshot symbolic link: ${filePath}`,
46
66
  };
47
67
  }
68
+ if (stat.isFile() && stat.size > MAX_SNAPSHOT_READ_BYTES) {
69
+ return {
70
+ exists: true,
71
+ content: null,
72
+ contentEncoding: BINARY_ARTIFACT_EXTENSIONS.has(path.extname(filePath).toLowerCase())
73
+ ? 'base64'
74
+ : 'utf8',
75
+ hash: hashFileInChunks(absPath),
76
+ error: `File is too large to snapshot (${stat.size} bytes).`,
77
+ };
78
+ }
48
79
  const content = readFileSync(absPath);
49
80
  const encoding = snapshotEncoding(filePath, content);
50
81
  return {
@@ -1,5 +1,5 @@
1
1
  import chalk from './colors.js';
2
- import { chmodSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from 'fs';
2
+ import { chmodSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, unlinkSync, writeFileSync, } from 'fs';
3
3
  import path from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { runCommand } from './executor.js';
@@ -271,22 +271,61 @@ export function writeProjectFileBuffer(rootDir, filePath, content) {
271
271
  }
272
272
  return { absPath, changed: true };
273
273
  }
274
+ const MAX_DELETED_CONTENT_BYTES = 64 * 1024;
275
+ const DELETED_BINARY_SNIFF_BYTES = 4096;
276
+ function headHasNulByte(absPath) {
277
+ const chunk = Buffer.allocUnsafe(DELETED_BINARY_SNIFF_BYTES);
278
+ let fd = null;
279
+ try {
280
+ fd = openSync(absPath, 'r');
281
+ const read = readSync(fd, chunk, 0, chunk.length, 0);
282
+ return read > 0 && chunk.subarray(0, read).includes(0);
283
+ }
284
+ catch {
285
+ return false;
286
+ }
287
+ finally {
288
+ if (fd !== null)
289
+ closeSync(fd);
290
+ }
291
+ }
274
292
  export function deleteProjectFile(rootDir, filePath) {
275
293
  const absPath = resolveProjectPath(rootDir, filePath);
276
294
  if (!existsSync(absPath)) {
277
295
  return { deleted: false, absPath };
278
296
  }
297
+ let bytes;
279
298
  let content;
299
+ let contentOmitted;
280
300
  try {
281
- if (!lstatSync(absPath).isSymbolicLink()) {
282
- content = readFileSync(absPath, 'utf-8');
301
+ const stat = lstatSync(absPath);
302
+ if (!stat.isSymbolicLink()) {
303
+ bytes = stat.size;
304
+ if (stat.size > MAX_DELETED_CONTENT_BYTES) {
305
+ contentOmitted = headHasNulByte(absPath) ? 'binary' : 'too_large';
306
+ }
307
+ else {
308
+ const buffer = readFileSync(absPath);
309
+ if (buffer.includes(0))
310
+ contentOmitted = 'binary';
311
+ else
312
+ content = buffer.toString('utf-8');
313
+ }
283
314
  }
284
315
  }
285
316
  catch {
317
+ bytes = undefined;
286
318
  content = undefined;
319
+ contentOmitted = undefined;
287
320
  }
288
321
  unlinkSync(absPath);
289
- return { deleted: true, absPath, content };
322
+ return {
323
+ deleted: true,
324
+ absPath,
325
+ ...(bytes === undefined ? {} : { bytes }),
326
+ ...(content === undefined ? {} : { content }),
327
+ ...(contentOmitted === undefined ? {} : { contentOmitted }),
328
+ };
290
329
  }
291
330
  export function readProjectFile(rootDir, filePath) {
292
331
  const absPath = resolveProjectPath(rootDir, filePath);
@@ -51,7 +51,11 @@ export async function deleteFile(context, args) {
51
51
  changed: result.deleted,
52
52
  deleted: result.deleted,
53
53
  ...(scratchPath ? { scratch: true } : {}),
54
- content: result.content,
54
+ ...(result.bytes === undefined ? {} : { bytes: result.bytes }),
55
+ ...(result.contentOmitted === undefined
56
+ ? {}
57
+ : { contentOmitted: result.contentOmitted }),
58
+ ...(result.content === undefined ? {} : { content: result.content }),
55
59
  diagnostics: result.deleted && !scratchPath ? runShellDiagnostics(rootDir) : undefined,
56
60
  };
57
61
  }
@@ -527,11 +527,43 @@ function buildWriteFileDiff(content) {
527
527
  return '';
528
528
  return `@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
529
529
  }
530
+ const MAX_DELETE_PREVIEW_LINES = 400;
530
531
  function buildDeleteFileDiff(content) {
531
532
  const lines = splitDiffLines(String(content ?? ''));
532
- if (lines.length === 0)
533
+ if (lines.length === 0) {
534
+ return { removed: 0, text: '' };
535
+ }
536
+ const shown = lines.slice(0, MAX_DELETE_PREVIEW_LINES);
537
+ const rows = [
538
+ `@@ -1,${lines.length} +0,0 @@`,
539
+ ...shown.map((line) => `-${line}`),
540
+ ];
541
+ const omitted = lines.length - shown.length;
542
+ if (omitted > 0) {
543
+ rows.push(`@@ ${omitted} more removed line(s) not shown @@`);
544
+ }
545
+ return { removed: lines.length, text: rows.join('\n') };
546
+ }
547
+ function deletedFileSummary(result) {
548
+ const bytes = typeof result?.bytes === 'number' ? result.bytes : null;
549
+ if (bytes === null) {
533
550
  return '';
534
- return `@@ -1,${lines.length} +0,0 @@\n${lines.map((line) => `-${line}`).join('\n')}`;
551
+ }
552
+ const reason = result?.contentOmitted === 'binary' ? ', binary' : '';
553
+ return ` (${formatFileSize(bytes)}${reason})`;
554
+ }
555
+ function formatFileSize(bytes) {
556
+ if (bytes < 1024) {
557
+ return `${bytes} B`;
558
+ }
559
+ const units = ['KiB', 'MiB', 'GiB', 'TiB'];
560
+ let value = bytes / 1024;
561
+ let unit = 0;
562
+ while (value >= 1024 && unit < units.length - 1) {
563
+ value /= 1024;
564
+ unit += 1;
565
+ }
566
+ return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
535
567
  }
536
568
  function isFileChangeTool(name) {
537
569
  return (name === 'patch_file' ||
@@ -659,9 +691,11 @@ function buildFileChangeEntry(event) {
659
691
  };
660
692
  }
661
693
  const content = typeof result?.content === 'string' ? result.content : '';
662
- const diffText = buildDeleteFileDiff(content);
663
- const preview = diffText ? parseDiffPreview(diffText) : undefined;
664
- const summary = preview ? ` (+0 -${preview.removed})` : '';
694
+ const diff = buildDeleteFileDiff(content);
695
+ const preview = diff.text ? parseDiffPreview(diff.text) : undefined;
696
+ const summary = preview
697
+ ? ` (+0 -${diff.removed})`
698
+ : deletedFileSummary(result);
665
699
  return {
666
700
  body: '',
667
701
  diffPreview: preview,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.36",
3
+ "version": "1.0.0-preview.37",
4
4
  "description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
5
5
  "keywords": [
6
6
  "agentic-ai",
@@ -44,11 +44,11 @@
44
44
  "@lydell/node-pty-linux-x64": "1.1.0",
45
45
  "@lydell/node-pty-win32-arm64": "1.1.0",
46
46
  "@lydell/node-pty-win32-x64": "1.1.0",
47
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.36",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.36",
49
- "@thegitai/tui-linux-arm64": "1.0.0-preview.36",
50
- "@thegitai/tui-linux-x64": "1.0.0-preview.36",
51
- "@thegitai/tui-win32-x64": "1.0.0-preview.36",
47
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.37",
48
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.37",
49
+ "@thegitai/tui-linux-arm64": "1.0.0-preview.37",
50
+ "@thegitai/tui-linux-x64": "1.0.0-preview.37",
51
+ "@thegitai/tui-win32-x64": "1.0.0-preview.37",
52
52
  "@vscode/ripgrep": "1.18.0"
53
53
  },
54
54
  "repository": {