@hapico/cli 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +413 -89
- 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/bun.lock +77 -0
- package/dist/index.js +413 -89
- 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/index.ts +609 -112
- package/package.json +4 -1
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 { exec } from "child_process";
|
|
16
|
+
import { promisify } from "util";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import pako from 'pako';
|
|
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,33 +304,47 @@ class RoomState {
|
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
this.ws = new WebSocket(
|
|
292
|
-
`
|
|
307
|
+
`wss://ws3.myworkbeast.com/ws?room=${this.roomId}`
|
|
293
308
|
);
|
|
294
309
|
|
|
295
|
-
|
|
296
|
-
|
|
310
|
+
// Set binaryType to 'arraybuffer' to handle binary data
|
|
311
|
+
this.ws.binaryType = 'arraybuffer';
|
|
312
|
+
|
|
313
|
+
this.ws.on("open", () => {
|
|
314
|
+
console.log(chalk.green(`Connected to room: ${this.roomId}`));
|
|
297
315
|
this.isConnected = true;
|
|
298
316
|
this.reconnectAttempts = 0;
|
|
299
317
|
onConnected?.(); // Call the onConnected callback if provided
|
|
300
|
-
};
|
|
318
|
+
});
|
|
301
319
|
|
|
302
|
-
this.ws.
|
|
303
|
-
console.log(
|
|
320
|
+
this.ws.on("close", () => {
|
|
321
|
+
console.log(chalk.yellow(`⚠ Disconnected from room: ${this.roomId}`));
|
|
304
322
|
this.isConnected = false;
|
|
305
323
|
|
|
306
324
|
this.reconnectAttempts++;
|
|
307
325
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
308
|
-
console.log(
|
|
326
|
+
console.log(
|
|
327
|
+
chalk.yellow(`Attempting to reconnect in ${delay / 1000}s...`)
|
|
328
|
+
);
|
|
309
329
|
|
|
310
330
|
this.reconnectTimeout = setTimeout(
|
|
311
331
|
() => this.connect(onConnected),
|
|
312
332
|
delay
|
|
313
333
|
);
|
|
314
|
-
};
|
|
334
|
+
});
|
|
315
335
|
|
|
316
|
-
this.ws.on("message", (data:
|
|
336
|
+
this.ws.on("message", (data: any) => {
|
|
317
337
|
try {
|
|
318
|
-
|
|
338
|
+
let jsonStr: string;
|
|
339
|
+
if (data instanceof ArrayBuffer) {
|
|
340
|
+
jsonStr = pako.inflate(data, { to: 'string' });
|
|
341
|
+
} else if (typeof data === "string") {
|
|
342
|
+
jsonStr = data;
|
|
343
|
+
} else if (Buffer.isBuffer(data)) {
|
|
344
|
+
jsonStr = pako.inflate(data, { to: 'string' });
|
|
345
|
+
} else {
|
|
346
|
+
jsonStr = data.toString(); // Fallback nếu không nén
|
|
347
|
+
}
|
|
319
348
|
const parsedData = JSON.parse(jsonStr);
|
|
320
349
|
const includes = [
|
|
321
350
|
"view",
|
|
@@ -360,24 +389,24 @@ class RoomState {
|
|
|
360
389
|
this.state = newState;
|
|
361
390
|
}
|
|
362
391
|
} catch (e) {
|
|
363
|
-
console.error("Error processing message:", e);
|
|
392
|
+
console.error(chalk.red("Error processing message:"), e);
|
|
364
393
|
}
|
|
365
394
|
});
|
|
366
395
|
|
|
367
396
|
this.ws.on("error", (err) => {
|
|
368
|
-
console.error("WebSocket error:", err);
|
|
397
|
+
console.error(chalk.red("WebSocket error:"), err);
|
|
369
398
|
this.ws?.close();
|
|
370
399
|
});
|
|
371
400
|
}
|
|
372
401
|
|
|
373
402
|
updateState(key: string, value: any): void {
|
|
374
403
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
);
|
|
404
|
+
const message = JSON.stringify({
|
|
405
|
+
type: "update",
|
|
406
|
+
state: { [key]: value },
|
|
407
|
+
});
|
|
408
|
+
const compressed = pako.deflate(message); // Nén dữ liệu
|
|
409
|
+
this.ws.send(compressed); // Gửi binary
|
|
381
410
|
}
|
|
382
411
|
}
|
|
383
412
|
|
|
@@ -386,7 +415,7 @@ class RoomState {
|
|
|
386
415
|
clearTimeout(this.reconnectTimeout);
|
|
387
416
|
}
|
|
388
417
|
if (this.ws) {
|
|
389
|
-
this.ws.
|
|
418
|
+
this.ws.removeAllListeners("close"); // Prevent reconnect on intentional close
|
|
390
419
|
this.ws.close();
|
|
391
420
|
}
|
|
392
421
|
}
|
|
@@ -400,7 +429,7 @@ class RoomState {
|
|
|
400
429
|
}
|
|
401
430
|
}
|
|
402
431
|
|
|
403
|
-
program.version("0.0.
|
|
432
|
+
program.version("0.0.19").description("Hapico CLI for project management");
|
|
404
433
|
|
|
405
434
|
program
|
|
406
435
|
.command("clone <id>")
|
|
@@ -408,43 +437,49 @@ program
|
|
|
408
437
|
.action(async (id: string) => {
|
|
409
438
|
const { accessToken } = getStoredToken();
|
|
410
439
|
if (!accessToken) {
|
|
411
|
-
console.error(
|
|
440
|
+
console.error(
|
|
441
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
442
|
+
);
|
|
412
443
|
return;
|
|
413
444
|
}
|
|
414
445
|
const projectDir: string = path.resolve(process.cwd(), id);
|
|
415
446
|
if (fs.existsSync(projectDir)) {
|
|
416
|
-
console.error(
|
|
447
|
+
console.error(chalk.red(`✗ Project directory "${id}" already exists.`));
|
|
417
448
|
return;
|
|
418
449
|
}
|
|
419
450
|
|
|
420
451
|
let files: FileContent[] = [];
|
|
421
|
-
const apiSpinner: Ora = ora("Fetching project data...").start();
|
|
452
|
+
const apiSpinner: Ora = ora(chalk.blue("Fetching project data...")).start();
|
|
422
453
|
|
|
423
454
|
try {
|
|
424
455
|
const response: ApiResponse = await axios.get(
|
|
425
456
|
`https://base.myworkbeast.com/api/views/${id}`
|
|
426
457
|
);
|
|
427
458
|
files = response.data.files || [];
|
|
428
|
-
apiSpinner.succeed("Project data fetched successfully!");
|
|
459
|
+
apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
|
|
429
460
|
|
|
430
|
-
const templateSpinner: Ora = ora(
|
|
461
|
+
const templateSpinner: Ora = ora(
|
|
462
|
+
chalk.blue("Downloading template...")
|
|
463
|
+
).start();
|
|
431
464
|
const TEMPLATE_URL: string =
|
|
432
465
|
"https://files.hcm04.vstorage.vngcloud.vn/assets/template_zalominiapp_devmode.zip";
|
|
433
466
|
const templateResponse = await axios.get(TEMPLATE_URL, {
|
|
434
467
|
responseType: "arraybuffer",
|
|
435
468
|
});
|
|
436
|
-
templateSpinner.succeed("Template downloaded successfully!");
|
|
469
|
+
templateSpinner.succeed(chalk.green("Template downloaded successfully!"));
|
|
437
470
|
|
|
438
471
|
const outputDir: string = path.resolve(process.cwd(), id);
|
|
439
472
|
if (!fs.existsSync(outputDir)) {
|
|
440
473
|
fs.mkdirSync(outputDir);
|
|
441
474
|
}
|
|
442
475
|
|
|
443
|
-
const unzipSpinner: Ora = ora(
|
|
476
|
+
const unzipSpinner: Ora = ora(
|
|
477
|
+
chalk.blue("Extracting template...")
|
|
478
|
+
).start();
|
|
444
479
|
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
445
480
|
directory.extract({ path: outputDir })
|
|
446
481
|
);
|
|
447
|
-
unzipSpinner.succeed("Template extracted successfully!");
|
|
482
|
+
unzipSpinner.succeed(chalk.green("Template extracted successfully!"));
|
|
448
483
|
|
|
449
484
|
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
450
485
|
if (fs.existsSync(macosxDir)) {
|
|
@@ -454,9 +489,11 @@ program
|
|
|
454
489
|
// Save project ID to hapico.config.json
|
|
455
490
|
saveProjectId(outputDir, id);
|
|
456
491
|
|
|
457
|
-
console.log("Project cloned successfully!");
|
|
492
|
+
console.log(chalk.green("Project cloned successfully!"));
|
|
458
493
|
|
|
459
|
-
const saveSpinner: Ora = ora(
|
|
494
|
+
const saveSpinner: Ora = ora(
|
|
495
|
+
chalk.blue("Saving project files...")
|
|
496
|
+
).start();
|
|
460
497
|
files.forEach((file: FileContent) => {
|
|
461
498
|
const filePath: string = path.join(process.cwd(), id, "src", file.path);
|
|
462
499
|
const dir: string = path.dirname(filePath);
|
|
@@ -467,12 +504,14 @@ program
|
|
|
467
504
|
|
|
468
505
|
fs.writeFileSync(filePath, file.content);
|
|
469
506
|
});
|
|
470
|
-
saveSpinner.succeed("Project files saved successfully!");
|
|
507
|
+
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
471
508
|
console.log(
|
|
472
|
-
|
|
509
|
+
chalk.cyan(
|
|
510
|
+
`💡 Run ${chalk.bold("cd ${id} && npm install && hapico dev")} to start the project.`
|
|
511
|
+
)
|
|
473
512
|
);
|
|
474
513
|
} catch (error: any) {
|
|
475
|
-
apiSpinner.fail(`Error cloning project: ${error.message}`);
|
|
514
|
+
apiSpinner.fail(chalk.red(`Error cloning project: ${error.message}`));
|
|
476
515
|
}
|
|
477
516
|
});
|
|
478
517
|
|
|
@@ -482,17 +521,21 @@ program
|
|
|
482
521
|
.action(() => {
|
|
483
522
|
const { accessToken } = getStoredToken();
|
|
484
523
|
if (!accessToken) {
|
|
485
|
-
console.error(
|
|
524
|
+
console.error(
|
|
525
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
526
|
+
);
|
|
486
527
|
return;
|
|
487
528
|
}
|
|
488
529
|
const devSpinner = ora(
|
|
489
|
-
"Starting the project in development mode..."
|
|
530
|
+
chalk.blue("Starting the project in development mode...")
|
|
490
531
|
).start();
|
|
491
532
|
const pwd = process.cwd();
|
|
492
533
|
const srcDir = path.join(pwd, "src");
|
|
493
534
|
if (!fs.existsSync(srcDir)) {
|
|
494
535
|
devSpinner.fail(
|
|
495
|
-
|
|
536
|
+
chalk.red(
|
|
537
|
+
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
538
|
+
)
|
|
496
539
|
);
|
|
497
540
|
return;
|
|
498
541
|
}
|
|
@@ -542,25 +585,29 @@ program
|
|
|
542
585
|
const projectId = Buffer.from(info).toString("base64");
|
|
543
586
|
if (!projectId) {
|
|
544
587
|
devSpinner.fail(
|
|
545
|
-
|
|
588
|
+
chalk.red(
|
|
589
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
590
|
+
)
|
|
546
591
|
);
|
|
547
592
|
return;
|
|
548
593
|
}
|
|
549
594
|
|
|
550
|
-
console.log(
|
|
595
|
+
console.log(chalk.cyan("🔗 Connecting to WebSocket server"));
|
|
551
596
|
const room = new RoomState(`view_${projectId}`, []);
|
|
552
597
|
const fileManager = new FileManager(srcDir);
|
|
553
598
|
const initialFiles = fileManager.listFiles();
|
|
554
599
|
room.files = initialFiles;
|
|
555
600
|
|
|
556
601
|
room.connect(async () => {
|
|
557
|
-
devSpinner.succeed("Project started in development mode!");
|
|
602
|
+
devSpinner.succeed(chalk.green("Project started in development mode!"));
|
|
558
603
|
|
|
559
604
|
room.updateState("view", initialFiles);
|
|
560
605
|
|
|
561
606
|
fileManager.setOnFileChange((filePath, content) => {
|
|
562
607
|
const es5 = compileES5(content, filePath) ?? "";
|
|
563
|
-
console.log(
|
|
608
|
+
console.log(
|
|
609
|
+
chalk.yellow(`📝 File changed: ${filePath?.replace(srcDir, ".")}`)
|
|
610
|
+
);
|
|
564
611
|
const updatedFiles = room.files.map((file) => {
|
|
565
612
|
if (path.join(srcDir, file.path) === filePath) {
|
|
566
613
|
return { ...file, content, es5 };
|
|
@@ -575,7 +622,9 @@ program
|
|
|
575
622
|
const projectInfo = getStoredProjectId(pwd);
|
|
576
623
|
if (!projectInfo) {
|
|
577
624
|
console.error(
|
|
578
|
-
|
|
625
|
+
chalk.red(
|
|
626
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
627
|
+
)
|
|
579
628
|
);
|
|
580
629
|
return;
|
|
581
630
|
}
|
|
@@ -590,7 +639,9 @@ program
|
|
|
590
639
|
);
|
|
591
640
|
|
|
592
641
|
if (project.status !== 200) {
|
|
593
|
-
console.error(
|
|
642
|
+
console.error(
|
|
643
|
+
chalk.red(`✗ Error fetching project info: ${project.statusText}`)
|
|
644
|
+
);
|
|
594
645
|
return;
|
|
595
646
|
}
|
|
596
647
|
|
|
@@ -598,10 +649,12 @@ program
|
|
|
598
649
|
|
|
599
650
|
if (projectType === "zalominiapp") {
|
|
600
651
|
QRCode.generate(
|
|
601
|
-
`https://zalo.me/s/3218692650896662017/player/${projectId}`,
|
|
652
|
+
`https://zalo.me/s/3218692650896662017/player/${projectId}?env=TESTING&version=75`,
|
|
602
653
|
{ small: true },
|
|
603
654
|
(qrcode) => {
|
|
604
|
-
console.log(
|
|
655
|
+
console.log(
|
|
656
|
+
chalk.cyan("📱 Scan this QR code to connect to the project:")
|
|
657
|
+
);
|
|
605
658
|
console.log(qrcode);
|
|
606
659
|
}
|
|
607
660
|
);
|
|
@@ -609,71 +662,100 @@ program
|
|
|
609
662
|
} else {
|
|
610
663
|
const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
|
|
611
664
|
console.log(
|
|
612
|
-
|
|
665
|
+
chalk.cyan(
|
|
666
|
+
`🌐 Open this URL in your browser to preview the project:\n${previewUrl}`
|
|
667
|
+
)
|
|
613
668
|
);
|
|
614
669
|
await open(previewUrl);
|
|
615
670
|
}
|
|
616
671
|
});
|
|
617
672
|
});
|
|
618
673
|
|
|
674
|
+
// Hàm tái sử dụng để push mã nguồn lên server
|
|
675
|
+
async function pushProject(spinner: Ora): Promise<boolean> {
|
|
676
|
+
const data = getStoredToken();
|
|
677
|
+
const { accessToken } = data || {};
|
|
678
|
+
if (!accessToken) {
|
|
679
|
+
spinner.fail(chalk.red("✗ You need to login first. Use 'hapico login' command."));
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
const pwd = process.cwd();
|
|
683
|
+
const projectId = getStoredProjectId(pwd);
|
|
684
|
+
if (!projectId) {
|
|
685
|
+
spinner.fail(
|
|
686
|
+
chalk.red(
|
|
687
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
688
|
+
)
|
|
689
|
+
);
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
const srcDir: string = path.join(pwd, "src");
|
|
693
|
+
if (!fs.existsSync(srcDir)) {
|
|
694
|
+
spinner.fail(
|
|
695
|
+
chalk.red(
|
|
696
|
+
"✗ Source directory 'src' does not exist. Please clone a project first."
|
|
697
|
+
)
|
|
698
|
+
);
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
const fileManager = new FileManager(srcDir);
|
|
702
|
+
const files = fileManager.listFiles();
|
|
703
|
+
|
|
704
|
+
// Nếu có file .env
|
|
705
|
+
const envFile = path.join(pwd, ".env");
|
|
706
|
+
console.log(chalk.cyan(`🔍 Checking for .env file at ${envFile}`));
|
|
707
|
+
if (fs.existsSync(envFile)) {
|
|
708
|
+
console.log(chalk.green(".env file found, including in push."));
|
|
709
|
+
const content = fs.readFileSync(envFile, { encoding: "utf8" });
|
|
710
|
+
files.push({
|
|
711
|
+
path: "./.env",
|
|
712
|
+
content,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
|
|
716
|
+
try {
|
|
717
|
+
await axios.put(
|
|
718
|
+
apiUrl,
|
|
719
|
+
{
|
|
720
|
+
code: JSON.stringify({
|
|
721
|
+
files,
|
|
722
|
+
}),
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
headers: {
|
|
726
|
+
Authorization: `Bearer ${accessToken}`,
|
|
727
|
+
"Content-Type": "application/json",
|
|
728
|
+
},
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
return true;
|
|
732
|
+
} catch (error) {
|
|
733
|
+
spinner.fail(chalk.red(`✗ Error saving project: ${(error as Error).message}`));
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
619
738
|
program
|
|
620
739
|
.command("push")
|
|
621
740
|
.description("Push the project source code to the server")
|
|
622
|
-
.action(() => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const pwd = process.cwd();
|
|
631
|
-
const projectId = getStoredProjectId(pwd);
|
|
632
|
-
if (!projectId) {
|
|
633
|
-
saveSpinner.fail(
|
|
634
|
-
"Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
635
|
-
);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const srcDir: string = path.join(pwd, "src");
|
|
639
|
-
if (!fs.existsSync(srcDir)) {
|
|
640
|
-
saveSpinner.fail(
|
|
641
|
-
"Source directory 'src' does not exist. Please clone a project first."
|
|
741
|
+
.action(async () => {
|
|
742
|
+
const saveSpinner: Ora = ora(
|
|
743
|
+
chalk.blue("Saving project source code...")
|
|
744
|
+
).start();
|
|
745
|
+
const success = await pushProject(saveSpinner);
|
|
746
|
+
if (success) {
|
|
747
|
+
saveSpinner.succeed(
|
|
748
|
+
chalk.green("Project source code saved successfully!")
|
|
642
749
|
);
|
|
643
|
-
return;
|
|
644
750
|
}
|
|
645
|
-
const fileManager = new FileManager(srcDir);
|
|
646
|
-
const files = fileManager.listFiles();
|
|
647
|
-
const apiUrl = `https://base.myworkbeast.com/api/views/${projectId}`;
|
|
648
|
-
axios
|
|
649
|
-
.put(
|
|
650
|
-
apiUrl,
|
|
651
|
-
{
|
|
652
|
-
code: JSON.stringify({
|
|
653
|
-
files,
|
|
654
|
-
}),
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
headers: {
|
|
658
|
-
Authorization: `Bearer ${accessToken}`,
|
|
659
|
-
"Content-Type": "application/json",
|
|
660
|
-
},
|
|
661
|
-
}
|
|
662
|
-
)
|
|
663
|
-
.then(() => {
|
|
664
|
-
saveSpinner.succeed("Project source code saved successfully!");
|
|
665
|
-
})
|
|
666
|
-
.catch((error) => {
|
|
667
|
-
saveSpinner.fail(`Error saving project: ${error.message}`);
|
|
668
|
-
});
|
|
669
751
|
});
|
|
670
752
|
|
|
671
753
|
program
|
|
672
754
|
.command("login")
|
|
673
755
|
.description("Login to the system")
|
|
674
756
|
.action(async () => {
|
|
675
|
-
console.log("Logging in to the system...");
|
|
676
|
-
const loginSpinner: Ora = ora("Initiating login...").start();
|
|
757
|
+
console.log(chalk.cyan("🔐 Logging in to the system..."));
|
|
758
|
+
const loginSpinner: Ora = ora(chalk.blue("Initiating login...")).start();
|
|
677
759
|
|
|
678
760
|
try {
|
|
679
761
|
const response = await axios.post(
|
|
@@ -682,14 +764,20 @@ program
|
|
|
682
764
|
const { device_code, user_code, verification_url, expires_in, interval } =
|
|
683
765
|
response.data;
|
|
684
766
|
|
|
685
|
-
loginSpinner.succeed("Login initiated!");
|
|
686
|
-
console.log(
|
|
687
|
-
|
|
688
|
-
|
|
767
|
+
loginSpinner.succeed(chalk.green("Login initiated!"));
|
|
768
|
+
console.log(
|
|
769
|
+
chalk.cyan(
|
|
770
|
+
`🌐 Please open this URL in your browser: ${verification_url}`
|
|
771
|
+
)
|
|
772
|
+
);
|
|
773
|
+
console.log(chalk.yellow(`🔑 And enter this code: ${user_code}`));
|
|
774
|
+
console.log(chalk.blue("⏳ Waiting for authentication..."));
|
|
689
775
|
|
|
690
776
|
await open(verification_url);
|
|
691
777
|
|
|
692
|
-
const pollSpinner: Ora = ora(
|
|
778
|
+
const pollSpinner: Ora = ora(
|
|
779
|
+
chalk.blue("Waiting for authentication...")
|
|
780
|
+
).start();
|
|
693
781
|
let tokens = null;
|
|
694
782
|
const startTime = Date.now();
|
|
695
783
|
while (Date.now() - startTime < expires_in * 1000) {
|
|
@@ -709,15 +797,19 @@ program
|
|
|
709
797
|
}
|
|
710
798
|
|
|
711
799
|
if (tokens) {
|
|
712
|
-
pollSpinner.succeed("Login successful!");
|
|
800
|
+
pollSpinner.succeed(chalk.green("Login successful!"));
|
|
713
801
|
saveToken(tokens);
|
|
714
802
|
} else {
|
|
715
803
|
pollSpinner.fail(
|
|
716
|
-
|
|
804
|
+
chalk.red(
|
|
805
|
+
"✗ Login failed: Timeout or user did not complete authentication."
|
|
806
|
+
)
|
|
717
807
|
);
|
|
718
808
|
}
|
|
719
809
|
} catch (error) {
|
|
720
|
-
loginSpinner.fail(
|
|
810
|
+
loginSpinner.fail(
|
|
811
|
+
chalk.red(`✗ Login error: ${(error as Error).message}`)
|
|
812
|
+
);
|
|
721
813
|
}
|
|
722
814
|
});
|
|
723
815
|
|
|
@@ -727,11 +819,11 @@ program
|
|
|
727
819
|
.action(() => {
|
|
728
820
|
const accessToken = getStoredToken();
|
|
729
821
|
if (!accessToken) {
|
|
730
|
-
console.log("You are not logged in.");
|
|
822
|
+
console.log(chalk.yellow("ℹ You are not logged in."));
|
|
731
823
|
return;
|
|
732
824
|
}
|
|
733
825
|
fs.unlinkSync(TOKEN_FILE);
|
|
734
|
-
console.log("Logout successful!");
|
|
826
|
+
console.log(chalk.green("Logout successful!"));
|
|
735
827
|
});
|
|
736
828
|
// Pull command to fetch the latest project files from the server
|
|
737
829
|
program
|
|
@@ -740,18 +832,24 @@ program
|
|
|
740
832
|
.action(async () => {
|
|
741
833
|
const token = getStoredToken();
|
|
742
834
|
if (!token) {
|
|
743
|
-
console.error(
|
|
835
|
+
console.error(
|
|
836
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
837
|
+
);
|
|
744
838
|
return;
|
|
745
839
|
}
|
|
746
840
|
const pwd: string = process.cwd();
|
|
747
841
|
const projectId = getStoredProjectId(pwd);
|
|
748
842
|
if (!projectId) {
|
|
749
843
|
console.error(
|
|
750
|
-
|
|
844
|
+
chalk.red(
|
|
845
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
846
|
+
)
|
|
751
847
|
);
|
|
752
848
|
return;
|
|
753
849
|
}
|
|
754
|
-
const apiSpinner: Ora = ora(
|
|
850
|
+
const apiSpinner: Ora = ora(
|
|
851
|
+
chalk.blue("Fetching latest project files...")
|
|
852
|
+
).start();
|
|
755
853
|
try {
|
|
756
854
|
const response: ApiResponse = await axios.get(
|
|
757
855
|
`https://base.myworkbeast.com/api/views/${projectId}`,
|
|
@@ -765,12 +863,411 @@ program
|
|
|
765
863
|
const files: FileContent[] = response.data.files || [];
|
|
766
864
|
const fileManager = new FileManager(path.join(pwd, "src"));
|
|
767
865
|
fileManager.syncFiles(files);
|
|
768
|
-
apiSpinner.succeed("Project files updated successfully!");
|
|
866
|
+
apiSpinner.succeed(chalk.green("Project files updated successfully!"));
|
|
769
867
|
} catch (error) {
|
|
770
868
|
apiSpinner.fail(
|
|
771
|
-
|
|
869
|
+
chalk.red(`✗ Error fetching project files: ${(error as Error).message}`)
|
|
772
870
|
);
|
|
773
871
|
}
|
|
774
872
|
});
|
|
775
873
|
|
|
776
|
-
|
|
874
|
+
// be {{id}} --be --port {{port}}
|
|
875
|
+
program
|
|
876
|
+
.command("fetch <id>")
|
|
877
|
+
.option("--port <port>", "Port to run the backend", "3000")
|
|
878
|
+
.option("--serve", "Flag to indicate serving the backend")
|
|
879
|
+
.option(
|
|
880
|
+
"--libs <libs>",
|
|
881
|
+
"Additional libraries to install (comma-separated)",
|
|
882
|
+
""
|
|
883
|
+
)
|
|
884
|
+
.option("--be", "Flag to indicate backend")
|
|
885
|
+
.description("Open backend for the project")
|
|
886
|
+
.action(
|
|
887
|
+
async (
|
|
888
|
+
id: string,
|
|
889
|
+
options: { port: string; serve?: boolean; libs?: string; be?: boolean }
|
|
890
|
+
) => {
|
|
891
|
+
const { accessToken } = getStoredToken();
|
|
892
|
+
if (!accessToken) {
|
|
893
|
+
console.error(
|
|
894
|
+
chalk.red("✗ You need to login first. Use 'hapico login' command.")
|
|
895
|
+
);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
console.log(chalk.cyan(`🌐 PORT = ${options.port}`));
|
|
900
|
+
|
|
901
|
+
// Chọn hỏi user port để vận hành
|
|
902
|
+
let port = 3000;
|
|
903
|
+
const response: ApiResponse = await axios.get(
|
|
904
|
+
`https://base.myworkbeast.com/api/views/${id}`
|
|
905
|
+
);
|
|
906
|
+
const portInput = options.port;
|
|
907
|
+
if (portInput) {
|
|
908
|
+
const parsedPort = parseInt(portInput, 10);
|
|
909
|
+
if (!isNaN(parsedPort)) {
|
|
910
|
+
port = parsedPort;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const projectDir: string = path.resolve(process.cwd(), id);
|
|
915
|
+
if (!fs.existsSync(projectDir)) {
|
|
916
|
+
// create folder
|
|
917
|
+
fs.mkdirSync(projectDir);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// download template https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/hapico-basic.zip
|
|
921
|
+
const TEMPLATE_URL: string = `https://main.hcm04.vstorage.vngcloud.vn/templates/hapico/be.zip?t=${Date.now()}`;
|
|
922
|
+
let templateResponse = undefined;
|
|
923
|
+
try {
|
|
924
|
+
templateResponse = await axios.get(TEMPLATE_URL, {
|
|
925
|
+
responseType: "arraybuffer",
|
|
926
|
+
});
|
|
927
|
+
} catch (error) {
|
|
928
|
+
console.error(
|
|
929
|
+
chalk.red("✗ Error downloading template:"),
|
|
930
|
+
(error as Error).message
|
|
931
|
+
);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const outputDir: string = path.resolve(process.cwd(), id);
|
|
936
|
+
if (!fs.existsSync(outputDir)) {
|
|
937
|
+
fs.mkdirSync(outputDir);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
await unzipper.Open.buffer(templateResponse.data).then((directory) =>
|
|
941
|
+
directory.extract({ path: outputDir })
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
const macosxDir: string = path.join(process.cwd(), id, "__MACOSX");
|
|
945
|
+
if (fs.existsSync(macosxDir)) {
|
|
946
|
+
fs.rmSync(macosxDir, { recursive: true, force: true });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// outputPath/src/server.ts có dòng (3006) thay thành port
|
|
950
|
+
const serverFile = path.join(process.cwd(), id, "src", "index.ts");
|
|
951
|
+
if (fs.existsSync(serverFile)) {
|
|
952
|
+
let serverContent = fs.readFileSync(serverFile, { encoding: "utf8" });
|
|
953
|
+
serverContent = serverContent.split("(3006)").join(`(${port})`);
|
|
954
|
+
fs.writeFileSync(serverFile, serverContent, { encoding: "utf8" });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// lấy danh sách migrations
|
|
958
|
+
const MIGRATIONS_URL = `https://base.myworkbeast.com/api/client/query?dbCode=${
|
|
959
|
+
response.data.dbCode || response.data.db_code
|
|
960
|
+
}&table=migration_logs&operation=select&columns=%5B%22*%22%5D`;
|
|
961
|
+
|
|
962
|
+
// Lấy danh sách các migration đã chạy
|
|
963
|
+
let migrations: Array<{
|
|
964
|
+
id: number;
|
|
965
|
+
created_at: string;
|
|
966
|
+
sql_statement: string;
|
|
967
|
+
}> = [];
|
|
968
|
+
const migrationsResponse = await axios.get(MIGRATIONS_URL, {
|
|
969
|
+
headers: {
|
|
970
|
+
Authorization: `Bearer ${accessToken}`,
|
|
971
|
+
"Content-Type": "application/json",
|
|
972
|
+
"x-db-code": response.data.dbCode || response.data.db_code,
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
if (migrationsResponse.status === 200) {
|
|
976
|
+
migrations = (migrationsResponse.data.data?.rows || []).map(
|
|
977
|
+
(item: any) => ({
|
|
978
|
+
created_at: item.created_at,
|
|
979
|
+
sql: item.sql_statement,
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
console.log(chalk.cyan(`🔍 Found ${migrations.length} migrations.`));
|
|
985
|
+
|
|
986
|
+
// Tạo thư mục migrations nếu chưa tồn tại
|
|
987
|
+
const migrationsDir = path.join(process.cwd(), id, "migrations");
|
|
988
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
989
|
+
fs.mkdirSync(migrationsDir);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Lưu từng migration vào file
|
|
993
|
+
migrations.forEach((migration, index) => {
|
|
994
|
+
const timestamp = new Date(migration.created_at)
|
|
995
|
+
.toISOString()
|
|
996
|
+
.replace(/[-:]/g, "")
|
|
997
|
+
.replace(/\..+/, "");
|
|
998
|
+
const filename = path.join(
|
|
999
|
+
migrationsDir,
|
|
1000
|
+
`${index + 1}_migration_${timestamp}.sql`
|
|
1001
|
+
);
|
|
1002
|
+
fs.writeFileSync(filename, (migration as any).sql, { encoding: "utf8" });
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
console.log(chalk.green("✓ Migrations saved successfully!"));
|
|
1006
|
+
|
|
1007
|
+
// Download project files and save to project
|
|
1008
|
+
let files: FileContent[] = [];
|
|
1009
|
+
const apiSpinner: Ora = ora(
|
|
1010
|
+
chalk.blue("Fetching project data...")
|
|
1011
|
+
).start();
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
files = response.data.files || [];
|
|
1015
|
+
apiSpinner.succeed(chalk.green("Project data fetched successfully!"));
|
|
1016
|
+
|
|
1017
|
+
const saveSpinner: Ora = ora(
|
|
1018
|
+
chalk.blue("Saving project files...")
|
|
1019
|
+
).start();
|
|
1020
|
+
files.forEach((file: FileContent) => {
|
|
1021
|
+
// skip ./app, ./components
|
|
1022
|
+
if (
|
|
1023
|
+
file.path.startsWith("./app") ||
|
|
1024
|
+
file.path.startsWith("./components")
|
|
1025
|
+
) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
// hoặc các file có đuôi là .tsx
|
|
1029
|
+
if (file.path.endsWith(".tsx")) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
if (file.content.trim().length == 0) return;
|
|
1034
|
+
const filePath: string = path.join(
|
|
1035
|
+
process.cwd(),
|
|
1036
|
+
id,
|
|
1037
|
+
"src",
|
|
1038
|
+
file.path
|
|
1039
|
+
);
|
|
1040
|
+
const dir: string = path.dirname(filePath);
|
|
1041
|
+
|
|
1042
|
+
if (!fs.existsSync(dir)) {
|
|
1043
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
fs.writeFileSync(filePath, file.content);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
console.log(chalk.red("✗ Error writing file"), file.path, error);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
saveSpinner.succeed(chalk.green("Project files saved successfully!"));
|
|
1052
|
+
} catch (error: any) {
|
|
1053
|
+
apiSpinner.fail(chalk.red(`✗ Error cloning project: ${error.message}`));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Download project files and save to project
|
|
1057
|
+
console.log(chalk.green("✓ Project cloned successfully!"));
|
|
1058
|
+
|
|
1059
|
+
// Save project ID to hapico.config.json
|
|
1060
|
+
saveProjectId(outputDir, id);
|
|
1061
|
+
|
|
1062
|
+
console.log(chalk.green("✓ Project setup successfully!"));
|
|
1063
|
+
|
|
1064
|
+
let serveProcess: any = null;
|
|
1065
|
+
|
|
1066
|
+
if (options.serve) {
|
|
1067
|
+
// Run bun install
|
|
1068
|
+
const { exec } = require("child_process");
|
|
1069
|
+
const installSpinner: Ora = ora(
|
|
1070
|
+
chalk.blue("Installing dependencies...")
|
|
1071
|
+
).start();
|
|
1072
|
+
|
|
1073
|
+
// create a loop to check if there is new version of project
|
|
1074
|
+
// https://main.hcm04.vstorage.vngcloud.vn/statics/{{id}}/version.json
|
|
1075
|
+
let currentVersionStr = fs.existsSync(
|
|
1076
|
+
path.join(outputDir, "version.json")
|
|
1077
|
+
)
|
|
1078
|
+
? JSON.parse(
|
|
1079
|
+
fs.readFileSync(path.join(outputDir, "version.json"), {
|
|
1080
|
+
encoding: "utf8",
|
|
1081
|
+
})
|
|
1082
|
+
).version
|
|
1083
|
+
: "0.0.1";
|
|
1084
|
+
|
|
1085
|
+
const checkVersion = async () => {
|
|
1086
|
+
try {
|
|
1087
|
+
// check if file version.json exists
|
|
1088
|
+
const flyCheck = await axios.head(
|
|
1089
|
+
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1090
|
+
);
|
|
1091
|
+
if (flyCheck.status !== 200) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
// get file version.json
|
|
1095
|
+
const versionResponse = await axios.get(
|
|
1096
|
+
`https://statics.hcm04.vstorage.vngcloud.vn/${id}/version.json`
|
|
1097
|
+
);
|
|
1098
|
+
const latestVersionData = versionResponse.data;
|
|
1099
|
+
const latestVersion = latestVersionData.version;
|
|
1100
|
+
if (latestVersion !== currentVersionStr) {
|
|
1101
|
+
console.log(
|
|
1102
|
+
chalk.yellow(`📦 New version available: ${latestVersion}`)
|
|
1103
|
+
);
|
|
1104
|
+
// Save new version.json
|
|
1105
|
+
fs.writeFileSync(
|
|
1106
|
+
path.join(outputDir, "version.json"),
|
|
1107
|
+
JSON.stringify(latestVersionData, null, 2),
|
|
1108
|
+
{ encoding: "utf8" }
|
|
1109
|
+
);
|
|
1110
|
+
// Install external libraries if any
|
|
1111
|
+
if (
|
|
1112
|
+
latestVersionData.external_libraries &&
|
|
1113
|
+
latestVersionData.external_libraries.length > 0
|
|
1114
|
+
) {
|
|
1115
|
+
console.log(chalk.blue("Installing new external libraries..."));
|
|
1116
|
+
for (const lib of latestVersionData.external_libraries) {
|
|
1117
|
+
try {
|
|
1118
|
+
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1119
|
+
console.log(chalk.green(`Added ${lib}`));
|
|
1120
|
+
} catch (addError) {
|
|
1121
|
+
console.error(
|
|
1122
|
+
chalk.red(`✗ Error adding ${lib}:`),
|
|
1123
|
+
(addError as Error).message
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
// Rerun bun install
|
|
1129
|
+
try {
|
|
1130
|
+
await execPromise("bun install", { cwd: outputDir });
|
|
1131
|
+
console.log(
|
|
1132
|
+
chalk.green("Dependencies reinstalled successfully!")
|
|
1133
|
+
);
|
|
1134
|
+
} catch (installError) {
|
|
1135
|
+
console.error(
|
|
1136
|
+
chalk.red("✗ Error reinstalling dependencies:"),
|
|
1137
|
+
(installError as Error).message
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
// Restart the process
|
|
1141
|
+
if (serveProcess && !serveProcess.killed) {
|
|
1142
|
+
console.log(chalk.blue("Restarting backend..."));
|
|
1143
|
+
serveProcess.kill("SIGTERM");
|
|
1144
|
+
}
|
|
1145
|
+
// Start new process
|
|
1146
|
+
const newServeProcess = exec("bun run start", { cwd: outputDir });
|
|
1147
|
+
newServeProcess.stdout?.on("data", (data: any) => {
|
|
1148
|
+
process.stdout.write(data);
|
|
1149
|
+
});
|
|
1150
|
+
newServeProcess.stderr?.on("data", (data: any) => {
|
|
1151
|
+
process.stderr.write(data);
|
|
1152
|
+
});
|
|
1153
|
+
newServeProcess.on("close", (code: any) => {
|
|
1154
|
+
console.log(
|
|
1155
|
+
chalk.yellow(`⚠ backend exited with code ${code}`)
|
|
1156
|
+
);
|
|
1157
|
+
});
|
|
1158
|
+
serveProcess = newServeProcess;
|
|
1159
|
+
currentVersionStr = latestVersion;
|
|
1160
|
+
}
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
// If the remote version.json does not exist, do not update
|
|
1163
|
+
console.error(
|
|
1164
|
+
chalk.yellow("⚠ Error checking for updates (skipping update):"),
|
|
1165
|
+
(error as Error).message
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// check every 3 seconds
|
|
1171
|
+
setInterval(async () => await checkVersion(), 10 * 1000);
|
|
1172
|
+
|
|
1173
|
+
exec(
|
|
1174
|
+
"bun install",
|
|
1175
|
+
{ cwd: outputDir },
|
|
1176
|
+
async (error: any, stdout: any, stderr: any) => {
|
|
1177
|
+
if (error) {
|
|
1178
|
+
installSpinner.fail(
|
|
1179
|
+
chalk.red(`✗ Error installing dependencies: ${error.message}`)
|
|
1180
|
+
);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (stderr) {
|
|
1184
|
+
console.error(chalk.red(`stderr: ${stderr}`));
|
|
1185
|
+
}
|
|
1186
|
+
installSpinner.succeed(
|
|
1187
|
+
chalk.green("Dependencies installed successfully!")
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
// Install additional libraries if --libs is provided
|
|
1191
|
+
if (options.libs && options.libs.trim()) {
|
|
1192
|
+
const libsSpinner: Ora = ora(
|
|
1193
|
+
chalk.blue("Installing additional libraries...")
|
|
1194
|
+
).start();
|
|
1195
|
+
const additionalLibs = options.libs
|
|
1196
|
+
.split(",")
|
|
1197
|
+
.map((lib) => lib.trim())
|
|
1198
|
+
.filter((lib) => lib);
|
|
1199
|
+
for (const lib of additionalLibs) {
|
|
1200
|
+
try {
|
|
1201
|
+
await execPromise(`bun add ${lib}`, { cwd: outputDir });
|
|
1202
|
+
console.log(chalk.green(`Added ${lib}`));
|
|
1203
|
+
} catch (addError) {
|
|
1204
|
+
console.error(
|
|
1205
|
+
chalk.red(`✗ Error adding ${lib}:`),
|
|
1206
|
+
(addError as Error).message
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
libsSpinner.succeed(
|
|
1211
|
+
chalk.green("Additional libraries installed successfully!")
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Run cd ${id} && bun run serve-be
|
|
1216
|
+
console.log(chalk.blue("🚀 Starting backend"));
|
|
1217
|
+
serveProcess = exec("bun run start", { cwd: outputDir });
|
|
1218
|
+
serveProcess.stdout.on("data", (data: any) => {
|
|
1219
|
+
process.stdout.write(data);
|
|
1220
|
+
});
|
|
1221
|
+
serveProcess.stderr.on("data", (data: any) => {
|
|
1222
|
+
process.stderr.write(data);
|
|
1223
|
+
});
|
|
1224
|
+
serveProcess.on("close", (code: any) => {
|
|
1225
|
+
console.log(chalk.yellow(`⚠ backend exited with code ${code}`));
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
// hapico publish
|
|
1234
|
+
program.command("publish").action(async () => {
|
|
1235
|
+
const publishSpinner: Ora = ora(chalk.blue("Publishing to Hapico...")).start();
|
|
1236
|
+
// Bước 1: Push mã nguồn lên server (tái sử dụng hàm pushProject)
|
|
1237
|
+
const pushSuccess = await pushProject(publishSpinner);
|
|
1238
|
+
if (!pushSuccess) {
|
|
1239
|
+
return; // Dừng nếu push thất bại
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Bước 2: Gọi API để publish
|
|
1243
|
+
const { accessToken } = getStoredToken();
|
|
1244
|
+
const pwd = process.cwd();
|
|
1245
|
+
const projectId = getStoredProjectId(pwd);
|
|
1246
|
+
if (!projectId) {
|
|
1247
|
+
publishSpinner.fail(
|
|
1248
|
+
chalk.red(
|
|
1249
|
+
"✗ Project ID not found. Please ensure hapico.config.json exists in the project directory."
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const apiUrl = "https://base.myworkbeast.com/api/views/publish";
|
|
1255
|
+
try {
|
|
1256
|
+
await axios.post(
|
|
1257
|
+
apiUrl,
|
|
1258
|
+
{ view_id: parseInt(projectId, 10) }, // Đảm bảo viewId là number
|
|
1259
|
+
{
|
|
1260
|
+
headers: {
|
|
1261
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1262
|
+
"Content-Type": "application/json",
|
|
1263
|
+
},
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
publishSpinner.succeed(chalk.green("Project published successfully!"));
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
console.log(error);
|
|
1269
|
+
publishSpinner.fail(chalk.red(`✗ Error publishing project: ${(error as Error).message}`));
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
program.parse(process.argv);
|