@hapico/cli 0.0.30 → 0.0.32

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.
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateNativeWindStyleMap = generateNativeWindStyleMap;
7
+ exports.compileAndSaveToFolder = compileAndSaveToFolder;
8
+ const postcss_1 = __importDefault(require("postcss"));
9
+ const tailwindcss_1 = __importDefault(require("tailwindcss"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ // @ts-ignore
13
+ const preset_1 = __importDefault(require("nativewind/preset"));
14
+ // ==========================================
15
+ // FIX LỖI IMPORT REACT-NATIVE-CSS-INTEROP
16
+ // ==========================================
17
+ let compileToNative;
18
+ try {
19
+ const compiler = require("react-native-css-interop/dist/compiler");
20
+ compileToNative = compiler.compile || compiler.default;
21
+ }
22
+ catch (e) {
23
+ try {
24
+ const cssToRn = require("react-native-css-interop/dist/css-to-rn");
25
+ compileToNative = cssToRn.cssToReactNativeRuntime || cssToRn.default;
26
+ }
27
+ catch (err) {
28
+ throw new Error("Không thể tìm thấy hàm compile. Kiểm tra lại node_modules.");
29
+ }
30
+ }
31
+ /**
32
+ * Hàm biên dịch Tailwind Class sang Compiled Object
33
+ */
34
+ async function generateNativeWindStyleMap(contentPaths, rawCss = "@tailwind base;\n@tailwind components;\n@tailwind utilities;") {
35
+ try {
36
+ const tailwindConfig = {
37
+ content: contentPaths,
38
+ presets: [preset_1.default],
39
+ theme: { extend: {} },
40
+ };
41
+ const processor = (0, postcss_1.default)([(0, tailwindcss_1.default)(tailwindConfig)]);
42
+ const result = await processor.process(rawCss, {
43
+ from: "tailwind.css",
44
+ to: "output.css",
45
+ });
46
+ return compileToNative(result.css, { native: true });
47
+ }
48
+ catch (error) {
49
+ console.error("❌ Compile Style Map Error:", error);
50
+ throw error;
51
+ }
52
+ }
53
+ /**
54
+ * Hàm hỗ trợ: Quét 1 thư mục và Lưu kết quả vào 1 thư mục khác
55
+ */
56
+ async function compileAndSaveToFolder(folderToScan, // Thư mục chứa code cần quét (VD: "./src/components")
57
+ outputFilePath // Nơi lưu kết quả (VD: "./dist/styles.json")
58
+ ) {
59
+ try {
60
+ // 1. Tạo đường dẫn quét toàn bộ file js, jsx, ts, tsx trong thư mục
61
+ // path.resolve(process.cwd(), ...) giúp lấy đúng đường dẫn gốc của project
62
+ const scanPattern = path_1.default.resolve(process.cwd(), folderToScan, "**/*.{js,jsx,ts,tsx}");
63
+ // console.log(`🔍 Scanning files in ${scanPattern}`);
64
+ // 2. Chạy hàm biên dịch
65
+ const compiledObject = await generateNativeWindStyleMap([scanPattern]);
66
+ // 3. Xử lý thư mục lưu file
67
+ const absoluteOutputPath = path_1.default.resolve(process.cwd(), outputFilePath);
68
+ const outputDir = path_1.default.dirname(absoluteOutputPath); // Lấy tên thư mục chứa file đầu ra
69
+ // Nếu thư mục đầu ra chưa tồn tại, Node.js sẽ tự động tạo thư mục đó (recursive: true)
70
+ if (!fs_1.default.existsSync(outputDir)) {
71
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
72
+ }
73
+ // 4. Ghi Object ra file JSON
74
+ fs_1.default.writeFileSync(absoluteOutputPath, JSON.stringify(compiledObject, null, 2), "utf-8");
75
+ // console.log(`✅ Sync CSS ${absoluteOutputPath}`);
76
+ }
77
+ catch (error) {
78
+ console.error("❌ Lỗi khi xuất file:", error);
79
+ }
80
+ }
@@ -213,6 +213,10 @@ const parseLLMPatch = (patchContent) => {
213
213
  let inReplace = false;
214
214
  let searchLines = [];
215
215
  let replaceLines = [];
216
+ // State for full file replacement detection
217
+ let inCodeBlock = false;
218
+ let codeBlockLines = [];
219
+ let hasSeenMarkersInBlock = false;
216
220
  const isFileMarker = (line) => {
217
221
  const match = line.match(/^### File:\s*[`']?([^`'\s]+)[`']?/i);
218
222
  return match ? match[1] : null;
@@ -224,11 +228,16 @@ const parseLLMPatch = (patchContent) => {
224
228
  const fileName = isFileMarker(line);
225
229
  if (fileName) {
226
230
  currentFile = fileName;
231
+ // Reset tracking when switching files
232
+ inCodeBlock = false;
233
+ codeBlockLines = [];
234
+ hasSeenMarkersInBlock = false;
227
235
  continue;
228
236
  }
229
237
  if (isSearchMarker(line)) {
230
238
  inSearch = true;
231
239
  inReplace = false;
240
+ hasSeenMarkersInBlock = true;
232
241
  searchLines = [];
233
242
  replaceLines = [];
234
243
  continue;
@@ -238,9 +247,11 @@ const parseLLMPatch = (patchContent) => {
238
247
  inSearch = false;
239
248
  inReplace = true;
240
249
  }
250
+ hasSeenMarkersInBlock = true;
241
251
  continue;
242
252
  }
243
253
  if (isEndMarker(line)) {
254
+ hasSeenMarkersInBlock = true;
244
255
  if (inReplace || inSearch) {
245
256
  if (searchLines.length > 0 || replaceLines.length > 0) {
246
257
  if (!filePatches[currentFile])
@@ -257,10 +268,33 @@ const parseLLMPatch = (patchContent) => {
257
268
  replaceLines = [];
258
269
  continue;
259
270
  }
271
+ // Detect triple backticks to identify potential full-file replacements
272
+ if (line.trim().startsWith("```")) {
273
+ if (!inCodeBlock) {
274
+ inCodeBlock = true;
275
+ codeBlockLines = [];
276
+ hasSeenMarkersInBlock = false;
277
+ }
278
+ else {
279
+ inCodeBlock = false;
280
+ // If the code block ended without any SEARCH/REPLACE markers, treat it as full-file code
281
+ if (!hasSeenMarkersInBlock && codeBlockLines.length > 0 && currentFile !== "unknown") {
282
+ if (!filePatches[currentFile])
283
+ filePatches[currentFile] = [];
284
+ filePatches[currentFile].push({
285
+ search: "__FULL_FILE_REPLACEMENT__",
286
+ replace: codeBlockLines.join("\n")
287
+ });
288
+ }
289
+ }
290
+ continue;
291
+ }
260
292
  if (inSearch)
261
293
  searchLines.push(line);
262
294
  else if (inReplace)
263
295
  replaceLines.push(line);
296
+ else if (inCodeBlock)
297
+ codeBlockLines.push(line);
264
298
  }
265
299
  return filePatches;
266
300
  };
@@ -275,6 +309,12 @@ const applyPatch = (originalContent, patches) => {
275
309
  let result = originalContent;
276
310
  for (const patch of patches) {
277
311
  const { search, replace } = patch;
312
+ // Strategy 0: Full File Replacement (Overwrite)
313
+ if (search === "__FULL_FILE_REPLACEMENT__") {
314
+ console.log(`[ApplyPatch] Overwriting file with full content.`);
315
+ result = replace;
316
+ continue;
317
+ }
278
318
  // NẾU SEARCH RỖNG (INSERT/APPEND)
279
319
  if (search.trim() === "") {
280
320
  console.log(`[ApplyPatch] Inserting/appending block.`);
@@ -360,30 +400,56 @@ const startServer = async (rootDir) => {
360
400
  });
361
401
  // API 3: Apply Patch
362
402
  app.post("/api/apply", async (req, res) => {
363
- var _a;
364
403
  const { llmResponse, filePaths: selectedPaths } = req.body;
365
404
  // 1. Parse all patches from LLM response
366
405
  const filePatches = parseLLMPatch(llmResponse);
367
406
  // 2. Scan project files
368
407
  const allFiles = await scanRecursively(rootDir, rootDir);
369
408
  const modifiedFiles = [];
370
- // 3. Match patches to files and apply
409
+ const appliedNormalizedPaths = new Set();
410
+ // 3. Match patches to existing files and apply
371
411
  for (const file of allFiles) {
372
- // Find patches meant for this specific file path
373
- // Note: LLM might use different path separators, so we normalize
374
412
  const normalizedPath = file.path.replace(/\\/g, '/');
375
- const patchesForFile = (_a = Object.entries(filePatches).find(([path]) => {
413
+ const patchesEntry = Object.entries(filePatches).find(([path]) => {
376
414
  const normalizedKey = path.replace(/\\/g, '/');
377
415
  return normalizedKey === normalizedPath || normalizedPath.endsWith('/' + normalizedKey);
378
- })) === null || _a === void 0 ? void 0 : _a[1];
379
- // Only apply if the file was selected in UI AND has patches in the response
380
- if (selectedPaths.includes(file.path) && patchesForFile) {
381
- console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
382
- const newContent = (0, exports.applyPatch)(file.content, patchesForFile);
383
- if (newContent !== file.content) {
384
- await promises_1.default.writeFile(file.fullPath, newContent, "utf-8");
385
- modifiedFiles.push(file.path);
416
+ });
417
+ if (patchesEntry) {
418
+ const [rawKey, patchesForFile] = patchesEntry;
419
+ // Only apply if the file was selected in UI
420
+ if (selectedPaths.includes(file.path)) {
421
+ console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
422
+ const newContent = (0, exports.applyPatch)(file.content, patchesForFile);
423
+ if (newContent !== file.content) {
424
+ await promises_1.default.writeFile(file.fullPath, newContent, "utf-8");
425
+ modifiedFiles.push(file.path);
426
+ }
386
427
  }
428
+ appliedNormalizedPaths.add(rawKey.replace(/\\/g, '/'));
429
+ }
430
+ }
431
+ // 4. Handle New Files (patches for paths that don't exist yet)
432
+ for (const [rawPath, patches] of Object.entries(filePatches)) {
433
+ const normalizedKey = rawPath.replace(/\\/g, '/');
434
+ if (normalizedKey === "unknown" || appliedNormalizedPaths.has(normalizedKey))
435
+ continue;
436
+ // Double check it doesn't exist (safety)
437
+ const alreadyExists = allFiles.some(f => {
438
+ const np = f.path.replace(/\\/g, '/');
439
+ return np === normalizedKey || np.endsWith('/' + normalizedKey);
440
+ });
441
+ if (alreadyExists)
442
+ continue;
443
+ console.log(`[Apply] Creating new file: ${rawPath}`);
444
+ const newContent = (0, exports.applyPatch)("", patches);
445
+ const fullPath = node_path_1.default.resolve(rootDir, rawPath);
446
+ try {
447
+ await promises_1.default.mkdir(node_path_1.default.dirname(fullPath), { recursive: true });
448
+ await promises_1.default.writeFile(fullPath, newContent, "utf-8");
449
+ modifiedFiles.push(rawPath);
450
+ }
451
+ catch (err) {
452
+ console.error(`[Apply] Failed to create file ${rawPath}:`, err);
387
453
  }
388
454
  }
389
455
  // Fallback: If no matched files but only one file was selected, try applying "unknown" patches
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyPatch = exports.parseLLMPatch = void 0;
4
+ const diff_match_patch_1 = require("diff-match-patch");
5
+ /**
6
+ * ------------------------------------------------------------------
7
+ * HELPER: STRING SIMILARITY (Levenshtein based)
8
+ * Tính độ giống nhau giữa 2 chuỗi (0 -> 1)
9
+ * ------------------------------------------------------------------
10
+ */
11
+ const calculateSimilarity = (s1, s2) => {
12
+ const longer = s1.length > s2.length ? s1 : s2;
13
+ const shorter = s1.length > s2.length ? s2 : s1;
14
+ const longerLength = longer.length;
15
+ if (longerLength === 0) {
16
+ return 1.0;
17
+ }
18
+ // Sử dụng dmp để tính Levenshtein distance cho chính xác và nhanh
19
+ const dmp = new diff_match_patch_1.diff_match_patch();
20
+ const diffs = dmp.diff_main(longer, shorter);
21
+ const levenshtein = dmp.diff_levenshtein(diffs);
22
+ return (longerLength - levenshtein) / longerLength;
23
+ };
24
+ /**
25
+ * ------------------------------------------------------------------
26
+ * HELPER: FUZZY LINE MATCHING
27
+ * Tìm đoạn code khớp nhất trong file gốc dựa trên điểm số (Confidence)
28
+ * ------------------------------------------------------------------
29
+ */
30
+ const applyFuzzyLineMatch = (original, search, replace, threshold = 0.85 // Ngưỡng chấp nhận (85% giống nhau)
31
+ ) => {
32
+ const originalLines = original.split(/\r?\n/);
33
+ const searchLines = search.split(/\r?\n/);
34
+ // Lọc bỏ dòng trống ở đầu/cuối search block để match chính xác hơn
35
+ while (searchLines.length > 0 && searchLines[0].trim() === "")
36
+ searchLines.shift();
37
+ while (searchLines.length > 0 && searchLines[searchLines.length - 1].trim() === "")
38
+ searchLines.pop();
39
+ if (searchLines.length === 0)
40
+ return { result: null, confidence: 0 };
41
+ let bestMatchIndex = -1;
42
+ let bestMatchScore = 0;
43
+ // Duyệt qua file gốc (Sliding Window)
44
+ // Chỉ cần duyệt đến vị trí mà số dòng còn lại đủ để chứa searchLines
45
+ for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
46
+ let currentScoreTotal = 0;
47
+ let possible = true;
48
+ for (let j = 0; j < searchLines.length; j++) {
49
+ const originalLine = originalLines[i + j].trim();
50
+ const searchLine = searchLines[j].trim();
51
+ // So sánh nhanh: Nếu giống hệt nhau
52
+ if (originalLine === searchLine) {
53
+ currentScoreTotal += 1;
54
+ continue;
55
+ }
56
+ // So sánh chậm: Tính độ tương đồng
57
+ const similarity = calculateSimilarity(originalLine, searchLine);
58
+ // Nếu một dòng quá khác biệt (< 60%), coi như block này không khớp
59
+ // (Giúp tối ưu hiệu năng, cắt nhánh sớm)
60
+ if (similarity < 0.6) {
61
+ possible = false;
62
+ break;
63
+ }
64
+ currentScoreTotal += similarity;
65
+ }
66
+ if (possible) {
67
+ const avgScore = currentScoreTotal / searchLines.length;
68
+ if (avgScore > bestMatchScore) {
69
+ bestMatchScore = avgScore;
70
+ bestMatchIndex = i;
71
+ }
72
+ }
73
+ }
74
+ // LOG CONFIDENCE ĐỂ DEBUG
75
+ if (bestMatchScore > 0) {
76
+ console.log(`[FuzzyMatch] Best match score: ${(bestMatchScore * 100).toFixed(2)}% at line ${bestMatchIndex + 1}`);
77
+ }
78
+ // Nếu tìm thấy vị trí có độ tin cậy cao hơn ngưỡng
79
+ if (bestMatchIndex !== -1 && bestMatchScore >= threshold) {
80
+ const before = originalLines.slice(0, bestMatchIndex);
81
+ // Lưu ý: Đoạn replace thay thế đúng số lượng dòng của searchLines
82
+ // (Giả định search và match trong original có cùng số dòng hiển thị)
83
+ const after = originalLines.slice(bestMatchIndex + searchLines.length);
84
+ return {
85
+ result: [...before, replace, ...after].join("\n"),
86
+ confidence: bestMatchScore
87
+ };
88
+ }
89
+ return { result: null, confidence: bestMatchScore };
90
+ };
91
+ /**
92
+ * ------------------------------------------------------------------
93
+ * 1. STATE MACHINE PARSER (GIỮ NGUYÊN)
94
+ * ------------------------------------------------------------------
95
+ */
96
+ const parseLLMPatch = (patchContent) => {
97
+ // ... (Code parse giữ nguyên như cũ của bạn)
98
+ const lines = patchContent.split(/\r?\n/);
99
+ const patches = [];
100
+ let inSearch = false;
101
+ let inReplace = false;
102
+ let searchLines = [];
103
+ let replaceLines = [];
104
+ const isSearchMarker = (line) => /^\s*<{7,}\s*SEARCH\s*$/.test(line);
105
+ const isDividerMarker = (line) => /^\s*={7,}\s*$/.test(line);
106
+ const isEndMarker = (line) => /^\s*>{7,}\s*REPLACE\s*$/.test(line);
107
+ for (const line of lines) {
108
+ if (isSearchMarker(line)) {
109
+ inSearch = true;
110
+ inReplace = false;
111
+ searchLines = [];
112
+ replaceLines = [];
113
+ continue;
114
+ }
115
+ if (isDividerMarker(line)) {
116
+ if (inSearch) {
117
+ inSearch = false;
118
+ inReplace = true;
119
+ }
120
+ continue;
121
+ }
122
+ if (isEndMarker(line)) {
123
+ if (inReplace || inSearch) {
124
+ // CẬP NHẬT: Cho phép lưu patch nếu có search HOẶC có replace
125
+ // (Trường hợp Search rỗng nhưng Replace có nội dung = Insert/Append)
126
+ if (searchLines.length > 0 || replaceLines.length > 0) {
127
+ patches.push({ search: searchLines.join("\n"), replace: replaceLines.join("\n") });
128
+ }
129
+ }
130
+ inSearch = false;
131
+ inReplace = false;
132
+ searchLines = [];
133
+ replaceLines = [];
134
+ continue;
135
+ }
136
+ if (inSearch)
137
+ searchLines.push(line);
138
+ else if (inReplace)
139
+ replaceLines.push(line);
140
+ }
141
+ return patches;
142
+ };
143
+ exports.parseLLMPatch = parseLLMPatch;
144
+ /**
145
+ * ------------------------------------------------------------------
146
+ * 2. APPLY PATCH (UPDATED)
147
+ * ------------------------------------------------------------------
148
+ */
149
+ const applyPatch = (originalContent, patchContent) => {
150
+ let patches;
151
+ if (typeof patchContent === 'string') {
152
+ if (!patchContent || !patchContent.includes("<<<<<<< SEARCH")) {
153
+ return patchContent || originalContent;
154
+ }
155
+ patches = (0, exports.parseLLMPatch)(patchContent);
156
+ }
157
+ else {
158
+ patches = patchContent;
159
+ }
160
+ if (!patches || patches.length === 0)
161
+ return originalContent;
162
+ let result = originalContent;
163
+ for (const patch of patches) {
164
+ const { search, replace } = patch;
165
+ // Strategy 0: Full File Replacement (Overwrite)
166
+ if (search === "__FULL_FILE_REPLACEMENT__") {
167
+ console.log(`[ApplyPatch] Overwriting file with full content.`);
168
+ result = replace;
169
+ continue;
170
+ }
171
+ // Nếu search và replace giống hệt nhau, bỏ qua
172
+ if (search === replace) {
173
+ console.log(`[ApplyPatch] Search and replace are identical. Skipping.`);
174
+ continue;
175
+ }
176
+ // NẾU SEARCH RỖNG (INSERT/APPEND hoặc TẠO FILE MỚI)
177
+ if (search.trim() === "") {
178
+ console.log(`[ApplyPatch] Inserting/appending block.`);
179
+ if (result.trim() === "") {
180
+ // 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)
181
+ result = replace;
182
+ }
183
+ else {
184
+ // 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.
185
+ const separator = result.endsWith("\n") ? "" : "\n";
186
+ result += separator + replace;
187
+ }
188
+ continue;
189
+ }
190
+ // --- STRATEGY 1: EXACT MATCH ---
191
+ if (result.includes(search)) {
192
+ console.log(`[ApplyPatch] Exact match found.`);
193
+ result = result.replace(search, replace);
194
+ continue;
195
+ }
196
+ // --- STRATEGY 2: NORMALIZED MATCH (Trim) ---
197
+ const trimmedSearch = search.trim();
198
+ if (result.includes(trimmedSearch)) {
199
+ console.log(`[ApplyPatch] Trimmed match found.`);
200
+ result = result.replace(trimmedSearch, replace.trim());
201
+ continue;
202
+ }
203
+ // --- STRATEGY 3: FUZZY LINE MATCH (BEST FOR CODE) ---
204
+ // Tìm vị trí tốt nhất dựa trên similarity score của từng dòng
205
+ const fuzzyMatch = applyFuzzyLineMatch(result, search, replace, 0.80); // 80% confidence
206
+ if (fuzzyMatch.result !== null) {
207
+ console.log(`[ApplyPatch] Fuzzy line match applied (Confidence: ${(fuzzyMatch.confidence * 100).toFixed(0)}%)`);
208
+ result = fuzzyMatch.result;
209
+ continue;
210
+ }
211
+ else {
212
+ console.warn(`[ApplyPatch] Failed to match block. Max confidence was: ${(fuzzyMatch.confidence * 100).toFixed(0)}%`);
213
+ }
214
+ // --- STRATEGY 4: DMP FALLBACK (LAST RESORT) ---
215
+ // 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)
216
+ // thì mới dùng DMP patch_apply.
217
+ try {
218
+ const dmp = new diff_match_patch_1.diff_match_patch();
219
+ dmp.Match_Threshold = 0.5;
220
+ dmp.Match_Distance = 1000;
221
+ const dmpPatches = dmp.patch_make(search, replace);
222
+ const [newText, applyResults] = dmp.patch_apply(dmpPatches, result);
223
+ // Kiểm tra xem có apply được hết các hunk không
224
+ const successCount = applyResults.filter(Boolean).length;
225
+ const successRate = successCount / applyResults.length;
226
+ if (successRate === 1 && newText !== result) {
227
+ console.log(`[ApplyPatch] DMP Patch applied via fuzzy logic.`);
228
+ result = newText;
229
+ }
230
+ else {
231
+ console.error(`[ApplyPatch] All strategies failed for block starting with: ${search.substring(0, 30)}...`);
232
+ console.log(`[ApplyPatch DATA]`, { search, replace, result });
233
+ // Tuỳ chọn: Throw error để báo cho LLM biết là apply thất bại
234
+ // throw new Error("Could not apply patch block");
235
+ }
236
+ }
237
+ catch (e) {
238
+ console.error(`[ApplyPatch] DMP error:`, e);
239
+ }
240
+ }
241
+ return result;
242
+ };
243
+ exports.applyPatch = applyPatch;