@hapico/cli 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -51,10 +51,16 @@ const Babel = __importStar(require("@babel/standalone"));
51
51
  const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
52
52
  const open_1 = __importDefault(require("open"));
53
53
  const crypto_1 = require("crypto");
54
+ const child_process_1 = require("child_process");
55
+ const util_1 = require("util");
56
+ const chalk_1 = __importDefault(require("chalk"));
57
+ const pako_1 = __importDefault(require("pako"));
58
+ // Promisify exec for async usage
59
+ const execPromise = (0, util_1.promisify)(child_process_1.exec);
54
60
  // Directory to store the token and project config
55
61
  const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || ".", ".hapico");
56
62
  const TOKEN_FILE = path.join(CONFIG_DIR, "auth_token.json");
57
- const connected = (0, ora_1.default)("Connected to WebSocket server");
63
+ const connected = (0, ora_1.default)(chalk_1.default.cyan("Connected to WebSocket server"));
58
64
  // Ensure config directory exists
59
65
  if (!fs.existsSync(CONFIG_DIR)) {
60
66
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -176,7 +182,7 @@ class FileManager {
176
182
  hasChanged = existingContent !== file.content;
177
183
  }
178
184
  catch (readError) {
179
- console.warn(`Warning: Could not read existing file ${fullPath}, treating as changed:`, readError);
185
+ console.warn(chalk_1.default.yellow(`Warning: Could not read existing file ${fullPath}, treating as changed:`), readError);
180
186
  hasChanged = true;
181
187
  }
182
188
  }
@@ -185,7 +191,7 @@ class FileManager {
185
191
  }
186
192
  }
187
193
  catch (error) {
188
- console.error(`Error processing file ${file.path}:`, error);
194
+ console.error(chalk_1.default.red(`Error processing file ${file.path}:`), error);
189
195
  throw error;
190
196
  }
191
197
  }
@@ -205,7 +211,7 @@ class FileManager {
205
211
  }
206
212
  }
207
213
  catch (error) {
208
- console.warn(`Error reading changed file ${fullPath}:`, error);
214
+ console.warn(chalk_1.default.yellow(`Error reading changed file ${fullPath}:`), error);
209
215
  }
210
216
  }
211
217
  });
@@ -244,24 +250,38 @@ class RoomState {
244
250
  if (this.reconnectTimeout) {
245
251
  clearTimeout(this.reconnectTimeout);
246
252
  }
247
- this.ws = new ws_1.WebSocket(`https://ws2.myworkbeast.com/ws?room=${this.roomId}`);
248
- this.ws.onopen = () => {
249
- console.log(`Connected to room: ${this.roomId}`);
253
+ this.ws = new ws_1.WebSocket(`wss://ws3.myworkbeast.com/ws?room=${this.roomId}`);
254
+ // Set binaryType to 'arraybuffer' to handle binary data
255
+ this.ws.binaryType = 'arraybuffer';
256
+ this.ws.on("open", () => {
257
+ console.log(chalk_1.default.green(`Connected to room: ${this.roomId}`));
250
258
  this.isConnected = true;
251
259
  this.reconnectAttempts = 0;
252
260
  onConnected === null || onConnected === void 0 ? void 0 : onConnected(); // Call the onConnected callback if provided
253
- };
254
- this.ws.onclose = () => {
255
- console.log(`Disconnected from room: ${this.roomId}`);
261
+ });
262
+ this.ws.on("close", () => {
263
+ console.log(chalk_1.default.yellow(`⚠ Disconnected from room: ${this.roomId}`));
256
264
  this.isConnected = false;
257
265
  this.reconnectAttempts++;
258
266
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
259
- console.log(`Attempting to reconnect in ${delay / 1000}s...`);
267
+ console.log(chalk_1.default.yellow(`Attempting to reconnect in ${delay / 1000}s...`));
260
268
  this.reconnectTimeout = setTimeout(() => this.connect(onConnected), delay);
261
- };
269
+ });
262
270
  this.ws.on("message", (data) => {
263
271
  try {
264
- const jsonStr = typeof data === "string" ? data : data.toString();
272
+ let jsonStr;
273
+ if (data instanceof ArrayBuffer) {
274
+ jsonStr = pako_1.default.inflate(data, { to: 'string' });
275
+ }
276
+ else if (typeof data === "string") {
277
+ jsonStr = data;
278
+ }
279
+ else if (Buffer.isBuffer(data)) {
280
+ jsonStr = pako_1.default.inflate(data, { to: 'string' });
281
+ }
282
+ else {
283
+ jsonStr = data.toString(); // Fallback nếu không nén
284
+ }
265
285
  const parsedData = JSON.parse(jsonStr);
266
286
  const includes = [
267
287
  "view",
@@ -300,21 +320,23 @@ class RoomState {
300
320
  }
301
321
  }
302
322
  catch (e) {
303
- console.error("Error processing message:", e);
323
+ console.error(chalk_1.default.red("Error processing message:"), e);
304
324
  }
305
325
  });
306
326
  this.ws.on("error", (err) => {
307
327
  var _a;
308
- console.error("WebSocket error:", err);
328
+ console.error(chalk_1.default.red("WebSocket error:"), err);
309
329
  (_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
310
330
  });
311
331
  }
312
332
  updateState(key, value) {
313
333
  if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN) {
314
- this.ws.send(JSON.stringify({
334
+ const message = JSON.stringify({
315
335
  type: "update",
316
336
  state: { [key]: value },
317
- }));
337
+ });
338
+ const compressed = pako_1.default.deflate(message); // Nén dữ liệu
339
+ this.ws.send(compressed); // Gửi binary
318
340
  }
319
341
  }
320
342
  disconnect() {
@@ -322,7 +344,7 @@ class RoomState {
322
344
  clearTimeout(this.reconnectTimeout);
323
345
  }
324
346
  if (this.ws) {
325
- this.ws.onclose = null; // Prevent reconnect on intentional close
347
+ this.ws.removeAllListeners("close"); // Prevent reconnect on intentional close
326
348
  this.ws.close();
327
349
  }
328
350
  }
@@ -333,48 +355,48 @@ class RoomState {
333
355
  return this.isConnected;
334
356
  }
335
357
  }
336
- commander_1.program.version("0.0.16").description("Hapico CLI for project management");
358
+ commander_1.program.version("0.0.19").description("Hapico CLI for project management");
337
359
  commander_1.program
338
360
  .command("clone <id>")
339
361
  .description("Clone a project by ID")
340
362
  .action(async (id) => {
341
363
  const { accessToken } = getStoredToken();
342
364
  if (!accessToken) {
343
- console.error("You need to login first. Use 'hapico login' command.");
365
+ console.error(chalk_1.default.red("You need to login first. Use 'hapico login' command."));
344
366
  return;
345
367
  }
346
368
  const projectDir = path.resolve(process.cwd(), id);
347
369
  if (fs.existsSync(projectDir)) {
348
- console.error(`Project directory "${id}" already exists.`);
370
+ console.error(chalk_1.default.red(`✗ Project directory "${id}" already exists.`));
349
371
  return;
350
372
  }
351
373
  let files = [];
352
- const apiSpinner = (0, ora_1.default)("Fetching project data...").start();
374
+ const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project data...")).start();
353
375
  try {
354
376
  const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${id}`);
355
377
  files = response.data.files || [];
356
- apiSpinner.succeed("Project data fetched successfully!");
357
- const templateSpinner = (0, ora_1.default)("Downloading template...").start();
378
+ apiSpinner.succeed(chalk_1.default.green("Project data fetched successfully!"));
379
+ const templateSpinner = (0, ora_1.default)(chalk_1.default.blue("Downloading template...")).start();
358
380
  const TEMPLATE_URL = "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
359
381
  const templateResponse = await axios_1.default.get(TEMPLATE_URL, {
360
382
  responseType: "arraybuffer",
361
383
  });
362
- templateSpinner.succeed("Template downloaded successfully!");
384
+ templateSpinner.succeed(chalk_1.default.green("Template downloaded successfully!"));
363
385
  const outputDir = path.resolve(process.cwd(), id);
364
386
  if (!fs.existsSync(outputDir)) {
365
387
  fs.mkdirSync(outputDir);
366
388
  }
367
- const unzipSpinner = (0, ora_1.default)("Extracting template...").start();
389
+ const unzipSpinner = (0, ora_1.default)(chalk_1.default.blue("Extracting template...")).start();
368
390
  await unzipper_1.default.Open.buffer(templateResponse.data).then((directory) => directory.extract({ path: outputDir }));
369
- unzipSpinner.succeed("Template extracted successfully!");
391
+ unzipSpinner.succeed(chalk_1.default.green("Template extracted successfully!"));
370
392
  const macosxDir = path.join(process.cwd(), id, "__MACOSX");
371
393
  if (fs.existsSync(macosxDir)) {
372
394
  fs.rmSync(macosxDir, { recursive: true, force: true });
373
395
  }
374
396
  // Save project ID to hapico.config.json
375
397
  saveProjectId(outputDir, id);
376
- console.log("Project cloned successfully!");
377
- const saveSpinner = (0, ora_1.default)("Saving project files...").start();
398
+ console.log(chalk_1.default.green("Project cloned successfully!"));
399
+ const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project files...")).start();
378
400
  files.forEach((file) => {
379
401
  const filePath = path.join(process.cwd(), id, "src", file.path);
380
402
  const dir = path.dirname(filePath);
@@ -383,11 +405,11 @@ commander_1.program
383
405
  }
384
406
  fs.writeFileSync(filePath, file.content);
385
407
  });
386
- saveSpinner.succeed("Project files saved successfully!");
387
- console.log(`Run 'cd ${id} && npm install && hapico dev' to start the project.`);
408
+ saveSpinner.succeed(chalk_1.default.green("Project files saved successfully!"));
409
+ console.log(chalk_1.default.cyan(`💡 Run ${chalk_1.default.bold("cd ${id} && npm install && hapico dev")} to start the project.`));
388
410
  }
389
411
  catch (error) {
390
- apiSpinner.fail(`Error cloning project: ${error.message}`);
412
+ apiSpinner.fail(chalk_1.default.red(`Error cloning project: ${error.message}`));
391
413
  }
392
414
  });
393
415
  commander_1.program
@@ -396,14 +418,14 @@ commander_1.program
396
418
  .action(() => {
397
419
  const { accessToken } = getStoredToken();
398
420
  if (!accessToken) {
399
- console.error("You need to login first. Use 'hapico login' command.");
421
+ console.error(chalk_1.default.red("You need to login first. Use 'hapico login' command."));
400
422
  return;
401
423
  }
402
- const devSpinner = (0, ora_1.default)("Starting the project in development mode...").start();
424
+ const devSpinner = (0, ora_1.default)(chalk_1.default.blue("Starting the project in development mode...")).start();
403
425
  const pwd = process.cwd();
404
426
  const srcDir = path.join(pwd, "src");
405
427
  if (!fs.existsSync(srcDir)) {
406
- devSpinner.fail("Source directory 'src' does not exist. Please clone a project first.");
428
+ devSpinner.fail(chalk_1.default.red("Source directory 'src' does not exist. Please clone a project first."));
407
429
  return;
408
430
  }
409
431
  // Directory to store session config
@@ -440,21 +462,21 @@ commander_1.program
440
462
  // Convert info to base64
441
463
  const projectId = Buffer.from(info).toString("base64");
442
464
  if (!projectId) {
443
- devSpinner.fail("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
465
+ devSpinner.fail(chalk_1.default.red("Project ID not found. Please ensure hapico.config.json exists in the project directory."));
444
466
  return;
445
467
  }
446
- console.log(`Connecting to WebSocket server`);
468
+ console.log(chalk_1.default.cyan("🔗 Connecting to WebSocket server"));
447
469
  const room = new RoomState(`view_${projectId}`, []);
448
470
  const fileManager = new FileManager(srcDir);
449
471
  const initialFiles = fileManager.listFiles();
450
472
  room.files = initialFiles;
451
473
  room.connect(async () => {
452
- devSpinner.succeed("Project started in development mode!");
474
+ devSpinner.succeed(chalk_1.default.green("Project started in development mode!"));
453
475
  room.updateState("view", initialFiles);
454
476
  fileManager.setOnFileChange((filePath, content) => {
455
477
  var _a;
456
478
  const es5 = (_a = (0, exports.compileES5)(content, filePath)) !== null && _a !== void 0 ? _a : "";
457
- console.log(`File changed: ${filePath === null || filePath === void 0 ? void 0 : filePath.replace(srcDir, ".")}`);
479
+ console.log(chalk_1.default.yellow(`📝 File changed: ${filePath === null || filePath === void 0 ? void 0 : filePath.replace(srcDir, ".")}`));
458
480
  const updatedFiles = room.files.map((file) => {
459
481
  if (path.join(srcDir, file.path) === filePath) {
460
482
  return { ...file, content, es5 };
@@ -467,7 +489,7 @@ commander_1.program
467
489
  // Fetch project info
468
490
  const projectInfo = getStoredProjectId(pwd);
469
491
  if (!projectInfo) {
470
- console.error("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
492
+ console.error(chalk_1.default.red("Project ID not found. Please ensure hapico.config.json exists in the project directory."));
471
493
  return;
472
494
  }
473
495
  const project = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${projectInfo}`, {
@@ -477,82 +499,100 @@ commander_1.program
477
499
  },
478
500
  });
479
501
  if (project.status !== 200) {
480
- console.error(`Error fetching project info: ${project.statusText}`);
502
+ console.error(chalk_1.default.red(`✗ Error fetching project info: ${project.statusText}`));
481
503
  return;
482
504
  }
483
505
  const projectType = project.data.type || "view";
484
506
  if (projectType === "zalominiapp") {
485
- qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}`, { small: true }, (qrcode) => {
486
- console.log("Scan this QR code to connect to the project:");
507
+ qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=75`, { small: true }, (qrcode) => {
508
+ console.log(chalk_1.default.cyan("📱 Scan this QR code to connect to the project:"));
487
509
  console.log(qrcode);
488
510
  });
489
511
  return;
490
512
  }
491
513
  else {
492
514
  const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
493
- console.log(`Open this URL in your browser to preview the project: \n${previewUrl}`);
515
+ console.log(chalk_1.default.cyan(`🌐 Open this URL in your browser to preview the project:\n${previewUrl}`));
494
516
  await (0, open_1.default)(previewUrl);
495
517
  }
496
518
  });
497
519
  });
498
- commander_1.program
499
- .command("push")
500
- .description("Push the project source code to the server")
501
- .action(() => {
520
+ // Hàm tái sử dụng để push mã nguồn lên server
521
+ async function pushProject(spinner) {
502
522
  const data = getStoredToken();
503
523
  const { accessToken } = data || {};
504
524
  if (!accessToken) {
505
- console.error("You need to login first. Use 'hapico login' command.");
506
- return;
525
+ spinner.fail(chalk_1.default.red("You need to login first. Use 'hapico login' command."));
526
+ return false;
507
527
  }
508
- const saveSpinner = (0, ora_1.default)("Saving project source code...").start();
509
528
  const pwd = process.cwd();
510
529
  const projectId = getStoredProjectId(pwd);
511
530
  if (!projectId) {
512
- saveSpinner.fail("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
513
- return;
531
+ spinner.fail(chalk_1.default.red("Project ID not found. Please ensure hapico.config.json exists in the project directory."));
532
+ return false;
514
533
  }
515
534
  const srcDir = path.join(pwd, "src");
516
535
  if (!fs.existsSync(srcDir)) {
517
- saveSpinner.fail("Source directory 'src' does not exist. Please clone a project first.");
518
- return;
536
+ spinner.fail(chalk_1.default.red("Source directory 'src' does not exist. Please clone a project first."));
537
+ return false;
519
538
  }
520
539
  const fileManager = new FileManager(srcDir);
521
540
  const files = fileManager.listFiles();
541
+ // Nếu có file .env
542
+ const envFile = path.join(pwd, ".env");
543
+ console.log(chalk_1.default.cyan(`🔍 Checking for .env file at ${envFile}`));
544
+ if (fs.existsSync(envFile)) {
545
+ console.log(chalk_1.default.green(".env file found, including in push."));
546
+ const content = fs.readFileSync(envFile, { encoding: "utf8" });
547
+ files.push({
548
+ path: "./.env",
549
+ content,
550
+ });
551
+ }
522
552
  const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
523
- axios_1.default
524
- .put(apiUrl, {
525
- code: JSON.stringify({
526
- files,
527
- }),
528
- }, {
529
- headers: {
530
- Authorization: `Bearer ${accessToken}`,
531
- "Content-Type": "application/json",
532
- },
533
- })
534
- .then(() => {
535
- saveSpinner.succeed("Project source code saved successfully!");
536
- })
537
- .catch((error) => {
538
- saveSpinner.fail(`Error saving project: ${error.message}`);
539
- });
553
+ try {
554
+ await axios_1.default.put(apiUrl, {
555
+ code: JSON.stringify({
556
+ files,
557
+ }),
558
+ }, {
559
+ headers: {
560
+ Authorization: `Bearer ${accessToken}`,
561
+ "Content-Type": "application/json",
562
+ },
563
+ });
564
+ return true;
565
+ }
566
+ catch (error) {
567
+ spinner.fail(chalk_1.default.red(`✗ Error saving project: ${error.message}`));
568
+ return false;
569
+ }
570
+ }
571
+ commander_1.program
572
+ .command("push")
573
+ .description("Push the project source code to the server")
574
+ .action(async () => {
575
+ const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project source code...")).start();
576
+ const success = await pushProject(saveSpinner);
577
+ if (success) {
578
+ saveSpinner.succeed(chalk_1.default.green("Project source code saved successfully!"));
579
+ }
540
580
  });
541
581
  commander_1.program
542
582
  .command("login")
543
583
  .description("Login to the system")
544
584
  .action(async () => {
545
- console.log("Logging in to the system...");
546
- const loginSpinner = (0, ora_1.default)("Initiating login...").start();
585
+ console.log(chalk_1.default.cyan("🔐 Logging in to the system..."));
586
+ const loginSpinner = (0, ora_1.default)(chalk_1.default.blue("Initiating login...")).start();
547
587
  try {
548
588
  const response = await axios_1.default.post("https://auth.myworkbeast.com/auth/device");
549
589
  const { device_code, user_code, verification_url, expires_in, interval } = response.data;
550
- loginSpinner.succeed("Login initiated!");
551
- console.log(`Please open this URL in your browser: ${verification_url}`);
552
- console.log(`And enter this code: ${user_code}`);
553
- console.log("Waiting for authentication...");
590
+ loginSpinner.succeed(chalk_1.default.green("Login initiated!"));
591
+ console.log(chalk_1.default.cyan(`🌐 Please open this URL in your browser: ${verification_url}`));
592
+ console.log(chalk_1.default.yellow(`🔑 And enter this code: ${user_code}`));
593
+ console.log(chalk_1.default.blue("Waiting for authentication..."));
554
594
  await (0, open_1.default)(verification_url);
555
- const pollSpinner = (0, ora_1.default)("Waiting for authentication...").start();
595
+ const pollSpinner = (0, ora_1.default)(chalk_1.default.blue("Waiting for authentication...")).start();
556
596
  let tokens = null;
557
597
  const startTime = Date.now();
558
598
  while (Date.now() - startTime < expires_in * 1000) {
@@ -569,15 +609,15 @@ commander_1.program
569
609
  await new Promise((resolve) => setTimeout(resolve, interval * 1000));
570
610
  }
571
611
  if (tokens) {
572
- pollSpinner.succeed("Login successful!");
612
+ pollSpinner.succeed(chalk_1.default.green("Login successful!"));
573
613
  saveToken(tokens);
574
614
  }
575
615
  else {
576
- pollSpinner.fail("Login failed: Timeout or user did not complete authentication.");
616
+ pollSpinner.fail(chalk_1.default.red("Login failed: Timeout or user did not complete authentication."));
577
617
  }
578
618
  }
579
619
  catch (error) {
580
- loginSpinner.fail(`Login error: ${error.message}`);
620
+ loginSpinner.fail(chalk_1.default.red(`✗ Login error: ${error.message}`));
581
621
  }
582
622
  });
583
623
  commander_1.program
@@ -586,11 +626,11 @@ commander_1.program
586
626
  .action(() => {
587
627
  const accessToken = getStoredToken();
588
628
  if (!accessToken) {
589
- console.log("You are not logged in.");
629
+ console.log(chalk_1.default.yellow("You are not logged in."));
590
630
  return;
591
631
  }
592
632
  fs.unlinkSync(TOKEN_FILE);
593
- console.log("Logout successful!");
633
+ console.log(chalk_1.default.green("Logout successful!"));
594
634
  });
595
635
  // Pull command to fetch the latest project files from the server
596
636
  commander_1.program
@@ -599,16 +639,16 @@ commander_1.program
599
639
  .action(async () => {
600
640
  const token = getStoredToken();
601
641
  if (!token) {
602
- console.error("You need to login first. Use 'hapico login' command.");
642
+ console.error(chalk_1.default.red("You need to login first. Use 'hapico login' command."));
603
643
  return;
604
644
  }
605
645
  const pwd = process.cwd();
606
646
  const projectId = getStoredProjectId(pwd);
607
647
  if (!projectId) {
608
- console.error("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
648
+ console.error(chalk_1.default.red("Project ID not found. Please ensure hapico.config.json exists in the project directory."));
609
649
  return;
610
650
  }
611
- const apiSpinner = (0, ora_1.default)("Fetching latest project files...").start();
651
+ const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching latest project files...")).start();
612
652
  try {
613
653
  const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${projectId}`, {
614
654
  headers: {
@@ -619,10 +659,294 @@ commander_1.program
619
659
  const files = response.data.files || [];
620
660
  const fileManager = new FileManager(path.join(pwd, "src"));
621
661
  fileManager.syncFiles(files);
622
- apiSpinner.succeed("Project files updated successfully!");
662
+ apiSpinner.succeed(chalk_1.default.green("Project files updated successfully!"));
663
+ }
664
+ catch (error) {
665
+ apiSpinner.fail(chalk_1.default.red(`✗ Error fetching project files: ${error.message}`));
666
+ }
667
+ });
668
+ // be {{id}} --be --port {{port}}
669
+ commander_1.program
670
+ .command("fetch <id>")
671
+ .option("--port <port>", "Port to run the backend", "3000")
672
+ .option("--serve", "Flag to indicate serving the backend")
673
+ .option("--libs <libs>", "Additional libraries to install (comma-separated)", "")
674
+ .option("--be", "Flag to indicate backend")
675
+ .description("Open backend for the project")
676
+ .action(async (id, options) => {
677
+ var _a;
678
+ const { accessToken } = getStoredToken();
679
+ if (!accessToken) {
680
+ console.error(chalk_1.default.red("✗ You need to login first. Use 'hapico login' command."));
681
+ return;
682
+ }
683
+ console.log(chalk_1.default.cyan(`🌐 PORT = ${options.port}`));
684
+ // Chọn hỏi user port để vận hành
685
+ let port = 3000;
686
+ const response = await axios_1.default.get(`https://base.myworkbeast.com/api/views/${id}`);
687
+ const portInput = options.port;
688
+ if (portInput) {
689
+ const parsedPort = parseInt(portInput, 10);
690
+ if (!isNaN(parsedPort)) {
691
+ port = parsedPort;
692
+ }
693
+ }
694
+ const projectDir = path.resolve(process.cwd(), id);
695
+ if (!fs.existsSync(projectDir)) {
696
+ // create folder
697
+ fs.mkdirSync(projectDir);
698
+ }
699
+ // download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
700
+ const TEMPLATE_URL = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
701
+ let templateResponse = undefined;
702
+ try {
703
+ templateResponse = await axios_1.default.get(TEMPLATE_URL, {
704
+ responseType: "arraybuffer",
705
+ });
706
+ }
707
+ catch (error) {
708
+ console.error(chalk_1.default.red("✗ Error downloading template:"), error.message);
709
+ return;
710
+ }
711
+ const outputDir = path.resolve(process.cwd(), id);
712
+ if (!fs.existsSync(outputDir)) {
713
+ fs.mkdirSync(outputDir);
714
+ }
715
+ await unzipper_1.default.Open.buffer(templateResponse.data).then((directory) => directory.extract({ path: outputDir }));
716
+ const macosxDir = path.join(process.cwd(), id, "__MACOSX");
717
+ if (fs.existsSync(macosxDir)) {
718
+ fs.rmSync(macosxDir, { recursive: true, force: true });
719
+ }
720
+ // outputPath/src/server.ts có dòng (3006) thay thành port
721
+ const serverFile = path.join(process.cwd(), id, "src", "index.ts");
722
+ if (fs.existsSync(serverFile)) {
723
+ let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
724
+ serverContent = serverContent.split("(3006)").join(`(${port})`);
725
+ fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
726
+ }
727
+ // lấy danh sách migrations
728
+ 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`;
729
+ // Lấy danh sách các migration đã chạy
730
+ let migrations = [];
731
+ const migrationsResponse = await axios_1.default.get(MIGRATIONS_URL, {
732
+ headers: {
733
+ Authorization: `Bearer ${accessToken}`,
734
+ "Content-Type": "application/json",
735
+ "x-db-code": response.data.dbCode || response.data.db_code,
736
+ },
737
+ });
738
+ if (migrationsResponse.status === 200) {
739
+ migrations = (((_a = migrationsResponse.data.data) === null || _a === void 0 ? void 0 : _a.rows) || []).map((item) => ({
740
+ created_at: item.created_at,
741
+ sql: item.sql_statement,
742
+ }));
743
+ }
744
+ console.log(chalk_1.default.cyan(`🔍 Found ${migrations.length} migrations.`));
745
+ // Tạo thư mục migrations nếu chưa tồn tại
746
+ const migrationsDir = path.join(process.cwd(), id, "migrations");
747
+ if (!fs.existsSync(migrationsDir)) {
748
+ fs.mkdirSync(migrationsDir);
749
+ }
750
+ // Lưu từng migration vào file
751
+ migrations.forEach((migration, index) => {
752
+ const timestamp = new Date(migration.created_at)
753
+ .toISOString()
754
+ .replace(/[-:]/g, "")
755
+ .replace(/\..+/, "");
756
+ const filename = path.join(migrationsDir, `${index + 1}_migration_${timestamp}.sql`);
757
+ fs.writeFileSync(filename, migration.sql, { encoding: "utf8" });
758
+ });
759
+ console.log(chalk_1.default.green("✓ Migrations saved successfully!"));
760
+ // Download project files and save to project
761
+ let files = [];
762
+ const apiSpinner = (0, ora_1.default)(chalk_1.default.blue("Fetching project data...")).start();
763
+ try {
764
+ files = response.data.files || [];
765
+ apiSpinner.succeed(chalk_1.default.green("Project data fetched successfully!"));
766
+ const saveSpinner = (0, ora_1.default)(chalk_1.default.blue("Saving project files...")).start();
767
+ files.forEach((file) => {
768
+ // skip ./app, ./components
769
+ if (file.path.startsWith("./app") ||
770
+ file.path.startsWith("./components")) {
771
+ return;
772
+ }
773
+ // hoặc các file có đuôi là .tsx
774
+ if (file.path.endsWith(".tsx")) {
775
+ return;
776
+ }
777
+ try {
778
+ if (file.content.trim().length == 0)
779
+ return;
780
+ const filePath = path.join(process.cwd(), id, "src", file.path);
781
+ const dir = path.dirname(filePath);
782
+ if (!fs.existsSync(dir)) {
783
+ fs.mkdirSync(dir, { recursive: true });
784
+ }
785
+ fs.writeFileSync(filePath, file.content);
786
+ }
787
+ catch (error) {
788
+ console.log(chalk_1.default.red("✗ Error writing file"), file.path, error);
789
+ }
790
+ });
791
+ saveSpinner.succeed(chalk_1.default.green("Project files saved successfully!"));
792
+ }
793
+ catch (error) {
794
+ apiSpinner.fail(chalk_1.default.red(`✗ Error cloning project: ${error.message}`));
795
+ }
796
+ // Download project files and save to project
797
+ console.log(chalk_1.default.green("✓ Project cloned successfully!"));
798
+ // Save project ID to hapico.config.json
799
+ saveProjectId(outputDir, id);
800
+ console.log(chalk_1.default.green("✓ Project setup successfully!"));
801
+ let serveProcess = null;
802
+ if (options.serve) {
803
+ // Run bun install
804
+ const { exec } = require("child_process");
805
+ const installSpinner = (0, ora_1.default)(chalk_1.default.blue("Installing dependencies...")).start();
806
+ // create a loop to check if there is new version of project
807
+ // https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
808
+ let currentVersionStr = fs.existsSync(path.join(outputDir, "version.json"))
809
+ ? JSON.parse(fs.readFileSync(path.join(outputDir, "version.json"), {
810
+ encoding: "utf8",
811
+ })).version
812
+ : "0.0.1";
813
+ const checkVersion = async () => {
814
+ var _a, _b;
815
+ try {
816
+ // check if file version.json exists
817
+ const flyCheck = await axios_1.default.head(`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`);
818
+ if (flyCheck.status !== 200) {
819
+ return;
820
+ }
821
+ // get file version.json
822
+ const versionResponse = await axios_1.default.get(`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`);
823
+ const latestVersionData = versionResponse.data;
824
+ const latestVersion = latestVersionData.version;
825
+ if (latestVersion !== currentVersionStr) {
826
+ console.log(chalk_1.default.yellow(`📦 New version available: ${latestVersion}`));
827
+ // Save new version.json
828
+ fs.writeFileSync(path.join(outputDir, "version.json"), JSON.stringify(latestVersionData, null, 2), { encoding: "utf8" });
829
+ // Install external libraries if any
830
+ if (latestVersionData.external_libraries &&
831
+ latestVersionData.external_libraries.length > 0) {
832
+ console.log(chalk_1.default.blue("Installing new external libraries..."));
833
+ for (const lib of latestVersionData.external_libraries) {
834
+ try {
835
+ await execPromise(`bun add ${lib}`, { cwd: outputDir });
836
+ console.log(chalk_1.default.green(`Added ${lib}`));
837
+ }
838
+ catch (addError) {
839
+ console.error(chalk_1.default.red(`✗ Error adding ${lib}:`), addError.message);
840
+ }
841
+ }
842
+ }
843
+ // Rerun bun install
844
+ try {
845
+ await execPromise("bun install", { cwd: outputDir });
846
+ console.log(chalk_1.default.green("Dependencies reinstalled successfully!"));
847
+ }
848
+ catch (installError) {
849
+ console.error(chalk_1.default.red("✗ Error reinstalling dependencies:"), installError.message);
850
+ }
851
+ // Restart the process
852
+ if (serveProcess && !serveProcess.killed) {
853
+ console.log(chalk_1.default.blue("Restarting backend..."));
854
+ serveProcess.kill("SIGTERM");
855
+ }
856
+ // Start new process
857
+ const newServeProcess = exec("bun run start", { cwd: outputDir });
858
+ (_a = newServeProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (data) => {
859
+ process.stdout.write(data);
860
+ });
861
+ (_b = newServeProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (data) => {
862
+ process.stderr.write(data);
863
+ });
864
+ newServeProcess.on("close", (code) => {
865
+ console.log(chalk_1.default.yellow(`⚠ backend exited with code ${code}`));
866
+ });
867
+ serveProcess = newServeProcess;
868
+ currentVersionStr = latestVersion;
869
+ }
870
+ }
871
+ catch (error) {
872
+ // If the remote version.json does not exist, do not update
873
+ console.error(chalk_1.default.yellow("⚠ Error checking for updates (skipping update):"), error.message);
874
+ }
875
+ };
876
+ // check every 3 seconds
877
+ setInterval(async () => await checkVersion(), 10 * 1000);
878
+ exec("bun install", { cwd: outputDir }, async (error, stdout, stderr) => {
879
+ if (error) {
880
+ installSpinner.fail(chalk_1.default.red(`✗ Error installing dependencies: ${error.message}`));
881
+ return;
882
+ }
883
+ if (stderr) {
884
+ console.error(chalk_1.default.red(`stderr: ${stderr}`));
885
+ }
886
+ installSpinner.succeed(chalk_1.default.green("Dependencies installed successfully!"));
887
+ // Install additional libraries if --libs is provided
888
+ if (options.libs && options.libs.trim()) {
889
+ const libsSpinner = (0, ora_1.default)(chalk_1.default.blue("Installing additional libraries...")).start();
890
+ const additionalLibs = options.libs
891
+ .split(",")
892
+ .map((lib) => lib.trim())
893
+ .filter((lib) => lib);
894
+ for (const lib of additionalLibs) {
895
+ try {
896
+ await execPromise(`bun add ${lib}`, { cwd: outputDir });
897
+ console.log(chalk_1.default.green(`Added ${lib}`));
898
+ }
899
+ catch (addError) {
900
+ console.error(chalk_1.default.red(`✗ Error adding ${lib}:`), addError.message);
901
+ }
902
+ }
903
+ libsSpinner.succeed(chalk_1.default.green("Additional libraries installed successfully!"));
904
+ }
905
+ // Run cd ${id} && bun run serve-be
906
+ console.log(chalk_1.default.blue("🚀 Starting backend"));
907
+ serveProcess = exec("bun run start", { cwd: outputDir });
908
+ serveProcess.stdout.on("data", (data) => {
909
+ process.stdout.write(data);
910
+ });
911
+ serveProcess.stderr.on("data", (data) => {
912
+ process.stderr.write(data);
913
+ });
914
+ serveProcess.on("close", (code) => {
915
+ console.log(chalk_1.default.yellow(`⚠ backend exited with code ${code}`));
916
+ });
917
+ });
918
+ }
919
+ });
920
+ // hapico publish
921
+ commander_1.program.command("publish").action(async () => {
922
+ const publishSpinner = (0, ora_1.default)(chalk_1.default.blue("Publishing to Hapico...")).start();
923
+ // Bước 1: Push mã nguồn lên server (tái sử dụng hàm pushProject)
924
+ const pushSuccess = await pushProject(publishSpinner);
925
+ if (!pushSuccess) {
926
+ return; // Dừng nếu push thất bại
927
+ }
928
+ // Bước 2: Gọi API để publish
929
+ const { accessToken } = getStoredToken();
930
+ const pwd = process.cwd();
931
+ const projectId = getStoredProjectId(pwd);
932
+ if (!projectId) {
933
+ publishSpinner.fail(chalk_1.default.red("✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."));
934
+ return;
935
+ }
936
+ const apiUrl = "https://base.myworkbeast.com/api/views/publish";
937
+ try {
938
+ await axios_1.default.post(apiUrl, { view_id: parseInt(projectId, 10) }, // Đảm bảo viewId là number
939
+ {
940
+ headers: {
941
+ Authorization: `Bearer ${accessToken}`,
942
+ "Content-Type": "application/json",
943
+ },
944
+ });
945
+ publishSpinner.succeed(chalk_1.default.green("Project published successfully!"));
623
946
  }
624
947
  catch (error) {
625
- apiSpinner.fail(`Error fetching project files: ${error.message}`);
948
+ console.log(error);
949
+ publishSpinner.fail(chalk_1.default.red(`✗ Error publishing project: ${error.message}`));
626
950
  }
627
951
  });
628
952
  commander_1.program.parse(process.argv);