@hapico/cli 0.0.17 → 0.0.18

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
@@ -12,6 +12,13 @@ import * as Babel from "@babel/standalone";
12
12
  import QRCode from "qrcode-terminal";
13
13
  import open from "open";
14
14
  import { randomUUID } from "crypto";
15
+ import inquirer from "inquirer";
16
+ import { exec } from "child_process";
17
+ import { promisify } from "util";
18
+ import chalk from "chalk";
19
+
20
+ // Promisify exec for async usage
21
+ const execPromise = promisify(exec);
15
22
 
16
23
  // Directory to store the token and project config
17
24
  const CONFIG_DIR = path.join(
@@ -20,7 +27,7 @@ const CONFIG_DIR = path.join(
20
27
  );
21
28
  const TOKEN_FILE = path.join(CONFIG_DIR, "auth_token.json");
22
29
 
23
- const connected = ora("Connected to WebSocket server");
30
+ const connected = ora(chalk.cyan("Connected to WebSocket server"));
24
31
 
25
32
  // Ensure config directory exists
26
33
  if (!fs.existsSync(CONFIG_DIR)) {
@@ -89,6 +96,9 @@ interface FileContent {
89
96
  interface ApiResponse {
90
97
  data: {
91
98
  files?: FileContent[];
99
+ type: "view" | "zalominiapp" | string;
100
+ dbCode: string;
101
+ db_code: string;
92
102
  };
93
103
  }
94
104
 
@@ -188,7 +198,9 @@ class FileManager {
188
198
  hasChanged = existingContent !== file.content;
189
199
  } catch (readError) {
190
200
  console.warn(
191
- `Warning: Could not read existing file ${fullPath}, treating as changed:`,
201
+ chalk.yellow(
202
+ `Warning: Could not read existing file ${fullPath}, treating as changed:`
203
+ ),
192
204
  readError
193
205
  );
194
206
  hasChanged = true;
@@ -199,7 +211,7 @@ class FileManager {
199
211
  fs.writeFileSync(fullPath, file.content, { encoding: "utf8" });
200
212
  }
201
213
  } catch (error) {
202
- console.error(`Error processing file ${file.path}:`, error);
214
+ console.error(chalk.red(`Error processing file ${file.path}:`), error);
203
215
  throw error;
204
216
  }
205
217
  }
@@ -223,7 +235,10 @@ class FileManager {
223
235
  this.onFileChange!(fullPath, content);
224
236
  }
225
237
  } catch (error) {
226
- console.warn(`Error reading changed file ${fullPath}:`, error);
238
+ console.warn(
239
+ chalk.yellow(`Error reading changed file ${fullPath}:`),
240
+ error
241
+ );
227
242
  }
228
243
  }
229
244
  }
@@ -293,19 +308,21 @@ class RoomState {
293
308
  );
294
309
 
295
310
  this.ws.onopen = () => {
296
- console.log(`Connected to room: ${this.roomId}`);
311
+ console.log(chalk.green(`Connected to room: ${this.roomId}`));
297
312
  this.isConnected = true;
298
313
  this.reconnectAttempts = 0;
299
314
  onConnected?.(); // Call the onConnected callback if provided
300
315
  };
301
316
 
302
317
  this.ws.onclose = () => {
303
- console.log(`Disconnected from room: ${this.roomId}`);
318
+ console.log(chalk.yellow(`⚠ Disconnected from room: ${this.roomId}`));
304
319
  this.isConnected = false;
305
320
 
306
321
  this.reconnectAttempts++;
307
322
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
308
- console.log(`Attempting to reconnect in ${delay / 1000}s...`);
323
+ console.log(
324
+ chalk.yellow(`Attempting to reconnect in ${delay / 1000}s...`)
325
+ );
309
326
 
310
327
  this.reconnectTimeout = setTimeout(
311
328
  () => this.connect(onConnected),
@@ -360,12 +377,12 @@ class RoomState {
360
377
  this.state = newState;
361
378
  }
362
379
  } catch (e) {
363
- console.error("Error processing message:", e);
380
+ console.error(chalk.red("Error processing message:"), e);
364
381
  }
365
382
  });
366
383
 
367
384
  this.ws.on("error", (err) => {
368
- console.error("WebSocket error:", err);
385
+ console.error(chalk.red("WebSocket error:"), err);
369
386
  this.ws?.close();
370
387
  });
371
388
  }
@@ -400,7 +417,7 @@ class RoomState {
400
417
  }
401
418
  }
402
419
 
403
- program.version("0.0.16").description("Hapico CLI for project management");
420
+ program.version("0.0.18").description("Hapico CLI for project management");
404
421
 
405
422
  program
406
423
  .command("clone <id>")
@@ -408,43 +425,49 @@ program
408
425
  .action(async (id: string) => {
409
426
  const { accessToken } = getStoredToken();
410
427
  if (!accessToken) {
411
- console.error("You need to login first. Use 'hapico login' command.");
428
+ console.error(
429
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
430
+ );
412
431
  return;
413
432
  }
414
433
  const projectDir: string = path.resolve(process.cwd(), id);
415
434
  if (fs.existsSync(projectDir)) {
416
- console.error(`Project directory "${id}" already exists.`);
435
+ console.error(chalk.red(`✗ Project directory "${id}" already exists.`));
417
436
  return;
418
437
  }
419
438
 
420
439
  let files: FileContent[] = [];
421
- const apiSpinner: Ora = ora("Fetching project data...").start();
440
+ const apiSpinner: Ora = ora(chalk.blue("Fetching project data...")).start();
422
441
 
423
442
  try {
424
443
  const response: ApiResponse = await axios.get(
425
444
  `https://base.myworkbeast.com/api/views/${id}`
426
445
  );
427
446
  files = response.data.files || [];
428
- apiSpinner.succeed("Project data fetched successfully!");
447
+ apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
429
448
 
430
- const templateSpinner: Ora = ora("Downloading template...").start();
449
+ const templateSpinner: Ora = ora(
450
+ chalk.blue("Downloading template...")
451
+ ).start();
431
452
  const TEMPLATE_URL: string =
432
453
  "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
433
454
  const templateResponse = await axios.get(TEMPLATE_URL, {
434
455
  responseType: "arraybuffer",
435
456
  });
436
- templateSpinner.succeed("Template downloaded successfully!");
457
+ templateSpinner.succeed(chalk.green("Template downloaded successfully!"));
437
458
 
438
459
  const outputDir: string = path.resolve(process.cwd(), id);
439
460
  if (!fs.existsSync(outputDir)) {
440
461
  fs.mkdirSync(outputDir);
441
462
  }
442
463
 
443
- const unzipSpinner: Ora = ora("Extracting template...").start();
464
+ const unzipSpinner: Ora = ora(
465
+ chalk.blue("Extracting template...")
466
+ ).start();
444
467
  await unzipper.Open.buffer(templateResponse.data).then((directory) =>
445
468
  directory.extract({ path: outputDir })
446
469
  );
447
- unzipSpinner.succeed("Template extracted successfully!");
470
+ unzipSpinner.succeed(chalk.green("Template extracted successfully!"));
448
471
 
449
472
  const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
450
473
  if (fs.existsSync(macosxDir)) {
@@ -454,9 +477,11 @@ program
454
477
  // Save project ID to hapico.config.json
455
478
  saveProjectId(outputDir, id);
456
479
 
457
- console.log("Project cloned successfully!");
480
+ console.log(chalk.green("Project cloned successfully!"));
458
481
 
459
- const saveSpinner: Ora = ora("Saving project files...").start();
482
+ const saveSpinner: Ora = ora(
483
+ chalk.blue("Saving project files...")
484
+ ).start();
460
485
  files.forEach((file: FileContent) => {
461
486
  const filePath: string = path.join(process.cwd(), id, "src", file.path);
462
487
  const dir: string = path.dirname(filePath);
@@ -467,12 +492,14 @@ program
467
492
 
468
493
  fs.writeFileSync(filePath, file.content);
469
494
  });
470
- saveSpinner.succeed("Project files saved successfully!");
495
+ saveSpinner.succeed(chalk.green("Project files saved successfully!"));
471
496
  console.log(
472
- `Run 'cd ${id} && npm install && hapico dev' to start the project.`
497
+ chalk.cyan(
498
+ `💡 Run ${chalk.bold("cd ${id} && npm install && hapico dev")} to start the project.`
499
+ )
473
500
  );
474
501
  } catch (error: any) {
475
- apiSpinner.fail(`Error cloning project: ${error.message}`);
502
+ apiSpinner.fail(chalk.red(`Error cloning project: ${error.message}`));
476
503
  }
477
504
  });
478
505
 
@@ -482,17 +509,21 @@ program
482
509
  .action(() => {
483
510
  const { accessToken } = getStoredToken();
484
511
  if (!accessToken) {
485
- console.error("You need to login first. Use 'hapico login' command.");
512
+ console.error(
513
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
514
+ );
486
515
  return;
487
516
  }
488
517
  const devSpinner = ora(
489
- "Starting the project in development mode..."
518
+ chalk.blue("Starting the project in development mode...")
490
519
  ).start();
491
520
  const pwd = process.cwd();
492
521
  const srcDir = path.join(pwd, "src");
493
522
  if (!fs.existsSync(srcDir)) {
494
523
  devSpinner.fail(
495
- "Source directory 'src' does not exist. Please clone a project first."
524
+ chalk.red(
525
+ "✗ Source directory 'src' does not exist. Please clone a project first."
526
+ )
496
527
  );
497
528
  return;
498
529
  }
@@ -542,25 +573,29 @@ program
542
573
  const projectId = Buffer.from(info).toString("base64");
543
574
  if (!projectId) {
544
575
  devSpinner.fail(
545
- "Project ID not found. Please ensure hapico.config.json exists in the project directory."
576
+ chalk.red(
577
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
578
+ )
546
579
  );
547
580
  return;
548
581
  }
549
582
 
550
- console.log(`Connecting to WebSocket server`);
583
+ console.log(chalk.cyan("🔗 Connecting to WebSocket server"));
551
584
  const room = new RoomState(`view_${projectId}`, []);
552
585
  const fileManager = new FileManager(srcDir);
553
586
  const initialFiles = fileManager.listFiles();
554
587
  room.files = initialFiles;
555
588
 
556
589
  room.connect(async () => {
557
- devSpinner.succeed("Project started in development mode!");
590
+ devSpinner.succeed(chalk.green("Project started in development mode!"));
558
591
 
559
592
  room.updateState("view", initialFiles);
560
593
 
561
594
  fileManager.setOnFileChange((filePath, content) => {
562
595
  const es5 = compileES5(content, filePath) ?? "";
563
- console.log(`File changed: ${filePath?.replace(srcDir, ".")}`);
596
+ console.log(
597
+ chalk.yellow(`📝 File changed: ${filePath?.replace(srcDir, ".")}`)
598
+ );
564
599
  const updatedFiles = room.files.map((file) => {
565
600
  if (path.join(srcDir, file.path) === filePath) {
566
601
  return { ...file, content, es5 };
@@ -575,7 +610,9 @@ program
575
610
  const projectInfo = getStoredProjectId(pwd);
576
611
  if (!projectInfo) {
577
612
  console.error(
578
- "Project ID not found. Please ensure hapico.config.json exists in the project directory."
613
+ chalk.red(
614
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
615
+ )
579
616
  );
580
617
  return;
581
618
  }
@@ -590,7 +627,9 @@ program
590
627
  );
591
628
 
592
629
  if (project.status !== 200) {
593
- console.error(`Error fetching project info: ${project.statusText}`);
630
+ console.error(
631
+ chalk.red(`✗ Error fetching project info: ${project.statusText}`)
632
+ );
594
633
  return;
595
634
  }
596
635
 
@@ -601,7 +640,9 @@ program
601
640
  `https://zalo.me/s/3218692650896662017/player/${projectId}`,
602
641
  { small: true },
603
642
  (qrcode) => {
604
- console.log("Scan this QR code to connect to the project:");
643
+ console.log(
644
+ chalk.cyan("📱 Scan this QR code to connect to the project:")
645
+ );
605
646
  console.log(qrcode);
606
647
  }
607
648
  );
@@ -609,7 +650,9 @@ program
609
650
  } else {
610
651
  const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
611
652
  console.log(
612
- `Open this URL in your browser to preview the project: \n${previewUrl}`
653
+ chalk.cyan(
654
+ `🌐 Open this URL in your browser to preview the project:\n${previewUrl}`
655
+ )
613
656
  );
614
657
  await open(previewUrl);
615
658
  }
@@ -623,27 +666,47 @@ program
623
666
  const data = getStoredToken();
624
667
  const { accessToken } = data || {};
625
668
  if (!accessToken) {
626
- console.error("You need to login first. Use 'hapico login' command.");
669
+ console.error(
670
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
671
+ );
627
672
  return;
628
673
  }
629
- const saveSpinner: Ora = ora("Saving project source code...").start();
674
+ const saveSpinner: Ora = ora(
675
+ chalk.blue("Saving project source code...")
676
+ ).start();
630
677
  const pwd = process.cwd();
631
678
  const projectId = getStoredProjectId(pwd);
632
679
  if (!projectId) {
633
680
  saveSpinner.fail(
634
- "Project ID not found. Please ensure hapico.config.json exists in the project directory."
681
+ chalk.red(
682
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
683
+ )
635
684
  );
636
685
  return;
637
686
  }
638
687
  const srcDir: string = path.join(pwd, "src");
639
688
  if (!fs.existsSync(srcDir)) {
640
689
  saveSpinner.fail(
641
- "Source directory 'src' does not exist. Please clone a project first."
690
+ chalk.red(
691
+ "✗ Source directory 'src' does not exist. Please clone a project first."
692
+ )
642
693
  );
643
694
  return;
644
695
  }
645
696
  const fileManager = new FileManager(srcDir);
646
697
  const files = fileManager.listFiles();
698
+
699
+ // Nếu có file .env
700
+ const envFile = path.join(pwd, ".env");
701
+ console.log(chalk.cyan(`🔍 Checking for .env file at ${envFile}`));
702
+ if (fs.existsSync(envFile)) {
703
+ console.log(chalk.green(".env file found, including in push."));
704
+ const content = fs.readFileSync(envFile, { encoding: "utf8" });
705
+ files.push({
706
+ path: "./.env",
707
+ content,
708
+ });
709
+ }
647
710
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
648
711
  axios
649
712
  .put(
@@ -661,10 +724,12 @@ program
661
724
  }
662
725
  )
663
726
  .then(() => {
664
- saveSpinner.succeed("Project source code saved successfully!");
727
+ saveSpinner.succeed(
728
+ chalk.green("Project source code saved successfully!")
729
+ );
665
730
  })
666
731
  .catch((error) => {
667
- saveSpinner.fail(`Error saving project: ${error.message}`);
732
+ saveSpinner.fail(chalk.red(`✗ Error saving project: ${error.message}`));
668
733
  });
669
734
  });
670
735
 
@@ -672,8 +737,8 @@ program
672
737
  .command("login")
673
738
  .description("Login to the system")
674
739
  .action(async () => {
675
- console.log("Logging in to the system...");
676
- const loginSpinner: Ora = ora("Initiating login...").start();
740
+ console.log(chalk.cyan("🔐 Logging in to the system..."));
741
+ const loginSpinner: Ora = ora(chalk.blue("Initiating login...")).start();
677
742
 
678
743
  try {
679
744
  const response = await axios.post(
@@ -682,14 +747,20 @@ program
682
747
  const { device_code, user_code, verification_url, expires_in, interval } =
683
748
  response.data;
684
749
 
685
- loginSpinner.succeed("Login initiated!");
686
- console.log(`Please open this URL in your browser: ${verification_url}`);
687
- console.log(`And enter this code: ${user_code}`);
688
- console.log("Waiting for authentication...");
750
+ loginSpinner.succeed(chalk.green("Login initiated!"));
751
+ console.log(
752
+ chalk.cyan(
753
+ `🌐 Please open this URL in your browser: ${verification_url}`
754
+ )
755
+ );
756
+ console.log(chalk.yellow(`🔑 And enter this code: ${user_code}`));
757
+ console.log(chalk.blue("⏳ Waiting for authentication..."));
689
758
 
690
759
  await open(verification_url);
691
760
 
692
- const pollSpinner: Ora = ora("Waiting for authentication...").start();
761
+ const pollSpinner: Ora = ora(
762
+ chalk.blue("Waiting for authentication...")
763
+ ).start();
693
764
  let tokens = null;
694
765
  const startTime = Date.now();
695
766
  while (Date.now() - startTime < expires_in * 1000) {
@@ -709,15 +780,19 @@ program
709
780
  }
710
781
 
711
782
  if (tokens) {
712
- pollSpinner.succeed("Login successful!");
783
+ pollSpinner.succeed(chalk.green("Login successful!"));
713
784
  saveToken(tokens);
714
785
  } else {
715
786
  pollSpinner.fail(
716
- "Login failed: Timeout or user did not complete authentication."
787
+ chalk.red(
788
+ "✗ Login failed: Timeout or user did not complete authentication."
789
+ )
717
790
  );
718
791
  }
719
792
  } catch (error) {
720
- loginSpinner.fail(`Login error: ${(error as Error).message}`);
793
+ loginSpinner.fail(
794
+ chalk.red(`✗ Login error: ${(error as Error).message}`)
795
+ );
721
796
  }
722
797
  });
723
798
 
@@ -727,11 +802,11 @@ program
727
802
  .action(() => {
728
803
  const accessToken = getStoredToken();
729
804
  if (!accessToken) {
730
- console.log("You are not logged in.");
805
+ console.log(chalk.yellow("You are not logged in."));
731
806
  return;
732
807
  }
733
808
  fs.unlinkSync(TOKEN_FILE);
734
- console.log("Logout successful!");
809
+ console.log(chalk.green("Logout successful!"));
735
810
  });
736
811
  // Pull command to fetch the latest project files from the server
737
812
  program
@@ -740,18 +815,24 @@ program
740
815
  .action(async () => {
741
816
  const token = getStoredToken();
742
817
  if (!token) {
743
- console.error("You need to login first. Use 'hapico login' command.");
818
+ console.error(
819
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
820
+ );
744
821
  return;
745
822
  }
746
823
  const pwd: string = process.cwd();
747
824
  const projectId = getStoredProjectId(pwd);
748
825
  if (!projectId) {
749
826
  console.error(
750
- "Project ID not found. Please ensure hapico.config.json exists in the project directory."
827
+ chalk.red(
828
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
829
+ )
751
830
  );
752
831
  return;
753
832
  }
754
- const apiSpinner: Ora = ora("Fetching latest project files...").start();
833
+ const apiSpinner: Ora = ora(
834
+ chalk.blue("Fetching latest project files...")
835
+ ).start();
755
836
  try {
756
837
  const response: ApiResponse = await axios.get(
757
838
  `https://base.myworkbeast.com/api/views/${projectId}`,
@@ -765,12 +846,371 @@ program
765
846
  const files: FileContent[] = response.data.files || [];
766
847
  const fileManager = new FileManager(path.join(pwd, "src"));
767
848
  fileManager.syncFiles(files);
768
- apiSpinner.succeed("Project files updated successfully!");
849
+ apiSpinner.succeed(chalk.green("Project files updated successfully!"));
769
850
  } catch (error) {
770
851
  apiSpinner.fail(
771
- `Error fetching project files: ${(error as Error).message}`
852
+ chalk.red(`✗ Error fetching project files: ${(error as Error).message}`)
772
853
  );
773
854
  }
774
855
  });
775
856
 
857
+ // be {{id}} --be --port {{port}}
858
+ program
859
+ .command("fetch <id>")
860
+ .option("--port <port>", "Port to run the backend", "3000")
861
+ .option("--serve", "Flag to indicate serving the backend")
862
+ .option(
863
+ "--libs <libs>",
864
+ "Additional libraries to install (comma-separated)",
865
+ ""
866
+ )
867
+ .option("--be", "Flag to indicate backend")
868
+ .description("Open backend for the project")
869
+ .action(
870
+ async (
871
+ id: string,
872
+ options: { port: string; serve?: boolean; libs?: string; be?: boolean }
873
+ ) => {
874
+ const { accessToken } = getStoredToken();
875
+ if (!accessToken) {
876
+ console.error(
877
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
878
+ );
879
+ return;
880
+ }
881
+
882
+ console.log(chalk.cyan(`🌐 PORT = ${options.port}`));
883
+
884
+ // Chọn hỏi user port để vận hành
885
+ let port = 3000;
886
+ const response: ApiResponse = await axios.get(
887
+ `https://base.myworkbeast.com/api/views/${id}`
888
+ );
889
+ const portInput = options.port;
890
+ if (portInput) {
891
+ const parsedPort = parseInt(portInput, 10);
892
+ if (!isNaN(parsedPort)) {
893
+ port = parsedPort;
894
+ }
895
+ }
896
+
897
+ const projectDir: string = path.resolve(process.cwd(), id);
898
+ if (!fs.existsSync(projectDir)) {
899
+ // create folder
900
+ fs.mkdirSync(projectDir);
901
+ }
902
+
903
+ // download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
904
+ const TEMPLATE_URL: string = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
905
+ let templateResponse = undefined;
906
+ try {
907
+ templateResponse = await axios.get(TEMPLATE_URL, {
908
+ responseType: "arraybuffer",
909
+ });
910
+ } catch (error) {
911
+ console.error(
912
+ chalk.red("✗ Error downloading template:"),
913
+ (error as Error).message
914
+ );
915
+ return;
916
+ }
917
+
918
+ const outputDir: string = path.resolve(process.cwd(), id);
919
+ if (!fs.existsSync(outputDir)) {
920
+ fs.mkdirSync(outputDir);
921
+ }
922
+
923
+ await unzipper.Open.buffer(templateResponse.data).then((directory) =>
924
+ directory.extract({ path: outputDir })
925
+ );
926
+
927
+ const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
928
+ if (fs.existsSync(macosxDir)) {
929
+ fs.rmSync(macosxDir, { recursive: true, force: true });
930
+ }
931
+
932
+ // outputPath/src/server.ts có dòng (3006) thay thành port
933
+ const serverFile = path.join(process.cwd(), id, "src", "index.ts");
934
+ if (fs.existsSync(serverFile)) {
935
+ let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
936
+ serverContent = serverContent.split("(3006)").join(`(${port})`);
937
+ fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
938
+ }
939
+
940
+ // lấy danh sách migrations
941
+ const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${
942
+ response.data.dbCode || response.data.db_code
943
+ }&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
944
+
945
+ // Lấy danh sách các migration đã chạy
946
+ let migrations: Array<{
947
+ id: number;
948
+ created_at: string;
949
+ sql_statement: string;
950
+ }> = [];
951
+ const migrationsResponse = await axios.get(MIGRATIONS_URL, {
952
+ headers: {
953
+ Authorization: `Bearer ${accessToken}`,
954
+ "Content-Type": "application/json",
955
+ "x-db-code": response.data.dbCode || response.data.db_code,
956
+ },
957
+ });
958
+ if (migrationsResponse.status === 200) {
959
+ migrations = (migrationsResponse.data.data?.rows || []).map(
960
+ (item: any) => ({
961
+ created_at: item.created_at,
962
+ sql: item.sql_statement,
963
+ })
964
+ );
965
+ }
966
+
967
+ console.log(chalk.cyan(`🔍 Found ${migrations.length} migrations.`));
968
+
969
+ // Tạo thư mục migrations nếu chưa tồn tại
970
+ const migrationsDir = path.join(process.cwd(), id, "migrations");
971
+ if (!fs.existsSync(migrationsDir)) {
972
+ fs.mkdirSync(migrationsDir);
973
+ }
974
+
975
+ // Lưu từng migration vào file
976
+ migrations.forEach((migration, index) => {
977
+ const timestamp = new Date(migration.created_at)
978
+ .toISOString()
979
+ .replace(/[-:]/g, "")
980
+ .replace(/\..+/, "");
981
+ const filename = path.join(
982
+ migrationsDir,
983
+ `${index + 1}_migration_${timestamp}.sql`
984
+ );
985
+ fs.writeFileSync(filename, (migration as any).sql, { encoding: "utf8" });
986
+ });
987
+
988
+ console.log(chalk.green("✓ Migrations saved successfully!"));
989
+
990
+ // Download project files and save to project
991
+ let files: FileContent[] = [];
992
+ const apiSpinner: Ora = ora(
993
+ chalk.blue("Fetching project data...")
994
+ ).start();
995
+
996
+ try {
997
+ files = response.data.files || [];
998
+ apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
999
+
1000
+ const saveSpinner: Ora = ora(
1001
+ chalk.blue("Saving project files...")
1002
+ ).start();
1003
+ files.forEach((file: FileContent) => {
1004
+ // skip ./app, ./components
1005
+ if (
1006
+ file.path.startsWith("./app") ||
1007
+ file.path.startsWith("./components")
1008
+ ) {
1009
+ return;
1010
+ }
1011
+ // hoặc các file có đuôi là .tsx
1012
+ if (file.path.endsWith(".tsx")) {
1013
+ return;
1014
+ }
1015
+ try {
1016
+ if (file.content.trim().length == 0) return;
1017
+ const filePath: string = path.join(
1018
+ process.cwd(),
1019
+ id,
1020
+ "src",
1021
+ file.path
1022
+ );
1023
+ const dir: string = path.dirname(filePath);
1024
+
1025
+ if (!fs.existsSync(dir)) {
1026
+ fs.mkdirSync(dir, { recursive: true });
1027
+ }
1028
+
1029
+ fs.writeFileSync(filePath, file.content);
1030
+ } catch (error) {
1031
+ console.log(chalk.red("✗ Error writing file"), file.path, error);
1032
+ }
1033
+ });
1034
+ saveSpinner.succeed(chalk.green("Project files saved successfully!"));
1035
+ } catch (error: any) {
1036
+ apiSpinner.fail(chalk.red(`✗ Error cloning project: ${error.message}`));
1037
+ }
1038
+
1039
+ // Download project files and save to project
1040
+ console.log(chalk.green("✓ Project cloned successfully!"));
1041
+
1042
+ // Save project ID to hapico.config.json
1043
+ saveProjectId(outputDir, id);
1044
+
1045
+ console.log(chalk.green("✓ Project setup successfully!"));
1046
+
1047
+ let serveProcess: any = null;
1048
+
1049
+ if (options.serve) {
1050
+ // Run bun install
1051
+ const { exec } = require("child_process");
1052
+ const installSpinner: Ora = ora(
1053
+ chalk.blue("Installing dependencies...")
1054
+ ).start();
1055
+
1056
+ // create a loop to check if there is new version of project
1057
+ // https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
1058
+ let currentVersionStr = fs.existsSync(
1059
+ path.join(outputDir, "version.json")
1060
+ )
1061
+ ? JSON.parse(
1062
+ fs.readFileSync(path.join(outputDir, "version.json"), {
1063
+ encoding: "utf8",
1064
+ })
1065
+ ).version
1066
+ : "0.0.1";
1067
+
1068
+ const checkVersion = async () => {
1069
+ try {
1070
+ // check if file version.json exists
1071
+ const flyCheck = await axios.head(
1072
+ `https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
1073
+ );
1074
+ if (flyCheck.status !== 200) {
1075
+ return;
1076
+ }
1077
+ // get file version.json
1078
+ const versionResponse = await axios.get(
1079
+ `https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
1080
+ );
1081
+ const latestVersionData = versionResponse.data;
1082
+ const latestVersion = latestVersionData.version;
1083
+ if (latestVersion !== currentVersionStr) {
1084
+ console.log(
1085
+ chalk.yellow(`📦 New version available: ${latestVersion}`)
1086
+ );
1087
+ // Save new version.json
1088
+ fs.writeFileSync(
1089
+ path.join(outputDir, "version.json"),
1090
+ JSON.stringify(latestVersionData, null, 2),
1091
+ { encoding: "utf8" }
1092
+ );
1093
+ // Install external libraries if any
1094
+ if (
1095
+ latestVersionData.external_libraries &&
1096
+ latestVersionData.external_libraries.length > 0
1097
+ ) {
1098
+ console.log(chalk.blue("Installing new external libraries..."));
1099
+ for (const lib of latestVersionData.external_libraries) {
1100
+ try {
1101
+ await execPromise(`bun add ${lib}`, { cwd: outputDir });
1102
+ console.log(chalk.green(`Added ${lib}`));
1103
+ } catch (addError) {
1104
+ console.error(
1105
+ chalk.red(`✗ Error adding ${lib}:`),
1106
+ (addError as Error).message
1107
+ );
1108
+ }
1109
+ }
1110
+ }
1111
+ // Rerun bun install
1112
+ try {
1113
+ await execPromise("bun install", { cwd: outputDir });
1114
+ console.log(
1115
+ chalk.green("Dependencies reinstalled successfully!")
1116
+ );
1117
+ } catch (installError) {
1118
+ console.error(
1119
+ chalk.red("✗ Error reinstalling dependencies:"),
1120
+ (installError as Error).message
1121
+ );
1122
+ }
1123
+ // Restart the process
1124
+ if (serveProcess && !serveProcess.killed) {
1125
+ console.log(chalk.blue("Restarting backend..."));
1126
+ serveProcess.kill("SIGTERM");
1127
+ }
1128
+ // Start new process
1129
+ const newServeProcess = exec("bun run start", { cwd: outputDir });
1130
+ newServeProcess.stdout?.on("data", (data: any) => {
1131
+ process.stdout.write(data);
1132
+ });
1133
+ newServeProcess.stderr?.on("data", (data: any) => {
1134
+ process.stderr.write(data);
1135
+ });
1136
+ newServeProcess.on("close", (code: any) => {
1137
+ console.log(
1138
+ chalk.yellow(`⚠ backend exited with code ${code}`)
1139
+ );
1140
+ });
1141
+ serveProcess = newServeProcess;
1142
+ currentVersionStr = latestVersion;
1143
+ }
1144
+ } catch (error) {
1145
+ // If the remote version.json does not exist, do not update
1146
+ console.error(
1147
+ chalk.yellow("⚠ Error checking for updates (skipping update):"),
1148
+ (error as Error).message
1149
+ );
1150
+ }
1151
+ };
1152
+
1153
+ // check every 3 seconds
1154
+ setInterval(async () => await checkVersion(), 10 * 1000);
1155
+
1156
+ exec(
1157
+ "bun install",
1158
+ { cwd: outputDir },
1159
+ async (error: any, stdout: any, stderr: any) => {
1160
+ if (error) {
1161
+ installSpinner.fail(
1162
+ chalk.red(`✗ Error installing dependencies: ${error.message}`)
1163
+ );
1164
+ return;
1165
+ }
1166
+ if (stderr) {
1167
+ console.error(chalk.red(`stderr: ${stderr}`));
1168
+ }
1169
+ installSpinner.succeed(
1170
+ chalk.green("Dependencies installed successfully!")
1171
+ );
1172
+
1173
+ // Install additional libraries if --libs is provided
1174
+ if (options.libs && options.libs.trim()) {
1175
+ const libsSpinner: Ora = ora(
1176
+ chalk.blue("Installing additional libraries...")
1177
+ ).start();
1178
+ const additionalLibs = options.libs
1179
+ .split(",")
1180
+ .map((lib) => lib.trim())
1181
+ .filter((lib) => lib);
1182
+ for (const lib of additionalLibs) {
1183
+ try {
1184
+ await execPromise(`bun add ${lib}`, { cwd: outputDir });
1185
+ console.log(chalk.green(`Added ${lib}`));
1186
+ } catch (addError) {
1187
+ console.error(
1188
+ chalk.red(`✗ Error adding ${lib}:`),
1189
+ (addError as Error).message
1190
+ );
1191
+ }
1192
+ }
1193
+ libsSpinner.succeed(
1194
+ chalk.green("Additional libraries installed successfully!")
1195
+ );
1196
+ }
1197
+
1198
+ // Run cd ${id} && bun run serve-be
1199
+ console.log(chalk.blue("🚀 Starting backend"));
1200
+ serveProcess = exec("bun run start", { cwd: outputDir });
1201
+ serveProcess.stdout.on("data", (data: any) => {
1202
+ process.stdout.write(data);
1203
+ });
1204
+ serveProcess.stderr.on("data", (data: any) => {
1205
+ process.stderr.write(data);
1206
+ });
1207
+ serveProcess.on("close", (code: any) => {
1208
+ console.log(chalk.yellow(`⚠ backend exited with code ${code}`));
1209
+ });
1210
+ }
1211
+ );
1212
+ }
1213
+ }
1214
+ );
1215
+
776
1216
  program.parse(process.argv);