@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hapico/cli",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "A simple CLI tool for project management",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -27,11 +27,15 @@
27
27
  "express": "^5.2.1",
28
28
  "inquirer": "^12.9.6",
29
29
  "lodash": "^4.17.21",
30
+ "nativewind": "^4.2.2",
30
31
  "open": "^10.2.0",
31
32
  "ora": "^8.2.0",
32
33
  "pako": "^2.1.0",
34
+ "postcss": "^8.5.8",
33
35
  "prettier": "^3.6.2",
34
36
  "qrcode-terminal": "^0.12.0",
37
+ "react-native-css-interop": "^0.2.2",
38
+ "tailwindcss": "3.4.17",
35
39
  "unzipper": "^0.12.3",
36
40
  "uuid": "^13.0.0",
37
41
  "ws": "^8.18.3"
@@ -0,0 +1,87 @@
1
+ import postcss from "postcss";
2
+ import tailwindcss, { type Config } from "tailwindcss";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ // @ts-ignore
6
+ import nativewindPreset from "nativewind/preset";
7
+
8
+ // ==========================================
9
+ // FIX LỖI IMPORT REACT-NATIVE-CSS-INTEROP
10
+ // ==========================================
11
+ let compileToNative: (css: string, options?: any) => any;
12
+ try {
13
+ const compiler = require("react-native-css-interop/dist/compiler");
14
+ compileToNative = compiler.compile || compiler.default;
15
+ } catch (e) {
16
+ try {
17
+ const cssToRn = require("react-native-css-interop/dist/css-to-rn");
18
+ compileToNative = cssToRn.cssToReactNativeRuntime || cssToRn.default;
19
+ } catch (err) {
20
+ throw new Error("Không thể tìm thấy hàm compile. Kiểm tra lại node_modules.");
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Hàm biên dịch Tailwind Class sang Compiled Object
26
+ */
27
+ export async function generateNativeWindStyleMap(
28
+ contentPaths: string[],
29
+ rawCss: string = "@tailwind base;\n@tailwind components;\n@tailwind utilities;"
30
+ ) {
31
+ try {
32
+ const tailwindConfig: Config = {
33
+ content: contentPaths,
34
+ presets: [nativewindPreset],
35
+ theme: { extend: {} },
36
+ };
37
+
38
+ const processor = postcss([tailwindcss(tailwindConfig)]);
39
+ const result = await processor.process(rawCss, {
40
+ from: "tailwind.css",
41
+ to: "output.css",
42
+ });
43
+
44
+ return compileToNative(result.css, { native: true });
45
+ } catch (error) {
46
+ console.error("❌ Compile Style Map Error:", error);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Hàm hỗ trợ: Quét 1 thư mục và Lưu kết quả vào 1 thư mục khác
53
+ */
54
+ export async function compileAndSaveToFolder(
55
+ folderToScan: string, // Thư mục chứa code cần quét (VD: "./src/components")
56
+ outputFilePath: string // Nơi lưu kết quả (VD: "./dist/styles.json")
57
+ ) {
58
+ try {
59
+ // 1. Tạo đường dẫn quét toàn bộ file js, jsx, ts, tsx trong thư mục
60
+ // path.resolve(process.cwd(), ...) giúp lấy đúng đường dẫn gốc của project
61
+ const scanPattern = path.resolve(process.cwd(), folderToScan, "**/*.{js,jsx,ts,tsx}");
62
+
63
+ // console.log(`🔍 Scanning files in ${scanPattern}`);
64
+
65
+ // 2. Chạy hàm biên dịch
66
+ const compiledObject = await generateNativeWindStyleMap([scanPattern]);
67
+
68
+ // 3. Xử lý thư mục lưu file
69
+ const absoluteOutputPath = path.resolve(process.cwd(), outputFilePath);
70
+ const outputDir = path.dirname(absoluteOutputPath); // Lấy tên thư mục chứa file đầu ra
71
+
72
+ // 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)
73
+ if (!fs.existsSync(outputDir)) {
74
+ fs.mkdirSync(outputDir, { recursive: true });
75
+ }
76
+
77
+ // 4. Ghi Object ra file JSON
78
+ fs.writeFileSync(absoluteOutputPath, JSON.stringify(compiledObject, null, 2), "utf-8");
79
+
80
+ // console.log(`✅ Sync CSS ${absoluteOutputPath}`);
81
+
82
+ } catch (error) {
83
+ console.error("❌ Lỗi khi xuất file:", error);
84
+ }
85
+ }
86
+
87
+
@@ -16,7 +16,8 @@ const scanRecursively = async (
16
16
 
17
17
  for (const entry of list) {
18
18
  const fullPath = path.join(dir, entry.name);
19
- const relativePath = path.relative(baseDir, fullPath);
19
+ // Chuẩn hóa đường dẫn về forward slash để nhất quán giữa các hệ điều hành
20
+ const relativePath = path.relative(baseDir, fullPath).split(path.sep).join('/');
20
21
 
21
22
  if (shouldIgnorePath(relativePath)) continue;
22
23
 
@@ -252,6 +253,11 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
252
253
  let searchLines: string[] = [];
253
254
  let replaceLines: string[] = [];
254
255
 
256
+ // State for full file replacement detection
257
+ let inCodeBlock = false;
258
+ let codeBlockLines: string[] = [];
259
+ let hasSeenMarkersInBlock = false;
260
+
255
261
  const isFileMarker = (line: string) => {
256
262
  const match = line.match(/^### File:\s*[`']?([^`'\s]+)[`']?/i);
257
263
  return match ? match[1] : null;
@@ -264,21 +270,28 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
264
270
  const fileName = isFileMarker(line);
265
271
  if (fileName) {
266
272
  currentFile = fileName;
273
+ // Reset tracking when switching files
274
+ inCodeBlock = false;
275
+ codeBlockLines = [];
276
+ hasSeenMarkersInBlock = false;
267
277
  continue;
268
278
  }
269
279
 
270
280
  if (isSearchMarker(line)) {
271
281
  inSearch = true;
272
282
  inReplace = false;
283
+ hasSeenMarkersInBlock = true;
273
284
  searchLines = [];
274
285
  replaceLines = [];
275
286
  continue;
276
287
  }
277
288
  if (isDividerMarker(line)) {
278
289
  if (inSearch) { inSearch = false; inReplace = true; }
290
+ hasSeenMarkersInBlock = true;
279
291
  continue;
280
292
  }
281
293
  if (isEndMarker(line)) {
294
+ hasSeenMarkersInBlock = true;
282
295
  if (inReplace || inSearch) {
283
296
  if (searchLines.length > 0 || replaceLines.length > 0) {
284
297
  if (!filePatches[currentFile]) filePatches[currentFile] = [];
@@ -291,8 +304,30 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
291
304
  inSearch = false; inReplace = false; searchLines = []; replaceLines = [];
292
305
  continue;
293
306
  }
307
+
308
+ // Detect triple backticks to identify potential full-file replacements
309
+ if (line.trim().startsWith("```")) {
310
+ if (!inCodeBlock) {
311
+ inCodeBlock = true;
312
+ codeBlockLines = [];
313
+ hasSeenMarkersInBlock = false;
314
+ } else {
315
+ inCodeBlock = false;
316
+ // If the code block ended without any SEARCH/REPLACE markers, treat it as full-file code
317
+ if (!hasSeenMarkersInBlock && codeBlockLines.length > 0 && currentFile !== "unknown") {
318
+ if (!filePatches[currentFile]) filePatches[currentFile] = [];
319
+ filePatches[currentFile].push({
320
+ search: "__FULL_FILE_REPLACEMENT__",
321
+ replace: codeBlockLines.join("\n")
322
+ });
323
+ }
324
+ }
325
+ continue;
326
+ }
327
+
294
328
  if (inSearch) searchLines.push(line);
295
329
  else if (inReplace) replaceLines.push(line);
330
+ else if (inCodeBlock) codeBlockLines.push(line);
296
331
  }
297
332
  return filePatches;
298
333
  };
@@ -313,6 +348,13 @@ export const applyPatch = (
313
348
  for (const patch of patches) {
314
349
  const { search, replace } = patch;
315
350
 
351
+ // Strategy 0: Full File Replacement (Overwrite)
352
+ if (search === "__FULL_FILE_REPLACEMENT__") {
353
+ console.log(`[ApplyPatch] Overwriting file with full content.`);
354
+ result = replace;
355
+ continue;
356
+ }
357
+
316
358
  // NẾU SEARCH RỖNG (INSERT/APPEND)
317
359
  if (search.trim() === "") {
318
360
  console.log(`[ApplyPatch] Inserting/appending block.`);
@@ -418,26 +460,54 @@ const startServer = async (rootDir: string) => {
418
460
  // 2. Scan project files
419
461
  const allFiles = await scanRecursively(rootDir, rootDir);
420
462
  const modifiedFiles: string[] = [];
463
+ const appliedNormalizedPaths = new Set<string>();
421
464
 
422
- // 3. Match patches to files and apply
465
+ // 3. Match patches to existing files and apply
423
466
  for (const file of allFiles) {
424
- // Find patches meant for this specific file path
425
- // Note: LLM might use different path separators, so we normalize
426
467
  const normalizedPath = file.path.replace(/\\/g, '/');
427
- const patchesForFile = Object.entries(filePatches).find(([path]) => {
468
+ const patchesEntry = Object.entries(filePatches).find(([path]) => {
428
469
  const normalizedKey = path.replace(/\\/g, '/');
429
470
  return normalizedKey === normalizedPath || normalizedPath.endsWith('/' + normalizedKey);
430
- })?.[1];
471
+ });
431
472
 
432
- // Only apply if the file was selected in UI AND has patches in the response
433
- if (selectedPaths.includes(file.path) && patchesForFile) {
434
- console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
435
- const newContent = applyPatch(file.content, patchesForFile);
436
-
437
- if (newContent !== file.content) {
438
- await fs.writeFile(file.fullPath, newContent, "utf-8");
439
- modifiedFiles.push(file.path);
473
+ if (patchesEntry) {
474
+ const [rawKey, patchesForFile] = patchesEntry;
475
+ // Only apply if the file was selected in UI
476
+ if (selectedPaths.includes(file.path)) {
477
+ console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
478
+ const newContent = applyPatch(file.content, patchesForFile);
479
+
480
+ if (newContent !== file.content) {
481
+ await fs.writeFile(file.fullPath, newContent, "utf-8");
482
+ modifiedFiles.push(file.path);
483
+ }
440
484
  }
485
+ appliedNormalizedPaths.add(rawKey.replace(/\\/g, '/'));
486
+ }
487
+ }
488
+
489
+ // 4. Handle New Files (patches for paths that don't exist yet)
490
+ for (const [rawPath, patches] of Object.entries(filePatches)) {
491
+ const normalizedKey = rawPath.replace(/\\/g, '/');
492
+ if (normalizedKey === "unknown" || appliedNormalizedPaths.has(normalizedKey)) continue;
493
+
494
+ // Double check it doesn't exist (safety)
495
+ const alreadyExists = allFiles.some(f => {
496
+ const np = f.path.replace(/\\/g, '/');
497
+ return np === normalizedKey || np.endsWith('/' + normalizedKey);
498
+ });
499
+ if (alreadyExists) continue;
500
+
501
+ console.log(`[Apply] Creating new file: ${rawPath}`);
502
+ const newContent = applyPatch("", patches);
503
+ const fullPath = path.resolve(rootDir, rawPath);
504
+
505
+ try {
506
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
507
+ await fs.writeFile(fullPath, newContent, "utf-8");
508
+ modifiedFiles.push(rawPath);
509
+ } catch (err) {
510
+ console.error(`[Apply] Failed to create file ${rawPath}:`, err);
441
511
  }
442
512
  }
443
513
 
@@ -662,7 +732,8 @@ const startServer = async (rootDir: string) => {
662
732
 
663
733
  async init() {
664
734
  try {
665
- const res = await fetch('http://localhost:${PORT}/api/files');
735
+ // Sử dụng relative URL để tránh lỗi phân giải localhost/127.0.0.1 trên Windows
736
+ const res = await fetch('/api/files');
666
737
  const data = await res.json();
667
738
  this.rawFiles = data;
668
739
  this.buildTree();
@@ -672,98 +743,10 @@ const startServer = async (rootDir: string) => {
672
743
  this.loading = false;
673
744
  }
674
745
 
675
- // Search watcher
676
746
  this.$watch('search', () => this.buildTree());
677
-
678
- // Auto-generate prompt
679
747
  this.$watch('instruction', () => this.debouncedFetchPrompt());
680
748
  this.$watch('selectedFiles', () => this.debouncedFetchPrompt());
681
749
  },
682
-
683
- // ===========================
684
- // Tree Logic
685
- // ===========================
686
-
687
- buildTree() {
688
- // 1. Convert paths to object structure
689
- const root = {};
690
- const searchLower = this.search.toLowerCase();
691
-
692
- this.rawFiles.forEach(file => {
693
- if (this.search && !file.path.toLowerCase().includes(searchLower)) return;
694
-
695
- const parts = file.path.split(/[\\\\/]/);
696
- let current = root;
697
-
698
- parts.forEach((part, index) => {
699
- if (!current[part]) {
700
- current[part] = {
701
- name: part,
702
- path: parts.slice(0, index + 1).join('/'),
703
- children: {},
704
- type: index === parts.length - 1 ? 'file' : 'folder',
705
- fullPath: file.path // Only meaningful for file
706
- };
707
- }
708
- current = current[part].children;
709
- });
710
- });
711
-
712
- // 2. Flatten object structure into renderable list with depth
713
- this.treeNodes = [];
714
- const traverse = (nodeMap, depth) => {
715
- // Sort: Folders first, then Files. Alphabetical.
716
- const keys = Object.keys(nodeMap).sort((a, b) => {
717
- const nodeA = nodeMap[a];
718
- const nodeB = nodeMap[b];
719
- if (nodeA.type !== nodeB.type) return nodeA.type === 'folder' ? -1 : 1;
720
- return a.localeCompare(b);
721
- });
722
-
723
- keys.forEach(key => {
724
- const node = nodeMap[key];
725
- const flatNode = {
726
- id: node.path,
727
- name: node.name,
728
- path: node.path, // Relative path for selection
729
- type: node.type,
730
- depth: depth,
731
- children: Object.keys(node.children).length > 0, // boolean for UI
732
- expanded: this.search.length > 0 || depth < 1, // Auto expand on search or root
733
- rawChildren: node.children // Keep ref for recursion
734
- };
735
-
736
- this.treeNodes.push(flatNode);
737
- if (flatNode.children && flatNode.expanded) {
738
- traverse(node.children, depth + 1);
739
- }
740
- });
741
- };
742
-
743
- traverse(root, 0);
744
- },
745
-
746
- // Re-calculate visible nodes (folding logic) without rebuilding everything
747
- // But for simplicity in this SFC, we just rebuild the list when expanded changes.
748
- toggleExpand(node) {
749
- if (node.type !== 'folder') return;
750
-
751
- // Find node in treeNodes and toggle
752
- // Since treeNodes is flat and generated, we need to persist state or smarter logic.
753
- // Simpler: Just modify the expanded state in the current flat list is tricky because children are dynamic.
754
- // Better approach for this lightweight UI:
755
- // We actually need a persistent "expanded" Set to survive re-renders if we want to be fancy.
756
- // For now, let's just cheat: Update the boolean in the flat list works for collapsing,
757
- // but expanding requires knowing the children.
758
-
759
- // Let's reload the tree logic but using a \`expandedPaths\` set.
760
- if (this.expandedPaths.has(node.path)) {
761
- this.expandedPaths.delete(node.path);
762
- } else {
763
- this.expandedPaths.add(node.path);
764
- }
765
- this.refreshTreeVisibility();
766
- },
767
750
 
768
751
  // Alternative: Just use a computed property for \`visibleNodes\`.
769
752
  // Since Alpine isn't React, we do this manually.
@@ -791,7 +774,8 @@ const startServer = async (rootDir: string) => {
791
774
 
792
775
  this.rawFiles.forEach(file => {
793
776
  if (this.search && !file.path.toLowerCase().includes(searchLower)) return;
794
- const parts = file.path.split(/[\\\\/]/);
777
+ // File path đã được chuẩn hóa '/' từ backend
778
+ const parts = file.path.split('/');
795
779
  let current = this.rootObject;
796
780
  parts.forEach((part, index) => {
797
781
  if (!current[part]) {
@@ -892,9 +876,8 @@ const startServer = async (rootDir: string) => {
892
876
  },
893
877
 
894
878
  getAllDescendants(folderPath) {
895
- // Simple filter from rawFiles
896
879
  return this.rawFiles
897
- .filter(f => f.path.startsWith(folderPath + '/') || f.path.startsWith(folderPath + '\\\\'))
880
+ .filter(f => f.path === folderPath || f.path.startsWith(folderPath + '/'))
898
881
  .map(f => f.path);
899
882
  },
900
883
 
@@ -927,7 +910,7 @@ const startServer = async (rootDir: string) => {
927
910
  }
928
911
  this.generating = true;
929
912
  try {
930
- const res = await fetch('http://localhost:${PORT}/api/prompt', {
913
+ const res = await fetch('/api/prompt', {
931
914
  method: 'POST',
932
915
  headers: {'Content-Type': 'application/json'},
933
916
  body: JSON.stringify({ filePaths: Array.from(this.selectedFiles), instruction: this.instruction })
@@ -954,7 +937,7 @@ const startServer = async (rootDir: string) => {
954
937
  async applyPatch() {
955
938
  this.applying = true;
956
939
  try {
957
- const res = await fetch('http://localhost:${PORT}/api/apply', {
940
+ const res = await fetch('/api/apply', {
958
941
  method: 'POST',
959
942
  headers: {'Content-Type': 'application/json'},
960
943
  body: JSON.stringify({ filePaths: Array.from(this.selectedFiles), llmResponse: this.llmResponse })