@hapico/cli 0.0.16 → 0.0.18
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 +326 -58
- package/bin/templates/base/.gen/functions.js +126 -0
- package/bin/templates/base/.gen/plugins/router.plugin.js +12 -0
- package/bin/templates/base/plugins/vite-plugin-app-router.js +108 -0
- package/bin/templates/base/src/app.js +18 -0
- package/bin/templates/base/src/db/index.js +132 -0
- package/bin/templates/base/src/server.js +17 -0
- package/bin/utils/codemod/utils/path.mockup.js +84 -0
- package/bun.lock +77 -0
- package/dist/index.js +326 -58
- package/dist/templates/base/.gen/functions.js +126 -0
- package/dist/templates/base/.gen/plugins/router.plugin.js +12 -0
- package/dist/templates/base/plugins/vite-plugin-app-router.js +108 -0
- package/dist/templates/base/src/app.js +18 -0
- package/dist/templates/base/src/db/index.js +132 -0
- package/dist/templates/base/src/server.js +17 -0
- package/dist/utils/codemod/utils/path.mockup.js +84 -0
- package/index.ts +498 -58
- package/package.json +4 -1
- package/utils/codemod/index.tsx +1588 -0
- package/utils/codemod/utils/path.mockup.ts +97 -0
package/index.ts
CHANGED
|
@@ -12,6 +12,13 @@ import * as Babel from "@babel/standalone";
|
|
|
12
12
|
import QRCode from "qrcode-terminal";
|
|
13
13
|
import open from "open";
|
|
14
14
|
import { randomUUID } from "crypto";
|
|
15
|
+
import inquirer from "inquirer";
|
|
16
|
+
import { exec } from "child_process";
|
|
17
|
+
import { promisify } from "util";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
|
|
20
|
+
// Promisify exec for async usage
|
|
21
|
+
const execPromise = promisify(exec);
|
|
15
22
|
|
|
16
23
|
// Directory to store the token and project config
|
|
17
24
|
const CONFIG_DIR = path.join(
|
|
@@ -20,7 +27,7 @@ const CONFIG_DIR = path.join(
|
|
|
20
27
|
);
|
|
21
28
|
const TOKEN_FILE = path.join(CONFIG_DIR, "auth_token.json");
|
|
22
29
|
|
|
23
|
-
const connected = ora("Connected to WebSocket server");
|
|
30
|
+
const connected = ora(chalk.cyan("Connected to WebSocket server"));
|
|
24
31
|
|
|
25
32
|
// Ensure config directory exists
|
|
26
33
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
@@ -89,6 +96,9 @@ interface FileContent {
|
|
|
89
96
|
interface ApiResponse {
|
|
90
97
|
data: {
|
|
91
98
|
files?: FileContent[];
|
|
99
|
+
type: "view" | "zalominiapp" | string;
|
|
100
|
+
dbCode: string;
|
|
101
|
+
db_code: string;
|
|
92
102
|
};
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -188,7 +198,9 @@ class FileManager {
|
|
|
188
198
|
hasChanged = existingContent !== file.content;
|
|
189
199
|
} catch (readError) {
|
|
190
200
|
console.warn(
|
|
191
|
-
|
|
201
|
+
chalk.yellow(
|
|
202
|
+
`Warning: Could not read existing file ${fullPath}, treating as changed:`
|
|
203
|
+
),
|
|
192
204
|
readError
|
|
193
205
|
);
|
|
194
206
|
hasChanged = true;
|
|
@@ -199,7 +211,7 @@ class FileManager {
|
|
|
199
211
|
fs.writeFileSync(fullPath, file.content, { encoding: "utf8" });
|
|
200
212
|
}
|
|
201
213
|
} catch (error) {
|
|
202
|
-
console.error(`Error processing file ${file.path}
|
|
214
|
+
console.error(chalk.red(`Error processing file ${file.path}:`), error);
|
|
203
215
|
throw error;
|
|
204
216
|
}
|
|
205
217
|
}
|
|
@@ -223,7 +235,10 @@ class FileManager {
|
|
|
223
235
|
this.onFileChange!(fullPath, content);
|
|
224
236
|
}
|
|
225
237
|
} catch (error) {
|
|
226
|
-
console.warn(
|
|
238
|
+
console.warn(
|
|
239
|
+
chalk.yellow(`Error reading changed file ${fullPath}:`),
|
|
240
|
+
error
|
|
241
|
+
);
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
244
|
}
|
|
@@ -289,23 +304,25 @@ class RoomState {
|
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
this.ws = new WebSocket(
|
|
292
|
-
`https://
|
|
307
|
+
`https://ws2.myworkbeast.com/ws?room=${this.roomId}`
|
|
293
308
|
);
|
|
294
309
|
|
|
295
310
|
this.ws.onopen = () => {
|
|
296
|
-
console.log(`Connected to room: ${this.roomId}`);
|
|
311
|
+
console.log(chalk.green(`Connected to room: ${this.roomId}`));
|
|
297
312
|
this.isConnected = true;
|
|
298
313
|
this.reconnectAttempts = 0;
|
|
299
314
|
onConnected?.(); // Call the onConnected callback if provided
|
|
300
315
|
};
|
|
301
316
|
|
|
302
317
|
this.ws.onclose = () => {
|
|
303
|
-
console.log(
|
|
318
|
+
console.log(chalk.yellow(`⚠ Disconnected from room: ${this.roomId}`));
|
|
304
319
|
this.isConnected = false;
|
|
305
320
|
|
|
306
321
|
this.reconnectAttempts++;
|
|
307
322
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
308
|
-
console.log(
|
|
323
|
+
console.log(
|
|
324
|
+
chalk.yellow(`Attempting to reconnect in ${delay / 1000}s...`)
|
|
325
|
+
);
|
|
309
326
|
|
|
310
327
|
this.reconnectTimeout = setTimeout(
|
|
311
328
|
() => this.connect(onConnected),
|
|
@@ -360,12 +377,12 @@ class RoomState {
|
|
|
360
377
|
this.state = newState;
|
|
361
378
|
}
|
|
362
379
|
} catch (e) {
|
|
363
|
-
console.error("Error processing message:", e);
|
|
380
|
+
console.error(chalk.red("Error processing message:"), e);
|
|
364
381
|
}
|
|
365
382
|
});
|
|
366
383
|
|
|
367
384
|
this.ws.on("error", (err) => {
|
|
368
|
-
console.error("WebSocket error:", err);
|
|
385
|
+
console.error(chalk.red("WebSocket error:"), err);
|
|
369
386
|
this.ws?.close();
|
|
370
387
|
});
|
|
371
388
|
}
|
|
@@ -400,7 +417,7 @@ class RoomState {
|
|
|
400
417
|
}
|
|
401
418
|
}
|
|
402
419
|
|
|
403
|
-
program.version("0.0.
|
|
420
|
+
program.version("0.0.18").description("Hapico CLI for project management");
|
|
404
421
|
|
|
405
422
|
program
|
|
406
423
|
.command("clone <id>")
|
|
@@ -408,43 +425,49 @@ program
|
|
|
408
425
|
.action(async (id: string) => {
|
|
409
426
|
const { accessToken } = getStoredToken();
|
|
410
427
|
if (!accessToken) {
|
|
411
|
-
console.error(
|
|
428
|
+
console.error(
|
|
429
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
430
|
+
);
|
|
412
431
|
return;
|
|
413
432
|
}
|
|
414
433
|
const projectDir: string = path.resolve(process.cwd(), id);
|
|
415
434
|
if (fs.existsSync(projectDir)) {
|
|
416
|
-
console.error(
|
|
435
|
+
console.error(chalk.red(`✗ Project directory "${id}" already exists.`));
|
|
417
436
|
return;
|
|
418
437
|
}
|
|
419
438
|
|
|
420
439
|
let files: FileContent[] = [];
|
|
421
|
-
const apiSpinner: Ora = ora("Fetching project data...").start();
|
|
440
|
+
const apiSpinner: Ora = ora(chalk.blue("Fetching project data...")).start();
|
|
422
441
|
|
|
423
442
|
try {
|
|
424
443
|
const response: ApiResponse = await axios.get(
|
|
425
444
|
`https://base.myworkbeast.com/api/views/${id}`
|
|
426
445
|
);
|
|
427
446
|
files = response.data.files || [];
|
|
428
|
-
apiSpinner.succeed("Project data fetched successfully!");
|
|
447
|
+
apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
|
|
429
448
|
|
|
430
|
-
const templateSpinner: Ora = ora(
|
|
449
|
+
const templateSpinner: Ora = ora(
|
|
450
|
+
chalk.blue("Downloading template...")
|
|
451
|
+
).start();
|
|
431
452
|
const TEMPLATE_URL: string =
|
|
432
453
|
"https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
|
|
433
454
|
const templateResponse = await axios.get(TEMPLATE_URL, {
|
|
434
455
|
responseType: "arraybuffer",
|
|
435
456
|
});
|
|
436
|
-
templateSpinner.succeed("Template downloaded successfully!");
|
|
457
|
+
templateSpinner.succeed(chalk.green("Template downloaded successfully!"));
|
|
437
458
|
|
|
438
459
|
const outputDir: string = path.resolve(process.cwd(), id);
|
|
439
460
|
if (!fs.existsSync(outputDir)) {
|
|
440
461
|
fs.mkdirSync(outputDir);
|
|
441
462
|
}
|
|
442
463
|
|
|
443
|
-
const unzipSpinner: Ora = ora(
|
|
464
|
+
const unzipSpinner: Ora = ora(
|
|
465
|
+
chalk.blue("Extracting template...")
|
|
466
|
+
).start();
|
|
444
467
|
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
445
468
|
directory.extract({ path: outputDir })
|
|
446
469
|
);
|
|
447
|
-
unzipSpinner.succeed("Template extracted successfully!");
|
|
470
|
+
unzipSpinner.succeed(chalk.green("Template extracted successfully!"));
|
|
448
471
|
|
|
449
472
|
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
450
473
|
if (fs.existsSync(macosxDir)) {
|
|
@@ -454,9 +477,11 @@ program
|
|
|
454
477
|
// Save project ID to hapico.config.json
|
|
455
478
|
saveProjectId(outputDir, id);
|
|
456
479
|
|
|
457
|
-
console.log("Project cloned successfully!");
|
|
480
|
+
console.log(chalk.green("Project cloned successfully!"));
|
|
458
481
|
|
|
459
|
-
const saveSpinner: Ora = ora(
|
|
482
|
+
const saveSpinner: Ora = ora(
|
|
483
|
+
chalk.blue("Saving project files...")
|
|
484
|
+
).start();
|
|
460
485
|
files.forEach((file: FileContent) => {
|
|
461
486
|
const filePath: string = path.join(process.cwd(), id, "src", file.path);
|
|
462
487
|
const dir: string = path.dirname(filePath);
|
|
@@ -467,12 +492,14 @@ program
|
|
|
467
492
|
|
|
468
493
|
fs.writeFileSync(filePath, file.content);
|
|
469
494
|
});
|
|
470
|
-
saveSpinner.succeed("Project files saved successfully!");
|
|
495
|
+
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
471
496
|
console.log(
|
|
472
|
-
|
|
497
|
+
chalk.cyan(
|
|
498
|
+
`💡 Run ${chalk.bold("cd ${id} && npm install && hapico dev")} to start the project.`
|
|
499
|
+
)
|
|
473
500
|
);
|
|
474
501
|
} catch (error: any) {
|
|
475
|
-
apiSpinner.fail(`Error cloning project: ${error.message}`);
|
|
502
|
+
apiSpinner.fail(chalk.red(`Error cloning project: ${error.message}`));
|
|
476
503
|
}
|
|
477
504
|
});
|
|
478
505
|
|
|
@@ -482,17 +509,21 @@ program
|
|
|
482
509
|
.action(() => {
|
|
483
510
|
const { accessToken } = getStoredToken();
|
|
484
511
|
if (!accessToken) {
|
|
485
|
-
console.error(
|
|
512
|
+
console.error(
|
|
513
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
514
|
+
);
|
|
486
515
|
return;
|
|
487
516
|
}
|
|
488
517
|
const devSpinner = ora(
|
|
489
|
-
"Starting the project in development mode..."
|
|
518
|
+
chalk.blue("Starting the project in development mode...")
|
|
490
519
|
).start();
|
|
491
520
|
const pwd = process.cwd();
|
|
492
521
|
const srcDir = path.join(pwd, "src");
|
|
493
522
|
if (!fs.existsSync(srcDir)) {
|
|
494
523
|
devSpinner.fail(
|
|
495
|
-
|
|
524
|
+
chalk.red(
|
|
525
|
+
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
526
|
+
)
|
|
496
527
|
);
|
|
497
528
|
return;
|
|
498
529
|
}
|
|
@@ -542,25 +573,29 @@ program
|
|
|
542
573
|
const projectId = Buffer.from(info).toString("base64");
|
|
543
574
|
if (!projectId) {
|
|
544
575
|
devSpinner.fail(
|
|
545
|
-
|
|
576
|
+
chalk.red(
|
|
577
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
578
|
+
)
|
|
546
579
|
);
|
|
547
580
|
return;
|
|
548
581
|
}
|
|
549
582
|
|
|
550
|
-
console.log(
|
|
583
|
+
console.log(chalk.cyan("🔗 Connecting to WebSocket server"));
|
|
551
584
|
const room = new RoomState(`view_${projectId}`, []);
|
|
552
585
|
const fileManager = new FileManager(srcDir);
|
|
553
586
|
const initialFiles = fileManager.listFiles();
|
|
554
587
|
room.files = initialFiles;
|
|
555
588
|
|
|
556
589
|
room.connect(async () => {
|
|
557
|
-
devSpinner.succeed("Project started in development mode!");
|
|
590
|
+
devSpinner.succeed(chalk.green("Project started in development mode!"));
|
|
558
591
|
|
|
559
592
|
room.updateState("view", initialFiles);
|
|
560
593
|
|
|
561
594
|
fileManager.setOnFileChange((filePath, content) => {
|
|
562
595
|
const es5 = compileES5(content, filePath) ?? "";
|
|
563
|
-
console.log(
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.yellow(`📝 File changed: ${filePath?.replace(srcDir, ".")}`)
|
|
598
|
+
);
|
|
564
599
|
const updatedFiles = room.files.map((file) => {
|
|
565
600
|
if (path.join(srcDir, file.path) === filePath) {
|
|
566
601
|
return { ...file, content, es5 };
|
|
@@ -575,7 +610,9 @@ program
|
|
|
575
610
|
const projectInfo = getStoredProjectId(pwd);
|
|
576
611
|
if (!projectInfo) {
|
|
577
612
|
console.error(
|
|
578
|
-
|
|
613
|
+
chalk.red(
|
|
614
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
615
|
+
)
|
|
579
616
|
);
|
|
580
617
|
return;
|
|
581
618
|
}
|
|
@@ -590,7 +627,9 @@ program
|
|
|
590
627
|
);
|
|
591
628
|
|
|
592
629
|
if (project.status !== 200) {
|
|
593
|
-
console.error(
|
|
630
|
+
console.error(
|
|
631
|
+
chalk.red(`✗ Error fetching project info: ${project.statusText}`)
|
|
632
|
+
);
|
|
594
633
|
return;
|
|
595
634
|
}
|
|
596
635
|
|
|
@@ -601,7 +640,9 @@ program
|
|
|
601
640
|
`https://zalo.me/s/3218692650896662017/player/${projectId}`,
|
|
602
641
|
{ small: true },
|
|
603
642
|
(qrcode) => {
|
|
604
|
-
console.log(
|
|
643
|
+
console.log(
|
|
644
|
+
chalk.cyan("📱 Scan this QR code to connect to the project:")
|
|
645
|
+
);
|
|
605
646
|
console.log(qrcode);
|
|
606
647
|
}
|
|
607
648
|
);
|
|
@@ -609,7 +650,9 @@ program
|
|
|
609
650
|
} else {
|
|
610
651
|
const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
|
|
611
652
|
console.log(
|
|
612
|
-
|
|
653
|
+
chalk.cyan(
|
|
654
|
+
`🌐 Open this URL in your browser to preview the project:\n${previewUrl}`
|
|
655
|
+
)
|
|
613
656
|
);
|
|
614
657
|
await open(previewUrl);
|
|
615
658
|
}
|
|
@@ -623,27 +666,47 @@ program
|
|
|
623
666
|
const data = getStoredToken();
|
|
624
667
|
const { accessToken } = data || {};
|
|
625
668
|
if (!accessToken) {
|
|
626
|
-
console.error(
|
|
669
|
+
console.error(
|
|
670
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
671
|
+
);
|
|
627
672
|
return;
|
|
628
673
|
}
|
|
629
|
-
const saveSpinner: Ora = ora(
|
|
674
|
+
const saveSpinner: Ora = ora(
|
|
675
|
+
chalk.blue("Saving project source code...")
|
|
676
|
+
).start();
|
|
630
677
|
const pwd = process.cwd();
|
|
631
678
|
const projectId = getStoredProjectId(pwd);
|
|
632
679
|
if (!projectId) {
|
|
633
680
|
saveSpinner.fail(
|
|
634
|
-
|
|
681
|
+
chalk.red(
|
|
682
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
683
|
+
)
|
|
635
684
|
);
|
|
636
685
|
return;
|
|
637
686
|
}
|
|
638
687
|
const srcDir: string = path.join(pwd, "src");
|
|
639
688
|
if (!fs.existsSync(srcDir)) {
|
|
640
689
|
saveSpinner.fail(
|
|
641
|
-
|
|
690
|
+
chalk.red(
|
|
691
|
+
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
692
|
+
)
|
|
642
693
|
);
|
|
643
694
|
return;
|
|
644
695
|
}
|
|
645
696
|
const fileManager = new FileManager(srcDir);
|
|
646
697
|
const files = fileManager.listFiles();
|
|
698
|
+
|
|
699
|
+
// Nếu có file .env
|
|
700
|
+
const envFile = path.join(pwd, ".env");
|
|
701
|
+
console.log(chalk.cyan(`🔍 Checking for .env file at ${envFile}`));
|
|
702
|
+
if (fs.existsSync(envFile)) {
|
|
703
|
+
console.log(chalk.green(".env file found, including in push."));
|
|
704
|
+
const content = fs.readFileSync(envFile, { encoding: "utf8" });
|
|
705
|
+
files.push({
|
|
706
|
+
path: "./.env",
|
|
707
|
+
content,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
647
710
|
const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
|
|
648
711
|
axios
|
|
649
712
|
.put(
|
|
@@ -661,10 +724,12 @@ program
|
|
|
661
724
|
}
|
|
662
725
|
)
|
|
663
726
|
.then(() => {
|
|
664
|
-
saveSpinner.succeed(
|
|
727
|
+
saveSpinner.succeed(
|
|
728
|
+
chalk.green("Project source code saved successfully!")
|
|
729
|
+
);
|
|
665
730
|
})
|
|
666
731
|
.catch((error) => {
|
|
667
|
-
saveSpinner.fail(
|
|
732
|
+
saveSpinner.fail(chalk.red(`✗ Error saving project: ${error.message}`));
|
|
668
733
|
});
|
|
669
734
|
});
|
|
670
735
|
|
|
@@ -672,8 +737,8 @@ program
|
|
|
672
737
|
.command("login")
|
|
673
738
|
.description("Login to the system")
|
|
674
739
|
.action(async () => {
|
|
675
|
-
console.log("Logging in to the system...");
|
|
676
|
-
const loginSpinner: Ora = ora("Initiating login...").start();
|
|
740
|
+
console.log(chalk.cyan("🔐 Logging in to the system..."));
|
|
741
|
+
const loginSpinner: Ora = ora(chalk.blue("Initiating login...")).start();
|
|
677
742
|
|
|
678
743
|
try {
|
|
679
744
|
const response = await axios.post(
|
|
@@ -682,14 +747,20 @@ program
|
|
|
682
747
|
const { device_code, user_code, verification_url, expires_in, interval } =
|
|
683
748
|
response.data;
|
|
684
749
|
|
|
685
|
-
loginSpinner.succeed("Login initiated!");
|
|
686
|
-
console.log(
|
|
687
|
-
|
|
688
|
-
|
|
750
|
+
loginSpinner.succeed(chalk.green("Login initiated!"));
|
|
751
|
+
console.log(
|
|
752
|
+
chalk.cyan(
|
|
753
|
+
`🌐 Please open this URL in your browser: ${verification_url}`
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
console.log(chalk.yellow(`🔑 And enter this code: ${user_code}`));
|
|
757
|
+
console.log(chalk.blue("⏳ Waiting for authentication..."));
|
|
689
758
|
|
|
690
759
|
await open(verification_url);
|
|
691
760
|
|
|
692
|
-
const pollSpinner: Ora = ora(
|
|
761
|
+
const pollSpinner: Ora = ora(
|
|
762
|
+
chalk.blue("Waiting for authentication...")
|
|
763
|
+
).start();
|
|
693
764
|
let tokens = null;
|
|
694
765
|
const startTime = Date.now();
|
|
695
766
|
while (Date.now() - startTime < expires_in * 1000) {
|
|
@@ -709,15 +780,19 @@ program
|
|
|
709
780
|
}
|
|
710
781
|
|
|
711
782
|
if (tokens) {
|
|
712
|
-
pollSpinner.succeed("Login successful!");
|
|
783
|
+
pollSpinner.succeed(chalk.green("Login successful!"));
|
|
713
784
|
saveToken(tokens);
|
|
714
785
|
} else {
|
|
715
786
|
pollSpinner.fail(
|
|
716
|
-
|
|
787
|
+
chalk.red(
|
|
788
|
+
"✗ Login failed: Timeout or user did not complete authentication."
|
|
789
|
+
)
|
|
717
790
|
);
|
|
718
791
|
}
|
|
719
792
|
} catch (error) {
|
|
720
|
-
loginSpinner.fail(
|
|
793
|
+
loginSpinner.fail(
|
|
794
|
+
chalk.red(`✗ Login error: ${(error as Error).message}`)
|
|
795
|
+
);
|
|
721
796
|
}
|
|
722
797
|
});
|
|
723
798
|
|
|
@@ -727,11 +802,11 @@ program
|
|
|
727
802
|
.action(() => {
|
|
728
803
|
const accessToken = getStoredToken();
|
|
729
804
|
if (!accessToken) {
|
|
730
|
-
console.log("You are not logged in.");
|
|
805
|
+
console.log(chalk.yellow("ℹ You are not logged in."));
|
|
731
806
|
return;
|
|
732
807
|
}
|
|
733
808
|
fs.unlinkSync(TOKEN_FILE);
|
|
734
|
-
console.log("Logout successful!");
|
|
809
|
+
console.log(chalk.green("Logout successful!"));
|
|
735
810
|
});
|
|
736
811
|
// Pull command to fetch the latest project files from the server
|
|
737
812
|
program
|
|
@@ -740,18 +815,24 @@ program
|
|
|
740
815
|
.action(async () => {
|
|
741
816
|
const token = getStoredToken();
|
|
742
817
|
if (!token) {
|
|
743
|
-
console.error(
|
|
818
|
+
console.error(
|
|
819
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
820
|
+
);
|
|
744
821
|
return;
|
|
745
822
|
}
|
|
746
823
|
const pwd: string = process.cwd();
|
|
747
824
|
const projectId = getStoredProjectId(pwd);
|
|
748
825
|
if (!projectId) {
|
|
749
826
|
console.error(
|
|
750
|
-
|
|
827
|
+
chalk.red(
|
|
828
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
829
|
+
)
|
|
751
830
|
);
|
|
752
831
|
return;
|
|
753
832
|
}
|
|
754
|
-
const apiSpinner: Ora = ora(
|
|
833
|
+
const apiSpinner: Ora = ora(
|
|
834
|
+
chalk.blue("Fetching latest project files...")
|
|
835
|
+
).start();
|
|
755
836
|
try {
|
|
756
837
|
const response: ApiResponse = await axios.get(
|
|
757
838
|
`https://base.myworkbeast.com/api/views/${projectId}`,
|
|
@@ -765,12 +846,371 @@ program
|
|
|
765
846
|
const files: FileContent[] = response.data.files || [];
|
|
766
847
|
const fileManager = new FileManager(path.join(pwd, "src"));
|
|
767
848
|
fileManager.syncFiles(files);
|
|
768
|
-
apiSpinner.succeed("Project files updated successfully!");
|
|
849
|
+
apiSpinner.succeed(chalk.green("Project files updated successfully!"));
|
|
769
850
|
} catch (error) {
|
|
770
851
|
apiSpinner.fail(
|
|
771
|
-
|
|
852
|
+
chalk.red(`✗ Error fetching project files: ${(error as Error).message}`)
|
|
772
853
|
);
|
|
773
854
|
}
|
|
774
855
|
});
|
|
775
856
|
|
|
857
|
+
// be {{id}} --be --port {{port}}
|
|
858
|
+
program
|
|
859
|
+
.command("fetch <id>")
|
|
860
|
+
.option("--port <port>", "Port to run the backend", "3000")
|
|
861
|
+
.option("--serve", "Flag to indicate serving the backend")
|
|
862
|
+
.option(
|
|
863
|
+
"--libs <libs>",
|
|
864
|
+
"Additional libraries to install (comma-separated)",
|
|
865
|
+
""
|
|
866
|
+
)
|
|
867
|
+
.option("--be", "Flag to indicate backend")
|
|
868
|
+
.description("Open backend for the project")
|
|
869
|
+
.action(
|
|
870
|
+
async (
|
|
871
|
+
id: string,
|
|
872
|
+
options: { port: string; serve?: boolean; libs?: string; be?: boolean }
|
|
873
|
+
) => {
|
|
874
|
+
const { accessToken } = getStoredToken();
|
|
875
|
+
if (!accessToken) {
|
|
876
|
+
console.error(
|
|
877
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
878
|
+
);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
console.log(chalk.cyan(`🌐 PORT = ${options.port}`));
|
|
883
|
+
|
|
884
|
+
// Chọn hỏi user port để vận hành
|
|
885
|
+
let port = 3000;
|
|
886
|
+
const response: ApiResponse = await axios.get(
|
|
887
|
+
`https://base.myworkbeast.com/api/views/${id}`
|
|
888
|
+
);
|
|
889
|
+
const portInput = options.port;
|
|
890
|
+
if (portInput) {
|
|
891
|
+
const parsedPort = parseInt(portInput, 10);
|
|
892
|
+
if (!isNaN(parsedPort)) {
|
|
893
|
+
port = parsedPort;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const projectDir: string = path.resolve(process.cwd(), id);
|
|
898
|
+
if (!fs.existsSync(projectDir)) {
|
|
899
|
+
// create folder
|
|
900
|
+
fs.mkdirSync(projectDir);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
|
|
904
|
+
const TEMPLATE_URL: string = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
|
|
905
|
+
let templateResponse = undefined;
|
|
906
|
+
try {
|
|
907
|
+
templateResponse = await axios.get(TEMPLATE_URL, {
|
|
908
|
+
responseType: "arraybuffer",
|
|
909
|
+
});
|
|
910
|
+
} catch (error) {
|
|
911
|
+
console.error(
|
|
912
|
+
chalk.red("✗ Error downloading template:"),
|
|
913
|
+
(error as Error).message
|
|
914
|
+
);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const outputDir: string = path.resolve(process.cwd(), id);
|
|
919
|
+
if (!fs.existsSync(outputDir)) {
|
|
920
|
+
fs.mkdirSync(outputDir);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
924
|
+
directory.extract({ path: outputDir })
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
928
|
+
if (fs.existsSync(macosxDir)) {
|
|
929
|
+
fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// outputPath/src/server.ts có dòng (3006) thay thành port
|
|
933
|
+
const serverFile = path.join(process.cwd(), id, "src", "index.ts");
|
|
934
|
+
if (fs.existsSync(serverFile)) {
|
|
935
|
+
let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
|
|
936
|
+
serverContent = serverContent.split("(3006)").join(`(${port})`);
|
|
937
|
+
fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// lấy danh sách migrations
|
|
941
|
+
const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${
|
|
942
|
+
response.data.dbCode || response.data.db_code
|
|
943
|
+
}&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
|
|
944
|
+
|
|
945
|
+
// Lấy danh sách các migration đã chạy
|
|
946
|
+
let migrations: Array<{
|
|
947
|
+
id: number;
|
|
948
|
+
created_at: string;
|
|
949
|
+
sql_statement: string;
|
|
950
|
+
}> = [];
|
|
951
|
+
const migrationsResponse = await axios.get(MIGRATIONS_URL, {
|
|
952
|
+
headers: {
|
|
953
|
+
Authorization: `Bearer ${accessToken}`,
|
|
954
|
+
"Content-Type": "application/json",
|
|
955
|
+
"x-db-code": response.data.dbCode || response.data.db_code,
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
if (migrationsResponse.status === 200) {
|
|
959
|
+
migrations = (migrationsResponse.data.data?.rows || []).map(
|
|
960
|
+
(item: any) => ({
|
|
961
|
+
created_at: item.created_at,
|
|
962
|
+
sql: item.sql_statement,
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
console.log(chalk.cyan(`🔍 Found ${migrations.length} migrations.`));
|
|
968
|
+
|
|
969
|
+
// Tạo thư mục migrations nếu chưa tồn tại
|
|
970
|
+
const migrationsDir = path.join(process.cwd(), id, "migrations");
|
|
971
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
972
|
+
fs.mkdirSync(migrationsDir);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Lưu từng migration vào file
|
|
976
|
+
migrations.forEach((migration, index) => {
|
|
977
|
+
const timestamp = new Date(migration.created_at)
|
|
978
|
+
.toISOString()
|
|
979
|
+
.replace(/[-:]/g, "")
|
|
980
|
+
.replace(/\..+/, "");
|
|
981
|
+
const filename = path.join(
|
|
982
|
+
migrationsDir,
|
|
983
|
+
`${index + 1}_migration_${timestamp}.sql`
|
|
984
|
+
);
|
|
985
|
+
fs.writeFileSync(filename, (migration as any).sql, { encoding: "utf8" });
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
console.log(chalk.green("✓ Migrations saved successfully!"));
|
|
989
|
+
|
|
990
|
+
// Download project files and save to project
|
|
991
|
+
let files: FileContent[] = [];
|
|
992
|
+
const apiSpinner: Ora = ora(
|
|
993
|
+
chalk.blue("Fetching project data...")
|
|
994
|
+
).start();
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
files = response.data.files || [];
|
|
998
|
+
apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
|
|
999
|
+
|
|
1000
|
+
const saveSpinner: Ora = ora(
|
|
1001
|
+
chalk.blue("Saving project files...")
|
|
1002
|
+
).start();
|
|
1003
|
+
files.forEach((file: FileContent) => {
|
|
1004
|
+
// skip ./app, ./components
|
|
1005
|
+
if (
|
|
1006
|
+
file.path.startsWith("./app") ||
|
|
1007
|
+
file.path.startsWith("./components")
|
|
1008
|
+
) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
// hoặc các file có đuôi là .tsx
|
|
1012
|
+
if (file.path.endsWith(".tsx")) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
if (file.content.trim().length == 0) return;
|
|
1017
|
+
const filePath: string = path.join(
|
|
1018
|
+
process.cwd(),
|
|
1019
|
+
id,
|
|
1020
|
+
"src",
|
|
1021
|
+
file.path
|
|
1022
|
+
);
|
|
1023
|
+
const dir: string = path.dirname(filePath);
|
|
1024
|
+
|
|
1025
|
+
if (!fs.existsSync(dir)) {
|
|
1026
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
fs.writeFileSync(filePath, file.content);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.log(chalk.red("✗ Error writing file"), file.path, error);
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
1035
|
+
} catch (error: any) {
|
|
1036
|
+
apiSpinner.fail(chalk.red(`✗ Error cloning project: ${error.message}`));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Download project files and save to project
|
|
1040
|
+
console.log(chalk.green("✓ Project cloned successfully!"));
|
|
1041
|
+
|
|
1042
|
+
// Save project ID to hapico.config.json
|
|
1043
|
+
saveProjectId(outputDir, id);
|
|
1044
|
+
|
|
1045
|
+
console.log(chalk.green("✓ Project setup successfully!"));
|
|
1046
|
+
|
|
1047
|
+
let serveProcess: any = null;
|
|
1048
|
+
|
|
1049
|
+
if (options.serve) {
|
|
1050
|
+
// Run bun install
|
|
1051
|
+
const { exec } = require("child_process");
|
|
1052
|
+
const installSpinner: Ora = ora(
|
|
1053
|
+
chalk.blue("Installing dependencies...")
|
|
1054
|
+
).start();
|
|
1055
|
+
|
|
1056
|
+
// create a loop to check if there is new version of project
|
|
1057
|
+
// https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
|
|
1058
|
+
let currentVersionStr = fs.existsSync(
|
|
1059
|
+
path.join(outputDir, "version.json")
|
|
1060
|
+
)
|
|
1061
|
+
? JSON.parse(
|
|
1062
|
+
fs.readFileSync(path.join(outputDir, "version.json"), {
|
|
1063
|
+
encoding: "utf8",
|
|
1064
|
+
})
|
|
1065
|
+
).version
|
|
1066
|
+
: "0.0.1";
|
|
1067
|
+
|
|
1068
|
+
const checkVersion = async () => {
|
|
1069
|
+
try {
|
|
1070
|
+
// check if file version.json exists
|
|
1071
|
+
const flyCheck = await axios.head(
|
|
1072
|
+
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1073
|
+
);
|
|
1074
|
+
if (flyCheck.status !== 200) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
// get file version.json
|
|
1078
|
+
const versionResponse = await axios.get(
|
|
1079
|
+
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1080
|
+
);
|
|
1081
|
+
const latestVersionData = versionResponse.data;
|
|
1082
|
+
const latestVersion = latestVersionData.version;
|
|
1083
|
+
if (latestVersion !== currentVersionStr) {
|
|
1084
|
+
console.log(
|
|
1085
|
+
chalk.yellow(`📦 New version available: ${latestVersion}`)
|
|
1086
|
+
);
|
|
1087
|
+
// Save new version.json
|
|
1088
|
+
fs.writeFileSync(
|
|
1089
|
+
path.join(outputDir, "version.json"),
|
|
1090
|
+
JSON.stringify(latestVersionData, null, 2),
|
|
1091
|
+
{ encoding: "utf8" }
|
|
1092
|
+
);
|
|
1093
|
+
// Install external libraries if any
|
|
1094
|
+
if (
|
|
1095
|
+
latestVersionData.external_libraries &&
|
|
1096
|
+
latestVersionData.external_libraries.length > 0
|
|
1097
|
+
) {
|
|
1098
|
+
console.log(chalk.blue("Installing new external libraries..."));
|
|
1099
|
+
for (const lib of latestVersionData.external_libraries) {
|
|
1100
|
+
try {
|
|
1101
|
+
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1102
|
+
console.log(chalk.green(`Added ${lib}`));
|
|
1103
|
+
} catch (addError) {
|
|
1104
|
+
console.error(
|
|
1105
|
+
chalk.red(`✗ Error adding ${lib}:`),
|
|
1106
|
+
(addError as Error).message
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// Rerun bun install
|
|
1112
|
+
try {
|
|
1113
|
+
await execPromise("bun install", { cwd: outputDir });
|
|
1114
|
+
console.log(
|
|
1115
|
+
chalk.green("Dependencies reinstalled successfully!")
|
|
1116
|
+
);
|
|
1117
|
+
} catch (installError) {
|
|
1118
|
+
console.error(
|
|
1119
|
+
chalk.red("✗ Error reinstalling dependencies:"),
|
|
1120
|
+
(installError as Error).message
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
// Restart the process
|
|
1124
|
+
if (serveProcess && !serveProcess.killed) {
|
|
1125
|
+
console.log(chalk.blue("Restarting backend..."));
|
|
1126
|
+
serveProcess.kill("SIGTERM");
|
|
1127
|
+
}
|
|
1128
|
+
// Start new process
|
|
1129
|
+
const newServeProcess = exec("bun run start", { cwd: outputDir });
|
|
1130
|
+
newServeProcess.stdout?.on("data", (data: any) => {
|
|
1131
|
+
process.stdout.write(data);
|
|
1132
|
+
});
|
|
1133
|
+
newServeProcess.stderr?.on("data", (data: any) => {
|
|
1134
|
+
process.stderr.write(data);
|
|
1135
|
+
});
|
|
1136
|
+
newServeProcess.on("close", (code: any) => {
|
|
1137
|
+
console.log(
|
|
1138
|
+
chalk.yellow(`⚠ backend exited with code ${code}`)
|
|
1139
|
+
);
|
|
1140
|
+
});
|
|
1141
|
+
serveProcess = newServeProcess;
|
|
1142
|
+
currentVersionStr = latestVersion;
|
|
1143
|
+
}
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
// If the remote version.json does not exist, do not update
|
|
1146
|
+
console.error(
|
|
1147
|
+
chalk.yellow("⚠ Error checking for updates (skipping update):"),
|
|
1148
|
+
(error as Error).message
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// check every 3 seconds
|
|
1154
|
+
setInterval(async () => await checkVersion(), 10 * 1000);
|
|
1155
|
+
|
|
1156
|
+
exec(
|
|
1157
|
+
"bun install",
|
|
1158
|
+
{ cwd: outputDir },
|
|
1159
|
+
async (error: any, stdout: any, stderr: any) => {
|
|
1160
|
+
if (error) {
|
|
1161
|
+
installSpinner.fail(
|
|
1162
|
+
chalk.red(`✗ Error installing dependencies: ${error.message}`)
|
|
1163
|
+
);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (stderr) {
|
|
1167
|
+
console.error(chalk.red(`stderr: ${stderr}`));
|
|
1168
|
+
}
|
|
1169
|
+
installSpinner.succeed(
|
|
1170
|
+
chalk.green("Dependencies installed successfully!")
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
// Install additional libraries if --libs is provided
|
|
1174
|
+
if (options.libs && options.libs.trim()) {
|
|
1175
|
+
const libsSpinner: Ora = ora(
|
|
1176
|
+
chalk.blue("Installing additional libraries...")
|
|
1177
|
+
).start();
|
|
1178
|
+
const additionalLibs = options.libs
|
|
1179
|
+
.split(",")
|
|
1180
|
+
.map((lib) => lib.trim())
|
|
1181
|
+
.filter((lib) => lib);
|
|
1182
|
+
for (const lib of additionalLibs) {
|
|
1183
|
+
try {
|
|
1184
|
+
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1185
|
+
console.log(chalk.green(`Added ${lib}`));
|
|
1186
|
+
} catch (addError) {
|
|
1187
|
+
console.error(
|
|
1188
|
+
chalk.red(`✗ Error adding ${lib}:`),
|
|
1189
|
+
(addError as Error).message
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
libsSpinner.succeed(
|
|
1194
|
+
chalk.green("Additional libraries installed successfully!")
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Run cd ${id} && bun run serve-be
|
|
1199
|
+
console.log(chalk.blue("🚀 Starting backend"));
|
|
1200
|
+
serveProcess = exec("bun run start", { cwd: outputDir });
|
|
1201
|
+
serveProcess.stdout.on("data", (data: any) => {
|
|
1202
|
+
process.stdout.write(data);
|
|
1203
|
+
});
|
|
1204
|
+
serveProcess.stderr.on("data", (data: any) => {
|
|
1205
|
+
process.stderr.write(data);
|
|
1206
|
+
});
|
|
1207
|
+
serveProcess.on("close", (code: any) => {
|
|
1208
|
+
console.log(chalk.yellow(`⚠ backend exited with code ${code}`));
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
);
|
|
1215
|
+
|
|
776
1216
|
program.parse(process.argv);
|