@thegitai/cli 1.0.0-beta.1 → 1.0.0-beta.10
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/README.md +4 -3
- package/dist/bin/ai.js +18 -61
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/api/auth.js +4 -2
- package/dist/src/api/browser-login.js +10 -28
- package/dist/src/api/chat.js +20 -9
- package/dist/src/api/http.js +32 -3
- package/dist/src/api/models.js +4 -2
- package/dist/src/artifact-policy.js +9 -0
- package/dist/src/cli-args.js +65 -0
- package/dist/src/client-environment.js +127 -0
- package/dist/src/colors.js +59 -0
- package/dist/src/core/clipboard.js +56 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +28 -5
- package/dist/src/help-text.js +15 -1
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +18 -1
- package/dist/src/scanner.js +67 -17
- package/dist/src/session-safety.js +64 -12
- package/dist/src/tool-executor.js +8 -0
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +14 -1
- package/dist/src/tools/path-suggest.js +66 -0
- package/dist/src/tools/read-document.js +14 -3
- package/dist/src/tools/read-file.js +9 -0
- package/dist/src/tools/replace-document-text.js +242 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +1 -1
- package/dist/src/tools/run-node-script.js +1 -1
- package/dist/src/tools/str-replace.js +14 -1
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +14 -1
- package/dist/src/ui/repl.js +13 -1
- package/dist/src/ui/tui/bridge.js +2 -2
- package/dist/src/ui/tui/build-frame.js +18 -2
- package/dist/src/ui/tui/shell-input.js +9 -1
- package/dist/src/version.js +35 -0
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +15 -16
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const PACKAGE_MANAGER_CANDIDATES = [
|
|
5
|
+
'apt',
|
|
6
|
+
'apt-get',
|
|
7
|
+
'dnf',
|
|
8
|
+
'yum',
|
|
9
|
+
'pacman',
|
|
10
|
+
'zypper',
|
|
11
|
+
'apk',
|
|
12
|
+
'brew',
|
|
13
|
+
'nix',
|
|
14
|
+
'snap',
|
|
15
|
+
'flatpak',
|
|
16
|
+
'winget',
|
|
17
|
+
'choco',
|
|
18
|
+
'scoop',
|
|
19
|
+
];
|
|
20
|
+
function detectShell(platform, env) {
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
const comspec = env.COMSPEC;
|
|
23
|
+
return comspec ? path.win32.basename(comspec) : 'unknown';
|
|
24
|
+
}
|
|
25
|
+
const shell = env.SHELL;
|
|
26
|
+
return shell ? path.basename(shell) : 'unknown';
|
|
27
|
+
}
|
|
28
|
+
function unquoteOsReleaseValue(value) {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (trimmed.length < 2)
|
|
31
|
+
return trimmed;
|
|
32
|
+
const quote = trimmed[0];
|
|
33
|
+
if ((quote !== '"' && quote !== "'") || trimmed.at(-1) !== quote) {
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
return trimmed
|
|
37
|
+
.slice(1, -1)
|
|
38
|
+
.replace(/\\(["'`$\\])/g, '$1')
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
export function parseLinuxOsRelease(text) {
|
|
42
|
+
const values = {};
|
|
43
|
+
for (const line of text.split(/\r?\n/)) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
46
|
+
continue;
|
|
47
|
+
const index = trimmed.indexOf('=');
|
|
48
|
+
if (index <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const key = trimmed.slice(0, index);
|
|
51
|
+
const value = unquoteOsReleaseValue(trimmed.slice(index + 1));
|
|
52
|
+
values[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
distroId: values.ID,
|
|
56
|
+
distroName: values.PRETTY_NAME ?? values.NAME,
|
|
57
|
+
distroVersion: values.VERSION_ID ?? values.VERSION,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function readLinuxOsReleaseText(options) {
|
|
61
|
+
if ('osReleaseText' in options) {
|
|
62
|
+
return options.osReleaseText ?? '';
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return readFileSync('/etc/os-release', 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function pathEnv(env) {
|
|
72
|
+
return env.PATH ?? env.Path ?? env.path ?? '';
|
|
73
|
+
}
|
|
74
|
+
function windowsExecutableNames(command, env) {
|
|
75
|
+
if (path.extname(command))
|
|
76
|
+
return [command];
|
|
77
|
+
const extensions = (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
78
|
+
.split(';')
|
|
79
|
+
.map((extension) => extension.trim())
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
return [command, ...extensions.map((extension) => `${command}${extension}`)];
|
|
82
|
+
}
|
|
83
|
+
function defaultExecutableExists(command, env, platform) {
|
|
84
|
+
const searchPath = pathEnv(env);
|
|
85
|
+
if (!searchPath)
|
|
86
|
+
return false;
|
|
87
|
+
const names = platform === 'win32' ? windowsExecutableNames(command, env) : [command];
|
|
88
|
+
for (const dir of searchPath.split(path.delimiter)) {
|
|
89
|
+
if (!dir.trim())
|
|
90
|
+
continue;
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
try {
|
|
93
|
+
accessSync(path.join(dir, name), constants.X_OK);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
try {
|
|
98
|
+
accessSync(path.join(dir, name));
|
|
99
|
+
return platform === 'win32';
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function detectPackageManagers(env, platform, executableExists) {
|
|
110
|
+
return PACKAGE_MANAGER_CANDIDATES.filter((command) => executableExists(command, env, platform));
|
|
111
|
+
}
|
|
112
|
+
export function collectClientEnvironment(options = {}) {
|
|
113
|
+
const platform = options.platform ?? process.platform;
|
|
114
|
+
const env = options.env ?? process.env;
|
|
115
|
+
const executableExists = options.executableExists ?? defaultExecutableExists;
|
|
116
|
+
const linuxDistro = platform === 'linux'
|
|
117
|
+
? parseLinuxOsRelease(readLinuxOsReleaseText(options))
|
|
118
|
+
: {};
|
|
119
|
+
return {
|
|
120
|
+
platform,
|
|
121
|
+
arch: options.arch ?? process.arch,
|
|
122
|
+
release: options.release ?? os.release(),
|
|
123
|
+
shell: detectShell(platform, env),
|
|
124
|
+
...linuxDistro,
|
|
125
|
+
packageManagers: detectPackageManagers(env, platform, executableExists),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Dependency-free ANSI styler with a chalk-compatible surface, imported as
|
|
2
|
+
// `chalk` at call sites. Color gating (NO_COLOR / FORCE_COLOR / non-TTY) lives
|
|
3
|
+
// in colorEnabled() below.
|
|
4
|
+
const STYLE_NAMES = ['bold', 'dim', 'red', 'green', 'yellow', 'cyan'];
|
|
5
|
+
// SGR open/close codes. Bold and dim share the 22 reset; colors share 39, so a
|
|
6
|
+
// nested inner style restores exactly its own attribute without clearing the
|
|
7
|
+
// outer one.
|
|
8
|
+
const OPEN = {
|
|
9
|
+
bold: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
};
|
|
16
|
+
const CLOSE = {
|
|
17
|
+
bold: '\x1b[22m',
|
|
18
|
+
dim: '\x1b[22m',
|
|
19
|
+
red: '\x1b[39m',
|
|
20
|
+
green: '\x1b[39m',
|
|
21
|
+
yellow: '\x1b[39m',
|
|
22
|
+
cyan: '\x1b[39m',
|
|
23
|
+
};
|
|
24
|
+
function colorEnabled() {
|
|
25
|
+
const force = process.env.FORCE_COLOR;
|
|
26
|
+
if (force !== undefined)
|
|
27
|
+
return force !== '0' && force !== 'false';
|
|
28
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return Boolean(process.stdout.isTTY);
|
|
32
|
+
}
|
|
33
|
+
function applyStyle(name, text) {
|
|
34
|
+
const open = OPEN[name];
|
|
35
|
+
const close = CLOSE[name];
|
|
36
|
+
// Re-open this style after any inner close of the same code, so a nested
|
|
37
|
+
// style (e.g. chalk.red(`a ${chalk.bold('b')} c`)) doesn't terminate it early.
|
|
38
|
+
const body = text.includes(close) ? text.split(close).join(close + open) : text;
|
|
39
|
+
return open + body + close;
|
|
40
|
+
}
|
|
41
|
+
function createStyler(styles) {
|
|
42
|
+
const fn = ((text) => {
|
|
43
|
+
const value = String(text);
|
|
44
|
+
if (!colorEnabled() || styles.length === 0)
|
|
45
|
+
return value;
|
|
46
|
+
// Apply right-to-left so the first style in the chain is outermost.
|
|
47
|
+
return styles.reduceRight((acc, name) => applyStyle(name, acc), value);
|
|
48
|
+
});
|
|
49
|
+
for (const name of STYLE_NAMES) {
|
|
50
|
+
Object.defineProperty(fn, name, {
|
|
51
|
+
configurable: true,
|
|
52
|
+
enumerable: false,
|
|
53
|
+
get: () => createStyler([...styles, name]),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return fn;
|
|
57
|
+
}
|
|
58
|
+
const colors = createStyler([]);
|
|
59
|
+
export default colors;
|
|
@@ -21,6 +21,14 @@ export function isSupportedImageMimeType(mime) {
|
|
|
21
21
|
}
|
|
22
22
|
function whichSync(cmd) {
|
|
23
23
|
try {
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
execFileSync('where.exe', [cmd], {
|
|
26
|
+
stdio: 'ignore',
|
|
27
|
+
timeout: 2000,
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
24
32
|
execFileSync('which', [cmd], { stdio: 'ignore', timeout: 2000 });
|
|
25
33
|
return true;
|
|
26
34
|
}
|
|
@@ -93,6 +101,52 @@ function readClipboardLinux() {
|
|
|
93
101
|
}
|
|
94
102
|
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
95
103
|
}
|
|
104
|
+
const WINDOWS_CLIPBOARD_IMAGE_PS = [
|
|
105
|
+
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;',
|
|
106
|
+
'$ErrorActionPreference = "Stop";',
|
|
107
|
+
'Add-Type -AssemblyName System.Drawing;',
|
|
108
|
+
'$img = Get-Clipboard -Format Image;',
|
|
109
|
+
'if ($null -eq $img) { exit 2 }',
|
|
110
|
+
'$ms = New-Object System.IO.MemoryStream;',
|
|
111
|
+
'$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png);',
|
|
112
|
+
'[Convert]::ToBase64String($ms.ToArray())',
|
|
113
|
+
].join(' ');
|
|
114
|
+
function readClipboardWindows() {
|
|
115
|
+
try {
|
|
116
|
+
const b64 = execFileSync('powershell.exe', ['-NoProfile', '-Command', WINDOWS_CLIPBOARD_IMAGE_PS], {
|
|
117
|
+
encoding: 'utf-8',
|
|
118
|
+
timeout: 5000,
|
|
119
|
+
maxBuffer: MAX_IMAGE_SIZE_BYTES * 2,
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
}).trim();
|
|
123
|
+
if (!b64) {
|
|
124
|
+
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
125
|
+
}
|
|
126
|
+
const buf = Buffer.from(b64, 'base64');
|
|
127
|
+
if (!buf.length) {
|
|
128
|
+
throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
|
|
129
|
+
}
|
|
130
|
+
if (buf.length > MAX_IMAGE_SIZE_BYTES) {
|
|
131
|
+
throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
|
|
132
|
+
}
|
|
133
|
+
return { base64Data: b64, mimeType: 'image/png' };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err instanceof ClipboardError)
|
|
137
|
+
throw err;
|
|
138
|
+
if (err?.status === 2) {
|
|
139
|
+
throw new ClipboardError('Clipboard contains no image data. Copy an image first (Win+Shift+S), then press Alt+V.', 'NO_IMAGE');
|
|
140
|
+
}
|
|
141
|
+
if (isMaxBufferError(err)) {
|
|
142
|
+
throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
|
|
143
|
+
}
|
|
144
|
+
const detail = [err?.message, err?.stderr?.toString?.()?.trim()]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(' — ');
|
|
147
|
+
throw new ClipboardError(`Failed to read clipboard image on Windows: ${detail || 'unknown error'}`, 'READ_FAILED');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
96
150
|
function readClipboardDarwin() {
|
|
97
151
|
if (whichSync('pngpaste')) {
|
|
98
152
|
try {
|
|
@@ -123,6 +177,8 @@ export function readClipboardImage(platform = process.platform) {
|
|
|
123
177
|
return readClipboardLinux();
|
|
124
178
|
case 'darwin':
|
|
125
179
|
return readClipboardDarwin();
|
|
180
|
+
case 'win32':
|
|
181
|
+
return readClipboardWindows();
|
|
126
182
|
default:
|
|
127
183
|
throw new ClipboardError(`Clipboard image paste is not supported on ${platform}.`, 'NO_TOOL');
|
|
128
184
|
}
|
package/dist/src/edit-journal.js
CHANGED
|
@@ -1,33 +1,64 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { BINARY_ARTIFACT_EXTENSIONS } from './artifact-policy.js';
|
|
4
6
|
import { resolveProjectPath } from './patcher.js';
|
|
5
7
|
export const MAX_EDIT_JOURNAL_RECORDS = 50;
|
|
6
|
-
const MAX_STORED_CONTENT_CHARS =
|
|
7
|
-
export function
|
|
8
|
+
const MAX_STORED_CONTENT_CHARS = 8_000_000;
|
|
9
|
+
export function hashBytes(content) {
|
|
8
10
|
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
9
11
|
}
|
|
12
|
+
export function hashContent(content) {
|
|
13
|
+
return hashBytes(Buffer.from(content, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
export function hashStoredContent(content, encoding = 'utf8') {
|
|
16
|
+
return encoding === 'base64'
|
|
17
|
+
? hashBytes(Buffer.from(content, 'base64'))
|
|
18
|
+
: hashContent(content);
|
|
19
|
+
}
|
|
20
|
+
export function storedContentBuffer(content, encoding = 'utf8') {
|
|
21
|
+
return encoding === 'base64'
|
|
22
|
+
? Buffer.from(content, 'base64')
|
|
23
|
+
: Buffer.from(content, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
function snapshotEncoding(filePath, content) {
|
|
26
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
27
|
+
if (BINARY_ARTIFACT_EXTENSIONS.has(ext))
|
|
28
|
+
return 'base64';
|
|
29
|
+
if (content.includes(0))
|
|
30
|
+
return 'base64';
|
|
31
|
+
return 'utf8';
|
|
32
|
+
}
|
|
10
33
|
export function readFileEditSnapshot(rootDir, filePath) {
|
|
11
34
|
try {
|
|
12
35
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
13
36
|
if (!existsSync(absPath)) {
|
|
14
|
-
return { exists: false, content: null, hash: null };
|
|
37
|
+
return { exists: false, content: null, contentEncoding: 'utf8', hash: null };
|
|
15
38
|
}
|
|
16
39
|
if (lstatSync(absPath).isSymbolicLink()) {
|
|
17
40
|
return {
|
|
18
41
|
exists: true,
|
|
19
42
|
content: null,
|
|
43
|
+
contentEncoding: 'utf8',
|
|
20
44
|
hash: null,
|
|
21
45
|
error: `Refusing to snapshot symbolic link: ${filePath}`,
|
|
22
46
|
};
|
|
23
47
|
}
|
|
24
|
-
const content = readFileSync(absPath
|
|
25
|
-
|
|
48
|
+
const content = readFileSync(absPath);
|
|
49
|
+
const encoding = snapshotEncoding(filePath, content);
|
|
50
|
+
return {
|
|
51
|
+
exists: true,
|
|
52
|
+
content: encoding === 'base64' ? content.toString('base64') : content.toString('utf8'),
|
|
53
|
+
contentEncoding: encoding,
|
|
54
|
+
hash: hashBytes(content),
|
|
55
|
+
};
|
|
26
56
|
}
|
|
27
57
|
catch (err) {
|
|
28
58
|
return {
|
|
29
59
|
exists: false,
|
|
30
60
|
content: null,
|
|
61
|
+
contentEncoding: 'utf8',
|
|
31
62
|
hash: null,
|
|
32
63
|
error: err?.message ? String(err.message) : String(err),
|
|
33
64
|
};
|
|
@@ -46,7 +77,8 @@ export function isEditToolName(toolName) {
|
|
|
46
77
|
return (toolName === 'write_file' ||
|
|
47
78
|
toolName === 'patch_file' ||
|
|
48
79
|
toolName === 'str_replace' ||
|
|
49
|
-
toolName === 'delete_file'
|
|
80
|
+
toolName === 'delete_file' ||
|
|
81
|
+
toolName === 'replace_document_text');
|
|
50
82
|
}
|
|
51
83
|
export function operationFromSnapshots(before, after) {
|
|
52
84
|
if (before.hash === after.hash)
|
|
@@ -102,6 +134,7 @@ export function normalizeAssistantEditJournal(value) {
|
|
|
102
134
|
? entry.afterHash
|
|
103
135
|
: null,
|
|
104
136
|
beforeContent: typeof entry.beforeContent === 'string' ? entry.beforeContent : null,
|
|
137
|
+
beforeContentEncoding: entry.beforeContentEncoding === 'base64' ? 'base64' : 'utf8',
|
|
105
138
|
createdAt: typeof entry.createdAt === 'string' && entry.createdAt
|
|
106
139
|
? entry.createdAt
|
|
107
140
|
: new Date().toISOString(),
|
package/dist/src/executor.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
import chalk from '
|
|
2
|
-
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
|
|
1
|
+
import chalk from './colors.js';
|
|
3
2
|
import { execFileSync, spawn } from 'child_process';
|
|
4
3
|
import { existsSync, statSync } from 'fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { ARTIFACT_INSPECT_BLOCK_DIRS, getBlockedArtifactInspectDir, relativeProjectPath, } from './artifact-policy.js';
|
|
8
8
|
import { emitCommandOutput, isTuiMode } from './runtime-mode.js';
|
|
9
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
10
|
+
let nodePtyCache;
|
|
11
|
+
// @lydell/node-pty ships its native binding as platform-specific optional
|
|
12
|
+
// packages. If none is installed for this OS/arch the require throws; we cache
|
|
13
|
+
// the failure and fall back to the non-interactive child_process path. A pty is
|
|
14
|
+
// only needed to answer interactive sudo prompts.
|
|
15
|
+
function loadNodePty() {
|
|
16
|
+
if (nodePtyCache !== undefined)
|
|
17
|
+
return nodePtyCache;
|
|
18
|
+
try {
|
|
19
|
+
nodePtyCache = requireFromHere('@lydell/node-pty');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
nodePtyCache = null;
|
|
23
|
+
}
|
|
24
|
+
return nodePtyCache;
|
|
25
|
+
}
|
|
9
26
|
const COMMON_TOOLCHAIN_BIN_DIRS = ['/usr/local/go/bin'];
|
|
10
27
|
function detectVenvBin(dir) {
|
|
11
28
|
for (const name of ['.venv', 'venv', 'env']) {
|
|
@@ -607,7 +624,7 @@ function buildCommandEnv(cwd) {
|
|
|
607
624
|
function sanitizePtyOutput(command, output, cwd, secrets) {
|
|
608
625
|
return sanitizeCommandText(command, stripSudoPromptText(redactSecrets(output, secrets)), cwd);
|
|
609
626
|
}
|
|
610
|
-
async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword) {
|
|
627
|
+
async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword, nodePty) {
|
|
611
628
|
return new Promise((resolve) => {
|
|
612
629
|
let output = '';
|
|
613
630
|
let timedOut = false;
|
|
@@ -623,7 +640,7 @@ async function runPtyCommand(command, cwd, effectiveTimeout, exploratory, reques
|
|
|
623
640
|
const args = process.platform === 'win32'
|
|
624
641
|
? ['/d', '/s', '/c', command]
|
|
625
642
|
: ['-lc', command];
|
|
626
|
-
const child =
|
|
643
|
+
const child = nodePty.spawn(shell, args, {
|
|
627
644
|
cols: 120,
|
|
628
645
|
rows: 30,
|
|
629
646
|
cwd,
|
|
@@ -776,7 +793,13 @@ export async function runCommand(command, cwd, { requestSudoPassword, timeout, }
|
|
|
776
793
|
}
|
|
777
794
|
const exploratory = isExploratoryCommand(command);
|
|
778
795
|
if (requestSudoPassword && commandUsesSudo(command)) {
|
|
779
|
-
|
|
796
|
+
const nodePty = loadNodePty();
|
|
797
|
+
if (nodePty) {
|
|
798
|
+
return runPtyCommand(command, cwd, effectiveTimeout, exploratory, requestSudoPassword, nodePty);
|
|
799
|
+
}
|
|
800
|
+
// No pty binding installed for this platform — fall through to the
|
|
801
|
+
// non-interactive spawn path. The command still runs; an interactive sudo
|
|
802
|
+
// prompt simply can't be answered here.
|
|
780
803
|
}
|
|
781
804
|
return new Promise((resolve) => {
|
|
782
805
|
let stdout = '';
|
package/dist/src/help-text.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from './colors.js';
|
|
2
|
+
import { getCliVersion, getPlatformTag } from './version.js';
|
|
2
3
|
// The bound keys (Enter/Esc/Ctrl+C/Tab/arrows) are identical across platforms in
|
|
3
4
|
// a terminal. The one thing that genuinely differs is the terminal's paste
|
|
4
5
|
// shortcut, so surface the one for the host OS (right-click paste works
|
|
@@ -45,6 +46,7 @@ const HELP_MARKDOWN = [
|
|
|
45
46
|
'',
|
|
46
47
|
'- `-y, --yes` — start in Auto-Accept mode',
|
|
47
48
|
'- `-h, --help` — show this help',
|
|
49
|
+
'- `-v, --version` — print the version and exit',
|
|
48
50
|
'',
|
|
49
51
|
'## Modes',
|
|
50
52
|
'',
|
|
@@ -70,6 +72,7 @@ const HELP_MARKDOWN = [
|
|
|
70
72
|
'## Chat commands',
|
|
71
73
|
'',
|
|
72
74
|
'- `/help` — show this help',
|
|
75
|
+
'- `/about` — show version and platform info',
|
|
73
76
|
'- `/usage` — show account usage percentage and reset times',
|
|
74
77
|
'- `/model` — list supported models and pick one',
|
|
75
78
|
'- `/model <id>` — switch the active model without clearing history',
|
|
@@ -100,6 +103,17 @@ const HELP_MARKDOWN = [
|
|
|
100
103
|
'- For anything else, re-run the command and report the printed error',
|
|
101
104
|
' message — there is no client-side debug mode by design.',
|
|
102
105
|
].join('\n');
|
|
106
|
+
export function formatAboutCard() {
|
|
107
|
+
// Fenced so the column alignment survives terminal markdown rendering.
|
|
108
|
+
return [
|
|
109
|
+
'```',
|
|
110
|
+
'TheGitAI',
|
|
111
|
+
` Version ${getCliVersion()}`,
|
|
112
|
+
` Platform ${getPlatformTag()}`,
|
|
113
|
+
` Node ${process.version}`,
|
|
114
|
+
'```',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
103
117
|
export function formatHelpMarkdown() {
|
|
104
118
|
return HELP_MARKDOWN;
|
|
105
119
|
}
|
package/dist/src/patcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from '
|
|
1
|
+
import chalk from './colors.js';
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
@@ -169,6 +169,23 @@ export function writeProjectFile(rootDir, filePath, content) {
|
|
|
169
169
|
writeFileSync(absPath, content, 'utf-8');
|
|
170
170
|
return { absPath, changed: true };
|
|
171
171
|
}
|
|
172
|
+
export function writeProjectFileBuffer(rootDir, filePath, content) {
|
|
173
|
+
const absPath = resolveProjectPath(rootDir, filePath);
|
|
174
|
+
if (existsSync(absPath)) {
|
|
175
|
+
try {
|
|
176
|
+
const existingContent = readFileSync(absPath);
|
|
177
|
+
if (existingContent.equals(content)) {
|
|
178
|
+
return { absPath, changed: false };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// If we can't read it for some reason, proceed with write
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
mkdirSync(path.dirname(absPath), { recursive: true });
|
|
186
|
+
writeFileSync(absPath, content);
|
|
187
|
+
return { absPath, changed: true };
|
|
188
|
+
}
|
|
172
189
|
export function deleteProjectFile(rootDir, filePath) {
|
|
173
190
|
const absPath = resolveProjectPath(rootDir, filePath);
|
|
174
191
|
if (!existsSync(absPath)) {
|
package/dist/src/scanner.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
-
import { readFileSync, statSync } from 'fs';
|
|
3
|
-
import { glob } from 'glob';
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { getNodePrimarySignature, getStructuralChildren, parseRepoSource, } from './tree-sitter-runtime.js';
|
|
6
|
-
import {
|
|
5
|
+
import { ARTIFACT_IGNORE_DIRS, ARTIFACT_IGNORE_FILES, ARTIFACT_INSPECT_BLOCK_DIRS, BINARY_ARTIFACT_EXTENSIONS, isSensitiveProjectPath, shouldIgnoreArtifactPath, } from './artifact-policy.js';
|
|
7
6
|
const BINARY_EXTENSIONS = BINARY_ARTIFACT_EXTENSIONS;
|
|
8
7
|
const ALWAYS_IGNORE_FILES = ARTIFACT_IGNORE_FILES;
|
|
9
8
|
export const ALWAYS_IGNORE_DIRS = ARTIFACT_IGNORE_DIRS;
|
|
10
9
|
export const BLOCKED_PATH_INSPECT_DIRS = ARTIFACT_INSPECT_BLOCK_DIRS;
|
|
11
|
-
const FALLBACK_IGNORE = ARTIFACT_FALLBACK_IGNORE_GLOBS;
|
|
12
10
|
export const SCANNER_MAX_SOURCE_FILE_BYTES = 100 * 1024;
|
|
13
11
|
const MAX_FILE_SIZE = SCANNER_MAX_SOURCE_FILE_BYTES;
|
|
14
12
|
const MAX_CHUNKS = 2000;
|
|
@@ -16,26 +14,78 @@ const TARGET_CHUNK_CHARS = 1800;
|
|
|
16
14
|
const MAX_CHUNK_CHARS = 2800;
|
|
17
15
|
const FALLBACK_OVERLAP_LINES = 10;
|
|
18
16
|
const MAX_STRUCTURE_DEPTH = 2;
|
|
17
|
+
function parseGitLsFilesOutput(output) {
|
|
18
|
+
return String(output)
|
|
19
|
+
.split('\0')
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.filter((filePath) => !shouldIgnorePath(filePath));
|
|
22
|
+
}
|
|
19
23
|
function getFiles(rootDir, { limit = Infinity } = {}) {
|
|
20
24
|
try {
|
|
21
|
-
const output = execSync('git ls-files --cached --others --exclude-standard', { cwd: rootDir,
|
|
22
|
-
const files = output
|
|
23
|
-
.split('\n')
|
|
24
|
-
.filter(Boolean)
|
|
25
|
-
.filter((filePath) => !shouldIgnorePath(filePath));
|
|
25
|
+
const output = execSync('git ls-files -z --cached --others --exclude-standard', { cwd: rootDir, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
26
|
+
const files = parseGitLsFilesOutput(output);
|
|
26
27
|
return Number.isFinite(limit) ? files.slice(0, limit) : files;
|
|
27
28
|
}
|
|
28
29
|
catch {
|
|
29
|
-
return
|
|
30
|
-
.sync('**/*', {
|
|
31
|
-
cwd: rootDir,
|
|
32
|
-
nodir: true,
|
|
33
|
-
dot: false,
|
|
34
|
-
ignore: FALLBACK_IGNORE,
|
|
35
|
-
})
|
|
36
|
-
.slice(0, Number.isFinite(limit) ? limit : undefined);
|
|
30
|
+
return walkProjectFilesFallback(rootDir, Number.isFinite(limit) ? limit : Infinity);
|
|
37
31
|
}
|
|
38
32
|
}
|
|
33
|
+
const FALLBACK_LOCKFILES = new Set([
|
|
34
|
+
'package-lock.json',
|
|
35
|
+
'yarn.lock',
|
|
36
|
+
'pnpm-lock.yaml',
|
|
37
|
+
]);
|
|
38
|
+
function isFallbackIgnoredFile(relPath, fileName) {
|
|
39
|
+
if (shouldIgnorePath(relPath))
|
|
40
|
+
return true;
|
|
41
|
+
if (fileName.endsWith('.lock'))
|
|
42
|
+
return true;
|
|
43
|
+
return FALLBACK_LOCKFILES.has(fileName);
|
|
44
|
+
}
|
|
45
|
+
// Local stand-in for the previous glob('**/*', { nodir: true, dot: false,
|
|
46
|
+
// ignore: FALLBACK_IGNORE }) call, used only when `git ls-files` fails (e.g. a
|
|
47
|
+
// non-git directory). Walks the tree depth-first, skips dotfiles and
|
|
48
|
+
// dot-directories (glob's dot: false), prunes ignored artifact dirs, and drops
|
|
49
|
+
// lockfiles plus sensitive/ignored paths — reproducing the old fallback without
|
|
50
|
+
// the glob dependency.
|
|
51
|
+
function walkProjectFilesFallback(rootDir, limit) {
|
|
52
|
+
const results = [];
|
|
53
|
+
const visit = (relDir) => {
|
|
54
|
+
if (results.length >= limit)
|
|
55
|
+
return;
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(path.join(rootDir, relDir), {
|
|
59
|
+
withFileTypes: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (results.length >= limit)
|
|
67
|
+
return;
|
|
68
|
+
const name = entry.name;
|
|
69
|
+
if (name.startsWith('.'))
|
|
70
|
+
continue;
|
|
71
|
+
const relPath = relDir ? `${relDir}/${name}` : name;
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
// Only the prefix-based artifact rule is safe to prune a directory by;
|
|
74
|
+
// the sensitive-basename check must stay at the file level so a dir
|
|
75
|
+
// merely named e.g. `secret` doesn't hide non-sensitive files under it.
|
|
76
|
+
if (ALWAYS_IGNORE_DIRS.has(name) || shouldIgnoreArtifactPath(relPath)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
visit(relPath);
|
|
80
|
+
}
|
|
81
|
+
else if (entry.isFile() && !isFallbackIgnoredFile(relPath, name)) {
|
|
82
|
+
results.push(relPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
visit('');
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
39
89
|
function shouldSkipFile(relPath, stat) {
|
|
40
90
|
if (shouldIgnorePath(relPath))
|
|
41
91
|
return true;
|