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