@hapico/cli 0.0.29 → 0.0.31
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/bin/index.js +382 -490
- package/bin/tools/nativewind/index.js +80 -0
- package/bin/tools/vibe/index.js +88 -108
- package/bin/tools/vibe/patch.utils.js +243 -0
- package/bun.lock +874 -0
- package/dist/index.js +382 -490
- package/dist/tools/nativewind/index.js +80 -0
- package/dist/tools/vibe/index.js +88 -108
- package/dist/tools/vibe/patch.utils.js +243 -0
- package/index.ts +475 -787
- package/package.json +5 -1
- package/tools/nativewind/index.ts +87 -0
- package/tools/vibe/index.ts +91 -108
- package/tools/vibe/patch.utils.ts +273 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { diff_match_patch } from "diff-match-patch";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* TYPES
|
|
6
|
+
* ------------------------------------------------------------------
|
|
7
|
+
*/
|
|
8
|
+
export interface PatchBlock {
|
|
9
|
+
search: string;
|
|
10
|
+
replace: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ------------------------------------------------------------------
|
|
15
|
+
* HELPER: STRING SIMILARITY (Levenshtein based)
|
|
16
|
+
* Tính độ giống nhau giữa 2 chuỗi (0 -> 1)
|
|
17
|
+
* ------------------------------------------------------------------
|
|
18
|
+
*/
|
|
19
|
+
const calculateSimilarity = (s1: string, s2: string): number => {
|
|
20
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
21
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
22
|
+
const longerLength = longer.length;
|
|
23
|
+
if (longerLength === 0) {
|
|
24
|
+
return 1.0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Sử dụng dmp để tính Levenshtein distance cho chính xác và nhanh
|
|
28
|
+
const dmp = new diff_match_patch();
|
|
29
|
+
const diffs = dmp.diff_main(longer, shorter);
|
|
30
|
+
const levenshtein = dmp.diff_levenshtein(diffs);
|
|
31
|
+
|
|
32
|
+
return (longerLength - levenshtein) / longerLength;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ------------------------------------------------------------------
|
|
37
|
+
* HELPER: FUZZY LINE MATCHING
|
|
38
|
+
* Tìm đoạn code khớp nhất trong file gốc dựa trên điểm số (Confidence)
|
|
39
|
+
* ------------------------------------------------------------------
|
|
40
|
+
*/
|
|
41
|
+
const applyFuzzyLineMatch = (
|
|
42
|
+
original: string,
|
|
43
|
+
search: string,
|
|
44
|
+
replace: string,
|
|
45
|
+
threshold: number = 0.85 // Ngưỡng chấp nhận (85% giống nhau)
|
|
46
|
+
): { result: string | null; confidence: number } => {
|
|
47
|
+
const originalLines = original.split(/\r?\n/);
|
|
48
|
+
const searchLines = search.split(/\r?\n/);
|
|
49
|
+
|
|
50
|
+
// Lọc bỏ dòng trống ở đầu/cuối search block để match chính xác hơn
|
|
51
|
+
while (searchLines.length > 0 && searchLines[0].trim() === "") searchLines.shift();
|
|
52
|
+
while (searchLines.length > 0 && searchLines[searchLines.length - 1].trim() === "") searchLines.pop();
|
|
53
|
+
|
|
54
|
+
if (searchLines.length === 0) return { result: null, confidence: 0 };
|
|
55
|
+
|
|
56
|
+
let bestMatchIndex = -1;
|
|
57
|
+
let bestMatchScore = 0;
|
|
58
|
+
|
|
59
|
+
// Duyệt qua file gốc (Sliding Window)
|
|
60
|
+
// Chỉ cần duyệt đến vị trí mà số dòng còn lại đủ để chứa searchLines
|
|
61
|
+
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
62
|
+
let currentScoreTotal = 0;
|
|
63
|
+
let possible = true;
|
|
64
|
+
|
|
65
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
66
|
+
const originalLine = originalLines[i + j].trim();
|
|
67
|
+
const searchLine = searchLines[j].trim();
|
|
68
|
+
|
|
69
|
+
// So sánh nhanh: Nếu giống hệt nhau
|
|
70
|
+
if (originalLine === searchLine) {
|
|
71
|
+
currentScoreTotal += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// So sánh chậm: Tính độ tương đồng
|
|
76
|
+
const similarity = calculateSimilarity(originalLine, searchLine);
|
|
77
|
+
|
|
78
|
+
// Nếu một dòng quá khác biệt (< 60%), coi như block này không khớp
|
|
79
|
+
// (Giúp tối ưu hiệu năng, cắt nhánh sớm)
|
|
80
|
+
if (similarity < 0.6) {
|
|
81
|
+
possible = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
currentScoreTotal += similarity;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (possible) {
|
|
88
|
+
const avgScore = currentScoreTotal / searchLines.length;
|
|
89
|
+
if (avgScore > bestMatchScore) {
|
|
90
|
+
bestMatchScore = avgScore;
|
|
91
|
+
bestMatchIndex = i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// LOG CONFIDENCE ĐỂ DEBUG
|
|
97
|
+
if (bestMatchScore > 0) {
|
|
98
|
+
console.log(`[FuzzyMatch] Best match score: ${(bestMatchScore * 100).toFixed(2)}% at line ${bestMatchIndex + 1}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Nếu tìm thấy vị trí có độ tin cậy cao hơn ngưỡng
|
|
102
|
+
if (bestMatchIndex !== -1 && bestMatchScore >= threshold) {
|
|
103
|
+
const before = originalLines.slice(0, bestMatchIndex);
|
|
104
|
+
// Lưu ý: Đoạn replace thay thế đúng số lượng dòng của searchLines
|
|
105
|
+
// (Giả định search và match trong original có cùng số dòng hiển thị)
|
|
106
|
+
const after = originalLines.slice(bestMatchIndex + searchLines.length);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
result: [...before, replace, ...after].join("\n"),
|
|
110
|
+
confidence: bestMatchScore
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { result: null, confidence: bestMatchScore };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ------------------------------------------------------------------
|
|
119
|
+
* 1. STATE MACHINE PARSER (GIỮ NGUYÊN)
|
|
120
|
+
* ------------------------------------------------------------------
|
|
121
|
+
*/
|
|
122
|
+
export const parseLLMPatch = (patchContent: string): PatchBlock[] => {
|
|
123
|
+
// ... (Code parse giữ nguyên như cũ của bạn)
|
|
124
|
+
const lines = patchContent.split(/\r?\n/);
|
|
125
|
+
const patches: PatchBlock[] = [];
|
|
126
|
+
let inSearch = false;
|
|
127
|
+
let inReplace = false;
|
|
128
|
+
let searchLines: string[] = [];
|
|
129
|
+
let replaceLines: string[] = [];
|
|
130
|
+
const isSearchMarker = (line: string) => /^\s*<{7,}\s*SEARCH\s*$/.test(line);
|
|
131
|
+
const isDividerMarker = (line: string) => /^\s*={7,}\s*$/.test(line);
|
|
132
|
+
const isEndMarker = (line: string) => /^\s*>{7,}\s*REPLACE\s*$/.test(line);
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (isSearchMarker(line)) {
|
|
136
|
+
inSearch = true;
|
|
137
|
+
inReplace = false;
|
|
138
|
+
searchLines = [];
|
|
139
|
+
replaceLines = [];
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (isDividerMarker(line)) {
|
|
143
|
+
if (inSearch) { inSearch = false; inReplace = true; }
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (isEndMarker(line)) {
|
|
147
|
+
if (inReplace || inSearch) {
|
|
148
|
+
// CẬP NHẬT: Cho phép lưu patch nếu có search HOẶC có replace
|
|
149
|
+
// (Trường hợp Search rỗng nhưng Replace có nội dung = Insert/Append)
|
|
150
|
+
if (searchLines.length > 0 || replaceLines.length > 0) {
|
|
151
|
+
patches.push({ search: searchLines.join("\n"), replace: replaceLines.join("\n") });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
inSearch = false; inReplace = false; searchLines = []; replaceLines = [];
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (inSearch) searchLines.push(line);
|
|
158
|
+
else if (inReplace) replaceLines.push(line);
|
|
159
|
+
}
|
|
160
|
+
return patches;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* ------------------------------------------------------------------
|
|
165
|
+
* 2. APPLY PATCH (UPDATED)
|
|
166
|
+
* ------------------------------------------------------------------
|
|
167
|
+
*/
|
|
168
|
+
export const applyPatch = (
|
|
169
|
+
originalContent: string,
|
|
170
|
+
patchContent: string | PatchBlock[]
|
|
171
|
+
): string => {
|
|
172
|
+
|
|
173
|
+
let patches: PatchBlock[];
|
|
174
|
+
if (typeof patchContent === 'string') {
|
|
175
|
+
if (!patchContent || !patchContent.includes("<<<<<<< SEARCH")) {
|
|
176
|
+
return patchContent || originalContent;
|
|
177
|
+
}
|
|
178
|
+
patches = parseLLMPatch(patchContent);
|
|
179
|
+
} else {
|
|
180
|
+
patches = patchContent;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!patches || patches.length === 0) return originalContent;
|
|
184
|
+
|
|
185
|
+
let result = originalContent;
|
|
186
|
+
|
|
187
|
+
for (const patch of patches) {
|
|
188
|
+
const { search, replace } = patch;
|
|
189
|
+
|
|
190
|
+
// Strategy 0: Full File Replacement (Overwrite)
|
|
191
|
+
if (search === "__FULL_FILE_REPLACEMENT__") {
|
|
192
|
+
console.log(`[ApplyPatch] Overwriting file with full content.`);
|
|
193
|
+
result = replace;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Nếu search và replace giống hệt nhau, bỏ qua
|
|
198
|
+
if (search === replace) {
|
|
199
|
+
console.log(`[ApplyPatch] Search and replace are identical. Skipping.`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// NẾU SEARCH RỖNG (INSERT/APPEND hoặc TẠO FILE MỚI)
|
|
204
|
+
if (search.trim() === "") {
|
|
205
|
+
console.log(`[ApplyPatch] Inserting/appending block.`);
|
|
206
|
+
if (result.trim() === "") {
|
|
207
|
+
// Nếu file hiện tại rỗng hoặc chỉ có khoảng trắng, gán luôn bằng nội dung replace (Hỗ trợ tạo file mới)
|
|
208
|
+
result = replace;
|
|
209
|
+
} else {
|
|
210
|
+
// Nếu file đã có nội dung, append vào cuối. Đảm bảo có ít nhất một dấu xuống dòng phân tách.
|
|
211
|
+
const separator = result.endsWith("\n") ? "" : "\n";
|
|
212
|
+
result += separator + replace;
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- STRATEGY 1: EXACT MATCH ---
|
|
218
|
+
if (result.includes(search)) {
|
|
219
|
+
console.log(`[ApplyPatch] Exact match found.`);
|
|
220
|
+
result = result.replace(search, replace);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- STRATEGY 2: NORMALIZED MATCH (Trim) ---
|
|
225
|
+
const trimmedSearch = search.trim();
|
|
226
|
+
if (result.includes(trimmedSearch)) {
|
|
227
|
+
console.log(`[ApplyPatch] Trimmed match found.`);
|
|
228
|
+
result = result.replace(trimmedSearch, replace.trim());
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- STRATEGY 3: FUZZY LINE MATCH (BEST FOR CODE) ---
|
|
233
|
+
// Tìm vị trí tốt nhất dựa trên similarity score của từng dòng
|
|
234
|
+
const fuzzyMatch = applyFuzzyLineMatch(result, search, replace, 0.80); // 80% confidence
|
|
235
|
+
if (fuzzyMatch.result !== null) {
|
|
236
|
+
console.log(`[ApplyPatch] Fuzzy line match applied (Confidence: ${(fuzzyMatch.confidence * 100).toFixed(0)}%)`);
|
|
237
|
+
result = fuzzyMatch.result;
|
|
238
|
+
continue;
|
|
239
|
+
} else {
|
|
240
|
+
console.warn(`[ApplyPatch] Failed to match block. Max confidence was: ${(fuzzyMatch.confidence * 100).toFixed(0)}%`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- STRATEGY 4: DMP FALLBACK (LAST RESORT) ---
|
|
244
|
+
// Nếu cả fuzzy line match cũng tạch (ví dụ do search block quá ngắn hoặc cấu trúc quá nát)
|
|
245
|
+
// thì mới dùng DMP patch_apply.
|
|
246
|
+
try {
|
|
247
|
+
const dmp = new diff_match_patch();
|
|
248
|
+
dmp.Match_Threshold = 0.5;
|
|
249
|
+
dmp.Match_Distance = 1000;
|
|
250
|
+
|
|
251
|
+
const dmpPatches = dmp.patch_make(search, replace);
|
|
252
|
+
const [newText, applyResults] = dmp.patch_apply(dmpPatches, result);
|
|
253
|
+
|
|
254
|
+
// Kiểm tra xem có apply được hết các hunk không
|
|
255
|
+
const successCount = applyResults.filter(Boolean).length;
|
|
256
|
+
const successRate = successCount / applyResults.length;
|
|
257
|
+
|
|
258
|
+
if (successRate === 1 && newText !== result) {
|
|
259
|
+
console.log(`[ApplyPatch] DMP Patch applied via fuzzy logic.`);
|
|
260
|
+
result = newText;
|
|
261
|
+
} else {
|
|
262
|
+
console.error(`[ApplyPatch] All strategies failed for block starting with: ${search.substring(0, 30)}...`);
|
|
263
|
+
console.log(`[ApplyPatch DATA]`, { search, replace, result });
|
|
264
|
+
// Tuỳ chọn: Throw error để báo cho LLM biết là apply thất bại
|
|
265
|
+
// throw new Error("Could not apply patch block");
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error(`[ApplyPatch] DMP error:`, e);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result;
|
|
273
|
+
};
|