@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/index.ts CHANGED
@@ -17,6 +17,7 @@ import { promisify } from "util";
17
17
  import chalk from "chalk";
18
18
  import pako from "pako";
19
19
  import { vibe } from "./tools/vibe";
20
+ import { compileAndSaveToFolder } from "./tools/nativewind";
20
21
 
21
22
  // Promisify exec for async usage
22
23
  const execPromise = promisify(exec);
@@ -96,7 +97,7 @@ const getStoredProjectId = (
96
97
  };
97
98
  };
98
99
 
99
- // Khởi tạo cache bằng Map để lưu trữ kết quả compile
100
+ // Initialize cache using Map to store compilation results
100
101
  const compileCache = new Map<string, string>();
101
102
 
102
103
  interface FileContent {
@@ -187,7 +188,7 @@ class FileManager {
187
188
  }
188
189
 
189
190
  private replaceSplashWithSeparator(filePath: string): string {
190
- // Thay thế dấu "\" với dấu "/" trong đường dẫn file
191
+ // Replace "\" with "/" in the file path
191
192
  return filePath.replace(/\\/g, "/");
192
193
  }
193
194
 
@@ -199,12 +200,12 @@ class FileManager {
199
200
  if (entry.isDirectory()) {
200
201
  traverseDirectory(fullPath);
201
202
  } else if (entry.isFile()) {
202
- // skip các file trong thư mục bị ignore
203
+ // skip files in ignored folders
203
204
  if (ignoreFolders.some((folder) => fullPath.includes(folder))) {
204
205
  return;
205
206
  }
206
207
 
207
- // chỉ hổ trợ các files nằm trong ./src
208
+ // only support files located in ./src
208
209
  if (!fullPath.includes("/src/") && !fullPath.includes("\\src\\")) {
209
210
  return;
210
211
  }
@@ -321,10 +322,6 @@ class FileManager {
321
322
  }
322
323
  }
323
324
 
324
- interface FileContent {
325
- // Define the structure of FileContent if needed
326
- }
327
-
328
325
  class RoomState {
329
326
  private roomId: string;
330
327
  private state: RoomStateData;
@@ -365,7 +362,9 @@ class RoomState {
365
362
  this.ws.binaryType = "arraybuffer";
366
363
 
367
364
  this.ws.on("open", () => {
368
- console.log(chalk.green(`Connected to room: ${this.roomId}`));
365
+ if (this.reconnectAttempts > 0) {
366
+ console.log(chalk.greenBright(`\n✨ Sync connection restored for session: ${this.roomId}`));
367
+ }
369
368
  this.isConnected = true;
370
369
  this.reconnectAttempts = 0;
371
370
  onConnected?.(); // Call the onConnected callback if provided
@@ -397,7 +396,7 @@ class RoomState {
397
396
  } else if (Buffer.isBuffer(data)) {
398
397
  jsonStr = pako.inflate(data, { to: "string" });
399
398
  } else {
400
- jsonStr = data.toString(); // Fallback nếu không nén
399
+ jsonStr = data.toString(); // Fallback if not compressed
401
400
  }
402
401
  const parsedData = JSON.parse(jsonStr);
403
402
  const includes = [
@@ -459,8 +458,8 @@ class RoomState {
459
458
  type: "update",
460
459
  state: { [key]: value },
461
460
  });
462
- const compressed = pako.deflate(message); // Nén dữ liệu
463
- this.ws.send(compressed); // Gửi binary
461
+ const compressed = pako.deflate(message); // Compress state data
462
+ this.ws.send(compressed); // Send binary payload
464
463
  }
465
464
  }
466
465
 
@@ -483,142 +482,103 @@ class RoomState {
483
482
  }
484
483
  }
485
484
 
486
- program.version("0.0.29").description("Hapico CLI for project management");
485
+ program.version("0.0.31").description("Hapico CLI for project management");
487
486
 
488
487
  program
489
488
  .command("clone <id>")
490
489
  .description("Clone a project by ID")
491
490
  .action(async (id: string) => {
491
+ console.clear();
492
+ console.log(chalk.bold.blue("\n📦 HAPICO CLONE PIPELINE"));
493
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
494
+
492
495
  const { accessToken } = getStoredToken();
493
496
  if (!accessToken) {
494
- console.error(
495
- chalk.red("✗ You need to login first. Use 'hapico login' command.")
496
- );
497
+ console.log(chalk.red("✗ Authentication required. Please run 'hapico login' first."));
497
498
  return;
498
499
  }
499
- const projectDir: string = path.resolve(process.cwd(), id);
500
- if (fs.existsSync(projectDir)) {
501
- console.error(chalk.red(`✗ Project directory "${id}" already exists.`));
500
+
501
+ const outputDir = path.resolve(process.cwd(), id);
502
+ if (fs.existsSync(outputDir)) {
503
+ console.log(chalk.red(`✗ Destination directory "${id}" already exists.`));
502
504
  return;
503
505
  }
504
506
 
505
- let files: FileContent[] = [];
506
- const apiSpinner: Ora = ora(chalk.blue("Fetching project data...")).start();
507
+ const statusSpinner = ora(chalk.blue("Fetching project metadata...")).start();
507
508
 
508
509
  try {
509
- // ===== 1. Lấy version public/published (giữ nguyên như cũ) =====
510
- const response: ApiResponse = await axios.get(
511
- `https://base.myworkbeast.com/api/views/${id}`
512
- );
510
+ // 1. Fetch metadata and initial files
511
+ const response: ApiResponse = await axios.get(`https://base.myworkbeast.com/api/views/${id}`);
512
+ const projectType = response.data.type || "view";
513
+
513
514
  const code = response?.data?.code;
514
515
  const decompressedCode = pako.inflate(
515
516
  Uint8Array.from(atob(code), (c) => c.charCodeAt(0)),
516
517
  { to: "string" }
517
518
  );
518
- files = tryJSONParse(decompressedCode)?.files || [];
519
- apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
520
-
521
- // ===== 2. Download + extract template =====
522
- const templateSpinner: Ora = ora(
523
- chalk.blue("Downloading template...")
524
- ).start();
525
- const TEMPLATE_URL: string =
526
- "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
527
- const templateResponse = await axios.get(TEMPLATE_URL, {
528
- responseType: "arraybuffer",
529
- });
530
- templateSpinner.succeed(chalk.green("Template downloaded successfully!"));
531
-
532
- const outputDir: string = path.resolve(process.cwd(), id);
533
- if (!fs.existsSync(outputDir)) {
534
- fs.mkdirSync(outputDir);
535
- }
536
-
537
- const unzipSpinner: Ora = ora(
538
- chalk.blue("Extracting template...")
539
- ).start();
519
+ const publicFiles = tryJSONParse(decompressedCode)?.files || [];
520
+
521
+ statusSpinner.succeed(chalk.green("Project metadata retrieved."));
522
+ console.log(chalk.bold.white("Project Details:"));
523
+ console.log(`${chalk.gray("├─")} ${chalk.bold("ID :")} ${chalk.cyan(id)}`);
524
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Type :")} ${chalk.magenta(projectType.toUpperCase())}\n`);
525
+
526
+ // 2. Setup Template
527
+ statusSpinner.start(chalk.blue("Initializing project template..."));
528
+ const TEMPLATE_URL = "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
529
+ const templateResponse = await axios.get(TEMPLATE_URL, { responseType: "arraybuffer" });
530
+
531
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
532
+
540
533
  await unzipper.Open.buffer(templateResponse.data).then((directory) =>
541
534
  directory.extract({ path: outputDir })
542
535
  );
543
- unzipSpinner.succeed(chalk.green("Template extracted successfully!"));
544
-
545
- const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
546
- if (fs.existsSync(macosxDir)) {
547
- fs.rmSync(macosxDir, { recursive: true, force: true });
548
- }
549
536
 
550
- // Save project ID
537
+ const macosxDir = path.join(outputDir, "__MACOSX");
538
+ if (fs.existsSync(macosxDir)) fs.rmSync(macosxDir, { recursive: true, force: true });
539
+
551
540
  saveProjectId(outputDir, id);
552
- console.log(chalk.green("Project cloned successfully!"));
553
-
554
- // ===== 3. Save file từ version public =====
555
- const saveSpinner: Ora = ora(
556
- chalk.blue("Saving project files...")
557
- ).start();
558
- files.forEach((file: FileContent) => {
559
- const filePath: string = path.join(process.cwd(), id, "src", file.path);
560
- const dir: string = path.dirname(filePath);
541
+ statusSpinner.succeed(chalk.green("Base template initialized."));
561
542
 
562
- if (!fs.existsSync(dir)) {
563
- fs.mkdirSync(dir, { recursive: true });
564
- }
565
-
566
- fs.writeFileSync(filePath, file.content);
567
- });
568
- saveSpinner.succeed(chalk.green("Project files saved successfully!"));
569
-
570
- // ===== 4. TỰ ĐỘNG PULL PHIÊN BẢN MỚI NHẤT (draft) – ĐÂY LÀ PHẦN MỚI =====
571
- const pullSpinner: Ora = ora(
572
- chalk.blue("Pulling latest changes from server...")
573
- ).start();
543
+ // 3. Sync Source Files
544
+ statusSpinner.start(chalk.blue("Syncing source files..."));
545
+ const srcPath = path.join(outputDir, "src");
546
+ const fileManager = new FileManager(srcPath);
547
+ fileManager.syncFiles(publicFiles);
548
+ statusSpinner.succeed(chalk.green(`${publicFiles.length} files synchronized from production.`));
574
549
 
550
+ // 4. Pull Latest Draft (v3)
551
+ statusSpinner.start(chalk.blue("Checking for latest development changes..."));
575
552
  try {
576
- const pullResponse = await axios.get(
577
- `https://base.myworkbeast.com/api/views/v3/${id}`,
578
- {
579
- headers: {
580
- Authorization: `Bearer ${accessToken}`,
581
- "Content-Type": "application/json",
582
- },
583
- }
584
- );
553
+ const pullResponse = await axios.get(`https://base.myworkbeast.com/api/views/v3/${id}`, {
554
+ headers: { Authorization: `Bearer ${accessToken}` },
555
+ });
585
556
 
586
557
  const pullCode = pullResponse?.data?.code;
587
- if (!pullCode) {
588
- pullSpinner.info(
589
- chalk.cyan("No draft version found – using published version.")
590
- );
591
- } else {
558
+ if (pullCode) {
592
559
  const pullDecompressed = pako.inflate(
593
560
  Uint8Array.from(atob(pullCode), (c) => c.charCodeAt(0)),
594
561
  { to: "string" }
595
562
  );
596
- const latestFiles: FileContent[] =
597
- tryJSONParse(pullDecompressed)?.files || [];
598
-
599
- const fileManager = new FileManager(path.join(outputDir, "src"));
563
+ const latestFiles = tryJSONParse(pullDecompressed)?.files || [];
600
564
  fileManager.syncFiles(latestFiles);
601
-
602
- pullSpinner.succeed(
603
- chalk.green(" Latest changes pulled successfully!")
604
- );
565
+ statusSpinner.succeed(chalk.green("Latest development changes integrated."));
566
+ } else {
567
+ statusSpinner.info(chalk.gray("No draft version found, using published code."));
605
568
  }
606
- } catch (pullErr: any) {
607
- pullSpinner.warn(
608
- chalk.yellow(
609
- `⚠ Could not pull latest changes: ${pullErr.message}\n Run "hapico pull" manually later.`
610
- )
611
- );
569
+ } catch (e) {
570
+ statusSpinner.warn(chalk.yellow("Draft sync skipped (using production code)."));
612
571
  }
613
572
 
614
- // ===== 5. Hướng dẫn chạy dev =====
615
- console.log(
616
- chalk.cyan(
617
- `💡 Run ${chalk.bold("cd ${id} && npm install && hapico dev")} to start the project.`
618
- )
619
- );
573
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
574
+ console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Project cloned successfully!\n")}`);
575
+ console.log(chalk.bold.white("Next steps:"));
576
+ console.log(`${chalk.gray("1.")} ${chalk.cyan(`cd ${id}`)}`);
577
+ console.log(`${chalk.gray("2.")} ${chalk.cyan("npm install")}`);
578
+ console.log(`${chalk.gray("3.")} ${chalk.cyan("hapico dev")}\n`);
579
+
620
580
  } catch (error: any) {
621
- apiSpinner.fail(chalk.red(`Error cloning project: ${error.message}`));
581
+ statusSpinner.fail(chalk.red(`Clone failed: ${error.message}`));
622
582
  }
623
583
  });
624
584
 
@@ -634,7 +594,7 @@ program
634
594
  .command("dev")
635
595
  .description("Start the project in development mode")
636
596
  .option("--zversion <version>", "Zalo version for QR code")
637
- .action((options) => {
597
+ .action(async (options) => {
638
598
  const { accessToken } = getStoredToken();
639
599
  if (!accessToken) {
640
600
  console.error(
@@ -642,86 +602,78 @@ program
642
602
  );
643
603
  return;
644
604
  }
645
- const devSpinner = ora(
646
- chalk.blue("Starting the project in development mode...")
647
- ).start();
605
+
606
+ console.clear();
607
+ console.log(chalk.bold.cyan("\n🚀 HAPICO DEVELOPMENT SERVER"));
608
+ console.log(chalk.gray("─────────────────────────────────────────"));
609
+
610
+ const devSpinner = ora(chalk.blue("Initializing environment...")).start();
648
611
  const pwd = process.cwd();
649
612
  const srcDir = path.join(pwd, "src");
613
+
650
614
  if (!fs.existsSync(srcDir)) {
651
615
  devSpinner.fail(
652
- chalk.red(
653
- "✗ Source directory 'src' does not exist. Please clone a project first."
654
- )
616
+ chalk.red("Source directory 'src' not found. Ensure you are in a valid project folder.")
655
617
  );
656
618
  return;
657
619
  }
658
620
 
659
- // Directory to store session config
660
621
  const tmpDir = path.join(pwd, ".tmp");
661
622
  const sessionConfigFile = path.join(tmpDir, "config.json");
623
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
662
624
 
663
- // Ensure .tmp directory exists
664
- if (!fs.existsSync(tmpDir)) {
665
- fs.mkdirSync(tmpDir, { recursive: true });
666
- }
667
-
668
- // Function to get stored session ID
669
625
  const getStoredSessionId = () => {
670
626
  if (fs.existsSync(sessionConfigFile)) {
671
- const data = fs.readFileSync(sessionConfigFile, { encoding: "utf8" });
672
- const json = JSON.parse(data);
673
- return json.sessionId || null;
627
+ try {
628
+ return JSON.parse(fs.readFileSync(sessionConfigFile, "utf8")).sessionId;
629
+ } catch { return null; }
674
630
  }
675
631
  return null;
676
632
  };
677
633
 
678
- // Function to save session ID
679
- const saveSessionId = (sessionId: string) => {
680
- fs.writeFileSync(
681
- sessionConfigFile,
682
- JSON.stringify({ sessionId }, null, 2),
683
- { encoding: "utf8" }
684
- );
685
- };
686
-
687
- // Get or generate session ID
688
- let sessionId = getStoredSessionId();
689
- if (!sessionId) {
690
- sessionId = randomUUID();
691
- saveSessionId(sessionId);
692
- }
634
+ let sessionId = getStoredSessionId() || randomUUID();
635
+ fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2));
693
636
 
637
+ const projectConfig = getStoredProjectId(pwd);
694
638
  const info = JSON.stringify({
695
639
  id: sessionId,
696
640
  createdAt: new Date().toISOString(),
697
- viewId: getStoredProjectId(pwd)?.projectId,
641
+ viewId: projectConfig?.projectId,
698
642
  });
699
643
 
700
- // Convert info to base64
701
644
  const projectId = Buffer.from(info).toString("base64");
702
- if (!projectId) {
703
- devSpinner.fail(
704
- chalk.red(
705
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
706
- )
707
- );
645
+ if (!projectConfig?.projectId) {
646
+ devSpinner.fail(chalk.red("Project ID missing in hapico.config.json"));
708
647
  return;
709
648
  }
710
649
 
711
- console.log(chalk.cyan("🔗 Connecting to WebSocket server"));
712
- const room = new RoomState(`view_${projectId}`, []);
650
+ devSpinner.text = chalk.blue("Scanning project directory...");
713
651
  const fileManager = new FileManager(srcDir);
714
652
  const initialFiles = fileManager.listFiles();
715
- // get tsconfig.json
716
- const tsconfigPath = path.join(srcDir, "..", "tsconfig.json");
717
- if (fs.existsSync(tsconfigPath)) {
718
- const content = fs.readFileSync(tsconfigPath, { encoding: "utf8" });
719
- initialFiles.push({
720
- path: "./tsconfig.json",
721
- content,
722
- });
723
- }
724
- // Remove All binary files
653
+
654
+ // Supported files outside src
655
+ const SUPPORT_FILES = [
656
+ "./.env",
657
+ "./.env.local",
658
+ "./.env.development",
659
+ "./.env.production",
660
+ "./package.json",
661
+ "./tsconfig.json",
662
+ "./nativewind.json"
663
+ ];
664
+
665
+ SUPPORT_FILES.forEach((relativePath) => {
666
+ const fullPath = path.join(pwd, relativePath);
667
+ if (fs.existsSync(fullPath)) {
668
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
669
+ initialFiles.push({
670
+ path: relativePath,
671
+ content,
672
+ es5: compileES5(content, fullPath) ?? "",
673
+ });
674
+ }
675
+ });
676
+
725
677
  const supportExtensions = [
726
678
  ".ts",
727
679
  ".tsx",
@@ -734,144 +686,137 @@ program
734
686
  ".env.development",
735
687
  ".env.production",
736
688
  ];
737
- const filteredFiles = initialFiles.filter((file) => {
738
- return supportExtensions.some((ext) => file.path.endsWith(ext));
739
- });
689
+ const filteredFiles = initialFiles.filter((file) =>
690
+ supportExtensions.some((ext) => file.path.endsWith(ext))
691
+ );
740
692
 
693
+ devSpinner.text = chalk.blue("Establishing WebSocket connection...");
694
+ const room = new RoomState(`view_${projectId}`, []);
741
695
  room.files = filteredFiles;
742
696
 
743
697
  room.connect(async () => {
744
- devSpinner.succeed(chalk.green("Project started in development mode!"));
698
+ devSpinner.succeed(chalk.greenBright("Sync Engine Ready"));
699
+
700
+ console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Connection established with workspace.\n")}`);
701
+
702
+ console.log(chalk.bold.white("Workspace Configuration:"));
703
+ console.log(`${chalk.gray("├─")} ${chalk.bold("Project ID :")} ${chalk.cyan(projectConfig.projectId)}`);
704
+ console.log(`${chalk.gray("├─")} ${chalk.bold("Session :")} ${chalk.gray(sessionId)}`);
705
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Local Path :")} ${chalk.gray(pwd)}`);
706
+ console.log("");
745
707
 
746
708
  room.updateState("view", filteredFiles);
747
709
 
748
- // Debounce the state update to avoid overwhelming the server while typing/saving
749
- const debouncedUpdate = debounce((updatedFiles: FileContent[]) => {
750
- room.updateState("view", updatedFiles);
751
- }, 200);
752
-
753
- fileManager.setOnFileChange((filePath, content) => {
754
- const es5 = compileES5(content, filePath) ?? "";
755
- console.log(
756
- chalk.yellow(`📝 File changed: ${filePath?.replace(srcDir, ".")}`)
757
- );
758
- const updatedFiles = room.files.map((file) => {
759
- if (path.join(srcDir, file.path) === filePath) {
760
- return { ...file, content, es5 };
761
- }
762
- return file;
710
+ try {
711
+ const project = await axios.get(`https://base.myworkbeast.com/api/views/${projectConfig.projectId}`, {
712
+ headers: { Authorization: `Bearer ${accessToken}` }
763
713
  });
764
- room.files = updatedFiles;
765
- debouncedUpdate(updatedFiles);
766
- });
767
714
 
768
- // Fetch project info
769
- const store = getStoredProjectId(pwd);
770
- if (!store.projectId) {
771
- console.error(
772
- chalk.red(
773
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
774
- )
775
- );
776
- return;
777
- }
778
- const project = await axios.get(
779
- `https://base.myworkbeast.com/api/views/${store.projectId}`,
780
- {
781
- headers: {
782
- Authorization: `Bearer ${accessToken}`,
783
- "Content-Type": "application/json",
784
- },
785
- }
786
- );
715
+ const projectType = project.data.type || "view";
716
+ const isExpo = projectType === "expo_app" || projectType === "emg_edu_lesson";
787
717
 
788
- if (project.status !== 200) {
789
- console.error(
790
- chalk.red(`✗ Error fetching project info: ${project.statusText}`)
791
- );
792
- return;
793
- }
718
+ console.log(chalk.bold.white("Environment Status:"));
719
+ console.log(`${chalk.gray("├─")} ${chalk.bold("Type :")} ${chalk.magenta(projectType.toUpperCase())}`);
720
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Watcher :")} ${chalk.greenBright("Active & Synchronizing")}`);
721
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────\n"));
794
722
 
795
- const projectType = project.data.type || "view";
796
- const zversion = options.zversion;
797
-
798
- if (projectType === "expo_app" || projectType === "emg_edu_lesson") {
799
- const BASE_EXPO_LINK = `exp://u.expo.dev/e362c6df-abe8-4503-8723-1362f015d167/group/e5d6b96e-3c78-485a-a170-1d8aa8b2c47e`;
800
- const link = `${BASE_EXPO_LINK}?sessionKey=${projectId}&mode=development`;
801
- QRCode.generate(link, { small: true }, (qrcode) => {
802
- console.log(
803
- chalk.cyan(
804
- "📱 Scan this QR code with Expo Go to connect to the project:"
805
- )
806
- );
807
- console.log(qrcode);
723
+ if (isExpo) {
724
+ try { await compileAndSaveToFolder("src", "nativewind.json"); } catch (e) {}
725
+ }
726
+
727
+ const debouncedNativeWind = debounce(async () => {
728
+ if (isExpo) {
729
+ process.stdout.write(chalk.gray(" [System] Style assets synchronized successfully.\r"));
730
+ try { await compileAndSaveToFolder("src", "nativewind.json"); } catch (e) {}
731
+ }
732
+ }, 500);
733
+
734
+ const debouncedUpdate = debounce((updatedFiles: FileContent[]) => {
735
+ room.updateState("view", updatedFiles);
736
+ }, 200);
737
+
738
+ fileManager.setOnFileChange((filePath, content) => {
739
+ const timestamp = new Date().toLocaleTimeString([], { hour12: false });
740
+ const relativePath = filePath.replace(srcDir, ".");
741
+ console.log(`${chalk.gray(`[${timestamp}]`)} ${chalk.yellow.bold("CHANGE")} ${chalk.white(relativePath)}`);
742
+
743
+ const es5 = compileES5(content, filePath) ?? "";
744
+ const updatedFiles = room.files.map((file) => {
745
+ if (path.join(srcDir, file.path) === filePath) return { ...file, content, es5 };
746
+ return file;
747
+ });
748
+ room.files = updatedFiles;
749
+ debouncedUpdate(updatedFiles);
750
+ if (isExpo) debouncedNativeWind();
808
751
  });
809
- return;
810
- } else if (projectType === "zalominiapp") {
811
- console.log("zversion", zversion);
812
- if (!zversion) {
813
- QRCode.generate(
814
- `https://zalo.me/s/3218692650896662017/player/${projectId}`,
815
- { small: true },
816
- (qrcode) => {
817
- console.log(
818
- chalk.cyan("📱 Scan this QR code to connect to the project:")
819
- );
820
- console.log(qrcode);
821
- }
822
- );
752
+
753
+ // UI Access Section
754
+ if (isExpo) {
755
+ const EXPO_GROUP_ID = "6f033d8b-7ed5-48dc-ab6f-f8348cf6993b";
756
+ const link = `exp://u.expo.dev/e362c6df-abe8-4503-8723-1362f015d167/group/${EXPO_GROUP_ID}?sessionKey=${projectId}&mode=development`;
757
+ console.log(chalk.bold.black.bgCyan(" 📱 EXPO GO ACCESS "));
758
+ QRCode.generate(link, { small: true }, (code) => {
759
+ console.log(chalk.white(code));
760
+ });
761
+ console.log(chalk.cyan(` Link: ${chalk.underline(link)}\n`));
762
+ } else if (projectType === "zalominiapp" || projectType === "zalominiapp2") {
763
+ const zversion = options.zversion;
764
+ const zaloUrl = zversion
765
+ ? `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`
766
+ : `https://zalo.me/s/3218692650896662017/player/${projectId}`;
767
+
768
+ const label = projectType === "zalominiapp2" ? " 🧩 ZALO MINI APP 2.0 " : " 🧩 ZALO MINI APP ";
769
+ console.log(chalk.bold.black.bgBlue(label));
770
+ QRCode.generate(zaloUrl, { small: true }, (code) => {
771
+ console.log(chalk.white(code));
772
+ });
773
+ console.log(chalk.blue(` Link: ${chalk.underline(zaloUrl)}\n`));
823
774
  } else {
824
- QRCode.generate(
825
- `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`,
826
- { small: true },
827
- (qrcode) => {
828
- console.log(
829
- chalk.cyan("📱 Scan this QR code to connect to the project:")
830
- );
831
- console.log(qrcode);
832
- }
833
- );
775
+ const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
776
+ const label = projectType === "web2" ? " 🌐 WEB 2.0 PREVIEW " : " 🌐 BROWSER PREVIEW ";
777
+ console.log(chalk.bold.black.bgGreen(label));
778
+ console.log(chalk.green(` URL: ${chalk.underline(previewUrl)}\n`));
779
+ await open(previewUrl);
834
780
  }
835
- return;
836
- } else {
837
- const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
838
- console.log(
839
- chalk.cyan(
840
- `🌐 Open this URL in your browser to preview the project:\n${previewUrl}`
841
- )
842
- );
843
- await open(previewUrl);
781
+
782
+ console.log(chalk.gray("👀 Monitoring file changes... (Press Ctrl+C to exit)"));
783
+
784
+ } catch (err: any) {
785
+ console.log(chalk.yellow(`\n⚠️ Environment metadata could not be fetched: ${err.message}`));
786
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────\n"));
844
787
  }
845
788
  });
846
789
  });
847
790
 
848
- // Hàm tái sử dụng để push mã nguồn lên server
849
- async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
791
+ interface PushResult {
792
+ success: boolean;
793
+ projectId: string;
794
+ totalSize: number;
795
+ fileCount: number;
796
+ error?: string;
797
+ }
798
+
799
+ // Chuyên nghiệp hóa việc push source code lên server
800
+ async function pushProject(projectId: string): Promise<PushResult> {
850
801
  const data = getStoredToken();
851
802
  const { accessToken } = data || {};
852
803
  if (!accessToken) {
853
- spinner.fail(
854
- chalk.red("✗ You need to login first. Use 'hapico login' command.")
855
- );
856
- return false;
804
+ return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Authentication required. Please run 'hapico login'." };
857
805
  }
806
+
858
807
  const pwd = process.cwd();
859
808
  const srcDir: string = path.join(pwd, "src");
860
809
  if (!fs.existsSync(srcDir)) {
861
- spinner.fail(
862
- chalk.red(
863
- "✗ Source directory 'src' does not exist. Please clone a project first."
864
- )
865
- );
866
- return false;
810
+ return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Source directory 'src' not found. Ensure you are in a valid project folder." };
867
811
  }
812
+
868
813
  const fileManager = new FileManager(srcDir);
869
814
  const files = fileManager.listFiles().filter((file) => {
870
815
  const extname = path.extname(file.path);
871
816
  return supportedExtensions.includes(extname);
872
817
  });
873
818
 
874
- // Supported files
819
+ // Supported extra files at root
875
820
  const SUPPORT_FILES = [
876
821
  "./.env",
877
822
  "./.env.local",
@@ -879,17 +824,12 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
879
824
  "./.env.production",
880
825
  "./package.json",
881
826
  "./tsconfig.json",
827
+ "./nativewind.json"
882
828
  ];
883
829
 
884
- // Include supported files
885
830
  SUPPORT_FILES.forEach((relativePath) => {
886
831
  const fullPath = path.join(pwd, relativePath);
887
832
  if (fs.existsSync(fullPath)) {
888
- console.log(
889
- chalk.green(
890
- `Including ${relativePath} in push for project ${projectId}.`
891
- )
892
- );
893
833
  const content = fs.readFileSync(fullPath, { encoding: "utf8" });
894
834
  files.push({
895
835
  path: relativePath,
@@ -898,24 +838,14 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
898
838
  });
899
839
  }
900
840
  });
901
- // Show ra thông tin dung lượng mã nguồn
902
- const totalSize = files.reduce((acc, file) => acc + file.content.length, 0);
903
- console.log(
904
- chalk.cyan(
905
- `Total source code size for project ${projectId}: ${(totalSize / 1024).toFixed(2)} KB`
906
- )
907
- );
908
- spinner.text = `Pushing project source code to server for project ${projectId}...`;
909
841
 
842
+ const totalSize = files.reduce((acc, file) => acc + file.content.length, 0);
910
843
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
844
+
911
845
  try {
912
846
  await axios.put(
913
847
  apiUrl,
914
- {
915
- code: JSON.stringify({
916
- files,
917
- }),
918
- },
848
+ { code: JSON.stringify({ files }) },
919
849
  {
920
850
  headers: {
921
851
  Authorization: `Bearer ${accessToken}`,
@@ -923,14 +853,29 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
923
853
  },
924
854
  }
925
855
  );
926
- return true;
927
- } catch (error) {
928
- spinner.fail(
929
- chalk.red(
930
- `✗ Error saving project ${projectId}: ${(error as Error).message}`
931
- )
856
+ return { success: true, projectId, totalSize, fileCount: files.length };
857
+ } catch (error: any) {
858
+ return { success: false, projectId, totalSize, fileCount: files.length, error: error.message };
859
+ }
860
+ }
861
+
862
+ async function publishProjectApi(projectId: string): Promise<{ success: boolean; error?: string }> {
863
+ const { accessToken } = getStoredToken();
864
+ const apiUrl = "https://base.myworkbeast.com/api/views/publish/v2";
865
+ try {
866
+ await axios.post(
867
+ apiUrl,
868
+ { view_id: parseInt(projectId, 10) },
869
+ {
870
+ headers: {
871
+ Authorization: `Bearer ${accessToken}`,
872
+ "Content-Type": "application/json",
873
+ },
874
+ }
932
875
  );
933
- return false;
876
+ return { success: true };
877
+ } catch (error: any) {
878
+ return { success: false, error: error.message };
934
879
  }
935
880
  }
936
881
 
@@ -938,116 +883,112 @@ program
938
883
  .command("push")
939
884
  .description("Push the project source code to the server")
940
885
  .action(async () => {
941
- const saveSpinner: Ora = ora(
942
- chalk.blue("Saving project source code...")
943
- ).start();
886
+ console.clear();
887
+ console.log(chalk.bold.cyan("\n🚀 HAPICO PUSH PIPELINE"));
888
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
889
+
944
890
  const pwd = process.cwd();
945
891
  const { projectId, replicate } = getStoredProjectId(pwd);
946
892
 
947
893
  if (!projectId) {
948
- saveSpinner.fail(
949
- chalk.red(
950
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
951
- )
952
- );
894
+ console.log(chalk.red("✗ Project ID not found. Ensure hapico.config.json exists."));
953
895
  return;
954
896
  }
955
897
 
956
- // Push to the main project
957
- const mainSuccess = await pushProject(saveSpinner, projectId);
958
- let allSuccess = mainSuccess;
898
+ console.log(chalk.bold.white("Target Environments:"));
899
+ console.log(`${chalk.gray("├─")} ${chalk.bold("Main Project :")} ${chalk.cyan(projectId)}`);
900
+ if (replicate && replicate.length > 0) {
901
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.cyan(replicate.join(", "))}`);
902
+ } else {
903
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.gray("None")}`);
904
+ }
905
+ console.log("");
906
+
907
+ const pushSpinner = ora(chalk.blue("Syncing source code with main project...")).start();
908
+
909
+ const mainResult = await pushProject(projectId);
910
+ if (mainResult.success) {
911
+ pushSpinner.succeed(chalk.green(`Main Project [${projectId}] code synced successfully.`));
912
+ console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(mainResult.fileCount)}`);
913
+ console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
914
+ } else {
915
+ pushSpinner.fail(chalk.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
916
+ return;
917
+ }
959
918
 
960
- // Push to replicated projects if replicate array exists
961
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
962
- saveSpinner.text = chalk.blue("Pushing to replicated projects...");
919
+ let allSuccess = true;
920
+ if (replicate && replicate.length > 0) {
963
921
  for (const repId of replicate) {
964
- const success = await pushProject(saveSpinner, repId);
965
- if (!success) {
966
- allSuccess = false;
967
- console.warn(
968
- chalk.yellow(
969
- `⚠ Failed to push to replicated project ${repId}. Continuing...`
970
- )
971
- );
922
+ const repSpinner = ora(chalk.blue(`Syncing replica [${repId}]...`)).start();
923
+ const repResult = await pushProject(repId);
924
+ if (repResult.success) {
925
+ repSpinner.succeed(chalk.green(`Replica [${repId}] code synced successfully.`));
926
+ console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(repResult.fileCount)}`);
927
+ console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
972
928
  } else {
973
- console.log(
974
- chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
975
- );
929
+ repSpinner.fail(chalk.red(`Replica [${repId}] sync failed: ${repResult.error}`));
930
+ allSuccess = false;
976
931
  }
977
932
  }
978
933
  }
979
934
 
935
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
980
936
  if (allSuccess) {
981
- saveSpinner.succeed(
982
- chalk.green("Project source code saved successfully to all projects!")
983
- );
937
+ console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("All source codes pushed successfully!\n")}`);
984
938
  } else {
985
- saveSpinner.warn(
986
- chalk.yellow("Project source code saved with some errors.")
987
- );
939
+ console.log(`\n${chalk.bold.white.bgYellow(" WARNING ")} ${chalk.yellow("Push completed with some replica errors.\n")}`);
988
940
  }
989
941
  });
990
942
 
991
943
  program
992
944
  .command("login")
993
- .description("Login to the system")
945
+ .description("Authenticate Hapico CLI with your account")
994
946
  .action(async () => {
995
- console.log(chalk.cyan("🔐 Logging in to the system..."));
996
- const loginSpinner: Ora = ora(chalk.blue("Initiating login...")).start();
947
+ console.clear();
948
+ console.log(chalk.bold.yellow("\n🔐 HAPICO SECURE LOGIN"));
949
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
950
+
951
+ const loginSpinner = ora(chalk.blue("Requesting device authorization...")).start();
997
952
 
998
953
  try {
999
- const response = await axios.post(
1000
- "https://auth.myworkbeast.com/auth/device"
1001
- );
1002
- const { device_code, user_code, verification_url, expires_in, interval } =
1003
- response.data;
954
+ const response = await axios.post("https://auth.myworkbeast.com/auth/device");
955
+ const { device_code, user_code, verification_url, expires_in, interval } = response.data;
1004
956
 
1005
- loginSpinner.succeed(chalk.green("Login initiated!"));
1006
- console.log(
1007
- chalk.cyan(
1008
- `🌐 Please open this URL in your browser: ${verification_url}`
1009
- )
1010
- );
1011
- console.log(chalk.yellow(`🔑 And enter this code: ${user_code}`));
1012
- console.log(chalk.blue("⏳ Waiting for authentication..."));
957
+ loginSpinner.succeed(chalk.green("Authorization request created."));
958
+
959
+ console.log(`\n${chalk.bold.white("ACTION REQUIRED")}`);
960
+ console.log(`${chalk.gray("1.")} Open URL : ${chalk.underline.cyan(verification_url)}`);
961
+ console.log(`${chalk.gray("2.")} Enter Code: ${chalk.bold.bgWhite.black(` ${user_code} `)}`);
962
+ console.log(chalk.gray(`\n(Code expires in ${Math.floor(expires_in / 60)} minutes)`));
1013
963
 
1014
964
  await open(verification_url);
1015
965
 
1016
- const pollSpinner: Ora = ora(
1017
- chalk.blue("Waiting for authentication...")
1018
- ).start();
966
+ const pollSpinner = ora(chalk.blue("Waiting for browser confirmation...")).start();
1019
967
  let tokens = null;
1020
968
  const startTime = Date.now();
969
+
1021
970
  while (Date.now() - startTime < expires_in * 1000) {
1022
971
  try {
1023
- const pollResponse = await axios.post(
1024
- "https://auth.myworkbeast.com/auth/device/poll",
1025
- { device_code }
1026
- );
972
+ const pollResponse = await axios.post("https://auth.myworkbeast.com/auth/device/poll", { device_code });
1027
973
  if (pollResponse.data.accessToken) {
1028
974
  tokens = pollResponse.data;
1029
975
  break;
1030
976
  }
1031
- } catch (error) {
1032
- // Ignore temporary errors and continue polling
1033
- }
977
+ } catch (e) { /* Poll interval logic */ }
1034
978
  await new Promise((resolve) => setTimeout(resolve, interval * 1000));
1035
979
  }
1036
980
 
1037
981
  if (tokens) {
1038
- pollSpinner.succeed(chalk.green("Login successful!"));
1039
982
  saveToken(tokens);
983
+ pollSpinner.succeed(chalk.green("Authentication successful."));
984
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
985
+ console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("You are now logged in.")}`);
986
+ console.log(`${chalk.gray("Token stored at:")} ${chalk.gray(TOKEN_FILE)}\n`);
1040
987
  } else {
1041
- pollSpinner.fail(
1042
- chalk.red(
1043
- "✗ Login failed: Timeout or user did not complete authentication."
1044
- )
1045
- );
988
+ pollSpinner.fail(chalk.red("Login timed out or was cancelled."));
1046
989
  }
1047
- } catch (error) {
1048
- loginSpinner.fail(
1049
- chalk.red(`✗ Login error: ${(error as Error).message}`)
1050
- );
990
+ } catch (error: any) {
991
+ loginSpinner.fail(chalk.red(`Login failed: ${error.message}`));
1051
992
  }
1052
993
  });
1053
994
 
@@ -1116,472 +1057,219 @@ program
1116
1057
  }
1117
1058
  });
1118
1059
 
1119
- // be {{id}} --be --port {{port}}
1120
1060
  program
1121
1061
  .command("fetch <id>")
1122
- .option("--port <port>", "Port to run the backend", "3000")
1123
- .option("--serve", "Flag to indicate serving the backend")
1124
- .option(
1125
- "--libs <libs>",
1126
- "Additional libraries to install (comma-separated)",
1127
- ""
1128
- )
1129
- .option("--be", "Flag to indicate backend")
1130
- .description("Open backend for the project")
1131
- .action(
1132
- async (
1133
- id: string,
1134
- options: { port: string; serve?: boolean; libs?: string; be?: boolean }
1135
- ) => {
1136
- const { accessToken } = getStoredToken();
1137
- if (!accessToken) {
1138
- console.error(
1139
- chalk.red("✗ You need to login first. Use 'hapico login' command.")
1140
- );
1141
- return;
1142
- }
1143
-
1144
- console.log(chalk.cyan(`🌐 PORT = ${options.port}`));
1062
+ .description("Fetch Zalo Mini App template for the project")
1063
+ .action(async (id: string) => {
1064
+ const { accessToken } = getStoredToken();
1065
+ if (!accessToken) {
1066
+ console.error(
1067
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
1068
+ );
1069
+ return;
1070
+ }
1145
1071
 
1146
- // Chọn hỏi user port để vận hành
1147
- let port = 3000;
1072
+ const apiSpinner = ora(chalk.blue("Fetching project data...")).start();
1073
+ try {
1148
1074
  const response: ApiResponse = await axios.get(
1149
1075
  `https://base.myworkbeast.com/api/views/${id}`
1150
1076
  );
1151
- const portInput = options.port;
1152
- if (portInput) {
1153
- const parsedPort = parseInt(portInput, 10);
1154
- if (!isNaN(parsedPort)) {
1155
- port = parsedPort;
1156
- }
1157
- }
1158
1077
 
1159
- const projectDir: string = path.resolve(process.cwd(), id);
1160
- if (!fs.existsSync(projectDir)) {
1161
- // create folder
1162
- fs.mkdirSync(projectDir);
1163
- }
1164
-
1165
- // download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
1166
- const TEMPLATE_URL: string = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
1167
- let templateResponse = undefined;
1168
- try {
1169
- templateResponse = await axios.get(TEMPLATE_URL, {
1170
- responseType: "arraybuffer",
1171
- });
1172
- } catch (error) {
1173
- console.error(
1174
- chalk.red("✗ Error downloading template:"),
1175
- (error as Error).message
1078
+ const projectType = response.data.type;
1079
+ if (projectType !== "zalominiapp" && projectType !== "zalominiapp2") {
1080
+ apiSpinner.fail(
1081
+ chalk.red(
1082
+ `✗ Error: Project type is "${projectType}". Only "zalominiapp" and "zalominiapp2" are supported for fetch command.`
1083
+ )
1176
1084
  );
1177
1085
  return;
1178
1086
  }
1087
+ apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
1179
1088
 
1180
- const outputDir: string = path.resolve(process.cwd(), id);
1181
- if (!fs.existsSync(outputDir)) {
1182
- fs.mkdirSync(outputDir);
1183
- }
1089
+ const outputDir = path.resolve(process.cwd(), id);
1090
+ const zaloDir = path.join(outputDir, "zalo");
1091
+ const backendDir = path.join(outputDir, "backend");
1184
1092
 
1185
- await unzipper.Open.buffer(templateResponse.data).then((directory) =>
1186
- directory.extract({ path: outputDir })
1093
+ const templateSpinner = ora(
1094
+ chalk.blue("Downloading Zalo template...")
1095
+ ).start();
1096
+ const TEMPLATE_URL =
1097
+ "https://hapico.hcm04.vstorage.vngcloud.vn/templates/zalominiapp_v0.zip";
1098
+ const templateResponse = await axios.get(TEMPLATE_URL, {
1099
+ responseType: "arraybuffer",
1100
+ });
1101
+ templateSpinner.succeed(
1102
+ chalk.green("Zalo template downloaded successfully!")
1187
1103
  );
1188
1104
 
1189
- const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
1190
- if (fs.existsSync(macosxDir)) {
1191
- fs.rmSync(macosxDir, { recursive: true, force: true });
1192
- }
1193
-
1194
- // outputPath/src/server.ts dòng (3006) thay thành port
1195
- const serverFile = path.join(process.cwd(), id, "src", "index.ts");
1196
- if (fs.existsSync(serverFile)) {
1197
- let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
1198
- serverContent = serverContent.split("(3006)").join(`(${port})`);
1199
- fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
1200
- }
1201
-
1202
- // lấy danh sách migrations
1203
- const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${
1204
- response.data.dbCode || response.data.db_code
1205
- }&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
1206
-
1207
- // Lấy danh sách các migration đã chạy
1208
- let migrations: Array<{
1209
- id: number;
1210
- created_at: string;
1211
- sql_statement: string;
1212
- }> = [];
1213
- const migrationsResponse = await axios.get(MIGRATIONS_URL, {
1214
- headers: {
1215
- Authorization: `Bearer ${accessToken}`,
1216
- "Content-Type": "application/json",
1217
- "x-db-code": response.data.dbCode || response.data.db_code,
1218
- },
1105
+ const backendSpinner = ora(
1106
+ chalk.blue("Downloading backend template...")
1107
+ ).start();
1108
+ const BACKEND_URL =
1109
+ "https://hapico.hcm04.vstorage.vngcloud.vn/templates/backend_v0.zip";
1110
+ const backendResponse = await axios.get(BACKEND_URL, {
1111
+ responseType: "arraybuffer",
1219
1112
  });
1220
- if (migrationsResponse.status === 200) {
1221
- migrations = (migrationsResponse.data.data?.rows || []).map(
1222
- (item: any) => ({
1223
- created_at: item.created_at,
1224
- sql: item.sql_statement,
1225
- })
1226
- );
1227
- }
1228
-
1229
- console.log(chalk.cyan(`🔍 Found ${migrations.length} migrations.`));
1113
+ backendSpinner.succeed(
1114
+ chalk.green("Backend template downloaded successfully!")
1115
+ );
1230
1116
 
1231
- // Tạo thư mục migrations nếu chưa tồn tại
1232
- const migrationsDir = path.join(process.cwd(), id, "migrations");
1233
- if (!fs.existsSync(migrationsDir)) {
1234
- fs.mkdirSync(migrationsDir);
1117
+ if (!fs.existsSync(outputDir)) {
1118
+ fs.mkdirSync(outputDir, { recursive: true });
1235
1119
  }
1120
+ if (!fs.existsSync(zaloDir)) fs.mkdirSync(zaloDir, { recursive: true });
1121
+ if (!fs.existsSync(backendDir))
1122
+ fs.mkdirSync(backendDir, { recursive: true });
1236
1123
 
1237
- // Lưu từng migration vào file
1238
- migrations.forEach((migration, index) => {
1239
- const timestamp = new Date(migration.created_at)
1240
- .toISOString()
1241
- .replace(/[-:]/g, "")
1242
- .replace(/\..+/, "");
1243
- const filename = path.join(
1244
- migrationsDir,
1245
- `${index + 1}_migration_${timestamp}.sql`
1246
- );
1247
- fs.writeFileSync(filename, (migration as any).sql, {
1248
- encoding: "utf8",
1249
- });
1250
- });
1251
-
1252
- console.log(chalk.green("✓ Migrations saved successfully!"));
1253
-
1254
- // Download project files and save to project
1255
- let files: FileContent[] = [];
1256
- const apiSpinner: Ora = ora(
1257
- chalk.blue("Fetching project data...")
1124
+ const unzipZaloSpinner = ora(
1125
+ chalk.blue("Extracting Zalo template...")
1258
1126
  ).start();
1127
+ await unzipper.Open.buffer(templateResponse.data).then((directory) =>
1128
+ directory.extract({ path: zaloDir })
1129
+ );
1130
+ unzipZaloSpinner.succeed(
1131
+ chalk.green("Zalo template extracted successfully!")
1132
+ );
1259
1133
 
1260
- try {
1261
- apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
1262
-
1263
- const saveSpinner: Ora = ora(
1264
- chalk.blue("Saving project files...")
1265
- ).start();
1266
- files.forEach((file: FileContent) => {
1267
- // skip ./app, ./components
1268
- if (
1269
- file.path.startsWith("./app") ||
1270
- file.path.startsWith("./components")
1271
- ) {
1272
- return;
1273
- }
1274
- // hoặc các file có đuôi là .tsx
1275
- if (file.path.endsWith(".tsx")) {
1276
- return;
1277
- }
1278
- try {
1279
- if (file.content.trim().length == 0) return;
1280
- const filePath: string = path.join(
1281
- process.cwd(),
1282
- id,
1283
- "src",
1284
- file.path
1285
- );
1286
- const dir: string = path.dirname(filePath);
1287
-
1288
- if (!fs.existsSync(dir)) {
1289
- fs.mkdirSync(dir, { recursive: true });
1290
- }
1291
-
1292
- fs.writeFileSync(filePath, file.content);
1293
- } catch (error) {
1294
- console.log(chalk.red("✗ Error writing file"), file.path, error);
1295
- }
1296
- });
1297
- saveSpinner.succeed(chalk.green("Project files saved successfully!"));
1298
- } catch (error: any) {
1299
- apiSpinner.fail(chalk.red(`✗ Error cloning project: ${error.message}`));
1300
- }
1301
-
1302
- // Download project files and save to project
1303
- console.log(chalk.green("✓ Project cloned successfully!"));
1304
-
1305
- // Save project ID to hapico.config.json
1306
- saveProjectId(outputDir, id);
1307
-
1308
- console.log(chalk.green("✓ Project setup successfully!"));
1309
-
1310
- let serveProcess: any = null;
1134
+ const unzipBackendSpinner = ora(
1135
+ chalk.blue("Extracting backend template...")
1136
+ ).start();
1137
+ await unzipper.Open.buffer(backendResponse.data).then((directory) =>
1138
+ directory.extract({ path: backendDir })
1139
+ );
1140
+ unzipBackendSpinner.succeed(
1141
+ chalk.green("Backend template extracted successfully!")
1142
+ );
1311
1143
 
1312
- if (options.serve) {
1313
- // Run bun install
1314
- const { exec } = require("child_process");
1315
- const installSpinner: Ora = ora(
1316
- chalk.blue("Installing dependencies...")
1317
- ).start();
1144
+ [zaloDir, backendDir].forEach((dir) => {
1145
+ const macosxDir = path.join(dir, "__MACOSX");
1146
+ if (fs.existsSync(macosxDir)) {
1147
+ fs.rmSync(macosxDir, { recursive: true, force: true });
1148
+ }
1318
1149
 
1319
- // create a loop to check if there is new version of project
1320
- // https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
1321
- let currentVersionStr = fs.existsSync(
1322
- path.join(outputDir, "version.json")
1323
- )
1324
- ? JSON.parse(
1325
- fs.readFileSync(path.join(outputDir, "version.json"), {
1326
- encoding: "utf8",
1327
- })
1328
- ).version
1329
- : "0.0.1";
1330
-
1331
- const checkVersion = async () => {
1332
- try {
1333
- // check if file version.json exists
1334
- const flyCheck = await axios.head(
1335
- `https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
1336
- );
1337
- if (flyCheck.status !== 200) {
1338
- return;
1339
- }
1340
- // get file version.json
1341
- const versionResponse = await axios.get(
1342
- `https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
1343
- );
1344
- const latestVersionData = versionResponse.data;
1345
- const latestVersion = latestVersionData.version;
1346
- if (latestVersion !== currentVersionStr) {
1347
- console.log(
1348
- chalk.yellow(`📦 New version available: ${latestVersion}`)
1349
- );
1350
- // Save new version.json
1351
- fs.writeFileSync(
1352
- path.join(outputDir, "version.json"),
1353
- JSON.stringify(latestVersionData, null, 2),
1354
- { encoding: "utf8" }
1355
- );
1356
- // Install external libraries if any
1357
- if (
1358
- latestVersionData.external_libraries &&
1359
- latestVersionData.external_libraries.length > 0
1360
- ) {
1361
- console.log(chalk.blue("Installing new external libraries..."));
1362
- for (const lib of latestVersionData.external_libraries) {
1363
- try {
1364
- await execPromise(`bun add ${lib}`, { cwd: outputDir });
1365
- console.log(chalk.green(`Added ${lib}`));
1366
- } catch (addError) {
1367
- console.error(
1368
- chalk.red(`✗ Error adding ${lib}:`),
1369
- (addError as Error).message
1370
- );
1371
- }
1150
+ // Replace {{APP_ID}} in .ts and .tsx files
1151
+ const traverseAndReplace = (currentDir: string) => {
1152
+ fs.readdirSync(currentDir, { withFileTypes: true }).forEach((entry) => {
1153
+ const fullPath = path.join(currentDir, entry.name);
1154
+ if (entry.isDirectory()) {
1155
+ traverseAndReplace(fullPath);
1156
+ } else if (entry.isFile()) {
1157
+ const ext = path.extname(fullPath);
1158
+ if (ext === ".ts" || ext === ".tsx") {
1159
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
1160
+ if (content.includes("{{APP_ID}}")) {
1161
+ const updatedContent = content.replace(/{{APP_ID}}/g, id);
1162
+ fs.writeFileSync(fullPath, updatedContent, {
1163
+ encoding: "utf8",
1164
+ });
1372
1165
  }
1373
1166
  }
1374
- // Rerun bun install
1375
- try {
1376
- await execPromise("bun install", { cwd: outputDir });
1377
- console.log(
1378
- chalk.green("Dependencies reinstalled successfully!")
1379
- );
1380
- } catch (installError) {
1381
- console.error(
1382
- chalk.red("✗ Error reinstalling dependencies:"),
1383
- (installError as Error).message
1384
- );
1385
- }
1386
- // Restart the process
1387
- if (serveProcess && !serveProcess.killed) {
1388
- console.log(chalk.blue("Restarting backend..."));
1389
- serveProcess.kill("SIGTERM");
1390
- }
1391
- // Start new process
1392
- const newServeProcess = exec("bun run start", { cwd: outputDir });
1393
- newServeProcess.stdout?.on("data", (data: any) => {
1394
- process.stdout.write(data);
1395
- });
1396
- newServeProcess.stderr?.on("data", (data: any) => {
1397
- process.stderr.write(data);
1398
- });
1399
- newServeProcess.on("close", (code: any) => {
1400
- console.log(
1401
- chalk.yellow(`⚠ backend exited with code ${code}`)
1402
- );
1403
- });
1404
- serveProcess = newServeProcess;
1405
- currentVersionStr = latestVersion;
1406
1167
  }
1407
- } catch (error) {
1408
- // If the remote version.json does not exist, do not update
1409
- console.error(
1410
- chalk.yellow("⚠ Error checking for updates (skipping update):"),
1411
- (error as Error).message
1412
- );
1413
- }
1168
+ });
1414
1169
  };
1170
+ traverseAndReplace(dir);
1171
+ });
1415
1172
 
1416
- // check every 3 seconds
1417
- setInterval(async () => await checkVersion(), 10 * 1000);
1418
-
1419
- exec(
1420
- "bun install",
1421
- { cwd: outputDir },
1422
- async (error: any, stdout: any, stderr: any) => {
1423
- if (error) {
1424
- installSpinner.fail(
1425
- chalk.red(`✗ Error installing dependencies: ${error.message}`)
1426
- );
1427
- return;
1428
- }
1429
- if (stderr) {
1430
- console.error(chalk.red(`stderr: ${stderr}`));
1431
- }
1432
- installSpinner.succeed(
1433
- chalk.green("Dependencies installed successfully!")
1434
- );
1435
-
1436
- // Install additional libraries if --libs is provided
1437
- if (options.libs && options.libs.trim()) {
1438
- const libsSpinner: Ora = ora(
1439
- chalk.blue("Installing additional libraries...")
1440
- ).start();
1441
- const additionalLibs = options.libs
1442
- .split(",")
1443
- .map((lib) => lib.trim())
1444
- .filter((lib) => lib);
1445
- for (const lib of additionalLibs) {
1446
- try {
1447
- await execPromise(`bun add ${lib}`, { cwd: outputDir });
1448
- console.log(chalk.green(`Added ${lib}`));
1449
- } catch (addError) {
1450
- console.error(
1451
- chalk.red(`✗ Error adding ${lib}:`),
1452
- (addError as Error).message
1453
- );
1454
- }
1455
- }
1456
- libsSpinner.succeed(
1457
- chalk.green("Additional libraries installed successfully!")
1458
- );
1459
- }
1173
+ // Save project ID
1174
+ saveProjectId(outputDir, id);
1460
1175
 
1461
- // Run cd ${id} && bun run serve-be
1462
- console.log(chalk.blue("🚀 Starting backend"));
1463
- serveProcess = exec("bun run start", { cwd: outputDir });
1464
- serveProcess.stdout.on("data", (data: any) => {
1465
- process.stdout.write(data);
1466
- });
1467
- serveProcess.stderr.on("data", (data: any) => {
1468
- process.stderr.write(data);
1469
- });
1470
- serveProcess.on("close", (code: any) => {
1471
- console.log(chalk.yellow(`⚠ backend exited with code ${code}`));
1472
- });
1473
- }
1474
- );
1475
- }
1176
+ console.log(
1177
+ chalk.green(`✓ Project "${id}" fetched and setup successfully!`)
1178
+ );
1179
+ } catch (error: any) {
1180
+ apiSpinner.fail(chalk.red(`✗ Error: ${error.message}`));
1476
1181
  }
1477
- );
1182
+ });
1478
1183
 
1479
1184
  // hapico publish
1480
- program.command("publish").action(async () => {
1481
- const publishSpinner: Ora = ora(
1482
- chalk.blue("Publishing to Hapico...")
1483
- ).start();
1484
- const pwd = process.cwd();
1485
- const { projectId, replicate } = getStoredProjectId(pwd);
1185
+ program
1186
+ .command("publish")
1187
+ .description("Publish the project to production environment")
1188
+ .action(async () => {
1189
+ console.clear();
1190
+ console.log(chalk.bold.magenta("\n🚀 HAPICO PUBLISH PIPELINE"));
1191
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
1192
+
1193
+ const pwd = process.cwd();
1194
+ const { projectId, replicate } = getStoredProjectId(pwd);
1486
1195
 
1487
- if (!projectId) {
1488
- publishSpinner.fail(
1489
- chalk.red(
1490
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
1491
- )
1492
- );
1493
- return;
1494
- }
1196
+ if (!projectId) {
1197
+ console.log(chalk.red("✗ Project ID not found. Ensure hapico.config.json exists."));
1198
+ return;
1199
+ }
1495
1200
 
1496
- // Step 1: Push source code to main project and replicas
1497
- const pushSuccess = await pushProject(publishSpinner, projectId);
1498
- if (!pushSuccess) {
1499
- return; // Stop if push to main project fails
1500
- }
1201
+ console.log(chalk.bold.white("Target Environments:"));
1202
+ console.log(`${chalk.gray("├─")} ${chalk.bold("Main Project :")} ${chalk.magenta(projectId)}`);
1203
+ if (replicate && replicate.length > 0) {
1204
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.magenta(replicate.join(", "))}`);
1205
+ } else {
1206
+ console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.gray("None")}`);
1207
+ }
1208
+ console.log("");
1209
+
1210
+ const pushSpinner = ora(chalk.blue("Phase 1/2: Syncing source code with main project...")).start();
1211
+
1212
+ // Step 1: Push Main
1213
+ const mainResult = await pushProject(projectId);
1214
+ if (mainResult.success) {
1215
+ pushSpinner.succeed(chalk.green(`Main Project [${projectId}] code synced successfully.`));
1216
+ console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(mainResult.fileCount)}`);
1217
+ console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
1218
+ } else {
1219
+ pushSpinner.fail(chalk.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
1220
+ return; // Stop if push to main fails
1221
+ }
1501
1222
 
1502
- // Push to replicated projects
1503
- let allPushSuccess = true;
1504
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1505
- publishSpinner.text = chalk.blue("Pushing to replicated projects...");
1506
- for (const repId of replicate) {
1507
- const success = await pushProject(publishSpinner, repId);
1508
- if (!success) {
1509
- allPushSuccess = false;
1510
- console.warn(
1511
- chalk.yellow(
1512
- `⚠ Failed to push to replicated project ${repId}. Continuing...`
1513
- )
1514
- );
1515
- } else {
1516
- console.log(
1517
- chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
1518
- );
1223
+ // Push Replicas
1224
+ let allPushSuccess = true;
1225
+ if (replicate && replicate.length > 0) {
1226
+ for (const repId of replicate) {
1227
+ const repSpinner = ora(chalk.blue(`Phase 1/2: Syncing replica code [${repId}]...`)).start();
1228
+ const repResult = await pushProject(repId);
1229
+ if (repResult.success) {
1230
+ repSpinner.succeed(chalk.green(`Replica [${repId}] code synced successfully.`));
1231
+ console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(repResult.fileCount)}`);
1232
+ console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
1233
+ } else {
1234
+ repSpinner.fail(chalk.red(`Replica [${repId}] sync failed: ${repResult.error}`));
1235
+ allPushSuccess = false;
1236
+ }
1519
1237
  }
1520
1238
  }
1521
- }
1522
1239
 
1523
- // Step 2: Publish main project
1524
- const { accessToken } = getStoredToken();
1525
- const apiUrl = "https://base.myworkbeast.com/api/views/publish/v2";
1526
- let allPublishSuccess = true;
1527
- try {
1528
- await axios.post(
1529
- apiUrl,
1530
- { view_id: parseInt(projectId, 10) },
1531
- {
1532
- headers: {
1533
- Authorization: `Bearer ${accessToken}`,
1534
- "Content-Type": "application/json",
1535
- },
1536
- }
1537
- );
1538
- publishSpinner.succeed(
1539
- chalk.green(`Project ${projectId} published successfully!`)
1540
- );
1541
- } catch (error) {
1542
- allPublishSuccess = false;
1543
- publishSpinner.fail(
1544
- chalk.red(
1545
- `✗ Error publishing project ${projectId}: ${(error as Error).message}`
1546
- )
1547
- );
1548
- }
1240
+ // Step 2: Publish Main
1241
+ const publishSpinner = ora(chalk.blue(`Phase 2/2: Publishing main project [${projectId}]...`)).start();
1242
+ const pubMainResult = await publishProjectApi(projectId);
1243
+ let allPublishSuccess = true;
1244
+
1245
+ if (pubMainResult.success) {
1246
+ publishSpinner.succeed(chalk.green(`Main Project [${projectId}] published successfully.`));
1247
+ } else {
1248
+ publishSpinner.fail(chalk.red(`Main Project [${projectId}] publish failed: ${pubMainResult.error}`));
1249
+ allPublishSuccess = false;
1250
+ }
1549
1251
 
1550
- // Step 3: Publish replicated projects
1551
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1552
- publishSpinner.text = chalk.blue("Publishing replicated projects...");
1553
- for (const repId of replicate) {
1554
- try {
1555
- await axios.post(
1556
- apiUrl,
1557
- { view_id: parseInt(repId, 10) },
1558
- {
1559
- headers: {
1560
- Authorization: `Bearer ${accessToken}`,
1561
- "Content-Type": "application/json",
1562
- },
1563
- }
1564
- );
1565
- console.log(
1566
- chalk.green(`✓ Successfully published replicated project ${repId}.`)
1567
- );
1568
- } catch (error) {
1569
- allPublishSuccess = false;
1570
- console.warn(
1571
- chalk.yellow(
1572
- `⚠ Error publishing replicated project ${repId}: ${(error as Error).message}`
1573
- )
1574
- );
1252
+ // Publish Replicas
1253
+ if (replicate && replicate.length > 0) {
1254
+ for (const repId of replicate) {
1255
+ const repPubSpinner = ora(chalk.blue(`Phase 2/2: Publishing replica [${repId}]...`)).start();
1256
+ const pubRepResult = await publishProjectApi(repId);
1257
+ if (pubRepResult.success) {
1258
+ repPubSpinner.succeed(chalk.green(`Replica [${repId}] published successfully.`));
1259
+ } else {
1260
+ repPubSpinner.fail(chalk.red(`Replica [${repId}] publish failed: ${pubRepResult.error}`));
1261
+ allPublishSuccess = false;
1262
+ }
1575
1263
  }
1576
1264
  }
1577
- }
1578
1265
 
1579
- if (allPushSuccess && allPublishSuccess) {
1580
- publishSpinner.succeed(chalk.green("All projects published successfully!"));
1581
- } else {
1582
- publishSpinner.warn(chalk.yellow("Publishing completed with some errors."));
1583
- }
1584
- });
1266
+ console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
1267
+ if (allPushSuccess && allPublishSuccess) {
1268
+ console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Deployment pipeline completed successfully!\n")}`);
1269
+ } else {
1270
+ console.log(`\n${chalk.bold.white.bgYellow(" WARNING ")} ${chalk.yellow("Deployment pipeline completed with some errors.\n")}`);
1271
+ }
1272
+ });
1585
1273
 
1586
1274
  program.command("mirror").action(() => {
1587
1275
  console.log(chalk.cyan("🌐 Starting mirror mode..."));
@@ -1599,7 +1287,7 @@ program.command("mirror").action(() => {
1599
1287
  const fileManager = new FileManager(srcDir);
1600
1288
  const initialFiles = fileManager.listFiles();
1601
1289
 
1602
- // Lấy danh sách file viết ra 1 file .txt
1290
+ // Collect project files and generate a single summary document
1603
1291
  let content = ``;
1604
1292
  map(initialFiles, (file) => {
1605
1293
  content += `\`\`\`typescript