@hapico/cli 0.0.19 → 0.0.20

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.20").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(
@@ -578,7 +601,7 @@ program
578
601
  const info = JSON.stringify({
579
602
  id: sessionId,
580
603
  createdAt: new Date().toISOString(),
581
- viewId: getStoredProjectId(pwd),
604
+ viewId: getStoredProjectId(pwd)?.projectId,
582
605
  });
583
606
 
584
607
  // Convert info to base64
@@ -596,12 +619,47 @@ program
596
619
  const room = new RoomState(`view_${projectId}`, []);
597
620
  const fileManager = new FileManager(srcDir);
598
621
  const initialFiles = fileManager.listFiles();
599
- room.files = initialFiles;
622
+ // get tsconfig.json
623
+ const tsconfigPath = path.join(srcDir, "..", "tsconfig.json");
624
+ if (fs.existsSync(tsconfigPath)) {
625
+ const content = fs.readFileSync(tsconfigPath, { encoding: "utf8" });
626
+ initialFiles.push({
627
+ path: "./tsconfig.json",
628
+ content,
629
+ });
630
+ }
631
+ // Remove All binary files
632
+ const supportExtensions = [
633
+ ".ts",
634
+ ".tsx",
635
+ ".js",
636
+ ".jsx",
637
+ ".json",
638
+ ".css",
639
+ ".html",
640
+ ".env",
641
+ ".env.local",
642
+ ".env.development",
643
+ ".env.production",
644
+ ".jsonc",
645
+ ".yml",
646
+ ".yaml",
647
+ ".md",
648
+ ".markdown",
649
+ ".txt",
650
+ ".xml",
651
+ ".config",
652
+ ];
653
+ const filteredFiles = initialFiles.filter((file) => {
654
+ return supportExtensions.some((ext) => file.path.endsWith(ext));
655
+ });
656
+
657
+ room.files = filteredFiles;
600
658
 
601
659
  room.connect(async () => {
602
660
  devSpinner.succeed(chalk.green("Project started in development mode!"));
603
661
 
604
- room.updateState("view", initialFiles);
662
+ room.updateState("view", filteredFiles);
605
663
 
606
664
  fileManager.setOnFileChange((filePath, content) => {
607
665
  const es5 = compileES5(content, filePath) ?? "";
@@ -619,8 +677,8 @@ program
619
677
  });
620
678
 
621
679
  // Fetch project info
622
- const projectInfo = getStoredProjectId(pwd);
623
- if (!projectInfo) {
680
+ const store = getStoredProjectId(pwd);
681
+ if (!store.projectId) {
624
682
  console.error(
625
683
  chalk.red(
626
684
  "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
@@ -629,7 +687,7 @@ program
629
687
  return;
630
688
  }
631
689
  const project = await axios.get(
632
- `https://base.myworkbeast.com/api/views/${projectInfo}`,
690
+ `https://base.myworkbeast.com/api/views/${store.projectId}`,
633
691
  {
634
692
  headers: {
635
693
  Authorization: `Bearer ${accessToken}`,
@@ -672,23 +730,16 @@ program
672
730
  });
673
731
 
674
732
  // Hàm tái sử dụng để push mã nguồn lên server
675
- async function pushProject(spinner: Ora): Promise<boolean> {
733
+ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
676
734
  const data = getStoredToken();
677
735
  const { accessToken } = data || {};
678
736
  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
737
  spinner.fail(
686
- chalk.red(
687
- "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
688
- )
738
+ chalk.red("✗ You need to login first. Use 'hapico login' command.")
689
739
  );
690
740
  return false;
691
741
  }
742
+ const pwd = process.cwd();
692
743
  const srcDir: string = path.join(pwd, "src");
693
744
  if (!fs.existsSync(srcDir)) {
694
745
  spinner.fail(
@@ -701,17 +752,35 @@ async function pushProject(spinner: Ora): Promise<boolean> {
701
752
  const fileManager = new FileManager(srcDir);
702
753
  const files = fileManager.listFiles();
703
754
 
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
- }
755
+ // Supported files
756
+ const SUPPORT_FILES = [
757
+ "./.env",
758
+ "./.env.local",
759
+ "./.env.development",
760
+ "./.env.production",
761
+ "./package.json",
762
+ "./tsconfig.json",
763
+ ];
764
+
765
+ // Include supported files
766
+ SUPPORT_FILES.forEach((relativePath) => {
767
+ const fullPath = path.join(pwd, relativePath);
768
+ if (fs.existsSync(fullPath)) {
769
+ console.log(
770
+ chalk.green(
771
+ `Including ${relativePath} in push for project ${projectId}.`
772
+ )
773
+ );
774
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
775
+ files.push({
776
+ path: relativePath,
777
+ content,
778
+ es5: compileES5(content, fullPath) ?? "",
779
+ });
780
+ }
781
+ });
782
+ spinner.text = `Pushing project source code to server for project ${projectId}...`;
783
+
715
784
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
716
785
  try {
717
786
  await axios.put(
@@ -730,7 +799,11 @@ async function pushProject(spinner: Ora): Promise<boolean> {
730
799
  );
731
800
  return true;
732
801
  } catch (error) {
733
- spinner.fail(chalk.red(`✗ Error saving project: ${(error as Error).message}`));
802
+ spinner.fail(
803
+ chalk.red(
804
+ `✗ Error saving project ${projectId}: ${(error as Error).message}`
805
+ )
806
+ );
734
807
  return false;
735
808
  }
736
809
  }
@@ -742,10 +815,49 @@ program
742
815
  const saveSpinner: Ora = ora(
743
816
  chalk.blue("Saving project source code...")
744
817
  ).start();
745
- const success = await pushProject(saveSpinner);
746
- if (success) {
818
+ const pwd = process.cwd();
819
+ const { projectId, replicate } = getStoredProjectId(pwd);
820
+
821
+ if (!projectId) {
822
+ saveSpinner.fail(
823
+ chalk.red(
824
+ "✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
825
+ )
826
+ );
827
+ return;
828
+ }
829
+
830
+ // Push to the main project
831
+ const mainSuccess = await pushProject(saveSpinner, projectId);
832
+ let allSuccess = mainSuccess;
833
+
834
+ // Push to replicated projects if replicate array exists
835
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
836
+ saveSpinner.text = chalk.blue("Pushing to replicated projects...");
837
+ for (const repId of replicate) {
838
+ const success = await pushProject(saveSpinner, repId);
839
+ if (!success) {
840
+ allSuccess = false;
841
+ console.warn(
842
+ chalk.yellow(
843
+ `⚠ Failed to push to replicated project ${repId}. Continuing...`
844
+ )
845
+ );
846
+ } else {
847
+ console.log(
848
+ chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
849
+ );
850
+ }
851
+ }
852
+ }
853
+
854
+ if (allSuccess) {
747
855
  saveSpinner.succeed(
748
- chalk.green("Project source code saved successfully!")
856
+ chalk.green("Project source code saved successfully to all projects!")
857
+ );
858
+ } else {
859
+ saveSpinner.warn(
860
+ chalk.yellow("Project source code saved with some errors.")
749
861
  );
750
862
  }
751
863
  });
@@ -838,7 +950,7 @@ program
838
950
  return;
839
951
  }
840
952
  const pwd: string = process.cwd();
841
- const projectId = getStoredProjectId(pwd);
953
+ const { projectId } = getStoredProjectId(pwd);
842
954
  if (!projectId) {
843
955
  console.error(
844
956
  chalk.red(
@@ -852,15 +964,22 @@ program
852
964
  ).start();
853
965
  try {
854
966
  const response: ApiResponse = await axios.get(
855
- `https://base.myworkbeast.com/api/views/${projectId}`,
967
+ `https://base.myworkbeast.com/api/views/v3/${projectId}`,
856
968
  {
857
969
  headers: {
858
- Authorization: `Bearer ${token}`,
970
+ Authorization: `Bearer ${token.accessToken}`,
859
971
  "Content-Type": "application/json",
860
972
  },
861
973
  }
862
974
  );
863
- const files: FileContent[] = response.data.files || [];
975
+
976
+ const code = response?.data?.code;
977
+ const decompressedCode = pako.inflate(
978
+ Uint8Array.from(atob(code), (c) => c.charCodeAt(0)),
979
+ { to: "string" }
980
+ );
981
+
982
+ const files: FileContent[] = tryJSONParse(decompressedCode)?.files || [];
864
983
  const fileManager = new FileManager(path.join(pwd, "src"));
865
984
  fileManager.syncFiles(files);
866
985
  apiSpinner.succeed(chalk.green("Project files updated successfully!"));
@@ -999,7 +1118,9 @@ program
999
1118
  migrationsDir,
1000
1119
  `${index + 1}_migration_${timestamp}.sql`
1001
1120
  );
1002
- fs.writeFileSync(filename, (migration as any).sql, { encoding: "utf8" });
1121
+ fs.writeFileSync(filename, (migration as any).sql, {
1122
+ encoding: "utf8",
1123
+ });
1003
1124
  });
1004
1125
 
1005
1126
  console.log(chalk.green("✓ Migrations saved successfully!"));
@@ -1011,7 +1132,6 @@ program
1011
1132
  ).start();
1012
1133
 
1013
1134
  try {
1014
- files = response.data.files || [];
1015
1135
  apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
1016
1136
 
1017
1137
  const saveSpinner: Ora = ora(
@@ -1232,17 +1352,12 @@ program
1232
1352
 
1233
1353
  // hapico publish
1234
1354
  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();
1355
+ const publishSpinner: Ora = ora(
1356
+ chalk.blue("Publishing to Hapico...")
1357
+ ).start();
1244
1358
  const pwd = process.cwd();
1245
- const projectId = getStoredProjectId(pwd);
1359
+ const { projectId, replicate } = getStoredProjectId(pwd);
1360
+
1246
1361
  if (!projectId) {
1247
1362
  publishSpinner.fail(
1248
1363
  chalk.red(
@@ -1251,11 +1366,42 @@ program.command("publish").action(async () => {
1251
1366
  );
1252
1367
  return;
1253
1368
  }
1369
+
1370
+ // Step 1: Push source code to main project and replicas
1371
+ const pushSuccess = await pushProject(publishSpinner, projectId);
1372
+ if (!pushSuccess) {
1373
+ return; // Stop if push to main project fails
1374
+ }
1375
+
1376
+ // Push to replicated projects
1377
+ let allPushSuccess = true;
1378
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1379
+ publishSpinner.text = chalk.blue("Pushing to replicated projects...");
1380
+ for (const repId of replicate) {
1381
+ const success = await pushProject(publishSpinner, repId);
1382
+ if (!success) {
1383
+ allPushSuccess = false;
1384
+ console.warn(
1385
+ chalk.yellow(
1386
+ `⚠ Failed to push to replicated project ${repId}. Continuing...`
1387
+ )
1388
+ );
1389
+ } else {
1390
+ console.log(
1391
+ chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
1392
+ );
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ // Step 2: Publish main project
1398
+ const { accessToken } = getStoredToken();
1254
1399
  const apiUrl = "https://base.myworkbeast.com/api/views/publish";
1400
+ let allPublishSuccess = true;
1255
1401
  try {
1256
1402
  await axios.post(
1257
1403
  apiUrl,
1258
- { view_id: parseInt(projectId, 10) }, // Đảm bảo viewId là number
1404
+ { view_id: parseInt(projectId, 10) },
1259
1405
  {
1260
1406
  headers: {
1261
1407
  Authorization: `Bearer ${accessToken}`,
@@ -1263,11 +1409,83 @@ program.command("publish").action(async () => {
1263
1409
  },
1264
1410
  }
1265
1411
  );
1266
- publishSpinner.succeed(chalk.green("Project published successfully!"));
1412
+ publishSpinner.succeed(
1413
+ chalk.green(`Project ${projectId} published successfully!`)
1414
+ );
1267
1415
  } catch (error) {
1268
- console.log(error);
1269
- publishSpinner.fail(chalk.red(`✗ Error publishing project: ${(error as Error).message}`));
1416
+ allPublishSuccess = false;
1417
+ publishSpinner.fail(
1418
+ chalk.red(
1419
+ `✗ Error publishing project ${projectId}: ${(error as Error).message}`
1420
+ )
1421
+ );
1422
+ }
1423
+
1424
+ // Step 3: Publish replicated projects
1425
+ if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1426
+ publishSpinner.text = chalk.blue("Publishing replicated projects...");
1427
+ for (const repId of replicate) {
1428
+ try {
1429
+ await axios.post(
1430
+ apiUrl,
1431
+ { view_id: parseInt(repId, 10) },
1432
+ {
1433
+ headers: {
1434
+ Authorization: `Bearer ${accessToken}`,
1435
+ "Content-Type": "application/json",
1436
+ },
1437
+ }
1438
+ );
1439
+ console.log(
1440
+ chalk.green(`✓ Successfully published replicated project ${repId}.`)
1441
+ );
1442
+ } catch (error) {
1443
+ allPublishSuccess = false;
1444
+ console.warn(
1445
+ chalk.yellow(
1446
+ `⚠ Error publishing replicated project ${repId}: ${(error as Error).message}`
1447
+ )
1448
+ );
1449
+ }
1450
+ }
1451
+ }
1452
+
1453
+ if (allPushSuccess && allPublishSuccess) {
1454
+ publishSpinner.succeed(chalk.green("All projects published successfully!"));
1455
+ } else {
1456
+ publishSpinner.warn(chalk.yellow("Publishing completed with some errors."));
1457
+ }
1458
+ });
1459
+
1460
+ program.command("mirror").action(() => {
1461
+ console.log(chalk.cyan("🌐 Starting mirror mode..."));
1462
+ const pwd = process.cwd();
1463
+ const srcDir = path.join(pwd, "src");
1464
+ if (!fs.existsSync(srcDir)) {
1465
+ console.error(
1466
+ chalk.red(
1467
+ "✗ Source directory 'src' does not exist. Please clone a project first."
1468
+ )
1469
+ );
1470
+ return;
1270
1471
  }
1472
+
1473
+ const fileManager = new FileManager(srcDir);
1474
+ const initialFiles = fileManager.listFiles();
1475
+
1476
+ // Lấy danh sách file và viết ra 1 file .txt
1477
+ let content = ``;
1478
+ map(initialFiles, (file) => {
1479
+ content += `\`\`\`typescript
1480
+ // Path: ${file.path}
1481
+ ${file.content}
1482
+
1483
+ \`\`\`
1484
+ `;
1485
+ });
1486
+ const outputFile = path.join(pwd, "hapico_files.txt");
1487
+ fs.writeFileSync(outputFile, content, { encoding: "utf8" });
1488
+ console.log(chalk.green(`✓ File list saved to ${outputFile}`));
1271
1489
  });
1272
1490
 
1273
- program.parse(process.argv);
1491
+ 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.20",
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