vg-coder-cli 2.0.14 → 2.0.16
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 +14 -13
- package/src/server/api-server.js +45 -4
- package/src/server/views/css/terminal.css +31 -0
- package/src/server/views/js/features/git-view.js +3 -3
- package/src/server/views/js/features/terminal.js +92 -14
- package/vg-coder-cli-2.0.15.tgz +0 -0
- package/vg-coder-cli-2.0.16.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vg-coder-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,26 +31,27 @@
|
|
|
31
31
|
"url": "https://github.com/tinhthanh/vg-coder-cli.git"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"body-parser": "^1.20.2",
|
|
35
|
+
"chalk": "^4.1.2",
|
|
34
36
|
"commander": "^11.1.0",
|
|
37
|
+
"cors": "^2.8.5",
|
|
35
38
|
"directory-tree": "^3.5.1",
|
|
36
|
-
"
|
|
39
|
+
"express": "^4.18.2",
|
|
40
|
+
"fs-extra": "^11.2.0",
|
|
37
41
|
"highlight.js": "^11.9.0",
|
|
38
42
|
"ignore": "^5.3.0",
|
|
39
|
-
"fs-extra": "^11.2.0",
|
|
40
|
-
"path": "^0.12.7",
|
|
41
|
-
"chalk": "^4.1.2",
|
|
42
|
-
"ora": "^5.4.1",
|
|
43
|
-
"express": "^4.18.2",
|
|
44
|
-
"cors": "^2.8.5",
|
|
45
|
-
"body-parser": "^1.20.2",
|
|
46
43
|
"node-pty": "^1.0.0",
|
|
47
|
-
"
|
|
44
|
+
"ora": "^5.4.1",
|
|
45
|
+
"path": "^0.12.7",
|
|
46
|
+
"socket.io": "^4.7.2",
|
|
47
|
+
"tiktoken": "^1.0.10",
|
|
48
|
+
"vg-coder-cli": "^2.0.15"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"jest": "^29.7.0",
|
|
51
|
-
"nodemon": "^3.0.2",
|
|
52
51
|
"@types/jest": "^29.5.8",
|
|
53
|
-
"adm-zip": "^0.5.10"
|
|
52
|
+
"adm-zip": "^0.5.10",
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"nodemon": "^3.0.2"
|
|
54
55
|
},
|
|
55
56
|
"engines": {
|
|
56
57
|
"node": ">=16.0.0"
|
package/src/server/api-server.js
CHANGED
|
@@ -52,10 +52,8 @@ class ApiServer {
|
|
|
52
52
|
setupSocketIO() {
|
|
53
53
|
this.io.on('connection', (socket) => {
|
|
54
54
|
// Nhận event init với termId
|
|
55
|
-
// FIX: Thêm default value = {} để tránh crash khi data undefined
|
|
56
55
|
socket.on('terminal:init', (data) => {
|
|
57
56
|
if (!data || !data.termId) {
|
|
58
|
-
// console.warn('[Socket] Ignored invalid terminal:init', data);
|
|
59
57
|
return;
|
|
60
58
|
}
|
|
61
59
|
const { termId, cols, rows } = data;
|
|
@@ -101,11 +99,54 @@ class ApiServer {
|
|
|
101
99
|
|
|
102
100
|
this.app.get('/api/git/diff', async (req, res) => {
|
|
103
101
|
try {
|
|
104
|
-
|
|
102
|
+
// 1. Lấy diff của các file đã track (modified, deleted, staged)
|
|
103
|
+
const { stdout: diffStdout } = await execAsync('git diff HEAD', {
|
|
105
104
|
cwd: this.workingDir,
|
|
106
105
|
maxBuffer: 20 * 1024 * 1024
|
|
107
106
|
});
|
|
108
|
-
|
|
107
|
+
|
|
108
|
+
// 2. Lấy danh sách untracked files (files mới tạo chưa add)
|
|
109
|
+
const { stdout: untrackedStdout } = await execAsync('git ls-files --others --exclude-standard', {
|
|
110
|
+
cwd: this.workingDir,
|
|
111
|
+
maxBuffer: 20 * 1024 * 1024
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let combinedDiff = diffStdout || '';
|
|
115
|
+
const untrackedFiles = untrackedStdout.split('\n').filter(f => f.trim());
|
|
116
|
+
|
|
117
|
+
// 3. Tạo diff giả lập cho untracked files để hiển thị trên UI
|
|
118
|
+
if (untrackedFiles.length > 0) {
|
|
119
|
+
if (combinedDiff && !combinedDiff.endsWith('\n')) combinedDiff += '\n';
|
|
120
|
+
|
|
121
|
+
for (const file of untrackedFiles) {
|
|
122
|
+
try {
|
|
123
|
+
const filePath = path.join(this.workingDir, file);
|
|
124
|
+
const stat = await fs.stat(filePath);
|
|
125
|
+
if (stat.isDirectory()) continue;
|
|
126
|
+
|
|
127
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
128
|
+
|
|
129
|
+
// Tạo header giống git diff
|
|
130
|
+
combinedDiff += `diff --git a/${file} b/${file}\n`;
|
|
131
|
+
combinedDiff += `new file mode 100644\n`;
|
|
132
|
+
combinedDiff += `--- /dev/null\n`;
|
|
133
|
+
combinedDiff += `+++ b/${file}\n`;
|
|
134
|
+
|
|
135
|
+
if (content.length > 0) {
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
combinedDiff += `@@ -0,0 +1,${lines.length} @@\n`;
|
|
138
|
+
lines.forEach(line => {
|
|
139
|
+
combinedDiff += `+${line}\n`;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
combinedDiff += `\n`;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Bỏ qua nếu lỗi đọc file (vd: binary)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
res.json({ diff: combinedDiff });
|
|
109
150
|
} catch (error) {
|
|
110
151
|
console.error(chalk.red('❌ [GIT] Error:'), error.message);
|
|
111
152
|
res.json({ diff: '', error: error.message });
|
|
@@ -23,6 +23,20 @@
|
|
|
23
23
|
resize: both; /* Cho phép resize cửa sổ */
|
|
24
24
|
min-width: 300px;
|
|
25
25
|
min-height: 200px;
|
|
26
|
+
transition: height 0.2s ease, width 0.2s ease;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* --- TRẠNG THÁI MINIMIZED --- */
|
|
30
|
+
.floating-terminal.minimized {
|
|
31
|
+
height: 36px !important; /* Chỉ hiện header */
|
|
32
|
+
min-height: 36px !important;
|
|
33
|
+
width: 200px !important; /* Thu nhỏ chiều ngang */
|
|
34
|
+
resize: none; /* Không cho resize khi đang minimize */
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.floating-terminal.minimized .terminal-body {
|
|
39
|
+
display: none;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/* Header dùng để drag */
|
|
@@ -35,6 +49,7 @@
|
|
|
35
49
|
cursor: grab;
|
|
36
50
|
border-bottom: 1px solid #333;
|
|
37
51
|
user-select: none;
|
|
52
|
+
height: 36px;
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
.terminal-header:active {
|
|
@@ -49,6 +64,9 @@
|
|
|
49
64
|
font-family: monospace;
|
|
50
65
|
font-size: 12px;
|
|
51
66
|
font-weight: 600;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
.terminal-controls {
|
|
@@ -62,6 +80,16 @@
|
|
|
62
80
|
border-radius: 50%;
|
|
63
81
|
border: none;
|
|
64
82
|
cursor: pointer;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
padding: 0;
|
|
87
|
+
font-size: 8px;
|
|
88
|
+
color: rgba(0,0,0,0.5);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.term-btn:hover {
|
|
92
|
+
color: rgba(0,0,0,0.8);
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
.term-btn.close {
|
|
@@ -74,6 +102,9 @@
|
|
|
74
102
|
.term-btn.minimize {
|
|
75
103
|
background: #FFBD2E;
|
|
76
104
|
}
|
|
105
|
+
.term-btn.minimize:hover {
|
|
106
|
+
background: #ffad08;
|
|
107
|
+
}
|
|
77
108
|
|
|
78
109
|
.term-btn.maximize {
|
|
79
110
|
background: #27C93F;
|
|
@@ -31,9 +31,8 @@ async function toggleGitMode() {
|
|
|
31
31
|
const rect = gitContainer.getBoundingClientRect();
|
|
32
32
|
console.log('[GitView] Container Size:', rect.width, 'x', rect.height);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
34
|
+
// FIX: Always load changes when opening, removing the empty check
|
|
35
|
+
await loadGitChanges();
|
|
37
36
|
} else {
|
|
38
37
|
gitContainer.classList.remove('active');
|
|
39
38
|
toggleBtn.classList.remove('active');
|
|
@@ -53,6 +52,7 @@ async function loadGitChanges() {
|
|
|
53
52
|
refreshBtn.disabled = true;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
// Only show loading if we are replacing content or it's empty
|
|
56
56
|
gitContainer.innerHTML = '<div class="git-loading-msg">Loading git changes... (Check Console if stuck)</div>';
|
|
57
57
|
|
|
58
58
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Terminal Logic: Multi-instance & Floating
|
|
2
2
|
|
|
3
3
|
let socket;
|
|
4
|
-
const activeTerminals = new Map(); // Map<termId, { term, fitAddon, element }>
|
|
4
|
+
const activeTerminals = new Map(); // Map<termId, { term, fitAddon, element, prevSize }>
|
|
5
5
|
|
|
6
6
|
// Z-Index Management
|
|
7
7
|
let maxZIndex = 10001;
|
|
@@ -39,21 +39,23 @@ export function createNewTerminal() {
|
|
|
39
39
|
const wrapper = document.createElement('div');
|
|
40
40
|
wrapper.className = 'floating-terminal';
|
|
41
41
|
wrapper.id = `wrapper-${termId}`;
|
|
42
|
-
|
|
43
|
-
wrapper.style.left = '100px';
|
|
42
|
+
|
|
44
43
|
// Offset vị trí một chút nếu mở nhiều cái
|
|
45
|
-
const offset = activeTerminals.size * 30;
|
|
44
|
+
const offset = (activeTerminals.size % 10) * 30;
|
|
46
45
|
wrapper.style.top = `${100 + offset}px`;
|
|
47
46
|
wrapper.style.left = `${400 + offset}px`;
|
|
48
47
|
wrapper.style.zIndex = ++maxZIndex;
|
|
49
48
|
|
|
49
|
+
// HTML Template - Đã thêm sự kiện onclick cho nút Minimize
|
|
50
50
|
wrapper.innerHTML = `
|
|
51
|
-
<div class="terminal-header" id="header-${termId}">
|
|
51
|
+
<div class="terminal-header" id="header-${termId}" ondblclick="window.toggleMinimize('${termId}')">
|
|
52
52
|
<div class="terminal-title-group">
|
|
53
53
|
<span>>_</span> Terminal (${activeTerminals.size + 1})
|
|
54
54
|
</div>
|
|
55
55
|
<div class="terminal-controls">
|
|
56
|
-
<button class="term-btn
|
|
56
|
+
<button class="term-btn minimize" onclick="window.toggleMinimize('${termId}')" title="Minimize/Restore">-</button>
|
|
57
|
+
<button class="term-btn maximize" onclick="window.toggleMaximize('${termId}')" title="Maximize">+</button>
|
|
58
|
+
<button class="term-btn close" onclick="window.closeTerminal('${termId}')" title="Close">x</button>
|
|
57
59
|
</div>
|
|
58
60
|
</div>
|
|
59
61
|
<div class="terminal-body" id="body-${termId}"></div>
|
|
@@ -92,14 +94,17 @@ export function createNewTerminal() {
|
|
|
92
94
|
|
|
93
95
|
// Resize Observer to refit terminal when window is resized
|
|
94
96
|
const resizeObserver = new ResizeObserver(() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
// Chỉ fit lại nếu không bị minimized
|
|
98
|
+
if (!wrapper.classList.contains('minimized')) {
|
|
99
|
+
try {
|
|
100
|
+
fitAddon.fit();
|
|
101
|
+
socket.emit('terminal:resize', {
|
|
102
|
+
termId,
|
|
103
|
+
cols: term.cols,
|
|
104
|
+
rows: term.rows
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {}
|
|
107
|
+
}
|
|
103
108
|
});
|
|
104
109
|
resizeObserver.observe(document.getElementById(`body-${termId}`));
|
|
105
110
|
|
|
@@ -132,6 +137,74 @@ export function closeTerminalUI(termId) {
|
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Toggle Minimize/Restore
|
|
142
|
+
*/
|
|
143
|
+
export function toggleMinimize(termId) {
|
|
144
|
+
const session = activeTerminals.get(termId);
|
|
145
|
+
if (!session) return;
|
|
146
|
+
|
|
147
|
+
const el = session.element;
|
|
148
|
+
const isMinimized = el.classList.contains('minimized');
|
|
149
|
+
|
|
150
|
+
if (isMinimized) {
|
|
151
|
+
// Restore
|
|
152
|
+
el.classList.remove('minimized');
|
|
153
|
+
|
|
154
|
+
// Restore size logic if needed, but CSS transition handles visualization
|
|
155
|
+
// Need to refit xterm after transition
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
try { session.fitAddon.fit(); } catch(e){}
|
|
158
|
+
}, 250);
|
|
159
|
+
} else {
|
|
160
|
+
// Minimize
|
|
161
|
+
// Store current width/height if we want to restore exact pixel values later
|
|
162
|
+
// (CSS handles the visual hiding)
|
|
163
|
+
el.classList.add('minimized');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Toggle Maximize (Simple Fullscreen simulation)
|
|
169
|
+
*/
|
|
170
|
+
export function toggleMaximize(termId) {
|
|
171
|
+
const session = activeTerminals.get(termId);
|
|
172
|
+
if (!session) return;
|
|
173
|
+
|
|
174
|
+
const el = session.element;
|
|
175
|
+
const isMaximized = el.classList.contains('maximized');
|
|
176
|
+
|
|
177
|
+
if (isMaximized) {
|
|
178
|
+
// Restore normal size
|
|
179
|
+
el.classList.remove('maximized');
|
|
180
|
+
el.style.width = session.prevSize?.width || '600px';
|
|
181
|
+
el.style.height = session.prevSize?.height || '400px';
|
|
182
|
+
el.style.top = session.prevSize?.top || '100px';
|
|
183
|
+
el.style.left = session.prevSize?.left || '400px';
|
|
184
|
+
} else {
|
|
185
|
+
// Save current state
|
|
186
|
+
session.prevSize = {
|
|
187
|
+
width: el.style.width,
|
|
188
|
+
height: el.style.height,
|
|
189
|
+
top: el.style.top,
|
|
190
|
+
left: el.style.left
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Go full "floating" screen
|
|
194
|
+
el.classList.add('maximized');
|
|
195
|
+
el.style.width = '90vw';
|
|
196
|
+
el.style.height = '80vh';
|
|
197
|
+
el.style.top = '10vh';
|
|
198
|
+
el.style.left = '5vw';
|
|
199
|
+
el.classList.remove('minimized'); // Ensure not minimized
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
try { session.fitAddon.fit(); } catch(e){}
|
|
204
|
+
}, 250);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
135
208
|
/**
|
|
136
209
|
* Logic Drag & Drop
|
|
137
210
|
*/
|
|
@@ -141,6 +214,9 @@ function makeDraggable(element, handle) {
|
|
|
141
214
|
handle.onmousedown = dragMouseDown;
|
|
142
215
|
|
|
143
216
|
function dragMouseDown(e) {
|
|
217
|
+
// Don't drag if clicking buttons
|
|
218
|
+
if(e.target.tagName === 'BUTTON') return;
|
|
219
|
+
|
|
144
220
|
e.preventDefault();
|
|
145
221
|
// Get mouse cursor position at startup
|
|
146
222
|
pos3 = e.clientX;
|
|
@@ -172,3 +248,5 @@ function makeDraggable(element, handle) {
|
|
|
172
248
|
// Global Exports for HTML onclick
|
|
173
249
|
window.createNewTerminal = createNewTerminal;
|
|
174
250
|
window.closeTerminal = closeTerminalUI;
|
|
251
|
+
window.toggleMinimize = toggleMinimize;
|
|
252
|
+
window.toggleMaximize = toggleMaximize;
|
|
Binary file
|
|
Binary file
|