@hapico/cli 0.0.17 → 0.0.19

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