@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 +382 -490
- package/bin/tools/nativewind/index.js +80 -0
- package/bin/tools/vibe/index.js +88 -108
- package/bin/tools/vibe/patch.utils.js +243 -0
- package/bun.lock +874 -0
- package/dist/index.js +382 -490
- package/dist/tools/nativewind/index.js +80 -0
- package/dist/tools/vibe/index.js +88 -108
- package/dist/tools/vibe/patch.utils.js +243 -0
- package/index.ts +475 -787
- package/package.json +5 -1
- package/tools/nativewind/index.ts +87 -0
- package/tools/vibe/index.ts +91 -108
- package/tools/vibe/patch.utils.ts +273 -0
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { promisify } from "util";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import pako from "pako";
|
|
19
19
|
import { vibe } from "./tools/vibe";
|
|
20
|
+
import { compileAndSaveToFolder } from "./tools/nativewind";
|
|
20
21
|
|
|
21
22
|
// Promisify exec for async usage
|
|
22
23
|
const execPromise = promisify(exec);
|
|
@@ -96,7 +97,7 @@ const getStoredProjectId = (
|
|
|
96
97
|
};
|
|
97
98
|
};
|
|
98
99
|
|
|
99
|
-
//
|
|
100
|
+
// Initialize cache using Map to store compilation results
|
|
100
101
|
const compileCache = new Map<string, string>();
|
|
101
102
|
|
|
102
103
|
interface FileContent {
|
|
@@ -187,7 +188,7 @@ class FileManager {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
private replaceSplashWithSeparator(filePath: string): string {
|
|
190
|
-
//
|
|
191
|
+
// Replace "\" with "/" in the file path
|
|
191
192
|
return filePath.replace(/\\/g, "/");
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -199,12 +200,12 @@ class FileManager {
|
|
|
199
200
|
if (entry.isDirectory()) {
|
|
200
201
|
traverseDirectory(fullPath);
|
|
201
202
|
} else if (entry.isFile()) {
|
|
202
|
-
// skip
|
|
203
|
+
// skip files in ignored folders
|
|
203
204
|
if (ignoreFolders.some((folder) => fullPath.includes(folder))) {
|
|
204
205
|
return;
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
//
|
|
208
|
+
// only support files located in ./src
|
|
208
209
|
if (!fullPath.includes("/src/") && !fullPath.includes("\\src\\")) {
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
@@ -321,10 +322,6 @@ class FileManager {
|
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
interface FileContent {
|
|
325
|
-
// Define the structure of FileContent if needed
|
|
326
|
-
}
|
|
327
|
-
|
|
328
325
|
class RoomState {
|
|
329
326
|
private roomId: string;
|
|
330
327
|
private state: RoomStateData;
|
|
@@ -365,7 +362,9 @@ class RoomState {
|
|
|
365
362
|
this.ws.binaryType = "arraybuffer";
|
|
366
363
|
|
|
367
364
|
this.ws.on("open", () => {
|
|
368
|
-
|
|
365
|
+
if (this.reconnectAttempts > 0) {
|
|
366
|
+
console.log(chalk.greenBright(`\n✨ Sync connection restored for session: ${this.roomId}`));
|
|
367
|
+
}
|
|
369
368
|
this.isConnected = true;
|
|
370
369
|
this.reconnectAttempts = 0;
|
|
371
370
|
onConnected?.(); // Call the onConnected callback if provided
|
|
@@ -397,7 +396,7 @@ class RoomState {
|
|
|
397
396
|
} else if (Buffer.isBuffer(data)) {
|
|
398
397
|
jsonStr = pako.inflate(data, { to: "string" });
|
|
399
398
|
} else {
|
|
400
|
-
jsonStr = data.toString(); // Fallback
|
|
399
|
+
jsonStr = data.toString(); // Fallback if not compressed
|
|
401
400
|
}
|
|
402
401
|
const parsedData = JSON.parse(jsonStr);
|
|
403
402
|
const includes = [
|
|
@@ -459,8 +458,8 @@ class RoomState {
|
|
|
459
458
|
type: "update",
|
|
460
459
|
state: { [key]: value },
|
|
461
460
|
});
|
|
462
|
-
const compressed = pako.deflate(message); //
|
|
463
|
-
this.ws.send(compressed); //
|
|
461
|
+
const compressed = pako.deflate(message); // Compress state data
|
|
462
|
+
this.ws.send(compressed); // Send binary payload
|
|
464
463
|
}
|
|
465
464
|
}
|
|
466
465
|
|
|
@@ -483,142 +482,103 @@ class RoomState {
|
|
|
483
482
|
}
|
|
484
483
|
}
|
|
485
484
|
|
|
486
|
-
program.version("0.0.
|
|
485
|
+
program.version("0.0.31").description("Hapico CLI for project management");
|
|
487
486
|
|
|
488
487
|
program
|
|
489
488
|
.command("clone <id>")
|
|
490
489
|
.description("Clone a project by ID")
|
|
491
490
|
.action(async (id: string) => {
|
|
491
|
+
console.clear();
|
|
492
|
+
console.log(chalk.bold.blue("\n📦 HAPICO CLONE PIPELINE"));
|
|
493
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
494
|
+
|
|
492
495
|
const { accessToken } = getStoredToken();
|
|
493
496
|
if (!accessToken) {
|
|
494
|
-
console.
|
|
495
|
-
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
496
|
-
);
|
|
497
|
+
console.log(chalk.red("✗ Authentication required. Please run 'hapico login' first."));
|
|
497
498
|
return;
|
|
498
499
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
|
|
501
|
+
const outputDir = path.resolve(process.cwd(), id);
|
|
502
|
+
if (fs.existsSync(outputDir)) {
|
|
503
|
+
console.log(chalk.red(`✗ Destination directory "${id}" already exists.`));
|
|
502
504
|
return;
|
|
503
505
|
}
|
|
504
506
|
|
|
505
|
-
|
|
506
|
-
const apiSpinner: Ora = ora(chalk.blue("Fetching project data...")).start();
|
|
507
|
+
const statusSpinner = ora(chalk.blue("Fetching project metadata...")).start();
|
|
507
508
|
|
|
508
509
|
try {
|
|
509
|
-
//
|
|
510
|
-
const response: ApiResponse = await axios.get(
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
// 1. Fetch metadata and initial files
|
|
511
|
+
const response: ApiResponse = await axios.get(`https://base.myworkbeast.com/api/views/${id}`);
|
|
512
|
+
const projectType = response.data.type || "view";
|
|
513
|
+
|
|
513
514
|
const code = response?.data?.code;
|
|
514
515
|
const decompressedCode = pako.inflate(
|
|
515
516
|
Uint8Array.from(atob(code), (c) => c.charCodeAt(0)),
|
|
516
517
|
{ to: "string" }
|
|
517
518
|
);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const outputDir: string = path.resolve(process.cwd(), id);
|
|
533
|
-
if (!fs.existsSync(outputDir)) {
|
|
534
|
-
fs.mkdirSync(outputDir);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const unzipSpinner: Ora = ora(
|
|
538
|
-
chalk.blue("Extracting template...")
|
|
539
|
-
).start();
|
|
519
|
+
const publicFiles = tryJSONParse(decompressedCode)?.files || [];
|
|
520
|
+
|
|
521
|
+
statusSpinner.succeed(chalk.green("Project metadata retrieved."));
|
|
522
|
+
console.log(chalk.bold.white("Project Details:"));
|
|
523
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("ID :")} ${chalk.cyan(id)}`);
|
|
524
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Type :")} ${chalk.magenta(projectType.toUpperCase())}\n`);
|
|
525
|
+
|
|
526
|
+
// 2. Setup Template
|
|
527
|
+
statusSpinner.start(chalk.blue("Initializing project template..."));
|
|
528
|
+
const TEMPLATE_URL = "https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
|
|
529
|
+
const templateResponse = await axios.get(TEMPLATE_URL, { responseType: "arraybuffer" });
|
|
530
|
+
|
|
531
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
532
|
+
|
|
540
533
|
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
541
534
|
directory.extract({ path: outputDir })
|
|
542
535
|
);
|
|
543
|
-
unzipSpinner.succeed(chalk.green("Template extracted successfully!"));
|
|
544
|
-
|
|
545
|
-
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
546
|
-
if (fs.existsSync(macosxDir)) {
|
|
547
|
-
fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
548
|
-
}
|
|
549
536
|
|
|
550
|
-
|
|
537
|
+
const macosxDir = path.join(outputDir, "__MACOSX");
|
|
538
|
+
if (fs.existsSync(macosxDir)) fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
539
|
+
|
|
551
540
|
saveProjectId(outputDir, id);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// ===== 3. Save file từ version public =====
|
|
555
|
-
const saveSpinner: Ora = ora(
|
|
556
|
-
chalk.blue("Saving project files...")
|
|
557
|
-
).start();
|
|
558
|
-
files.forEach((file: FileContent) => {
|
|
559
|
-
const filePath: string = path.join(process.cwd(), id, "src", file.path);
|
|
560
|
-
const dir: string = path.dirname(filePath);
|
|
541
|
+
statusSpinner.succeed(chalk.green("Base template initialized."));
|
|
561
542
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
});
|
|
568
|
-
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
569
|
-
|
|
570
|
-
// ===== 4. TỰ ĐỘNG PULL PHIÊN BẢN MỚI NHẤT (draft) – ĐÂY LÀ PHẦN MỚI =====
|
|
571
|
-
const pullSpinner: Ora = ora(
|
|
572
|
-
chalk.blue("Pulling latest changes from server...")
|
|
573
|
-
).start();
|
|
543
|
+
// 3. Sync Source Files
|
|
544
|
+
statusSpinner.start(chalk.blue("Syncing source files..."));
|
|
545
|
+
const srcPath = path.join(outputDir, "src");
|
|
546
|
+
const fileManager = new FileManager(srcPath);
|
|
547
|
+
fileManager.syncFiles(publicFiles);
|
|
548
|
+
statusSpinner.succeed(chalk.green(`${publicFiles.length} files synchronized from production.`));
|
|
574
549
|
|
|
550
|
+
// 4. Pull Latest Draft (v3)
|
|
551
|
+
statusSpinner.start(chalk.blue("Checking for latest development changes..."));
|
|
575
552
|
try {
|
|
576
|
-
const pullResponse = await axios.get(
|
|
577
|
-
`
|
|
578
|
-
|
|
579
|
-
headers: {
|
|
580
|
-
Authorization: `Bearer ${accessToken}`,
|
|
581
|
-
"Content-Type": "application/json",
|
|
582
|
-
},
|
|
583
|
-
}
|
|
584
|
-
);
|
|
553
|
+
const pullResponse = await axios.get(`https://base.myworkbeast.com/api/views/v3/${id}`, {
|
|
554
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
555
|
+
});
|
|
585
556
|
|
|
586
557
|
const pullCode = pullResponse?.data?.code;
|
|
587
|
-
if (
|
|
588
|
-
pullSpinner.info(
|
|
589
|
-
chalk.cyan("No draft version found – using published version.")
|
|
590
|
-
);
|
|
591
|
-
} else {
|
|
558
|
+
if (pullCode) {
|
|
592
559
|
const pullDecompressed = pako.inflate(
|
|
593
560
|
Uint8Array.from(atob(pullCode), (c) => c.charCodeAt(0)),
|
|
594
561
|
{ to: "string" }
|
|
595
562
|
);
|
|
596
|
-
const latestFiles
|
|
597
|
-
tryJSONParse(pullDecompressed)?.files || [];
|
|
598
|
-
|
|
599
|
-
const fileManager = new FileManager(path.join(outputDir, "src"));
|
|
563
|
+
const latestFiles = tryJSONParse(pullDecompressed)?.files || [];
|
|
600
564
|
fileManager.syncFiles(latestFiles);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
);
|
|
565
|
+
statusSpinner.succeed(chalk.green("Latest development changes integrated."));
|
|
566
|
+
} else {
|
|
567
|
+
statusSpinner.info(chalk.gray("No draft version found, using published code."));
|
|
605
568
|
}
|
|
606
|
-
} catch (
|
|
607
|
-
|
|
608
|
-
chalk.yellow(
|
|
609
|
-
`⚠ Could not pull latest changes: ${pullErr.message}\n Run "hapico pull" manually later.`
|
|
610
|
-
)
|
|
611
|
-
);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
statusSpinner.warn(chalk.yellow("Draft sync skipped (using production code)."));
|
|
612
571
|
}
|
|
613
572
|
|
|
614
|
-
|
|
615
|
-
console.log(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
573
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
574
|
+
console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Project cloned successfully!\n")}`);
|
|
575
|
+
console.log(chalk.bold.white("Next steps:"));
|
|
576
|
+
console.log(`${chalk.gray("1.")} ${chalk.cyan(`cd ${id}`)}`);
|
|
577
|
+
console.log(`${chalk.gray("2.")} ${chalk.cyan("npm install")}`);
|
|
578
|
+
console.log(`${chalk.gray("3.")} ${chalk.cyan("hapico dev")}\n`);
|
|
579
|
+
|
|
620
580
|
} catch (error: any) {
|
|
621
|
-
|
|
581
|
+
statusSpinner.fail(chalk.red(`Clone failed: ${error.message}`));
|
|
622
582
|
}
|
|
623
583
|
});
|
|
624
584
|
|
|
@@ -634,7 +594,7 @@ program
|
|
|
634
594
|
.command("dev")
|
|
635
595
|
.description("Start the project in development mode")
|
|
636
596
|
.option("--zversion <version>", "Zalo version for QR code")
|
|
637
|
-
.action((options) => {
|
|
597
|
+
.action(async (options) => {
|
|
638
598
|
const { accessToken } = getStoredToken();
|
|
639
599
|
if (!accessToken) {
|
|
640
600
|
console.error(
|
|
@@ -642,86 +602,78 @@ program
|
|
|
642
602
|
);
|
|
643
603
|
return;
|
|
644
604
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
605
|
+
|
|
606
|
+
console.clear();
|
|
607
|
+
console.log(chalk.bold.cyan("\n🚀 HAPICO DEVELOPMENT SERVER"));
|
|
608
|
+
console.log(chalk.gray("─────────────────────────────────────────"));
|
|
609
|
+
|
|
610
|
+
const devSpinner = ora(chalk.blue("Initializing environment...")).start();
|
|
648
611
|
const pwd = process.cwd();
|
|
649
612
|
const srcDir = path.join(pwd, "src");
|
|
613
|
+
|
|
650
614
|
if (!fs.existsSync(srcDir)) {
|
|
651
615
|
devSpinner.fail(
|
|
652
|
-
chalk.red(
|
|
653
|
-
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
654
|
-
)
|
|
616
|
+
chalk.red("Source directory 'src' not found. Ensure you are in a valid project folder.")
|
|
655
617
|
);
|
|
656
618
|
return;
|
|
657
619
|
}
|
|
658
620
|
|
|
659
|
-
// Directory to store session config
|
|
660
621
|
const tmpDir = path.join(pwd, ".tmp");
|
|
661
622
|
const sessionConfigFile = path.join(tmpDir, "config.json");
|
|
623
|
+
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
662
624
|
|
|
663
|
-
// Ensure .tmp directory exists
|
|
664
|
-
if (!fs.existsSync(tmpDir)) {
|
|
665
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Function to get stored session ID
|
|
669
625
|
const getStoredSessionId = () => {
|
|
670
626
|
if (fs.existsSync(sessionConfigFile)) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
627
|
+
try {
|
|
628
|
+
return JSON.parse(fs.readFileSync(sessionConfigFile, "utf8")).sessionId;
|
|
629
|
+
} catch { return null; }
|
|
674
630
|
}
|
|
675
631
|
return null;
|
|
676
632
|
};
|
|
677
633
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
fs.writeFileSync(
|
|
681
|
-
sessionConfigFile,
|
|
682
|
-
JSON.stringify({ sessionId }, null, 2),
|
|
683
|
-
{ encoding: "utf8" }
|
|
684
|
-
);
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// Get or generate session ID
|
|
688
|
-
let sessionId = getStoredSessionId();
|
|
689
|
-
if (!sessionId) {
|
|
690
|
-
sessionId = randomUUID();
|
|
691
|
-
saveSessionId(sessionId);
|
|
692
|
-
}
|
|
634
|
+
let sessionId = getStoredSessionId() || randomUUID();
|
|
635
|
+
fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2));
|
|
693
636
|
|
|
637
|
+
const projectConfig = getStoredProjectId(pwd);
|
|
694
638
|
const info = JSON.stringify({
|
|
695
639
|
id: sessionId,
|
|
696
640
|
createdAt: new Date().toISOString(),
|
|
697
|
-
viewId:
|
|
641
|
+
viewId: projectConfig?.projectId,
|
|
698
642
|
});
|
|
699
643
|
|
|
700
|
-
// Convert info to base64
|
|
701
644
|
const projectId = Buffer.from(info).toString("base64");
|
|
702
|
-
if (!projectId) {
|
|
703
|
-
devSpinner.fail(
|
|
704
|
-
chalk.red(
|
|
705
|
-
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
706
|
-
)
|
|
707
|
-
);
|
|
645
|
+
if (!projectConfig?.projectId) {
|
|
646
|
+
devSpinner.fail(chalk.red("Project ID missing in hapico.config.json"));
|
|
708
647
|
return;
|
|
709
648
|
}
|
|
710
649
|
|
|
711
|
-
|
|
712
|
-
const room = new RoomState(`view_${projectId}`, []);
|
|
650
|
+
devSpinner.text = chalk.blue("Scanning project directory...");
|
|
713
651
|
const fileManager = new FileManager(srcDir);
|
|
714
652
|
const initialFiles = fileManager.listFiles();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
653
|
+
|
|
654
|
+
// Supported files outside src
|
|
655
|
+
const SUPPORT_FILES = [
|
|
656
|
+
"./.env",
|
|
657
|
+
"./.env.local",
|
|
658
|
+
"./.env.development",
|
|
659
|
+
"./.env.production",
|
|
660
|
+
"./package.json",
|
|
661
|
+
"./tsconfig.json",
|
|
662
|
+
"./nativewind.json"
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
SUPPORT_FILES.forEach((relativePath) => {
|
|
666
|
+
const fullPath = path.join(pwd, relativePath);
|
|
667
|
+
if (fs.existsSync(fullPath)) {
|
|
668
|
+
const content = fs.readFileSync(fullPath, { encoding: "utf8" });
|
|
669
|
+
initialFiles.push({
|
|
670
|
+
path: relativePath,
|
|
671
|
+
content,
|
|
672
|
+
es5: compileES5(content, fullPath) ?? "",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
725
677
|
const supportExtensions = [
|
|
726
678
|
".ts",
|
|
727
679
|
".tsx",
|
|
@@ -734,144 +686,137 @@ program
|
|
|
734
686
|
".env.development",
|
|
735
687
|
".env.production",
|
|
736
688
|
];
|
|
737
|
-
const filteredFiles = initialFiles.filter((file) =>
|
|
738
|
-
|
|
739
|
-
|
|
689
|
+
const filteredFiles = initialFiles.filter((file) =>
|
|
690
|
+
supportExtensions.some((ext) => file.path.endsWith(ext))
|
|
691
|
+
);
|
|
740
692
|
|
|
693
|
+
devSpinner.text = chalk.blue("Establishing WebSocket connection...");
|
|
694
|
+
const room = new RoomState(`view_${projectId}`, []);
|
|
741
695
|
room.files = filteredFiles;
|
|
742
696
|
|
|
743
697
|
room.connect(async () => {
|
|
744
|
-
devSpinner.succeed(chalk.
|
|
698
|
+
devSpinner.succeed(chalk.greenBright("Sync Engine Ready"));
|
|
699
|
+
|
|
700
|
+
console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Connection established with workspace.\n")}`);
|
|
701
|
+
|
|
702
|
+
console.log(chalk.bold.white("Workspace Configuration:"));
|
|
703
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("Project ID :")} ${chalk.cyan(projectConfig.projectId)}`);
|
|
704
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("Session :")} ${chalk.gray(sessionId)}`);
|
|
705
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Local Path :")} ${chalk.gray(pwd)}`);
|
|
706
|
+
console.log("");
|
|
745
707
|
|
|
746
708
|
room.updateState("view", filteredFiles);
|
|
747
709
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}, 200);
|
|
752
|
-
|
|
753
|
-
fileManager.setOnFileChange((filePath, content) => {
|
|
754
|
-
const es5 = compileES5(content, filePath) ?? "";
|
|
755
|
-
console.log(
|
|
756
|
-
chalk.yellow(`📝 File changed: ${filePath?.replace(srcDir, ".")}`)
|
|
757
|
-
);
|
|
758
|
-
const updatedFiles = room.files.map((file) => {
|
|
759
|
-
if (path.join(srcDir, file.path) === filePath) {
|
|
760
|
-
return { ...file, content, es5 };
|
|
761
|
-
}
|
|
762
|
-
return file;
|
|
710
|
+
try {
|
|
711
|
+
const project = await axios.get(`https://base.myworkbeast.com/api/views/${projectConfig.projectId}`, {
|
|
712
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
763
713
|
});
|
|
764
|
-
room.files = updatedFiles;
|
|
765
|
-
debouncedUpdate(updatedFiles);
|
|
766
|
-
});
|
|
767
714
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (!store.projectId) {
|
|
771
|
-
console.error(
|
|
772
|
-
chalk.red(
|
|
773
|
-
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
774
|
-
)
|
|
775
|
-
);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const project = await axios.get(
|
|
779
|
-
`https://base.myworkbeast.com/api/views/${store.projectId}`,
|
|
780
|
-
{
|
|
781
|
-
headers: {
|
|
782
|
-
Authorization: `Bearer ${accessToken}`,
|
|
783
|
-
"Content-Type": "application/json",
|
|
784
|
-
},
|
|
785
|
-
}
|
|
786
|
-
);
|
|
715
|
+
const projectType = project.data.type || "view";
|
|
716
|
+
const isExpo = projectType === "expo_app" || projectType === "emg_edu_lesson";
|
|
787
717
|
|
|
788
|
-
|
|
789
|
-
console.
|
|
790
|
-
|
|
791
|
-
);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
718
|
+
console.log(chalk.bold.white("Environment Status:"));
|
|
719
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("Type :")} ${chalk.magenta(projectType.toUpperCase())}`);
|
|
720
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Watcher :")} ${chalk.greenBright("Active & Synchronizing")}`);
|
|
721
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────\n"));
|
|
794
722
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
723
|
+
if (isExpo) {
|
|
724
|
+
try { await compileAndSaveToFolder("src", "nativewind.json"); } catch (e) {}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const debouncedNativeWind = debounce(async () => {
|
|
728
|
+
if (isExpo) {
|
|
729
|
+
process.stdout.write(chalk.gray(" [System] Style assets synchronized successfully.\r"));
|
|
730
|
+
try { await compileAndSaveToFolder("src", "nativewind.json"); } catch (e) {}
|
|
731
|
+
}
|
|
732
|
+
}, 500);
|
|
733
|
+
|
|
734
|
+
const debouncedUpdate = debounce((updatedFiles: FileContent[]) => {
|
|
735
|
+
room.updateState("view", updatedFiles);
|
|
736
|
+
}, 200);
|
|
737
|
+
|
|
738
|
+
fileManager.setOnFileChange((filePath, content) => {
|
|
739
|
+
const timestamp = new Date().toLocaleTimeString([], { hour12: false });
|
|
740
|
+
const relativePath = filePath.replace(srcDir, ".");
|
|
741
|
+
console.log(`${chalk.gray(`[${timestamp}]`)} ${chalk.yellow.bold("CHANGE")} ${chalk.white(relativePath)}`);
|
|
742
|
+
|
|
743
|
+
const es5 = compileES5(content, filePath) ?? "";
|
|
744
|
+
const updatedFiles = room.files.map((file) => {
|
|
745
|
+
if (path.join(srcDir, file.path) === filePath) return { ...file, content, es5 };
|
|
746
|
+
return file;
|
|
747
|
+
});
|
|
748
|
+
room.files = updatedFiles;
|
|
749
|
+
debouncedUpdate(updatedFiles);
|
|
750
|
+
if (isExpo) debouncedNativeWind();
|
|
808
751
|
});
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
(
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
752
|
+
|
|
753
|
+
// UI Access Section
|
|
754
|
+
if (isExpo) {
|
|
755
|
+
const EXPO_GROUP_ID = "6f033d8b-7ed5-48dc-ab6f-f8348cf6993b";
|
|
756
|
+
const link = `exp://u.expo.dev/e362c6df-abe8-4503-8723-1362f015d167/group/${EXPO_GROUP_ID}?sessionKey=${projectId}&mode=development`;
|
|
757
|
+
console.log(chalk.bold.black.bgCyan(" 📱 EXPO GO ACCESS "));
|
|
758
|
+
QRCode.generate(link, { small: true }, (code) => {
|
|
759
|
+
console.log(chalk.white(code));
|
|
760
|
+
});
|
|
761
|
+
console.log(chalk.cyan(` Link: ${chalk.underline(link)}\n`));
|
|
762
|
+
} else if (projectType === "zalominiapp" || projectType === "zalominiapp2") {
|
|
763
|
+
const zversion = options.zversion;
|
|
764
|
+
const zaloUrl = zversion
|
|
765
|
+
? `https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=${zversion}`
|
|
766
|
+
: `https://zalo.me/s/3218692650896662017/player/${projectId}`;
|
|
767
|
+
|
|
768
|
+
const label = projectType === "zalominiapp2" ? " 🧩 ZALO MINI APP 2.0 " : " 🧩 ZALO MINI APP ";
|
|
769
|
+
console.log(chalk.bold.black.bgBlue(label));
|
|
770
|
+
QRCode.generate(zaloUrl, { small: true }, (code) => {
|
|
771
|
+
console.log(chalk.white(code));
|
|
772
|
+
});
|
|
773
|
+
console.log(chalk.blue(` Link: ${chalk.underline(zaloUrl)}\n`));
|
|
823
774
|
} else {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
chalk.cyan("📱 Scan this QR code to connect to the project:")
|
|
830
|
-
);
|
|
831
|
-
console.log(qrcode);
|
|
832
|
-
}
|
|
833
|
-
);
|
|
775
|
+
const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
|
|
776
|
+
const label = projectType === "web2" ? " 🌐 WEB 2.0 PREVIEW " : " 🌐 BROWSER PREVIEW ";
|
|
777
|
+
console.log(chalk.bold.black.bgGreen(label));
|
|
778
|
+
console.log(chalk.green(` URL: ${chalk.underline(previewUrl)}\n`));
|
|
779
|
+
await open(previewUrl);
|
|
834
780
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
)
|
|
842
|
-
);
|
|
843
|
-
await open(previewUrl);
|
|
781
|
+
|
|
782
|
+
console.log(chalk.gray("👀 Monitoring file changes... (Press Ctrl+C to exit)"));
|
|
783
|
+
|
|
784
|
+
} catch (err: any) {
|
|
785
|
+
console.log(chalk.yellow(`\n⚠️ Environment metadata could not be fetched: ${err.message}`));
|
|
786
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────\n"));
|
|
844
787
|
}
|
|
845
788
|
});
|
|
846
789
|
});
|
|
847
790
|
|
|
848
|
-
|
|
849
|
-
|
|
791
|
+
interface PushResult {
|
|
792
|
+
success: boolean;
|
|
793
|
+
projectId: string;
|
|
794
|
+
totalSize: number;
|
|
795
|
+
fileCount: number;
|
|
796
|
+
error?: string;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Chuyên nghiệp hóa việc push source code lên server
|
|
800
|
+
async function pushProject(projectId: string): Promise<PushResult> {
|
|
850
801
|
const data = getStoredToken();
|
|
851
802
|
const { accessToken } = data || {};
|
|
852
803
|
if (!accessToken) {
|
|
853
|
-
|
|
854
|
-
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
855
|
-
);
|
|
856
|
-
return false;
|
|
804
|
+
return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Authentication required. Please run 'hapico login'." };
|
|
857
805
|
}
|
|
806
|
+
|
|
858
807
|
const pwd = process.cwd();
|
|
859
808
|
const srcDir: string = path.join(pwd, "src");
|
|
860
809
|
if (!fs.existsSync(srcDir)) {
|
|
861
|
-
|
|
862
|
-
chalk.red(
|
|
863
|
-
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
864
|
-
)
|
|
865
|
-
);
|
|
866
|
-
return false;
|
|
810
|
+
return { success: false, projectId, totalSize: 0, fileCount: 0, error: "Source directory 'src' not found. Ensure you are in a valid project folder." };
|
|
867
811
|
}
|
|
812
|
+
|
|
868
813
|
const fileManager = new FileManager(srcDir);
|
|
869
814
|
const files = fileManager.listFiles().filter((file) => {
|
|
870
815
|
const extname = path.extname(file.path);
|
|
871
816
|
return supportedExtensions.includes(extname);
|
|
872
817
|
});
|
|
873
818
|
|
|
874
|
-
// Supported files
|
|
819
|
+
// Supported extra files at root
|
|
875
820
|
const SUPPORT_FILES = [
|
|
876
821
|
"./.env",
|
|
877
822
|
"./.env.local",
|
|
@@ -879,17 +824,12 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
|
|
|
879
824
|
"./.env.production",
|
|
880
825
|
"./package.json",
|
|
881
826
|
"./tsconfig.json",
|
|
827
|
+
"./nativewind.json"
|
|
882
828
|
];
|
|
883
829
|
|
|
884
|
-
// Include supported files
|
|
885
830
|
SUPPORT_FILES.forEach((relativePath) => {
|
|
886
831
|
const fullPath = path.join(pwd, relativePath);
|
|
887
832
|
if (fs.existsSync(fullPath)) {
|
|
888
|
-
console.log(
|
|
889
|
-
chalk.green(
|
|
890
|
-
`Including ${relativePath} in push for project ${projectId}.`
|
|
891
|
-
)
|
|
892
|
-
);
|
|
893
833
|
const content = fs.readFileSync(fullPath, { encoding: "utf8" });
|
|
894
834
|
files.push({
|
|
895
835
|
path: relativePath,
|
|
@@ -898,24 +838,14 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
|
|
|
898
838
|
});
|
|
899
839
|
}
|
|
900
840
|
});
|
|
901
|
-
// Show ra thông tin dung lượng mã nguồn
|
|
902
|
-
const totalSize = files.reduce((acc, file) => acc + file.content.length, 0);
|
|
903
|
-
console.log(
|
|
904
|
-
chalk.cyan(
|
|
905
|
-
`Total source code size for project ${projectId}: ${(totalSize / 1024).toFixed(2)} KB`
|
|
906
|
-
)
|
|
907
|
-
);
|
|
908
|
-
spinner.text = `Pushing project source code to server for project ${projectId}...`;
|
|
909
841
|
|
|
842
|
+
const totalSize = files.reduce((acc, file) => acc + file.content.length, 0);
|
|
910
843
|
const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
|
|
844
|
+
|
|
911
845
|
try {
|
|
912
846
|
await axios.put(
|
|
913
847
|
apiUrl,
|
|
914
|
-
{
|
|
915
|
-
code: JSON.stringify({
|
|
916
|
-
files,
|
|
917
|
-
}),
|
|
918
|
-
},
|
|
848
|
+
{ code: JSON.stringify({ files }) },
|
|
919
849
|
{
|
|
920
850
|
headers: {
|
|
921
851
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -923,14 +853,29 @@ async function pushProject(spinner: Ora, projectId: string): Promise<boolean> {
|
|
|
923
853
|
},
|
|
924
854
|
}
|
|
925
855
|
);
|
|
926
|
-
return true;
|
|
927
|
-
} catch (error) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
856
|
+
return { success: true, projectId, totalSize, fileCount: files.length };
|
|
857
|
+
} catch (error: any) {
|
|
858
|
+
return { success: false, projectId, totalSize, fileCount: files.length, error: error.message };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function publishProjectApi(projectId: string): Promise<{ success: boolean; error?: string }> {
|
|
863
|
+
const { accessToken } = getStoredToken();
|
|
864
|
+
const apiUrl = "https://base.myworkbeast.com/api/views/publish/v2";
|
|
865
|
+
try {
|
|
866
|
+
await axios.post(
|
|
867
|
+
apiUrl,
|
|
868
|
+
{ view_id: parseInt(projectId, 10) },
|
|
869
|
+
{
|
|
870
|
+
headers: {
|
|
871
|
+
Authorization: `Bearer ${accessToken}`,
|
|
872
|
+
"Content-Type": "application/json",
|
|
873
|
+
},
|
|
874
|
+
}
|
|
932
875
|
);
|
|
933
|
-
return
|
|
876
|
+
return { success: true };
|
|
877
|
+
} catch (error: any) {
|
|
878
|
+
return { success: false, error: error.message };
|
|
934
879
|
}
|
|
935
880
|
}
|
|
936
881
|
|
|
@@ -938,116 +883,112 @@ program
|
|
|
938
883
|
.command("push")
|
|
939
884
|
.description("Push the project source code to the server")
|
|
940
885
|
.action(async () => {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
886
|
+
console.clear();
|
|
887
|
+
console.log(chalk.bold.cyan("\n🚀 HAPICO PUSH PIPELINE"));
|
|
888
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
889
|
+
|
|
944
890
|
const pwd = process.cwd();
|
|
945
891
|
const { projectId, replicate } = getStoredProjectId(pwd);
|
|
946
892
|
|
|
947
893
|
if (!projectId) {
|
|
948
|
-
|
|
949
|
-
chalk.red(
|
|
950
|
-
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
951
|
-
)
|
|
952
|
-
);
|
|
894
|
+
console.log(chalk.red("✗ Project ID not found. Ensure hapico.config.json exists."));
|
|
953
895
|
return;
|
|
954
896
|
}
|
|
955
897
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
898
|
+
console.log(chalk.bold.white("Target Environments:"));
|
|
899
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("Main Project :")} ${chalk.cyan(projectId)}`);
|
|
900
|
+
if (replicate && replicate.length > 0) {
|
|
901
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.cyan(replicate.join(", "))}`);
|
|
902
|
+
} else {
|
|
903
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.gray("None")}`);
|
|
904
|
+
}
|
|
905
|
+
console.log("");
|
|
906
|
+
|
|
907
|
+
const pushSpinner = ora(chalk.blue("Syncing source code with main project...")).start();
|
|
908
|
+
|
|
909
|
+
const mainResult = await pushProject(projectId);
|
|
910
|
+
if (mainResult.success) {
|
|
911
|
+
pushSpinner.succeed(chalk.green(`Main Project [${projectId}] code synced successfully.`));
|
|
912
|
+
console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(mainResult.fileCount)}`);
|
|
913
|
+
console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
|
|
914
|
+
} else {
|
|
915
|
+
pushSpinner.fail(chalk.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
959
918
|
|
|
960
|
-
|
|
961
|
-
if (replicate &&
|
|
962
|
-
saveSpinner.text = chalk.blue("Pushing to replicated projects...");
|
|
919
|
+
let allSuccess = true;
|
|
920
|
+
if (replicate && replicate.length > 0) {
|
|
963
921
|
for (const repId of replicate) {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
)
|
|
971
|
-
);
|
|
922
|
+
const repSpinner = ora(chalk.blue(`Syncing replica [${repId}]...`)).start();
|
|
923
|
+
const repResult = await pushProject(repId);
|
|
924
|
+
if (repResult.success) {
|
|
925
|
+
repSpinner.succeed(chalk.green(`Replica [${repId}] code synced successfully.`));
|
|
926
|
+
console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(repResult.fileCount)}`);
|
|
927
|
+
console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
|
|
972
928
|
} else {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
);
|
|
929
|
+
repSpinner.fail(chalk.red(`Replica [${repId}] sync failed: ${repResult.error}`));
|
|
930
|
+
allSuccess = false;
|
|
976
931
|
}
|
|
977
932
|
}
|
|
978
933
|
}
|
|
979
934
|
|
|
935
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
980
936
|
if (allSuccess) {
|
|
981
|
-
|
|
982
|
-
chalk.green("Project source code saved successfully to all projects!")
|
|
983
|
-
);
|
|
937
|
+
console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("All source codes pushed successfully!\n")}`);
|
|
984
938
|
} else {
|
|
985
|
-
|
|
986
|
-
chalk.yellow("Project source code saved with some errors.")
|
|
987
|
-
);
|
|
939
|
+
console.log(`\n${chalk.bold.white.bgYellow(" WARNING ")} ${chalk.yellow("Push completed with some replica errors.\n")}`);
|
|
988
940
|
}
|
|
989
941
|
});
|
|
990
942
|
|
|
991
943
|
program
|
|
992
944
|
.command("login")
|
|
993
|
-
.description("
|
|
945
|
+
.description("Authenticate Hapico CLI with your account")
|
|
994
946
|
.action(async () => {
|
|
995
|
-
console.
|
|
996
|
-
|
|
947
|
+
console.clear();
|
|
948
|
+
console.log(chalk.bold.yellow("\n🔐 HAPICO SECURE LOGIN"));
|
|
949
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
950
|
+
|
|
951
|
+
const loginSpinner = ora(chalk.blue("Requesting device authorization...")).start();
|
|
997
952
|
|
|
998
953
|
try {
|
|
999
|
-
const response = await axios.post(
|
|
1000
|
-
|
|
1001
|
-
);
|
|
1002
|
-
const { device_code, user_code, verification_url, expires_in, interval } =
|
|
1003
|
-
response.data;
|
|
954
|
+
const response = await axios.post("https://auth.myworkbeast.com/auth/device");
|
|
955
|
+
const { device_code, user_code, verification_url, expires_in, interval } = response.data;
|
|
1004
956
|
|
|
1005
|
-
loginSpinner.succeed(chalk.green("
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
);
|
|
1011
|
-
console.log(chalk.yellow(`🔑 And enter this code: ${user_code}`));
|
|
1012
|
-
console.log(chalk.blue("⏳ Waiting for authentication..."));
|
|
957
|
+
loginSpinner.succeed(chalk.green("Authorization request created."));
|
|
958
|
+
|
|
959
|
+
console.log(`\n${chalk.bold.white("ACTION REQUIRED")}`);
|
|
960
|
+
console.log(`${chalk.gray("1.")} Open URL : ${chalk.underline.cyan(verification_url)}`);
|
|
961
|
+
console.log(`${chalk.gray("2.")} Enter Code: ${chalk.bold.bgWhite.black(` ${user_code} `)}`);
|
|
962
|
+
console.log(chalk.gray(`\n(Code expires in ${Math.floor(expires_in / 60)} minutes)`));
|
|
1013
963
|
|
|
1014
964
|
await open(verification_url);
|
|
1015
965
|
|
|
1016
|
-
const pollSpinner
|
|
1017
|
-
chalk.blue("Waiting for authentication...")
|
|
1018
|
-
).start();
|
|
966
|
+
const pollSpinner = ora(chalk.blue("Waiting for browser confirmation...")).start();
|
|
1019
967
|
let tokens = null;
|
|
1020
968
|
const startTime = Date.now();
|
|
969
|
+
|
|
1021
970
|
while (Date.now() - startTime < expires_in * 1000) {
|
|
1022
971
|
try {
|
|
1023
|
-
const pollResponse = await axios.post(
|
|
1024
|
-
"https://auth.myworkbeast.com/auth/device/poll",
|
|
1025
|
-
{ device_code }
|
|
1026
|
-
);
|
|
972
|
+
const pollResponse = await axios.post("https://auth.myworkbeast.com/auth/device/poll", { device_code });
|
|
1027
973
|
if (pollResponse.data.accessToken) {
|
|
1028
974
|
tokens = pollResponse.data;
|
|
1029
975
|
break;
|
|
1030
976
|
}
|
|
1031
|
-
} catch (
|
|
1032
|
-
// Ignore temporary errors and continue polling
|
|
1033
|
-
}
|
|
977
|
+
} catch (e) { /* Poll interval logic */ }
|
|
1034
978
|
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
1035
979
|
}
|
|
1036
980
|
|
|
1037
981
|
if (tokens) {
|
|
1038
|
-
pollSpinner.succeed(chalk.green("Login successful!"));
|
|
1039
982
|
saveToken(tokens);
|
|
983
|
+
pollSpinner.succeed(chalk.green("Authentication successful."));
|
|
984
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
985
|
+
console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("You are now logged in.")}`);
|
|
986
|
+
console.log(`${chalk.gray("Token stored at:")} ${chalk.gray(TOKEN_FILE)}\n`);
|
|
1040
987
|
} else {
|
|
1041
|
-
pollSpinner.fail(
|
|
1042
|
-
chalk.red(
|
|
1043
|
-
"✗ Login failed: Timeout or user did not complete authentication."
|
|
1044
|
-
)
|
|
1045
|
-
);
|
|
988
|
+
pollSpinner.fail(chalk.red("Login timed out or was cancelled."));
|
|
1046
989
|
}
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
loginSpinner.fail(
|
|
1049
|
-
chalk.red(`✗ Login error: ${(error as Error).message}`)
|
|
1050
|
-
);
|
|
990
|
+
} catch (error: any) {
|
|
991
|
+
loginSpinner.fail(chalk.red(`Login failed: ${error.message}`));
|
|
1051
992
|
}
|
|
1052
993
|
});
|
|
1053
994
|
|
|
@@ -1116,472 +1057,219 @@ program
|
|
|
1116
1057
|
}
|
|
1117
1058
|
});
|
|
1118
1059
|
|
|
1119
|
-
// be {{id}} --be --port {{port}}
|
|
1120
1060
|
program
|
|
1121
1061
|
.command("fetch <id>")
|
|
1122
|
-
.
|
|
1123
|
-
.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
.action(
|
|
1132
|
-
async (
|
|
1133
|
-
id: string,
|
|
1134
|
-
options: { port: string; serve?: boolean; libs?: string; be?: boolean }
|
|
1135
|
-
) => {
|
|
1136
|
-
const { accessToken } = getStoredToken();
|
|
1137
|
-
if (!accessToken) {
|
|
1138
|
-
console.error(
|
|
1139
|
-
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
1140
|
-
);
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
console.log(chalk.cyan(`🌐 PORT = ${options.port}`));
|
|
1062
|
+
.description("Fetch Zalo Mini App template for the project")
|
|
1063
|
+
.action(async (id: string) => {
|
|
1064
|
+
const { accessToken } = getStoredToken();
|
|
1065
|
+
if (!accessToken) {
|
|
1066
|
+
console.error(
|
|
1067
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
1068
|
+
);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1145
1071
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1072
|
+
const apiSpinner = ora(chalk.blue("Fetching project data...")).start();
|
|
1073
|
+
try {
|
|
1148
1074
|
const response: ApiResponse = await axios.get(
|
|
1149
1075
|
`https://base.myworkbeast.com/api/views/${id}`
|
|
1150
1076
|
);
|
|
1151
|
-
const portInput = options.port;
|
|
1152
|
-
if (portInput) {
|
|
1153
|
-
const parsedPort = parseInt(portInput, 10);
|
|
1154
|
-
if (!isNaN(parsedPort)) {
|
|
1155
|
-
port = parsedPort;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
1077
|
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
// download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
|
|
1166
|
-
const TEMPLATE_URL: string = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
|
|
1167
|
-
let templateResponse = undefined;
|
|
1168
|
-
try {
|
|
1169
|
-
templateResponse = await axios.get(TEMPLATE_URL, {
|
|
1170
|
-
responseType: "arraybuffer",
|
|
1171
|
-
});
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
console.error(
|
|
1174
|
-
chalk.red("✗ Error downloading template:"),
|
|
1175
|
-
(error as Error).message
|
|
1078
|
+
const projectType = response.data.type;
|
|
1079
|
+
if (projectType !== "zalominiapp" && projectType !== "zalominiapp2") {
|
|
1080
|
+
apiSpinner.fail(
|
|
1081
|
+
chalk.red(
|
|
1082
|
+
`✗ Error: Project type is "${projectType}". Only "zalominiapp" and "zalominiapp2" are supported for fetch command.`
|
|
1083
|
+
)
|
|
1176
1084
|
);
|
|
1177
1085
|
return;
|
|
1178
1086
|
}
|
|
1087
|
+
apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
|
|
1179
1088
|
|
|
1180
|
-
const outputDir
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1089
|
+
const outputDir = path.resolve(process.cwd(), id);
|
|
1090
|
+
const zaloDir = path.join(outputDir, "zalo");
|
|
1091
|
+
const backendDir = path.join(outputDir, "backend");
|
|
1184
1092
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1093
|
+
const templateSpinner = ora(
|
|
1094
|
+
chalk.blue("Downloading Zalo template...")
|
|
1095
|
+
).start();
|
|
1096
|
+
const TEMPLATE_URL =
|
|
1097
|
+
"https://hapico.hcm04.vstorage.vngcloud.vn/templates/zalominiapp_v0.zip";
|
|
1098
|
+
const templateResponse = await axios.get(TEMPLATE_URL, {
|
|
1099
|
+
responseType: "arraybuffer",
|
|
1100
|
+
});
|
|
1101
|
+
templateSpinner.succeed(
|
|
1102
|
+
chalk.green("Zalo template downloaded successfully!")
|
|
1187
1103
|
);
|
|
1188
1104
|
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (fs.existsSync(serverFile)) {
|
|
1197
|
-
let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
|
|
1198
|
-
serverContent = serverContent.split("(3006)").join(`(${port})`);
|
|
1199
|
-
fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// lấy danh sách migrations
|
|
1203
|
-
const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${
|
|
1204
|
-
response.data.dbCode || response.data.db_code
|
|
1205
|
-
}&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
|
|
1206
|
-
|
|
1207
|
-
// Lấy danh sách các migration đã chạy
|
|
1208
|
-
let migrations: Array<{
|
|
1209
|
-
id: number;
|
|
1210
|
-
created_at: string;
|
|
1211
|
-
sql_statement: string;
|
|
1212
|
-
}> = [];
|
|
1213
|
-
const migrationsResponse = await axios.get(MIGRATIONS_URL, {
|
|
1214
|
-
headers: {
|
|
1215
|
-
Authorization: `Bearer ${accessToken}`,
|
|
1216
|
-
"Content-Type": "application/json",
|
|
1217
|
-
"x-db-code": response.data.dbCode || response.data.db_code,
|
|
1218
|
-
},
|
|
1105
|
+
const backendSpinner = ora(
|
|
1106
|
+
chalk.blue("Downloading backend template...")
|
|
1107
|
+
).start();
|
|
1108
|
+
const BACKEND_URL =
|
|
1109
|
+
"https://hapico.hcm04.vstorage.vngcloud.vn/templates/backend_v0.zip";
|
|
1110
|
+
const backendResponse = await axios.get(BACKEND_URL, {
|
|
1111
|
+
responseType: "arraybuffer",
|
|
1219
1112
|
});
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
created_at: item.created_at,
|
|
1224
|
-
sql: item.sql_statement,
|
|
1225
|
-
})
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
console.log(chalk.cyan(`🔍 Found ${migrations.length} migrations.`));
|
|
1113
|
+
backendSpinner.succeed(
|
|
1114
|
+
chalk.green("Backend template downloaded successfully!")
|
|
1115
|
+
);
|
|
1230
1116
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
if (!fs.existsSync(migrationsDir)) {
|
|
1234
|
-
fs.mkdirSync(migrationsDir);
|
|
1117
|
+
if (!fs.existsSync(outputDir)) {
|
|
1118
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1235
1119
|
}
|
|
1120
|
+
if (!fs.existsSync(zaloDir)) fs.mkdirSync(zaloDir, { recursive: true });
|
|
1121
|
+
if (!fs.existsSync(backendDir))
|
|
1122
|
+
fs.mkdirSync(backendDir, { recursive: true });
|
|
1236
1123
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
const timestamp = new Date(migration.created_at)
|
|
1240
|
-
.toISOString()
|
|
1241
|
-
.replace(/[-:]/g, "")
|
|
1242
|
-
.replace(/\..+/, "");
|
|
1243
|
-
const filename = path.join(
|
|
1244
|
-
migrationsDir,
|
|
1245
|
-
`${index + 1}_migration_${timestamp}.sql`
|
|
1246
|
-
);
|
|
1247
|
-
fs.writeFileSync(filename, (migration as any).sql, {
|
|
1248
|
-
encoding: "utf8",
|
|
1249
|
-
});
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
console.log(chalk.green("✓ Migrations saved successfully!"));
|
|
1253
|
-
|
|
1254
|
-
// Download project files and save to project
|
|
1255
|
-
let files: FileContent[] = [];
|
|
1256
|
-
const apiSpinner: Ora = ora(
|
|
1257
|
-
chalk.blue("Fetching project data...")
|
|
1124
|
+
const unzipZaloSpinner = ora(
|
|
1125
|
+
chalk.blue("Extracting Zalo template...")
|
|
1258
1126
|
).start();
|
|
1127
|
+
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
1128
|
+
directory.extract({ path: zaloDir })
|
|
1129
|
+
);
|
|
1130
|
+
unzipZaloSpinner.succeed(
|
|
1131
|
+
chalk.green("Zalo template extracted successfully!")
|
|
1132
|
+
);
|
|
1259
1133
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
file.path.startsWith("./app") ||
|
|
1270
|
-
file.path.startsWith("./components")
|
|
1271
|
-
) {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
// hoặc các file có đuôi là .tsx
|
|
1275
|
-
if (file.path.endsWith(".tsx")) {
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
try {
|
|
1279
|
-
if (file.content.trim().length == 0) return;
|
|
1280
|
-
const filePath: string = path.join(
|
|
1281
|
-
process.cwd(),
|
|
1282
|
-
id,
|
|
1283
|
-
"src",
|
|
1284
|
-
file.path
|
|
1285
|
-
);
|
|
1286
|
-
const dir: string = path.dirname(filePath);
|
|
1287
|
-
|
|
1288
|
-
if (!fs.existsSync(dir)) {
|
|
1289
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
fs.writeFileSync(filePath, file.content);
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
console.log(chalk.red("✗ Error writing file"), file.path, error);
|
|
1295
|
-
}
|
|
1296
|
-
});
|
|
1297
|
-
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
1298
|
-
} catch (error: any) {
|
|
1299
|
-
apiSpinner.fail(chalk.red(`✗ Error cloning project: ${error.message}`));
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Download project files and save to project
|
|
1303
|
-
console.log(chalk.green("✓ Project cloned successfully!"));
|
|
1304
|
-
|
|
1305
|
-
// Save project ID to hapico.config.json
|
|
1306
|
-
saveProjectId(outputDir, id);
|
|
1307
|
-
|
|
1308
|
-
console.log(chalk.green("✓ Project setup successfully!"));
|
|
1309
|
-
|
|
1310
|
-
let serveProcess: any = null;
|
|
1134
|
+
const unzipBackendSpinner = ora(
|
|
1135
|
+
chalk.blue("Extracting backend template...")
|
|
1136
|
+
).start();
|
|
1137
|
+
await unzipper.Open.buffer(backendResponse.data).then((directory) =>
|
|
1138
|
+
directory.extract({ path: backendDir })
|
|
1139
|
+
);
|
|
1140
|
+
unzipBackendSpinner.succeed(
|
|
1141
|
+
chalk.green("Backend template extracted successfully!")
|
|
1142
|
+
);
|
|
1311
1143
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
).start();
|
|
1144
|
+
[zaloDir, backendDir].forEach((dir) => {
|
|
1145
|
+
const macosxDir = path.join(dir, "__MACOSX");
|
|
1146
|
+
if (fs.existsSync(macosxDir)) {
|
|
1147
|
+
fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
1148
|
+
}
|
|
1318
1149
|
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const flyCheck = await axios.head(
|
|
1335
|
-
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1336
|
-
);
|
|
1337
|
-
if (flyCheck.status !== 200) {
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
// get file version.json
|
|
1341
|
-
const versionResponse = await axios.get(
|
|
1342
|
-
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1343
|
-
);
|
|
1344
|
-
const latestVersionData = versionResponse.data;
|
|
1345
|
-
const latestVersion = latestVersionData.version;
|
|
1346
|
-
if (latestVersion !== currentVersionStr) {
|
|
1347
|
-
console.log(
|
|
1348
|
-
chalk.yellow(`📦 New version available: ${latestVersion}`)
|
|
1349
|
-
);
|
|
1350
|
-
// Save new version.json
|
|
1351
|
-
fs.writeFileSync(
|
|
1352
|
-
path.join(outputDir, "version.json"),
|
|
1353
|
-
JSON.stringify(latestVersionData, null, 2),
|
|
1354
|
-
{ encoding: "utf8" }
|
|
1355
|
-
);
|
|
1356
|
-
// Install external libraries if any
|
|
1357
|
-
if (
|
|
1358
|
-
latestVersionData.external_libraries &&
|
|
1359
|
-
latestVersionData.external_libraries.length > 0
|
|
1360
|
-
) {
|
|
1361
|
-
console.log(chalk.blue("Installing new external libraries..."));
|
|
1362
|
-
for (const lib of latestVersionData.external_libraries) {
|
|
1363
|
-
try {
|
|
1364
|
-
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1365
|
-
console.log(chalk.green(`Added ${lib}`));
|
|
1366
|
-
} catch (addError) {
|
|
1367
|
-
console.error(
|
|
1368
|
-
chalk.red(`✗ Error adding ${lib}:`),
|
|
1369
|
-
(addError as Error).message
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1150
|
+
// Replace {{APP_ID}} in .ts and .tsx files
|
|
1151
|
+
const traverseAndReplace = (currentDir: string) => {
|
|
1152
|
+
fs.readdirSync(currentDir, { withFileTypes: true }).forEach((entry) => {
|
|
1153
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
1154
|
+
if (entry.isDirectory()) {
|
|
1155
|
+
traverseAndReplace(fullPath);
|
|
1156
|
+
} else if (entry.isFile()) {
|
|
1157
|
+
const ext = path.extname(fullPath);
|
|
1158
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
1159
|
+
const content = fs.readFileSync(fullPath, { encoding: "utf8" });
|
|
1160
|
+
if (content.includes("{{APP_ID}}")) {
|
|
1161
|
+
const updatedContent = content.replace(/{{APP_ID}}/g, id);
|
|
1162
|
+
fs.writeFileSync(fullPath, updatedContent, {
|
|
1163
|
+
encoding: "utf8",
|
|
1164
|
+
});
|
|
1372
1165
|
}
|
|
1373
1166
|
}
|
|
1374
|
-
// Rerun bun install
|
|
1375
|
-
try {
|
|
1376
|
-
await execPromise("bun install", { cwd: outputDir });
|
|
1377
|
-
console.log(
|
|
1378
|
-
chalk.green("Dependencies reinstalled successfully!")
|
|
1379
|
-
);
|
|
1380
|
-
} catch (installError) {
|
|
1381
|
-
console.error(
|
|
1382
|
-
chalk.red("✗ Error reinstalling dependencies:"),
|
|
1383
|
-
(installError as Error).message
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
// Restart the process
|
|
1387
|
-
if (serveProcess && !serveProcess.killed) {
|
|
1388
|
-
console.log(chalk.blue("Restarting backend..."));
|
|
1389
|
-
serveProcess.kill("SIGTERM");
|
|
1390
|
-
}
|
|
1391
|
-
// Start new process
|
|
1392
|
-
const newServeProcess = exec("bun run start", { cwd: outputDir });
|
|
1393
|
-
newServeProcess.stdout?.on("data", (data: any) => {
|
|
1394
|
-
process.stdout.write(data);
|
|
1395
|
-
});
|
|
1396
|
-
newServeProcess.stderr?.on("data", (data: any) => {
|
|
1397
|
-
process.stderr.write(data);
|
|
1398
|
-
});
|
|
1399
|
-
newServeProcess.on("close", (code: any) => {
|
|
1400
|
-
console.log(
|
|
1401
|
-
chalk.yellow(`⚠ backend exited with code ${code}`)
|
|
1402
|
-
);
|
|
1403
|
-
});
|
|
1404
|
-
serveProcess = newServeProcess;
|
|
1405
|
-
currentVersionStr = latestVersion;
|
|
1406
1167
|
}
|
|
1407
|
-
}
|
|
1408
|
-
// If the remote version.json does not exist, do not update
|
|
1409
|
-
console.error(
|
|
1410
|
-
chalk.yellow("⚠ Error checking for updates (skipping update):"),
|
|
1411
|
-
(error as Error).message
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1168
|
+
});
|
|
1414
1169
|
};
|
|
1170
|
+
traverseAndReplace(dir);
|
|
1171
|
+
});
|
|
1415
1172
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
exec(
|
|
1420
|
-
"bun install",
|
|
1421
|
-
{ cwd: outputDir },
|
|
1422
|
-
async (error: any, stdout: any, stderr: any) => {
|
|
1423
|
-
if (error) {
|
|
1424
|
-
installSpinner.fail(
|
|
1425
|
-
chalk.red(`✗ Error installing dependencies: ${error.message}`)
|
|
1426
|
-
);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
if (stderr) {
|
|
1430
|
-
console.error(chalk.red(`stderr: ${stderr}`));
|
|
1431
|
-
}
|
|
1432
|
-
installSpinner.succeed(
|
|
1433
|
-
chalk.green("Dependencies installed successfully!")
|
|
1434
|
-
);
|
|
1435
|
-
|
|
1436
|
-
// Install additional libraries if --libs is provided
|
|
1437
|
-
if (options.libs && options.libs.trim()) {
|
|
1438
|
-
const libsSpinner: Ora = ora(
|
|
1439
|
-
chalk.blue("Installing additional libraries...")
|
|
1440
|
-
).start();
|
|
1441
|
-
const additionalLibs = options.libs
|
|
1442
|
-
.split(",")
|
|
1443
|
-
.map((lib) => lib.trim())
|
|
1444
|
-
.filter((lib) => lib);
|
|
1445
|
-
for (const lib of additionalLibs) {
|
|
1446
|
-
try {
|
|
1447
|
-
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1448
|
-
console.log(chalk.green(`Added ${lib}`));
|
|
1449
|
-
} catch (addError) {
|
|
1450
|
-
console.error(
|
|
1451
|
-
chalk.red(`✗ Error adding ${lib}:`),
|
|
1452
|
-
(addError as Error).message
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
libsSpinner.succeed(
|
|
1457
|
-
chalk.green("Additional libraries installed successfully!")
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1173
|
+
// Save project ID
|
|
1174
|
+
saveProjectId(outputDir, id);
|
|
1460
1175
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
});
|
|
1467
|
-
serveProcess.stderr.on("data", (data: any) => {
|
|
1468
|
-
process.stderr.write(data);
|
|
1469
|
-
});
|
|
1470
|
-
serveProcess.on("close", (code: any) => {
|
|
1471
|
-
console.log(chalk.yellow(`⚠ backend exited with code ${code}`));
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
);
|
|
1475
|
-
}
|
|
1176
|
+
console.log(
|
|
1177
|
+
chalk.green(`✓ Project "${id}" fetched and setup successfully!`)
|
|
1178
|
+
);
|
|
1179
|
+
} catch (error: any) {
|
|
1180
|
+
apiSpinner.fail(chalk.red(`✗ Error: ${error.message}`));
|
|
1476
1181
|
}
|
|
1477
|
-
);
|
|
1182
|
+
});
|
|
1478
1183
|
|
|
1479
1184
|
// hapico publish
|
|
1480
|
-
program
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1185
|
+
program
|
|
1186
|
+
.command("publish")
|
|
1187
|
+
.description("Publish the project to production environment")
|
|
1188
|
+
.action(async () => {
|
|
1189
|
+
console.clear();
|
|
1190
|
+
console.log(chalk.bold.magenta("\n🚀 HAPICO PUBLISH PIPELINE"));
|
|
1191
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
1192
|
+
|
|
1193
|
+
const pwd = process.cwd();
|
|
1194
|
+
const { projectId, replicate } = getStoredProjectId(pwd);
|
|
1486
1195
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
)
|
|
1492
|
-
);
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1196
|
+
if (!projectId) {
|
|
1197
|
+
console.log(chalk.red("✗ Project ID not found. Ensure hapico.config.json exists."));
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1495
1200
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1201
|
+
console.log(chalk.bold.white("Target Environments:"));
|
|
1202
|
+
console.log(`${chalk.gray("├─")} ${chalk.bold("Main Project :")} ${chalk.magenta(projectId)}`);
|
|
1203
|
+
if (replicate && replicate.length > 0) {
|
|
1204
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.magenta(replicate.join(", "))}`);
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log(`${chalk.gray("└─")} ${chalk.bold("Replicas :")} ${chalk.gray("None")}`);
|
|
1207
|
+
}
|
|
1208
|
+
console.log("");
|
|
1209
|
+
|
|
1210
|
+
const pushSpinner = ora(chalk.blue("Phase 1/2: Syncing source code with main project...")).start();
|
|
1211
|
+
|
|
1212
|
+
// Step 1: Push Main
|
|
1213
|
+
const mainResult = await pushProject(projectId);
|
|
1214
|
+
if (mainResult.success) {
|
|
1215
|
+
pushSpinner.succeed(chalk.green(`Main Project [${projectId}] code synced successfully.`));
|
|
1216
|
+
console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(mainResult.fileCount)}`);
|
|
1217
|
+
console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((mainResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
|
|
1218
|
+
} else {
|
|
1219
|
+
pushSpinner.fail(chalk.red(`Main Project [${projectId}] sync failed: ${mainResult.error}`));
|
|
1220
|
+
return; // Stop if push to main fails
|
|
1221
|
+
}
|
|
1501
1222
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
chalk.
|
|
1512
|
-
|
|
1513
|
-
)
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
console.log(
|
|
1517
|
-
chalk.green(`✓ Successfully pushed to replicated project ${repId}.`)
|
|
1518
|
-
);
|
|
1223
|
+
// Push Replicas
|
|
1224
|
+
let allPushSuccess = true;
|
|
1225
|
+
if (replicate && replicate.length > 0) {
|
|
1226
|
+
for (const repId of replicate) {
|
|
1227
|
+
const repSpinner = ora(chalk.blue(`Phase 1/2: Syncing replica code [${repId}]...`)).start();
|
|
1228
|
+
const repResult = await pushProject(repId);
|
|
1229
|
+
if (repResult.success) {
|
|
1230
|
+
repSpinner.succeed(chalk.green(`Replica [${repId}] code synced successfully.`));
|
|
1231
|
+
console.log(`${chalk.gray(" ├─")} ${chalk.bold("Files :")} ${chalk.white(repResult.fileCount)}`);
|
|
1232
|
+
console.log(`${chalk.gray(" └─")} ${chalk.bold("Size :")} ${chalk.white((repResult.totalSize / 1024).toFixed(2) + " KB")}\n`);
|
|
1233
|
+
} else {
|
|
1234
|
+
repSpinner.fail(chalk.red(`Replica [${repId}] sync failed: ${repResult.error}`));
|
|
1235
|
+
allPushSuccess = false;
|
|
1236
|
+
}
|
|
1519
1237
|
}
|
|
1520
1238
|
}
|
|
1521
|
-
}
|
|
1522
1239
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
{
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
"Content-Type": "application/json",
|
|
1535
|
-
},
|
|
1536
|
-
}
|
|
1537
|
-
);
|
|
1538
|
-
publishSpinner.succeed(
|
|
1539
|
-
chalk.green(`Project ${projectId} published successfully!`)
|
|
1540
|
-
);
|
|
1541
|
-
} catch (error) {
|
|
1542
|
-
allPublishSuccess = false;
|
|
1543
|
-
publishSpinner.fail(
|
|
1544
|
-
chalk.red(
|
|
1545
|
-
`✗ Error publishing project ${projectId}: ${(error as Error).message}`
|
|
1546
|
-
)
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1240
|
+
// Step 2: Publish Main
|
|
1241
|
+
const publishSpinner = ora(chalk.blue(`Phase 2/2: Publishing main project [${projectId}]...`)).start();
|
|
1242
|
+
const pubMainResult = await publishProjectApi(projectId);
|
|
1243
|
+
let allPublishSuccess = true;
|
|
1244
|
+
|
|
1245
|
+
if (pubMainResult.success) {
|
|
1246
|
+
publishSpinner.succeed(chalk.green(`Main Project [${projectId}] published successfully.`));
|
|
1247
|
+
} else {
|
|
1248
|
+
publishSpinner.fail(chalk.red(`Main Project [${projectId}] publish failed: ${pubMainResult.error}`));
|
|
1249
|
+
allPublishSuccess = false;
|
|
1250
|
+
}
|
|
1549
1251
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
{
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
"Content-Type": "application/json",
|
|
1562
|
-
},
|
|
1563
|
-
}
|
|
1564
|
-
);
|
|
1565
|
-
console.log(
|
|
1566
|
-
chalk.green(`✓ Successfully published replicated project ${repId}.`)
|
|
1567
|
-
);
|
|
1568
|
-
} catch (error) {
|
|
1569
|
-
allPublishSuccess = false;
|
|
1570
|
-
console.warn(
|
|
1571
|
-
chalk.yellow(
|
|
1572
|
-
`⚠ Error publishing replicated project ${repId}: ${(error as Error).message}`
|
|
1573
|
-
)
|
|
1574
|
-
);
|
|
1252
|
+
// Publish Replicas
|
|
1253
|
+
if (replicate && replicate.length > 0) {
|
|
1254
|
+
for (const repId of replicate) {
|
|
1255
|
+
const repPubSpinner = ora(chalk.blue(`Phase 2/2: Publishing replica [${repId}]...`)).start();
|
|
1256
|
+
const pubRepResult = await publishProjectApi(repId);
|
|
1257
|
+
if (pubRepResult.success) {
|
|
1258
|
+
repPubSpinner.succeed(chalk.green(`Replica [${repId}] published successfully.`));
|
|
1259
|
+
} else {
|
|
1260
|
+
repPubSpinner.fail(chalk.red(`Replica [${repId}] publish failed: ${pubRepResult.error}`));
|
|
1261
|
+
allPublishSuccess = false;
|
|
1262
|
+
}
|
|
1575
1263
|
}
|
|
1576
1264
|
}
|
|
1577
|
-
}
|
|
1578
1265
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
}
|
|
1266
|
+
console.log(chalk.gray("──────────────────────────────────────────────────────────────────"));
|
|
1267
|
+
if (allPushSuccess && allPublishSuccess) {
|
|
1268
|
+
console.log(`\n${chalk.bold.white.bgGreen(" SUCCESS ")} ${chalk.green("Deployment pipeline completed successfully!\n")}`);
|
|
1269
|
+
} else {
|
|
1270
|
+
console.log(`\n${chalk.bold.white.bgYellow(" WARNING ")} ${chalk.yellow("Deployment pipeline completed with some errors.\n")}`);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1585
1273
|
|
|
1586
1274
|
program.command("mirror").action(() => {
|
|
1587
1275
|
console.log(chalk.cyan("🌐 Starting mirror mode..."));
|
|
@@ -1599,7 +1287,7 @@ program.command("mirror").action(() => {
|
|
|
1599
1287
|
const fileManager = new FileManager(srcDir);
|
|
1600
1288
|
const initialFiles = fileManager.listFiles();
|
|
1601
1289
|
|
|
1602
|
-
//
|
|
1290
|
+
// Collect project files and generate a single summary document
|
|
1603
1291
|
let content = ``;
|
|
1604
1292
|
map(initialFiles, (file) => {
|
|
1605
1293
|
content += `\`\`\`typescript
|