@hapico/cli 0.0.19 → 0.0.21

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { isEqual, find } from "lodash";
3
+ import { isEqual, find, map } from "lodash";
4
4
  import { program } from "commander";
5
5
  import axios from "axios";
6
6
  import * as fs from "fs";
@@ -15,7 +15,7 @@ import { randomUUID } from "crypto";
15
15
  import { exec } from "child_process";
16
16
  import { promisify } from "util";
17
17
  import chalk from "chalk";
18
- import pako from 'pako';
18
+ import pako from "pako";
19
19
 
20
20
  // Promisify exec for async usage
21
21
  const execPromise = promisify(exec);
@@ -74,14 +74,25 @@ const saveProjectId = (projectDir: string, id: string) => {
74
74
  };
75
75
 
76
76
  // Function to get stored project ID
77
- const getStoredProjectId = (projectDir: string): string | null => {
77
+ const getStoredProjectId = (
78
+ projectDir: string
79
+ ): {
80
+ projectId: string | null;
81
+ replicate?: string[];
82
+ } => {
78
83
  const configFile = path.join(projectDir, "hapico.config.json");
79
84
  if (fs.existsSync(configFile)) {
80
85
  const data = fs.readFileSync(configFile, { encoding: "utf8" });
81
86
  const json = JSON.parse(data);
82
- return json.projectId || null;
87
+ return {
88
+ projectId: json.projectId || null,
89
+ replicate: json.replicate || [],
90
+ };
83
91
  }
84
- return null;
92
+ return {
93
+ projectId: null,
94
+ replicate: [],
95
+ };
85
96
  };
86
97
 
87
98
  // Khởi tạo cache bằng Map để lưu trữ kết quả compile
@@ -95,7 +106,7 @@ interface FileContent {
95
106
 
96
107
  interface ApiResponse {
97
108
  data: {
98
- files?: FileContent[];
109
+ code: string;
99
110
  type: "view" | "zalominiapp" | string;
100
111
  dbCode: string;
101
112
  db_code: string;
@@ -111,6 +122,14 @@ interface WebSocketMessage {
111
122
  state?: RoomStateData;
112
123
  }
113
124
 
125
+ export const tryJSONParse = (str: string): any => {
126
+ try {
127
+ return JSON.parse(str);
128
+ } catch {
129
+ return null;
130
+ }
131
+ };
132
+
114
133
  export const compileES5 = (code: string, filePath?: string) => {
115
134
  if (filePath && !filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
116
135
  return code;
@@ -131,7 +150,8 @@ export const compileES5 = (code: string, filePath?: string) => {
131
150
 
132
151
  compileCache.set(code, result.code || "");
133
152
  return result.code;
134
- } catch (error) {
153
+ } catch (error: any) {
154
+ console.log(chalk.red(`Error compiling code: ${error?.message}`));
135
155
  return "";
136
156
  }
137
157
  };
@@ -303,12 +323,10 @@ class RoomState {
303
323
  clearTimeout(this.reconnectTimeout);
304
324
  }
305
325
 
306
- this.ws = new WebSocket(
307
- `wss://ws3.myworkbeast.com/ws?room=${this.roomId}`
308
- );
326
+ this.ws = new WebSocket(`wss://ws3.myworkbeast.com/ws?room=${this.roomId}`);
309
327
 
310
328
  // Set binaryType to 'arraybuffer' to handle binary data
311
- this.ws.binaryType = 'arraybuffer';
329
+ this.ws.binaryType = "arraybuffer";
312
330
 
313
331
  this.ws.on("open", () => {
314
332
  console.log(chalk.green(`Connected to room: ${this.roomId}`));
@@ -337,11 +355,11 @@ class RoomState {
337
355
  try {
338
356
  let jsonStr: string;
339
357
  if (data instanceof ArrayBuffer) {
340
- jsonStr = pako.inflate(data, { to: 'string' });
358
+ jsonStr = pako.inflate(data, { to: "string" });
341
359
  } else if (typeof data === "string") {
342
360
  jsonStr = data;
343
361
  } else if (Buffer.isBuffer(data)) {
344
- jsonStr = pako.inflate(data, { to: 'string' });
362
+ jsonStr = pako.inflate(data, { to: "string" });
345
363
  } else {
346
364
  jsonStr = data.toString(); // Fallback nếu không nén
347
365
  }
@@ -429,7 +447,7 @@ class RoomState {
429
447
  }
430
448
  }
431
449
 
432
- program.version("0.0.19").description("Hapico CLI for project management");
450
+ program.version("0.0.21").description("Hapico CLI for project management");
433
451
 
434
452
  program
435
453
  .command("clone <id>")
@@ -455,7 +473,12 @@ program
455
473
  const response: ApiResponse = await axios.get(
456
474
  `https://base.myworkbeast.com/api/views/${id}`
457
475
  );
458
- files = response.data.files || [];
476
+ const code = response?.data?.code;
477
+ const decompressedCode = pako.inflate(
478
+ Uint8Array.from(atob(code), (c) => c.charCodeAt(0)),
479
+ { to: "string" }
480
+ );
481
+ files = tryJSONParse(decompressedCode)?.files || [];
459
482
  apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
460
483
 
461
484
  const templateSpinner: Ora = ora(
@@ -518,7 +541,8 @@ program
518
541
  program
519
542
  .command("dev")
520
543
  .description("Start the project in development mode")
521
- .action(() => {
544
+ .option('--zversion <version>', 'Zalo version for QR code', '75')
545
+ .action((options) => {
522
546
  const { accessToken } = getStoredToken();
523
547
  if (!accessToken) {
524
548
  console.error(
@@ -578,7 +602,7 @@ program
578
602
  const info = JSON.stringify({
579
603
  id: sessionId,
580
604
  createdAt: new Date().toISOString(),
581
- viewId: getStoredProjectId(pwd),
605
+ viewId: getStoredProjectId(pwd)?.projectId,
582
606
  });
583
607
 
584
608
  // Convert info to base64
@@ -596,12 +620,47 @@ program
596
620
  const room = new RoomState(`view_${projectId}`, []);
597
621
  const fileManager = new FileManager(srcDir);
598
622
  const initialFiles = fileManager.listFiles();
599
- room.files = initialFiles;
623
+ // get tsconfig.json
624
+ const tsconfigPath = path.join(srcDir, "..", "tsconfig.json");
625
+ if (fs.existsSync(tsconfigPath)) {
626
+ const content = fs.readFileSync(tsconfigPath, { encoding: "utf8" });
627
+ initialFiles.push({
628
+ path: "./tsconfig.json",
629
+ content,
630
+ });
631
+ }
632
+ // Remove All binary files
633
+ const supportExtensions = [
634
+ ".ts",
635
+ ".tsx",
636
+ ".js",
637
+ ".jsx",
638
+ ".json",
639
+ ".css",
640
+ ".html",
641
+ ".env",
642
+ ".env.local",
643
+ ".env.development",
644
+ ".env.production",
645
+ ".jsonc",
646
+ ".yml",
647
+ ".yaml",
648
+ ".md",
649
+ ".markdown",
650
+ ".txt",
651
+ ".xml",
652
+ ".config",
653
+ ];
654
+ const filteredFiles = initialFiles.filter((file) => {
655
+ return supportExtensions.some((ext) => file.path.endsWith(ext));
656
+ });
657
+
658
+ room.files = filteredFiles;
600
659
 
601
660
  room.connect(async () => {
602
661
  devSpinner.succeed(chalk.green("Project started in development mode!"));
603
662
 
604
- room.updateState("view", initialFiles);
663
+ room.updateState("view", filteredFiles);
605
664
 
606
665
  fileManager.setOnFileChange((filePath, content) => {
607
666
  const es5 = compileES5(content, filePath) ?? "";
@@ -619,8 +678,8 @@ program
619
678
  });
620
679
 
621
680
  // Fetch project info
622
- const projectInfo = getStoredProjectId(pwd);
623
- if (!projectInfo) {
681
+ const store = getStoredProjectId(pwd);
682
+ if (!store.projectId) {
624
683
  console.error(
625
684
  chalk.red(
626
685
  "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
@@ -629,7 +688,7 @@ program
629
688
  return;
630
689
  }
631
690
  const project = await axios.get(
632
- `https://base.myworkbeast.com/api/views/${projectInfo}`,
691
+ `https://base.myworkbeast.com/api/views/${store.projectId}`,
633
692
  {
634
693
  headers: {
635
694
  Authorization: `Bearer ${accessToken}`,
@@ -646,10 +705,11 @@ program
646
705
  }
647
706
 
648
707
  const projectType = project.data.type || "view";
708
+ const zversion = options.zversion || '75';
649
709
 
650
710
  if (projectType === "zalominiapp") {
651
711
  QRCode.generate(
652
- `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=75`,
712
+ `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`,
653
713
  { small: true },
654
714
  (qrcode) => {
655
715
  console.log(
@@ -672,23 +732,16 @@ program
672
732
  });
673
733
 
674
734
  // Hàm tái sử dụng để push mã nguồn lên server
675
- async function pushProject(spinner: Ora): Promise<boolean> {
735
+ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
676
736
  const data = getStoredToken();
677
737
  const { accessToken } = data || {};
678
738
  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
739
  spinner.fail(
686
- chalk.red(
687
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
688
- )
740
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
689
741
  );
690
742
  return false;
691
743
  }
744
+ const pwd = process.cwd();
692
745
  const srcDir: string = path.join(pwd, "src");
693
746
  if (!fs.existsSync(srcDir)) {
694
747
  spinner.fail(
@@ -701,17 +754,35 @@ async function pushProject(spinner: Ora): Promise<boolean> {
701
754
  const fileManager = new FileManager(srcDir);
702
755
  const files = fileManager.listFiles();
703
756
 
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
- }
757
+ // Supported files
758
+ const SUPPORT_FILES = [
759
+ "./.env",
760
+ "./.env.local",
761
+ "./.env.development",
762
+ "./.env.production",
763
+ "./package.json",
764
+ "./tsconfig.json",
765
+ ];
766
+
767
+ // Include supported files
768
+ SUPPORT_FILES.forEach((relativePath) => {
769
+ const fullPath = path.join(pwd, relativePath);
770
+ if (fs.existsSync(fullPath)) {
771
+ console.log(
772
+ chalk.green(
773
+ `Including ${relativePath} in push for project ${projectId}.`
774
+ )
775
+ );
776
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
777
+ files.push({
778
+ path: relativePath,
779
+ content,
780
+ es5: compileES5(content, fullPath) ?? "",
781
+ });
782
+ }
783
+ });
784
+ spinner.text = `Pushing project source code to server for project ${projectId}...`;
785
+
715
786
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
716
787
  try {
717
788
  await axios.put(
@@ -730,7 +801,11 @@ async function pushProject(spinner: Ora): Promise<boolean> {
730
801
  );
731
802
  return true;
732
803
  } catch (error) {
733
- spinner.fail(chalk.red(`✗ Error saving project: ${(error as Error).message}`));
804
+ spinner.fail(
805
+ chalk.red(
806
+ `✗ Error saving project ${projectId}: ${(error as Error).message}`
807
+ )
808
+ );
734
809
  return false;
735
810
  }
736
811
  }
@@ -742,10 +817,49 @@ program
742
817
  const saveSpinner: Ora = ora(
743
818
  chalk.blue("Saving project source code...")
744
819
  ).start();
745
- const success = await pushProject(saveSpinner);
746
- if (success) {
820
+ const pwd = process.cwd();
821
+ const { projectId, replicate } = getStoredProjectId(pwd);
822
+
823
+ if (!projectId) {
824
+ saveSpinner.fail(
825
+ chalk.red(
826
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
827
+ )
828
+ );
829
+ return;
830
+ }
831
+
832
+ // Push to the main project
833
+ const mainSuccess = await pushProject(saveSpinner, projectId);
834
+ let allSuccess = mainSuccess;
835
+
836
+ // Push to replicated projects if replicate array exists
837
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
838
+ saveSpinner.text = chalk.blue("Pushing to replicated projects...");
839
+ for (const repId of replicate) {
840
+ const success = await pushProject(saveSpinner, repId);
841
+ if (!success) {
842
+ allSuccess = false;
843
+ console.warn(
844
+ chalk.yellow(
845
+ `⚠ Failed to push to replicated project ${repId}. Continuing...`
846
+ )
847
+ );
848
+ } else {
849
+ console.log(
850
+ chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
851
+ );
852
+ }
853
+ }
854
+ }
855
+
856
+ if (allSuccess) {
747
857
  saveSpinner.succeed(
748
- chalk.green("Project source code saved successfully!")
858
+ chalk.green("Project source code saved successfully to all projects!")
859
+ );
860
+ } else {
861
+ saveSpinner.warn(
862
+ chalk.yellow("Project source code saved with some errors.")
749
863
  );
750
864
  }
751
865
  });
@@ -838,7 +952,7 @@ program
838
952
  return;
839
953
  }
840
954
  const pwd: string = process.cwd();
841
- const projectId = getStoredProjectId(pwd);
955
+ const { projectId } = getStoredProjectId(pwd);
842
956
  if (!projectId) {
843
957
  console.error(
844
958
  chalk.red(
@@ -852,15 +966,22 @@ program
852
966
  ).start();
853
967
  try {
854
968
  const response: ApiResponse = await axios.get(
855
- `https://base.myworkbeast.com/api/views/${projectId}`,
969
+ `https://base.myworkbeast.com/api/views/v3/${projectId}`,
856
970
  {
857
971
  headers: {
858
- Authorization: `Bearer ${token}`,
972
+ Authorization: `Bearer ${token.accessToken}`,
859
973
  "Content-Type": "application/json",
860
974
  },
861
975
  }
862
976
  );
863
- const files: FileContent[] = response.data.files || [];
977
+
978
+ const code = response?.data?.code;
979
+ const decompressedCode = pako.inflate(
980
+ Uint8Array.from(atob(code), (c) => c.charCodeAt(0)),
981
+ { to: "string" }
982
+ );
983
+
984
+ const files: FileContent[] = tryJSONParse(decompressedCode)?.files || [];
864
985
  const fileManager = new FileManager(path.join(pwd, "src"));
865
986
  fileManager.syncFiles(files);
866
987
  apiSpinner.succeed(chalk.green("Project files updated successfully!"));
@@ -999,7 +1120,9 @@ program
999
1120
  migrationsDir,
1000
1121
  `${index + 1}_migration_${timestamp}.sql`
1001
1122
  );
1002
- fs.writeFileSync(filename, (migration as any).sql, { encoding: "utf8" });
1123
+ fs.writeFileSync(filename, (migration as any).sql, {
1124
+ encoding: "utf8",
1125
+ });
1003
1126
  });
1004
1127
 
1005
1128
  console.log(chalk.green("✓ Migrations saved successfully!"));
@@ -1011,7 +1134,6 @@ program
1011
1134
  ).start();
1012
1135
 
1013
1136
  try {
1014
- files = response.data.files || [];
1015
1137
  apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
1016
1138
 
1017
1139
  const saveSpinner: Ora = ora(
@@ -1232,17 +1354,12 @@ program
1232
1354
 
1233
1355
  // hapico publish
1234
1356
  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();
1357
+ const publishSpinner: Ora = ora(
1358
+ chalk.blue("Publishing to Hapico...")
1359
+ ).start();
1244
1360
  const pwd = process.cwd();
1245
- const projectId = getStoredProjectId(pwd);
1361
+ const { projectId, replicate } = getStoredProjectId(pwd);
1362
+
1246
1363
  if (!projectId) {
1247
1364
  publishSpinner.fail(
1248
1365
  chalk.red(
@@ -1251,11 +1368,42 @@ program.command("publish").action(async () => {
1251
1368
  );
1252
1369
  return;
1253
1370
  }
1371
+
1372
+ // Step 1: Push source code to main project and replicas
1373
+ const pushSuccess = await pushProject(publishSpinner, projectId);
1374
+ if (!pushSuccess) {
1375
+ return; // Stop if push to main project fails
1376
+ }
1377
+
1378
+ // Push to replicated projects
1379
+ let allPushSuccess = true;
1380
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1381
+ publishSpinner.text = chalk.blue("Pushing to replicated projects...");
1382
+ for (const repId of replicate) {
1383
+ const success = await pushProject(publishSpinner, repId);
1384
+ if (!success) {
1385
+ allPushSuccess = false;
1386
+ console.warn(
1387
+ chalk.yellow(
1388
+ `⚠ Failed to push to replicated project ${repId}. Continuing...`
1389
+ )
1390
+ );
1391
+ } else {
1392
+ console.log(
1393
+ chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
1394
+ );
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ // Step 2: Publish main project
1400
+ const { accessToken } = getStoredToken();
1254
1401
  const apiUrl = "https://base.myworkbeast.com/api/views/publish";
1402
+ let allPublishSuccess = true;
1255
1403
  try {
1256
1404
  await axios.post(
1257
1405
  apiUrl,
1258
- { view_id: parseInt(projectId, 10) }, // Đảm bảo viewId là number
1406
+ { view_id: parseInt(projectId, 10) },
1259
1407
  {
1260
1408
  headers: {
1261
1409
  Authorization: `Bearer ${accessToken}`,
@@ -1263,11 +1411,83 @@ program.command("publish").action(async () => {
1263
1411
  },
1264
1412
  }
1265
1413
  );
1266
- publishSpinner.succeed(chalk.green("Project published successfully!"));
1414
+ publishSpinner.succeed(
1415
+ chalk.green(`Project ${projectId} published successfully!`)
1416
+ );
1267
1417
  } catch (error) {
1268
- console.log(error);
1269
- publishSpinner.fail(chalk.red(`✗ Error publishing project: ${(error as Error).message}`));
1418
+ allPublishSuccess = false;
1419
+ publishSpinner.fail(
1420
+ chalk.red(
1421
+ `✗ Error publishing project ${projectId}: ${(error as Error).message}`
1422
+ )
1423
+ );
1270
1424
  }
1425
+
1426
+ // Step 3: Publish replicated projects
1427
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1428
+ publishSpinner.text = chalk.blue("Publishing replicated projects...");
1429
+ for (const repId of replicate) {
1430
+ try {
1431
+ await axios.post(
1432
+ apiUrl,
1433
+ { view_id: parseInt(repId, 10) },
1434
+ {
1435
+ headers: {
1436
+ Authorization: `Bearer ${accessToken}`,
1437
+ "Content-Type": "application/json",
1438
+ },
1439
+ }
1440
+ );
1441
+ console.log(
1442
+ chalk.green(`✓ Successfully published replicated project ${repId}.`)
1443
+ );
1444
+ } catch (error) {
1445
+ allPublishSuccess = false;
1446
+ console.warn(
1447
+ chalk.yellow(
1448
+ `⚠ Error publishing replicated project ${repId}: ${(error as Error).message}`
1449
+ )
1450
+ );
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ if (allPushSuccess && allPublishSuccess) {
1456
+ publishSpinner.succeed(chalk.green("All projects published successfully!"));
1457
+ } else {
1458
+ publishSpinner.warn(chalk.yellow("Publishing completed with some errors."));
1459
+ }
1460
+ });
1461
+
1462
+ program.command("mirror").action(() => {
1463
+ console.log(chalk.cyan("🌐 Starting mirror mode..."));
1464
+ const pwd = process.cwd();
1465
+ const srcDir = path.join(pwd, "src");
1466
+ if (!fs.existsSync(srcDir)) {
1467
+ console.error(
1468
+ chalk.red(
1469
+ "✗ Source directory 'src' does not exist. Please clone a project first."
1470
+ )
1471
+ );
1472
+ return;
1473
+ }
1474
+
1475
+ const fileManager = new FileManager(srcDir);
1476
+ const initialFiles = fileManager.listFiles();
1477
+
1478
+ // Lấy danh sách file và viết ra 1 file .txt
1479
+ let content = ``;
1480
+ map(initialFiles, (file) => {
1481
+ content += `\`\`\`typescript
1482
+ // Path: ${file.path}
1483
+ ${file.content}
1484
+
1485
+ \`\`\`
1486
+ `;
1487
+ });
1488
+ const outputFile = path.join(pwd, "hapico_files.txt");
1489
+ fs.writeFileSync(outputFile, content, { encoding: "utf8" });
1490
+ console.log(chalk.green(`✓ File list saved to ${outputFile}`));
1271
1491
  });
1272
1492
 
1273
1493
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hapico/cli",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "A simple CLI tool for project management",
5
5
  "main": "index.js",
6
6
  "bin": {
package/test.tsx ADDED
File without changes