@hapico/cli 0.0.29 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -56,6 +56,7 @@ const util_1 = require("util");
56
56
  const chalk_1 = __importDefault(require("chalk"));
57
57
  const pako_1 = __importDefault(require("pako"));
58
58
  const vibe_1 = require("./tools/vibe");
59
+ const nativewind_1 = require("./tools/nativewind");
59
60
  // Promisify exec for async usage
60
61
  const execPromise = (0, util_1.promisify)(child_process_1.exec);
61
62
  // Directory to store the token and project config
@@ -113,7 +114,7 @@ const getStoredProjectId = (projectDir) => {
113
114
  replicate: [],
114
115
  };
115
116
  };
116
- // Khởi tạo cache bằng Map để lưu trữ kết quả compile
117
+ // Initialize cache using Map to store compilation results
117
118
  const compileCache = new Map();
118
119
  const tryJSONParse = (str) => {
119
120
  try {
@@ -173,7 +174,7 @@ class FileManager {
173
174
  }
174
175
  }
175
176
  replaceSplashWithSeparator(filePath) {
176
- // Thay thế dấu "\" với dấu "/" trong đường dẫn file
177
+ // Replace "\" with "/" in the file path
177
178
  return filePath.replace(/\\/g, "/");
178
179
  }
179
180
  listFiles() {
@@ -186,11 +187,11 @@ class FileManager {
186
187
  traverseDirectory(fullPath);
187
188
  }
188
189
  else if (entry.isFile()) {
189
- // skip các file trong thư mục bị ignore
190
+ // skip files in ignored folders
190
191
  if (ignoreFolders.some((folder) => fullPath.includes(folder))) {
191
192
  return;
192
193
  }
193
- // chỉ hổ trợ các files nằm trong ./src
194
+ // only support files located in ./src
194
195
  if (!fullPath.includes("/src/") && !fullPath.includes("\\src\\")) {
195
196
  return;
196
197
  }
@@ -310,7 +311,9 @@ class RoomState {
310
311
  // Set binaryType to 'arraybuffer' to handle binary data
311
312
  this.ws.binaryType = "arraybuffer";
312
313
  this.ws.on("open", () => {
313
- console.log(chalk_1.default.green(`Connected to room: ${this.roomId}`));
314
+ if (this.reconnectAttempts > 0) {
315
+ console.log(chalk_1.default.greenBright(`\n✨ Sync connection restored for session: ${this.roomId}`));
316
+ }
314
317
  this.isConnected = true;
315
318
  this.reconnectAttempts = 0;
316
319
  onConnected === null || onConnected === void 0 ? void 0 : onConnected(); // Call the onConnected callback if provided
@@ -336,7 +339,7 @@ class RoomState {
336
339
  jsonStr = pako_1.default.inflate(data, { to: "string" });
337
340
  }
338
341
  else {
339
- jsonStr = data.toString(); // Fallback nếu không nén
342
+ jsonStr = data.toString(); // Fallback if not compressed
340
343
  }
341
344
  const parsedData = JSON.parse(jsonStr);
342
345
  const includes = [
@@ -391,8 +394,8 @@ class RoomState {
391
394
  type: "update",
392
395
  state: { [key]: value },
393
396
  });
394
- const compressed = pako_1.default.deflate(message); // Nén dữ liệu
395
- this.ws.send(compressed); // Gửi binary
397
+ const compressed = pako_1.default.deflate(message); // Compress state data
398
+ this.ws.send(compressed); // Send binary payload
396
399
  }
397
400
  }
398
401
  disconnect() {
@@ -411,92 +414,84 @@ class RoomState {
411
414
  return this.isConnected;
412
415
  }
413
416
  }
414
- commander_1.program.version("0.0.29").description("Hapico CLI for project management");
417
+ commander_1.program.version("0.0.31").description("Hapico CLI for project management");
415
418
  commander_1.program
416
419
  .command("clone <id>")
417
420
  .description("Clone a project by ID")
418
421
  .action(async (id) => {
419
422
  var _a, _b, _c, _d;
423
+ console.clear();
424
+ console.log(chalk_1.default.bold.blue("\n📦 HAPICO CLONE PIPELINE"));
425
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
420
426
  const { accessToken } = getStoredToken();
421
427
  if (!accessToken) {
422
- console.error(chalk_1.default.red("✗ You need to login first. Use 'hapico login' command."));
428
+ console.log(chalk_1.default.red("✗ Authentication required. Please run 'hapico login' first."));
423
429
  return;
424
430
  }
425
- const projectDir = path.resolve(process.cwd(), id);
426
- if (fs.existsSync(projectDir)) {
427
- console.error(chalk_1.default.red(`✗ Project directory "${id}" already exists.`));
431
+ const outputDir = path.resolve(process.cwd(), id);
432
+ if (fs.existsSync(outputDir)) {
433
+ console.log(chalk_1.default.red(`✗ Destination directory "${id}" already exists.`));
428
434
  return;
429
435
  }
430
- let files = [];
431
- const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project data...")).start();
436
+ const statusSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project metadata...")).start();
432
437
  try {
433
- // ===== 1. Lấy version public/published (giữ nguyên như cũ) =====
438
+ // 1. Fetch metadata and initial files
434
439
  const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${id}`);
440
+ const projectType = response.data.type || "view";
435
441
  const code = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.code;
436
442
  const decompressedCode = pako_1.default.inflate(Uint8Array.from(atob(code), (c) => c.charCodeAt(0)), { to: "string" });
437
- files = ((_b = (0, exports.tryJSONParse)(decompressedCode)) === null || _b === void 0 ? void 0 : _b.files) || [];
438
- apiSpinner.succeed(chalk_1.default.green("Project data fetched successfully!"));
439
- // ===== 2. Download + extract template =====
440
- const templateSpinner = (0, ora_1.default)(chalk_1.default.blue("Downloading template...")).start();
443
+ const publicFiles = ((_b = (0, exports.tryJSONParse)(decompressedCode)) === null || _b === void 0 ? void 0 : _b.files) || [];
444
+ statusSpinner.succeed(chalk_1.default.green("Project metadata retrieved."));
445
+ console.log(chalk_1.default.bold.white("Project Details:"));
446
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("ID :")} ${chalk_1.default.cyan(id)}`);
447
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Type :")} ${chalk_1.default.magenta(projectType.toUpperCase())}\n`);
448
+ // 2. Setup Template
449
+ statusSpinner.start(chalk_1.default.blue("Initializing project template..."));
441
450
  const TEMPLATE_URL = "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
442
- const templateResponse = await axios_1.default.get(TEMPLATE_URL, {
443
- responseType: "arraybuffer",
444
- });
445
- templateSpinner.succeed(chalk_1.default.green("Template downloaded successfully!"));
446
- const outputDir = path.resolve(process.cwd(), id);
447
- if (!fs.existsSync(outputDir)) {
448
- fs.mkdirSync(outputDir);
449
- }
450
- const unzipSpinner = (0, ora_1.default)(chalk_1.default.blue("Extracting template...")).start();
451
+ const templateResponse = await axios_1.default.get(TEMPLATE_URL, { responseType: "arraybuffer" });
452
+ if (!fs.existsSync(outputDir))
453
+ fs.mkdirSync(outputDir, { recursive: true });
451
454
  await unzipper_1.default.Open.buffer(templateResponse.data).then((directory) => directory.extract({ path: outputDir }));
452
- unzipSpinner.succeed(chalk_1.default.green("Template extracted successfully!"));
453
- const macosxDir = path.join(process.cwd(), id, "__MACOSX");
454
- if (fs.existsSync(macosxDir)) {
455
+ const macosxDir = path.join(outputDir, "__MACOSX");
456
+ if (fs.existsSync(macosxDir))
455
457
  fs.rmSync(macosxDir, { recursive: true, force: true });
456
- }
457
- // Save project ID
458
458
  saveProjectId(outputDir, id);
459
- console.log(chalk_1.default.green("Project cloned successfully!"));
460
- // ===== 3. Save file từ version public =====
461
- const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project files...")).start();
462
- files.forEach((file) => {
463
- const filePath = path.join(process.cwd(), id, "src", file.path);
464
- const dir = path.dirname(filePath);
465
- if (!fs.existsSync(dir)) {
466
- fs.mkdirSync(dir, { recursive: true });
467
- }
468
- fs.writeFileSync(filePath, file.content);
469
- });
470
- saveSpinner.succeed(chalk_1.default.green("Project files saved successfully!"));
471
- // ===== 4. TỰ ĐỘNG PULL PHIÊN BẢN MỚI NHẤT (draft) – ĐÂY LÀ PHẦN MỚI =====
472
- const pullSpinner = (0, ora_1.default)(chalk_1.default.blue("Pulling latest changes from server...")).start();
459
+ statusSpinner.succeed(chalk_1.default.green("Base template initialized."));
460
+ // 3. Sync Source Files
461
+ statusSpinner.start(chalk_1.default.blue("Syncing source files..."));
462
+ const srcPath = path.join(outputDir, "src");
463
+ const fileManager = new FileManager(srcPath);
464
+ fileManager.syncFiles(publicFiles);
465
+ statusSpinner.succeed(chalk_1.default.green(`${publicFiles.length} files synchronized from production.`));
466
+ // 4. Pull Latest Draft (v3)
467
+ statusSpinner.start(chalk_1.default.blue("Checking for latest development changes..."));
473
468
  try {
474
469
  const pullResponse = await axios_1.default.get(`https://base.myworkbeast.com/api/views/v3/${id}`, {
475
- headers: {
476
- Authorization: `Bearer ${accessToken}`,
477
- "Content-Type": "application/json",
478
- },
470
+ headers: { Authorization: `Bearer ${accessToken}` },
479
471
  });
480
472
  const pullCode = (_c = pullResponse === null || pullResponse === void 0 ? void 0 : pullResponse.data) === null || _c === void 0 ? void 0 : _c.code;
481
- if (!pullCode) {
482
- pullSpinner.info(chalk_1.default.cyan("No draft version found – using published version."));
483
- }
484
- else {
473
+ if (pullCode) {
485
474
  const pullDecompressed = pako_1.default.inflate(Uint8Array.from(atob(pullCode), (c) => c.charCodeAt(0)), { to: "string" });
486
475
  const latestFiles = ((_d = (0, exports.tryJSONParse)(pullDecompressed)) === null || _d === void 0 ? void 0 : _d.files) || [];
487
- const fileManager = new FileManager(path.join(outputDir, "src"));
488
476
  fileManager.syncFiles(latestFiles);
489
- pullSpinner.succeed(chalk_1.default.green("Latest changes pulled successfully!"));
477
+ statusSpinner.succeed(chalk_1.default.green("Latest development changes integrated."));
478
+ }
479
+ else {
480
+ statusSpinner.info(chalk_1.default.gray("No draft version found, using published code."));
490
481
  }
491
482
  }
492
- catch (pullErr) {
493
- pullSpinner.warn(chalk_1.default.yellow(`⚠ Could not pull latest changes: ${pullErr.message}\n Run "hapico pull" manually later.`));
483
+ catch (e) {
484
+ statusSpinner.warn(chalk_1.default.yellow("Draft sync skipped (using production code)."));
494
485
  }
495
- // ===== 5. Hướng dẫn chạy dev =====
496
- console.log(chalk_1.default.cyan(`💡 Run ${chalk_1.default.bold("cd ${id} && npm install && hapico dev")} to start the project.`));
486
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
487
+ console.log(`\n${chalk_1.default.bold.white.bgGreen(" SUCCESS ")} ${chalk_1.default.green("Project cloned successfully!\n")}`);
488
+ console.log(chalk_1.default.bold.white("Next steps:"));
489
+ console.log(`${chalk_1.default.gray("1.")} ${chalk_1.default.cyan(`cd ${id}`)}`);
490
+ console.log(`${chalk_1.default.gray("2.")} ${chalk_1.default.cyan("npm install")}`);
491
+ console.log(`${chalk_1.default.gray("3.")} ${chalk_1.default.cyan("hapico dev")}\n`);
497
492
  }
498
493
  catch (error) {
499
- apiSpinner.fail(chalk_1.default.red(`Error cloning project: ${error.message}`));
494
+ statusSpinner.fail(chalk_1.default.red(`Clone failed: ${error.message}`));
500
495
  }
501
496
  });
502
497
  commander_1.program
@@ -510,71 +505,75 @@ commander_1.program
510
505
  .command("dev")
511
506
  .description("Start the project in development mode")
512
507
  .option("--zversion <version>", "Zalo version for QR code")
513
- .action((options) => {
514
- var _a;
508
+ .action(async (options) => {
515
509
  const { accessToken } = getStoredToken();
516
510
  if (!accessToken) {
517
511
  console.error(chalk_1.default.red("✗ You need to login first. Use 'hapico login' command."));
518
512
  return;
519
513
  }
520
- const devSpinner = (0, ora_1.default)(chalk_1.default.blue("Starting the project in development mode...")).start();
514
+ console.clear();
515
+ console.log(chalk_1.default.bold.cyan("\n🚀 HAPICO DEVELOPMENT SERVER"));
516
+ console.log(chalk_1.default.gray("─────────────────────────────────────────"));
517
+ const devSpinner = (0, ora_1.default)(chalk_1.default.blue("Initializing environment...")).start();
521
518
  const pwd = process.cwd();
522
519
  const srcDir = path.join(pwd, "src");
523
520
  if (!fs.existsSync(srcDir)) {
524
- devSpinner.fail(chalk_1.default.red("Source directory 'src' does not exist. Please clone a project first."));
521
+ devSpinner.fail(chalk_1.default.red("Source directory 'src' not found. Ensure you are in a valid project folder."));
525
522
  return;
526
523
  }
527
- // Directory to store session config
528
524
  const tmpDir = path.join(pwd, ".tmp");
529
525
  const sessionConfigFile = path.join(tmpDir, "config.json");
530
- // Ensure .tmp directory exists
531
- if (!fs.existsSync(tmpDir)) {
526
+ if (!fs.existsSync(tmpDir))
532
527
  fs.mkdirSync(tmpDir, { recursive: true });
533
- }
534
- // Function to get stored session ID
535
528
  const getStoredSessionId = () => {
536
529
  if (fs.existsSync(sessionConfigFile)) {
537
- const data = fs.readFileSync(sessionConfigFile, { encoding: "utf8" });
538
- const json = JSON.parse(data);
539
- return json.sessionId || null;
530
+ try {
531
+ return JSON.parse(fs.readFileSync(sessionConfigFile, "utf8")).sessionId;
532
+ }
533
+ catch (_a) {
534
+ return null;
535
+ }
540
536
  }
541
537
  return null;
542
538
  };
543
- // Function to save session ID
544
- const saveSessionId = (sessionId) => {
545
- fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2), { encoding: "utf8" });
546
- };
547
- // Get or generate session ID
548
- let sessionId = getStoredSessionId();
549
- if (!sessionId) {
550
- sessionId = (0, crypto_1.randomUUID)();
551
- saveSessionId(sessionId);
552
- }
539
+ let sessionId = getStoredSessionId() || (0, crypto_1.randomUUID)();
540
+ fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2));
541
+ const projectConfig = getStoredProjectId(pwd);
553
542
  const info = JSON.stringify({
554
543
  id: sessionId,
555
544
  createdAt: new Date().toISOString(),
556
- viewId: (_a = getStoredProjectId(pwd)) === null || _a === void 0 ? void 0 : _a.projectId,
545
+ viewId: projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.projectId,
557
546
  });
558
- // Convert info to base64
559
547
  const projectId = Buffer.from(info).toString("base64");
560
- if (!projectId) {
561
- devSpinner.fail(chalk_1.default.red("Project ID not found. Please ensure hapico.config.json exists in the project directory."));
548
+ if (!(projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.projectId)) {
549
+ devSpinner.fail(chalk_1.default.red("Project ID missing in hapico.config.json"));
562
550
  return;
563
551
  }
564
- console.log(chalk_1.default.cyan("🔗 Connecting to WebSocket server"));
565
- const room = new RoomState(`view_${projectId}`, []);
552
+ devSpinner.text = chalk_1.default.blue("Scanning project directory...");
566
553
  const fileManager = new FileManager(srcDir);
567
554
  const initialFiles = fileManager.listFiles();
568
- // get tsconfig.json
569
- const tsconfigPath = path.join(srcDir, "..", "tsconfig.json");
570
- if (fs.existsSync(tsconfigPath)) {
571
- const content = fs.readFileSync(tsconfigPath, { encoding: "utf8" });
572
- initialFiles.push({
573
- path: "./tsconfig.json",
574
- content,
575
- });
576
- }
577
- // Remove All binary files
555
+ // Supported files outside src
556
+ const SUPPORT_FILES = [
557
+ "./.env",
558
+ "./.env.local",
559
+ "./.env.development",
560
+ "./.env.production",
561
+ "./package.json",
562
+ "./tsconfig.json",
563
+ "./nativewind.json"
564
+ ];
565
+ SUPPORT_FILES.forEach((relativePath) => {
566
+ var _a;
567
+ const fullPath = path.join(pwd, relativePath);
568
+ if (fs.existsSync(fullPath)) {
569
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
570
+ initialFiles.push({
571
+ path: relativePath,
572
+ content,
573
+ es5: (_a = (0, exports.compileES5)(content, fullPath)) !== null && _a !== void 0 ? _a : "",
574
+ });
575
+ }
576
+ });
578
577
  const supportExtensions = [
579
578
  ".ts",
580
579
  ".tsx",
@@ -587,100 +586,118 @@ commander_1.program
587
586
  ".env.development",
588
587
  ".env.production",
589
588
  ];
590
- const filteredFiles = initialFiles.filter((file) => {
591
- return supportExtensions.some((ext) => file.path.endsWith(ext));
592
- });
589
+ const filteredFiles = initialFiles.filter((file) => supportExtensions.some((ext) => file.path.endsWith(ext)));
590
+ devSpinner.text = chalk_1.default.blue("Establishing WebSocket connection...");
591
+ const room = new RoomState(`view_${projectId}`, []);
593
592
  room.files = filteredFiles;
594
593
  room.connect(async () => {
595
- devSpinner.succeed(chalk_1.default.green("Project started in development mode!"));
594
+ devSpinner.succeed(chalk_1.default.greenBright("Sync Engine Ready"));
595
+ console.log(`\n${chalk_1.default.bold.white.bgGreen(" SUCCESS ")} ${chalk_1.default.green("Connection established with workspace.\n")}`);
596
+ console.log(chalk_1.default.bold.white("Workspace Configuration:"));
597
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("Project ID :")} ${chalk_1.default.cyan(projectConfig.projectId)}`);
598
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("Session :")} ${chalk_1.default.gray(sessionId)}`);
599
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Local Path :")} ${chalk_1.default.gray(pwd)}`);
600
+ console.log("");
596
601
  room.updateState("view", filteredFiles);
597
- // Debounce the state update to avoid overwhelming the server while typing/saving
598
- const debouncedUpdate = (0, lodash_1.debounce)((updatedFiles) => {
599
- room.updateState("view", updatedFiles);
600
- }, 200);
601
- fileManager.setOnFileChange((filePath, content) => {
602
- var _a;
603
- const es5 = (_a = (0, exports.compileES5)(content, filePath)) !== null && _a !== void 0 ? _a : "";
604
- console.log(chalk_1.default.yellow(`📝 File changed: ${filePath === null || filePath === void 0 ? void 0 : filePath.replace(srcDir, ".")}`));
605
- const updatedFiles = room.files.map((file) => {
606
- if (path.join(srcDir, file.path) === filePath) {
607
- return { ...file, content, es5 };
608
- }
609
- return file;
602
+ try {
603
+ const project = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${projectConfig.projectId}`, {
604
+ headers: { Authorization: `Bearer ${accessToken}` }
610
605
  });
611
- room.files = updatedFiles;
612
- debouncedUpdate(updatedFiles);
613
- });
614
- // Fetch project info
615
- const store = getStoredProjectId(pwd);
616
- if (!store.projectId) {
617
- console.error(chalk_1.default.red("✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."));
618
- return;
619
- }
620
- const project = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${store.projectId}`, {
621
- headers: {
622
- Authorization: `Bearer ${accessToken}`,
623
- "Content-Type": "application/json",
624
- },
625
- });
626
- if (project.status !== 200) {
627
- console.error(chalk_1.default.red(`✗ Error fetching project info: ${project.statusText}`));
628
- return;
629
- }
630
- const projectType = project.data.type || "view";
631
- const zversion = options.zversion;
632
- if (projectType === "expo_app" || projectType === "emg_edu_lesson") {
633
- const BASE_EXPO_LINK = `exp://u.expo.dev/e362c6df-abe8-4503-8723-1362f015d167/group/e5d6b96e-3c78-485a-a170-1d8aa8b2c47e`;
634
- const link = `${BASE_EXPO_LINK}?sessionKey=${projectId}&mode=development`;
635
- qrcode_terminal_1.default.generate(link, { small: true }, (qrcode) => {
636
- console.log(chalk_1.default.cyan("📱 Scan this QR code with Expo Go to connect to the project:"));
637
- console.log(qrcode);
606
+ const projectType = project.data.type || "view";
607
+ const isExpo = projectType === "expo_app" || projectType === "emg_edu_lesson";
608
+ console.log(chalk_1.default.bold.white("Environment Status:"));
609
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("Type :")} ${chalk_1.default.magenta(projectType.toUpperCase())}`);
610
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Watcher :")} ${chalk_1.default.greenBright("Active & Synchronizing")}`);
611
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────\n"));
612
+ if (isExpo) {
613
+ try {
614
+ await (0, nativewind_1.compileAndSaveToFolder)("src", "nativewind.json");
615
+ }
616
+ catch (e) { }
617
+ }
618
+ const debouncedNativeWind = (0, lodash_1.debounce)(async () => {
619
+ if (isExpo) {
620
+ process.stdout.write(chalk_1.default.gray(" [System] Style assets synchronized successfully.\r"));
621
+ try {
622
+ await (0, nativewind_1.compileAndSaveToFolder)("src", "nativewind.json");
623
+ }
624
+ catch (e) { }
625
+ }
626
+ }, 500);
627
+ const debouncedUpdate = (0, lodash_1.debounce)((updatedFiles) => {
628
+ room.updateState("view", updatedFiles);
629
+ }, 200);
630
+ fileManager.setOnFileChange((filePath, content) => {
631
+ var _a;
632
+ const timestamp = new Date().toLocaleTimeString([], { hour12: false });
633
+ const relativePath = filePath.replace(srcDir, ".");
634
+ console.log(`${chalk_1.default.gray(`[${timestamp}]`)} ${chalk_1.default.yellow.bold("CHANGE")} ${chalk_1.default.white(relativePath)}`);
635
+ const es5 = (_a = (0, exports.compileES5)(content, filePath)) !== null && _a !== void 0 ? _a : "";
636
+ const updatedFiles = room.files.map((file) => {
637
+ if (path.join(srcDir, file.path) === filePath)
638
+ return { ...file, content, es5 };
639
+ return file;
640
+ });
641
+ room.files = updatedFiles;
642
+ debouncedUpdate(updatedFiles);
643
+ if (isExpo)
644
+ debouncedNativeWind();
638
645
  });
639
- return;
640
- }
641
- else if (projectType === "zalominiapp") {
642
- console.log("zversion", zversion);
643
- if (!zversion) {
644
- qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}`, { small: true }, (qrcode) => {
645
- console.log(chalk_1.default.cyan("📱 Scan this QR code to connect to the project:"));
646
- console.log(qrcode);
646
+ // UI Access Section
647
+ if (isExpo) {
648
+ const EXPO_GROUP_ID = "6f033d8b-7ed5-48dc-ab6f-f8348cf6993b";
649
+ const link = `exp://u.expo.dev/e362c6df-abe8-4503-8723-1362f015d167/group/${EXPO_GROUP_ID}?sessionKey=${projectId}&mode=development`;
650
+ console.log(chalk_1.default.bold.black.bgCyan(" 📱 EXPO GO ACCESS "));
651
+ qrcode_terminal_1.default.generate(link, { small: true }, (code) => {
652
+ console.log(chalk_1.default.white(code));
647
653
  });
654
+ console.log(chalk_1.default.cyan(` Link: ${chalk_1.default.underline(link)}\n`));
648
655
  }
649
- else {
650
- qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`, { small: true }, (qrcode) => {
651
- console.log(chalk_1.default.cyan("📱 Scan this QR code to connect to the project:"));
652
- console.log(qrcode);
656
+ else if (projectType === "zalominiapp" || projectType === "zalominiapp2") {
657
+ const zversion = options.zversion;
658
+ const zaloUrl = zversion
659
+ ? `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`
660
+ : `https://zalo.me/s/3218692650896662017/player/${projectId}`;
661
+ const label = projectType === "zalominiapp2" ? " 🧩 ZALO MINI APP 2.0 " : " 🧩 ZALO MINI APP ";
662
+ console.log(chalk_1.default.bold.black.bgBlue(label));
663
+ qrcode_terminal_1.default.generate(zaloUrl, { small: true }, (code) => {
664
+ console.log(chalk_1.default.white(code));
653
665
  });
666
+ console.log(chalk_1.default.blue(` Link: ${chalk_1.default.underline(zaloUrl)}\n`));
654
667
  }
655
- return;
668
+ else {
669
+ const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
670
+ const label = projectType === "web2" ? " 🌐 WEB 2.0 PREVIEW " : " 🌐 BROWSER PREVIEW ";
671
+ console.log(chalk_1.default.bold.black.bgGreen(label));
672
+ console.log(chalk_1.default.green(` URL: ${chalk_1.default.underline(previewUrl)}\n`));
673
+ await (0, open_1.default)(previewUrl);
674
+ }
675
+ console.log(chalk_1.default.gray("👀 Monitoring file changes... (Press Ctrl+C to exit)"));
656
676
  }
657
- else {
658
- const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
659
- console.log(chalk_1.default.cyan(`🌐 Open this URL in your browser to preview the project:\n${previewUrl}`));
660
- await (0, open_1.default)(previewUrl);
677
+ catch (err) {
678
+ console.log(chalk_1.default.yellow(`\n⚠️ Environment metadata could not be fetched: ${err.message}`));
679
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────\n"));
661
680
  }
662
681
  });
663
682
  });
664
- // Hàm tái sử dụng để push nguồn lên server
665
- async function pushProject(spinner, projectId) {
683
+ // Chuyên nghiệp hóa việc push source code lên server
684
+ async function pushProject(projectId) {
666
685
  const data = getStoredToken();
667
686
  const { accessToken } = data || {};
668
687
  if (!accessToken) {
669
- spinner.fail(chalk_1.default.red("✗ You need to login first. Use 'hapico login' command."));
670
- return false;
688
+ return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Authentication required. Please run 'hapico login'." };
671
689
  }
672
690
  const pwd = process.cwd();
673
691
  const srcDir = path.join(pwd, "src");
674
692
  if (!fs.existsSync(srcDir)) {
675
- spinner.fail(chalk_1.default.red("Source directory 'src' does not exist. Please clone a project first."));
676
- return false;
693
+ return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Source directory 'src' not found. Ensure you are in a valid project folder." };
677
694
  }
678
695
  const fileManager = new FileManager(srcDir);
679
696
  const files = fileManager.listFiles().filter((file) => {
680
697
  const extname = path.extname(file.path);
681
698
  return supportedExtensions.includes(extname);
682
699
  });
683
- // Supported files
700
+ // Supported extra files at root
684
701
  const SUPPORT_FILES = [
685
702
  "./.env",
686
703
  "./.env.local",
@@ -688,13 +705,12 @@ async function pushProject(spinner, projectId) {
688
705
  "./.env.production",
689
706
  "./package.json",
690
707
  "./tsconfig.json",
708
+ "./nativewind.json"
691
709
  ];
692
- // Include supported files
693
710
  SUPPORT_FILES.forEach((relativePath) => {
694
711
  var _a;
695
712
  const fullPath = path.join(pwd, relativePath);
696
713
  if (fs.existsSync(fullPath)) {
697
- console.log(chalk_1.default.green(`Including ${relativePath} in push for project ${projectId}.`));
698
714
  const content = fs.readFileSync(fullPath, { encoding: "utf8" });
699
715
  files.push({
700
716
  path: relativePath,
@@ -703,79 +719,112 @@ async function pushProject(spinner, projectId) {
703
719
  });
704
720
  }
705
721
  });
706
- // Show ra thông tin dung lượng mã nguồn
707
722
  const totalSize = files.reduce((acc, file) => acc + file.content.length, 0);
708
- console.log(chalk_1.default.cyan(`Total source code size for project ${projectId}: ${(totalSize / 1024).toFixed(2)} KB`));
709
- spinner.text = `Pushing project source code to server for project ${projectId}...`;
710
723
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
711
724
  try {
712
- await axios_1.default.put(apiUrl, {
713
- code: JSON.stringify({
714
- files,
715
- }),
716
- }, {
725
+ await axios_1.default.put(apiUrl, { code: JSON.stringify({ files }) }, {
726
+ headers: {
727
+ Authorization: `Bearer ${accessToken}`,
728
+ "Content-Type": "application/json",
729
+ },
730
+ });
731
+ return { success: true, projectId, totalSize, fileCount: files.length };
732
+ }
733
+ catch (error) {
734
+ return { success: false, projectId, totalSize, fileCount: files.length, error: error.message };
735
+ }
736
+ }
737
+ async function publishProjectApi(projectId) {
738
+ const { accessToken } = getStoredToken();
739
+ const apiUrl = "https://base.myworkbeast.com/api/views/publish/v2";
740
+ try {
741
+ await axios_1.default.post(apiUrl, { view_id: parseInt(projectId, 10) }, {
717
742
  headers: {
718
743
  Authorization: `Bearer ${accessToken}`,
719
744
  "Content-Type": "application/json",
720
745
  },
721
746
  });
722
- return true;
747
+ return { success: true };
723
748
  }
724
749
  catch (error) {
725
- spinner.fail(chalk_1.default.red(`✗ Error saving project ${projectId}: ${error.message}`));
726
- return false;
750
+ return { success: false, error: error.message };
727
751
  }
728
752
  }
729
753
  commander_1.program
730
754
  .command("push")
731
755
  .description("Push the project source code to the server")
732
756
  .action(async () => {
733
- const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project source code...")).start();
757
+ console.clear();
758
+ console.log(chalk_1.default.bold.cyan("\n🚀 HAPICO PUSH PIPELINE"));
759
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
734
760
  const pwd = process.cwd();
735
761
  const { projectId, replicate } = getStoredProjectId(pwd);
736
762
  if (!projectId) {
737
- saveSpinner.fail(chalk_1.default.red("✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."));
763
+ console.log(chalk_1.default.red("✗ Project ID not found. Ensure hapico.config.json exists."));
764
+ return;
765
+ }
766
+ console.log(chalk_1.default.bold.white("Target Environments:"));
767
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("Main Project :")} ${chalk_1.default.cyan(projectId)}`);
768
+ if (replicate && replicate.length > 0) {
769
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Replicas :")} ${chalk_1.default.cyan(replicate.join(", "))}`);
770
+ }
771
+ else {
772
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Replicas :")} ${chalk_1.default.gray("None")}`);
773
+ }
774
+ console.log("");
775
+ const pushSpinner = (0, ora_1.default)(chalk_1.default.blue("Syncing source code with main project...")).start();
776
+ const mainResult = await pushProject(projectId);
777
+ if (mainResult.success) {
778
+ pushSpinner.succeed(chalk_1.default.green(`Main Project [${projectId}] code synced successfully.`));
779
+ console.log(`${chalk_1.default.gray(" ├─")} ${chalk_1.default.bold("Files :")} ${chalk_1.default.white(mainResult.fileCount)}`);
780
+ console.log(`${chalk_1.default.gray(" └─")} ${chalk_1.default.bold("Size :")} ${chalk_1.default.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
781
+ }
782
+ else {
783
+ pushSpinner.fail(chalk_1.default.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
738
784
  return;
739
785
  }
740
- // Push to the main project
741
- const mainSuccess = await pushProject(saveSpinner, projectId);
742
- let allSuccess = mainSuccess;
743
- // Push to replicated projects if replicate array exists
744
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
745
- saveSpinner.text = chalk_1.default.blue("Pushing to replicated projects...");
786
+ let allSuccess = true;
787
+ if (replicate && replicate.length > 0) {
746
788
  for (const repId of replicate) {
747
- const success = await pushProject(saveSpinner, repId);
748
- if (!success) {
749
- allSuccess = false;
750
- console.warn(chalk_1.default.yellow(`⚠ Failed to push to replicated project ${repId}. Continuing...`));
789
+ const repSpinner = (0, ora_1.default)(chalk_1.default.blue(`Syncing replica [${repId}]...`)).start();
790
+ const repResult = await pushProject(repId);
791
+ if (repResult.success) {
792
+ repSpinner.succeed(chalk_1.default.green(`Replica [${repId}] code synced successfully.`));
793
+ console.log(`${chalk_1.default.gray(" ├─")} ${chalk_1.default.bold("Files :")} ${chalk_1.default.white(repResult.fileCount)}`);
794
+ console.log(`${chalk_1.default.gray(" └─")} ${chalk_1.default.bold("Size :")} ${chalk_1.default.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
751
795
  }
752
796
  else {
753
- console.log(chalk_1.default.green(`✓ Successfully pushed to replicated project ${repId}.`));
797
+ repSpinner.fail(chalk_1.default.red(`Replica [${repId}] sync failed: ${repResult.error}`));
798
+ allSuccess = false;
754
799
  }
755
800
  }
756
801
  }
802
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
757
803
  if (allSuccess) {
758
- saveSpinner.succeed(chalk_1.default.green("Project source code saved successfully to all projects!"));
804
+ console.log(`\n${chalk_1.default.bold.white.bgGreen(" SUCCESS ")} ${chalk_1.default.green("All source codes pushed successfully!\n")}`);
759
805
  }
760
806
  else {
761
- saveSpinner.warn(chalk_1.default.yellow("Project source code saved with some errors."));
807
+ console.log(`\n${chalk_1.default.bold.white.bgYellow(" WARNING ")} ${chalk_1.default.yellow("Push completed with some replica errors.\n")}`);
762
808
  }
763
809
  });
764
810
  commander_1.program
765
811
  .command("login")
766
- .description("Login to the system")
812
+ .description("Authenticate Hapico CLI with your account")
767
813
  .action(async () => {
768
- console.log(chalk_1.default.cyan("🔐 Logging in to the system..."));
769
- const loginSpinner = (0, ora_1.default)(chalk_1.default.blue("Initiating login...")).start();
814
+ console.clear();
815
+ console.log(chalk_1.default.bold.yellow("\n🔐 HAPICO SECURE LOGIN"));
816
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
817
+ const loginSpinner = (0, ora_1.default)(chalk_1.default.blue("Requesting device authorization...")).start();
770
818
  try {
771
819
  const response = await axios_1.default.post("https://auth.myworkbeast.com/auth/device");
772
820
  const { device_code, user_code, verification_url, expires_in, interval } = response.data;
773
- loginSpinner.succeed(chalk_1.default.green("Login initiated!"));
774
- console.log(chalk_1.default.cyan(`🌐 Please open this URL in your browser: ${verification_url}`));
775
- console.log(chalk_1.default.yellow(`🔑 And enter this code: ${user_code}`));
776
- console.log(chalk_1.default.blue(" Waiting for authentication..."));
821
+ loginSpinner.succeed(chalk_1.default.green("Authorization request created."));
822
+ console.log(`\n${chalk_1.default.bold.white("ACTION REQUIRED")}`);
823
+ console.log(`${chalk_1.default.gray("1.")} Open URL : ${chalk_1.default.underline.cyan(verification_url)}`);
824
+ console.log(`${chalk_1.default.gray("2.")} Enter Code: ${chalk_1.default.bold.bgWhite.black(` ${user_code} `)}`);
825
+ console.log(chalk_1.default.gray(`\n(Code expires in ${Math.floor(expires_in / 60)} minutes)`));
777
826
  await (0, open_1.default)(verification_url);
778
- const pollSpinner = (0, ora_1.default)(chalk_1.default.blue("Waiting for authentication...")).start();
827
+ const pollSpinner = (0, ora_1.default)(chalk_1.default.blue("Waiting for browser confirmation...")).start();
779
828
  let tokens = null;
780
829
  const startTime = Date.now();
781
830
  while (Date.now() - startTime < expires_in * 1000) {
@@ -786,21 +835,22 @@ commander_1.program
786
835
  break;
787
836
  }
788
837
  }
789
- catch (error) {
790
- // Ignore temporary errors and continue polling
791
- }
838
+ catch (e) { /* Poll interval logic */ }
792
839
  await new Promise((resolve) => setTimeout(resolve, interval * 1000));
793
840
  }
794
841
  if (tokens) {
795
- pollSpinner.succeed(chalk_1.default.green("Login successful!"));
796
842
  saveToken(tokens);
843
+ pollSpinner.succeed(chalk_1.default.green("Authentication successful."));
844
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
845
+ console.log(`\n${chalk_1.default.bold.white.bgGreen(" SUCCESS ")} ${chalk_1.default.green("You are now logged in.")}`);
846
+ console.log(`${chalk_1.default.gray("Token stored at:")} ${chalk_1.default.gray(TOKEN_FILE)}\n`);
797
847
  }
798
848
  else {
799
- pollSpinner.fail(chalk_1.default.red("Login failed: Timeout or user did not complete authentication."));
849
+ pollSpinner.fail(chalk_1.default.red("Login timed out or was cancelled."));
800
850
  }
801
851
  }
802
852
  catch (error) {
803
- loginSpinner.fail(chalk_1.default.red(`✗ Login error: ${error.message}`));
853
+ loginSpinner.fail(chalk_1.default.red(`Login failed: ${error.message}`));
804
854
  }
805
855
  });
806
856
  commander_1.program
@@ -851,329 +901,171 @@ commander_1.program
851
901
  apiSpinner.fail(chalk_1.default.red(`✗ Error fetching project files: ${error.message}`));
852
902
  }
853
903
  });
854
- // be {{id}} --be --port {{port}}
855
904
  commander_1.program
856
905
  .command("fetch <id>")
857
- .option("--port <port>", "Port to run the backend", "3000")
858
- .option("--serve", "Flag to indicate serving the backend")
859
- .option("--libs <libs>", "Additional libraries to install (comma-separated)", "")
860
- .option("--be", "Flag to indicate backend")
861
- .description("Open backend for the project")
862
- .action(async (id, options) => {
863
- var _a;
906
+ .description("Fetch Zalo Mini App template for the project")
907
+ .action(async (id) => {
864
908
  const { accessToken } = getStoredToken();
865
909
  if (!accessToken) {
866
910
  console.error(chalk_1.default.red("✗ You need to login first. Use 'hapico login' command."));
867
911
  return;
868
912
  }
869
- console.log(chalk_1.default.cyan(`🌐 PORT = ${options.port}`));
870
- // Chọn hỏi user port để vận hành
871
- let port = 3000;
872
- const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${id}`);
873
- const portInput = options.port;
874
- if (portInput) {
875
- const parsedPort = parseInt(portInput, 10);
876
- if (!isNaN(parsedPort)) {
877
- port = parsedPort;
878
- }
879
- }
880
- const projectDir = path.resolve(process.cwd(), id);
881
- if (!fs.existsSync(projectDir)) {
882
- // create folder
883
- fs.mkdirSync(projectDir);
884
- }
885
- // download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
886
- const TEMPLATE_URL = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
887
- let templateResponse = undefined;
913
+ const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project data...")).start();
888
914
  try {
889
- templateResponse = await axios_1.default.get(TEMPLATE_URL, {
915
+ const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${id}`);
916
+ const projectType = response.data.type;
917
+ if (projectType !== "zalominiapp" && projectType !== "zalominiapp2") {
918
+ apiSpinner.fail(chalk_1.default.red(`✗ Error: Project type is "${projectType}". Only "zalominiapp" and "zalominiapp2" are supported for fetch command.`));
919
+ return;
920
+ }
921
+ apiSpinner.succeed(chalk_1.default.green("Project data fetched successfully!"));
922
+ const outputDir = path.resolve(process.cwd(), id);
923
+ const zaloDir = path.join(outputDir, "zalo");
924
+ const backendDir = path.join(outputDir, "backend");
925
+ const templateSpinner = (0, ora_1.default)(chalk_1.default.blue("Downloading Zalo template...")).start();
926
+ const TEMPLATE_URL = "https://hapico.hcm04.vstorage.vngcloud.vn/templates/zalominiapp_v0.zip";
927
+ const templateResponse = await axios_1.default.get(TEMPLATE_URL, {
890
928
  responseType: "arraybuffer",
891
929
  });
892
- }
893
- catch (error) {
894
- console.error(chalk_1.default.red("✗ Error downloading template:"), error.message);
895
- return;
896
- }
897
- const outputDir = path.resolve(process.cwd(), id);
898
- if (!fs.existsSync(outputDir)) {
899
- fs.mkdirSync(outputDir);
900
- }
901
- await unzipper_1.default.Open.buffer(templateResponse.data).then((directory) => directory.extract({ path: outputDir }));
902
- const macosxDir = path.join(process.cwd(), id, "__MACOSX");
903
- if (fs.existsSync(macosxDir)) {
904
- fs.rmSync(macosxDir, { recursive: true, force: true });
905
- }
906
- // outputPath/src/server.ts có dòng (3006) thay thành port
907
- const serverFile = path.join(process.cwd(), id, "src", "index.ts");
908
- if (fs.existsSync(serverFile)) {
909
- let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
910
- serverContent = serverContent.split("(3006)").join(`(${port})`);
911
- fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
912
- }
913
- // lấy danh sách migrations
914
- const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${response.data.dbCode || response.data.db_code}&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
915
- // Lấy danh sách các migration đã chạy
916
- let migrations = [];
917
- const migrationsResponse = await axios_1.default.get(MIGRATIONS_URL, {
918
- headers: {
919
- Authorization: `Bearer ${accessToken}`,
920
- "Content-Type": "application/json",
921
- "x-db-code": response.data.dbCode || response.data.db_code,
922
- },
923
- });
924
- if (migrationsResponse.status === 200) {
925
- migrations = (((_a = migrationsResponse.data.data) === null || _a === void 0 ? void 0 : _a.rows) || []).map((item) => ({
926
- created_at: item.created_at,
927
- sql: item.sql_statement,
928
- }));
929
- }
930
- console.log(chalk_1.default.cyan(`🔍 Found ${migrations.length} migrations.`));
931
- // Tạo thư mục migrations nếu chưa tồn tại
932
- const migrationsDir = path.join(process.cwd(), id, "migrations");
933
- if (!fs.existsSync(migrationsDir)) {
934
- fs.mkdirSync(migrationsDir);
935
- }
936
- // Lưu từng migration vào file
937
- migrations.forEach((migration, index) => {
938
- const timestamp = new Date(migration.created_at)
939
- .toISOString()
940
- .replace(/[-:]/g, "")
941
- .replace(/\..+/, "");
942
- const filename = path.join(migrationsDir, `${index + 1}_migration_${timestamp}.sql`);
943
- fs.writeFileSync(filename, migration.sql, {
944
- encoding: "utf8",
930
+ templateSpinner.succeed(chalk_1.default.green("Zalo template downloaded successfully!"));
931
+ const backendSpinner = (0, ora_1.default)(chalk_1.default.blue("Downloading backend template...")).start();
932
+ const BACKEND_URL = "https://hapico.hcm04.vstorage.vngcloud.vn/templates/backend_v0.zip";
933
+ const backendResponse = await axios_1.default.get(BACKEND_URL, {
934
+ responseType: "arraybuffer",
945
935
  });
946
- });
947
- console.log(chalk_1.default.green("✓ Migrations saved successfully!"));
948
- // Download project files and save to project
949
- let files = [];
950
- const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project data...")).start();
951
- try {
952
- apiSpinner.succeed(chalk_1.default.green("Project data fetched successfully!"));
953
- const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project files...")).start();
954
- files.forEach((file) => {
955
- // skip ./app, ./components
956
- if (file.path.startsWith("./app") ||
957
- file.path.startsWith("./components")) {
958
- return;
959
- }
960
- // hoặc các file có đuôi là .tsx
961
- if (file.path.endsWith(".tsx")) {
962
- return;
963
- }
964
- try {
965
- if (file.content.trim().length == 0)
966
- return;
967
- const filePath = path.join(process.cwd(), id, "src", file.path);
968
- const dir = path.dirname(filePath);
969
- if (!fs.existsSync(dir)) {
970
- fs.mkdirSync(dir, { recursive: true });
971
- }
972
- fs.writeFileSync(filePath, file.content);
973
- }
974
- catch (error) {
975
- console.log(chalk_1.default.red("✗ Error writing file"), file.path, error);
936
+ backendSpinner.succeed(chalk_1.default.green("Backend template downloaded successfully!"));
937
+ if (!fs.existsSync(outputDir)) {
938
+ fs.mkdirSync(outputDir, { recursive: true });
939
+ }
940
+ if (!fs.existsSync(zaloDir))
941
+ fs.mkdirSync(zaloDir, { recursive: true });
942
+ if (!fs.existsSync(backendDir))
943
+ fs.mkdirSync(backendDir, { recursive: true });
944
+ const unzipZaloSpinner = (0, ora_1.default)(chalk_1.default.blue("Extracting Zalo template...")).start();
945
+ await unzipper_1.default.Open.buffer(templateResponse.data).then((directory) => directory.extract({ path: zaloDir }));
946
+ unzipZaloSpinner.succeed(chalk_1.default.green("Zalo template extracted successfully!"));
947
+ const unzipBackendSpinner = (0, ora_1.default)(chalk_1.default.blue("Extracting backend template...")).start();
948
+ await unzipper_1.default.Open.buffer(backendResponse.data).then((directory) => directory.extract({ path: backendDir }));
949
+ unzipBackendSpinner.succeed(chalk_1.default.green("Backend template extracted successfully!"));
950
+ [zaloDir, backendDir].forEach((dir) => {
951
+ const macosxDir = path.join(dir, "__MACOSX");
952
+ if (fs.existsSync(macosxDir)) {
953
+ fs.rmSync(macosxDir, { recursive: true, force: true });
976
954
  }
977
- });
978
- saveSpinner.succeed(chalk_1.default.green("Project files saved successfully!"));
979
- }
980
- catch (error) {
981
- apiSpinner.fail(chalk_1.default.red(`✗ Error cloning project: ${error.message}`));
982
- }
983
- // Download project files and save to project
984
- console.log(chalk_1.default.green("✓ Project cloned successfully!"));
985
- // Save project ID to hapico.config.json
986
- saveProjectId(outputDir, id);
987
- console.log(chalk_1.default.green("✓ Project setup successfully!"));
988
- let serveProcess = null;
989
- if (options.serve) {
990
- // Run bun install
991
- const { exec } = require("child_process");
992
- const installSpinner = (0, ora_1.default)(chalk_1.default.blue("Installing dependencies...")).start();
993
- // create a loop to check if there is new version of project
994
- // https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
995
- let currentVersionStr = fs.existsSync(path.join(outputDir, "version.json"))
996
- ? JSON.parse(fs.readFileSync(path.join(outputDir, "version.json"), {
997
- encoding: "utf8",
998
- })).version
999
- : "0.0.1";
1000
- const checkVersion = async () => {
1001
- var _a, _b;
1002
- try {
1003
- // check if file version.json exists
1004
- const flyCheck = await axios_1.default.head(`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`);
1005
- if (flyCheck.status !== 200) {
1006
- return;
1007
- }
1008
- // get file version.json
1009
- const versionResponse = await axios_1.default.get(`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`);
1010
- const latestVersionData = versionResponse.data;
1011
- const latestVersion = latestVersionData.version;
1012
- if (latestVersion !== currentVersionStr) {
1013
- console.log(chalk_1.default.yellow(`📦 New version available: ${latestVersion}`));
1014
- // Save new version.json
1015
- fs.writeFileSync(path.join(outputDir, "version.json"), JSON.stringify(latestVersionData, null, 2), { encoding: "utf8" });
1016
- // Install external libraries if any
1017
- if (latestVersionData.external_libraries &&
1018
- latestVersionData.external_libraries.length > 0) {
1019
- console.log(chalk_1.default.blue("Installing new external libraries..."));
1020
- for (const lib of latestVersionData.external_libraries) {
1021
- try {
1022
- await execPromise(`bun add ${lib}`, { cwd: outputDir });
1023
- console.log(chalk_1.default.green(`Added ${lib}`));
1024
- }
1025
- catch (addError) {
1026
- console.error(chalk_1.default.red(`✗ Error adding ${lib}:`), addError.message);
955
+ // Replace {{APP_ID}} in .ts and .tsx files
956
+ const traverseAndReplace = (currentDir) => {
957
+ fs.readdirSync(currentDir, { withFileTypes: true }).forEach((entry) => {
958
+ const fullPath = path.join(currentDir, entry.name);
959
+ if (entry.isDirectory()) {
960
+ traverseAndReplace(fullPath);
961
+ }
962
+ else if (entry.isFile()) {
963
+ const ext = path.extname(fullPath);
964
+ if (ext === ".ts" || ext === ".tsx") {
965
+ const content = fs.readFileSync(fullPath, { encoding: "utf8" });
966
+ if (content.includes("{{APP_ID}}")) {
967
+ const updatedContent = content.replace(/{{APP_ID}}/g, id);
968
+ fs.writeFileSync(fullPath, updatedContent, {
969
+ encoding: "utf8",
970
+ });
1027
971
  }
1028
972
  }
1029
973
  }
1030
- // Rerun bun install
1031
- try {
1032
- await execPromise("bun install", { cwd: outputDir });
1033
- console.log(chalk_1.default.green("Dependencies reinstalled successfully!"));
1034
- }
1035
- catch (installError) {
1036
- console.error(chalk_1.default.red("✗ Error reinstalling dependencies:"), installError.message);
1037
- }
1038
- // Restart the process
1039
- if (serveProcess && !serveProcess.killed) {
1040
- console.log(chalk_1.default.blue("Restarting backend..."));
1041
- serveProcess.kill("SIGTERM");
1042
- }
1043
- // Start new process
1044
- const newServeProcess = exec("bun run start", { cwd: outputDir });
1045
- (_a = newServeProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (data) => {
1046
- process.stdout.write(data);
1047
- });
1048
- (_b = newServeProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (data) => {
1049
- process.stderr.write(data);
1050
- });
1051
- newServeProcess.on("close", (code) => {
1052
- console.log(chalk_1.default.yellow(`⚠ backend exited with code ${code}`));
1053
- });
1054
- serveProcess = newServeProcess;
1055
- currentVersionStr = latestVersion;
1056
- }
1057
- }
1058
- catch (error) {
1059
- // If the remote version.json does not exist, do not update
1060
- console.error(chalk_1.default.yellow("⚠ Error checking for updates (skipping update):"), error.message);
1061
- }
1062
- };
1063
- // check every 3 seconds
1064
- setInterval(async () => await checkVersion(), 10 * 1000);
1065
- exec("bun install", { cwd: outputDir }, async (error, stdout, stderr) => {
1066
- if (error) {
1067
- installSpinner.fail(chalk_1.default.red(`✗ Error installing dependencies: ${error.message}`));
1068
- return;
1069
- }
1070
- if (stderr) {
1071
- console.error(chalk_1.default.red(`stderr: ${stderr}`));
1072
- }
1073
- installSpinner.succeed(chalk_1.default.green("Dependencies installed successfully!"));
1074
- // Install additional libraries if --libs is provided
1075
- if (options.libs && options.libs.trim()) {
1076
- const libsSpinner = (0, ora_1.default)(chalk_1.default.blue("Installing additional libraries...")).start();
1077
- const additionalLibs = options.libs
1078
- .split(",")
1079
- .map((lib) => lib.trim())
1080
- .filter((lib) => lib);
1081
- for (const lib of additionalLibs) {
1082
- try {
1083
- await execPromise(`bun add ${lib}`, { cwd: outputDir });
1084
- console.log(chalk_1.default.green(`Added ${lib}`));
1085
- }
1086
- catch (addError) {
1087
- console.error(chalk_1.default.red(`✗ Error adding ${lib}:`), addError.message);
1088
- }
1089
- }
1090
- libsSpinner.succeed(chalk_1.default.green("Additional libraries installed successfully!"));
1091
- }
1092
- // Run cd ${id} && bun run serve-be
1093
- console.log(chalk_1.default.blue("🚀 Starting backend"));
1094
- serveProcess = exec("bun run start", { cwd: outputDir });
1095
- serveProcess.stdout.on("data", (data) => {
1096
- process.stdout.write(data);
1097
- });
1098
- serveProcess.stderr.on("data", (data) => {
1099
- process.stderr.write(data);
1100
- });
1101
- serveProcess.on("close", (code) => {
1102
- console.log(chalk_1.default.yellow(`⚠ backend exited with code ${code}`));
1103
- });
974
+ });
975
+ };
976
+ traverseAndReplace(dir);
1104
977
  });
978
+ // Save project ID
979
+ saveProjectId(outputDir, id);
980
+ console.log(chalk_1.default.green(`✓ Project "${id}" fetched and setup successfully!`));
981
+ }
982
+ catch (error) {
983
+ apiSpinner.fail(chalk_1.default.red(`✗ Error: ${error.message}`));
1105
984
  }
1106
985
  });
1107
986
  // hapico publish
1108
- commander_1.program.command("publish").action(async () => {
1109
- const publishSpinner = (0, ora_1.default)(chalk_1.default.blue("Publishing to Hapico...")).start();
987
+ commander_1.program
988
+ .command("publish")
989
+ .description("Publish the project to production environment")
990
+ .action(async () => {
991
+ console.clear();
992
+ console.log(chalk_1.default.bold.magenta("\n🚀 HAPICO PUBLISH PIPELINE"));
993
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
1110
994
  const pwd = process.cwd();
1111
995
  const { projectId, replicate } = getStoredProjectId(pwd);
1112
996
  if (!projectId) {
1113
- publishSpinner.fail(chalk_1.default.red("✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."));
997
+ console.log(chalk_1.default.red("✗ Project ID not found. Ensure hapico.config.json exists."));
1114
998
  return;
1115
999
  }
1116
- // Step 1: Push source code to main project and replicas
1117
- const pushSuccess = await pushProject(publishSpinner, projectId);
1118
- if (!pushSuccess) {
1119
- return; // Stop if push to main project fails
1000
+ console.log(chalk_1.default.bold.white("Target Environments:"));
1001
+ console.log(`${chalk_1.default.gray("├─")} ${chalk_1.default.bold("Main Project :")} ${chalk_1.default.magenta(projectId)}`);
1002
+ if (replicate && replicate.length > 0) {
1003
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Replicas :")} ${chalk_1.default.magenta(replicate.join(", "))}`);
1004
+ }
1005
+ else {
1006
+ console.log(`${chalk_1.default.gray("└─")} ${chalk_1.default.bold("Replicas :")} ${chalk_1.default.gray("None")}`);
1007
+ }
1008
+ console.log("");
1009
+ const pushSpinner = (0, ora_1.default)(chalk_1.default.blue("Phase 1/2: Syncing source code with main project...")).start();
1010
+ // Step 1: Push Main
1011
+ const mainResult = await pushProject(projectId);
1012
+ if (mainResult.success) {
1013
+ pushSpinner.succeed(chalk_1.default.green(`Main Project [${projectId}] code synced successfully.`));
1014
+ console.log(`${chalk_1.default.gray(" ├─")} ${chalk_1.default.bold("Files :")} ${chalk_1.default.white(mainResult.fileCount)}`);
1015
+ console.log(`${chalk_1.default.gray(" └─")} ${chalk_1.default.bold("Size :")} ${chalk_1.default.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
1120
1016
  }
1121
- // Push to replicated projects
1017
+ else {
1018
+ pushSpinner.fail(chalk_1.default.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
1019
+ return; // Stop if push to main fails
1020
+ }
1021
+ // Push Replicas
1122
1022
  let allPushSuccess = true;
1123
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1124
- publishSpinner.text = chalk_1.default.blue("Pushing to replicated projects...");
1023
+ if (replicate && replicate.length > 0) {
1125
1024
  for (const repId of replicate) {
1126
- const success = await pushProject(publishSpinner, repId);
1127
- if (!success) {
1128
- allPushSuccess = false;
1129
- console.warn(chalk_1.default.yellow(`⚠ Failed to push to replicated project ${repId}. Continuing...`));
1025
+ const repSpinner = (0, ora_1.default)(chalk_1.default.blue(`Phase 1/2: Syncing replica code [${repId}]...`)).start();
1026
+ const repResult = await pushProject(repId);
1027
+ if (repResult.success) {
1028
+ repSpinner.succeed(chalk_1.default.green(`Replica [${repId}] code synced successfully.`));
1029
+ console.log(`${chalk_1.default.gray(" ├─")} ${chalk_1.default.bold("Files :")} ${chalk_1.default.white(repResult.fileCount)}`);
1030
+ console.log(`${chalk_1.default.gray(" └─")} ${chalk_1.default.bold("Size :")} ${chalk_1.default.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
1130
1031
  }
1131
1032
  else {
1132
- console.log(chalk_1.default.green(`✓ Successfully pushed to replicated project ${repId}.`));
1033
+ repSpinner.fail(chalk_1.default.red(`Replica [${repId}] sync failed: ${repResult.error}`));
1034
+ allPushSuccess = false;
1133
1035
  }
1134
1036
  }
1135
1037
  }
1136
- // Step 2: Publish main project
1137
- const { accessToken } = getStoredToken();
1138
- const apiUrl = "https://base.myworkbeast.com/api/views/publish/v2";
1038
+ // Step 2: Publish Main
1039
+ const publishSpinner = (0, ora_1.default)(chalk_1.default.blue(`Phase 2/2: Publishing main project [${projectId}]...`)).start();
1040
+ const pubMainResult = await publishProjectApi(projectId);
1139
1041
  let allPublishSuccess = true;
1140
- try {
1141
- await axios_1.default.post(apiUrl, { view_id: parseInt(projectId, 10) }, {
1142
- headers: {
1143
- Authorization: `Bearer ${accessToken}`,
1144
- "Content-Type": "application/json",
1145
- },
1146
- });
1147
- publishSpinner.succeed(chalk_1.default.green(`Project ${projectId} published successfully!`));
1042
+ if (pubMainResult.success) {
1043
+ publishSpinner.succeed(chalk_1.default.green(`Main Project [${projectId}] published successfully.`));
1148
1044
  }
1149
- catch (error) {
1045
+ else {
1046
+ publishSpinner.fail(chalk_1.default.red(`Main Project [${projectId}] publish failed: ${pubMainResult.error}`));
1150
1047
  allPublishSuccess = false;
1151
- publishSpinner.fail(chalk_1.default.red(`✗ Error publishing project ${projectId}: ${error.message}`));
1152
1048
  }
1153
- // Step 3: Publish replicated projects
1154
- if (replicate && Array.isArray(replicate) && replicate.length > 0) {
1155
- publishSpinner.text = chalk_1.default.blue("Publishing replicated projects...");
1049
+ // Publish Replicas
1050
+ if (replicate && replicate.length > 0) {
1156
1051
  for (const repId of replicate) {
1157
- try {
1158
- await axios_1.default.post(apiUrl, { view_id: parseInt(repId, 10) }, {
1159
- headers: {
1160
- Authorization: `Bearer ${accessToken}`,
1161
- "Content-Type": "application/json",
1162
- },
1163
- });
1164
- console.log(chalk_1.default.green(`✓ Successfully published replicated project ${repId}.`));
1052
+ const repPubSpinner = (0, ora_1.default)(chalk_1.default.blue(`Phase 2/2: Publishing replica [${repId}]...`)).start();
1053
+ const pubRepResult = await publishProjectApi(repId);
1054
+ if (pubRepResult.success) {
1055
+ repPubSpinner.succeed(chalk_1.default.green(`Replica [${repId}] published successfully.`));
1165
1056
  }
1166
- catch (error) {
1057
+ else {
1058
+ repPubSpinner.fail(chalk_1.default.red(`Replica [${repId}] publish failed: ${pubRepResult.error}`));
1167
1059
  allPublishSuccess = false;
1168
- console.warn(chalk_1.default.yellow(`⚠ Error publishing replicated project ${repId}: ${error.message}`));
1169
1060
  }
1170
1061
  }
1171
1062
  }
1063
+ console.log(chalk_1.default.gray("──────────────────────────────────────────────────────────────────"));
1172
1064
  if (allPushSuccess && allPublishSuccess) {
1173
- publishSpinner.succeed(chalk_1.default.green("All projects published successfully!"));
1065
+ console.log(`\n${chalk_1.default.bold.white.bgGreen(" SUCCESS ")} ${chalk_1.default.green("Deployment pipeline completed successfully!\n")}`);
1174
1066
  }
1175
1067
  else {
1176
- publishSpinner.warn(chalk_1.default.yellow("Publishing completed with some errors."));
1068
+ console.log(`\n${chalk_1.default.bold.white.bgYellow(" WARNING ")} ${chalk_1.default.yellow("Deployment pipeline completed with some errors.\n")}`);
1177
1069
  }
1178
1070
  });
1179
1071
  commander_1.program.command("mirror").action(() => {
@@ -1186,7 +1078,7 @@ commander_1.program.command("mirror").action(() => {
1186
1078
  }
1187
1079
  const fileManager = new FileManager(srcDir);
1188
1080
  const initialFiles = fileManager.listFiles();
1189
- // Lấy danh sách file viết ra 1 file .txt
1081
+ // Collect project files and generate a single summary document
1190
1082
  let content = ``;
1191
1083
  (0, lodash_1.map)(initialFiles, (file) => {
1192
1084
  content += `\`\`\`typescript