@hapico/cli 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -38,6 +38,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.compileES5 = void 0;
41
+ /* eslint-disable @typescript-eslint/no-explicit-any */
42
+ const lodash_1 = require("lodash");
41
43
  const commander_1 = require("commander");
42
44
  const axios_1 = __importDefault(require("axios"));
43
45
  const fs = __importStar(require("fs"));
@@ -226,7 +228,7 @@ class FileManager {
226
228
  }
227
229
  }
228
230
  class RoomState {
229
- constructor(roomId) {
231
+ constructor(roomId, onChangeListeners) {
230
232
  this.files = [];
231
233
  this.roomId = roomId;
232
234
  this.state = {};
@@ -234,6 +236,7 @@ class RoomState {
234
236
  this.ws = null;
235
237
  this.reconnectTimeout = null;
236
238
  this.reconnectAttempts = 0;
239
+ this.onChangeListeners = onChangeListeners;
237
240
  }
238
241
  connect(onConnected) {
239
242
  if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN)
@@ -242,25 +245,50 @@ class RoomState {
242
245
  clearTimeout(this.reconnectTimeout);
243
246
  }
244
247
  this.ws = new ws_1.WebSocket(`https://base.myworkbeast.com/ws?room=${this.roomId}`);
245
- this.ws.on("open", () => {
248
+ this.ws.onopen = () => {
249
+ console.log(`Connected to room: ${this.roomId}`);
246
250
  this.isConnected = true;
247
251
  this.reconnectAttempts = 0;
248
- onConnected === null || onConnected === void 0 ? void 0 : onConnected();
249
- connected.succeed(`Connected to WebSocket server`);
250
- });
251
- this.ws.on("close", () => {
252
+ onConnected === null || onConnected === void 0 ? void 0 : onConnected(); // Call the onConnected callback if provided
253
+ };
254
+ this.ws.onclose = () => {
255
+ console.log(`Disconnected from room: ${this.roomId}`);
252
256
  this.isConnected = false;
253
257
  this.reconnectAttempts++;
254
- connected.start(`Retry connection...`);
255
- this.connect();
256
- });
258
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
259
+ console.log(`Attempting to reconnect in ${delay / 1000}s...`);
260
+ this.reconnectTimeout = setTimeout(() => this.connect(onConnected), delay);
261
+ };
257
262
  this.ws.on("message", (data) => {
258
263
  try {
259
- const message = JSON.parse(data.toString());
260
- if (message.type === "state" && message.state) {
261
- this.state = message.state;
262
- const view = this.state.view;
263
- this.files = view;
264
+ const jsonStr = typeof data === "string" ? data : data.toString();
265
+ const parsedData = JSON.parse(jsonStr);
266
+ const includes = ["view", "coding", "refresh_key", "useActiveIdFocus", "activeId", "active_file", "figma"];
267
+ let newState;
268
+ let changedKeys;
269
+ if (parsedData.type === "state") {
270
+ // Full state (e.g., on initial connect)
271
+ newState = parsedData.state;
272
+ changedKeys = Object.keys(newState).filter((key) => !(0, lodash_1.isEqual)(newState[key], this.state[key]));
273
+ }
274
+ else if (parsedData.type === "update") {
275
+ // Delta update
276
+ const delta = parsedData.state;
277
+ newState = { ...this.state, ...delta };
278
+ changedKeys = Object.keys(delta).filter((key) => !(0, lodash_1.isEqual)(delta[key], this.state[key]));
279
+ }
280
+ else {
281
+ return;
282
+ }
283
+ const filteredChangedKeys = changedKeys.filter((key) => includes.includes(key));
284
+ if (filteredChangedKeys.length > 0) {
285
+ filteredChangedKeys.forEach((key) => {
286
+ const listener = (0, lodash_1.find)(this.onChangeListeners, (l) => l.key === key);
287
+ if (listener) {
288
+ listener.callback(newState[key]);
289
+ }
290
+ });
291
+ this.state = newState;
264
292
  }
265
293
  }
266
294
  catch (e) {
@@ -280,16 +308,13 @@ class RoomState {
280
308
  state: { [key]: value },
281
309
  }));
282
310
  }
283
- else {
284
- console.log("Cannot update state: Not connected");
285
- }
286
311
  }
287
312
  disconnect() {
288
313
  if (this.reconnectTimeout) {
289
314
  clearTimeout(this.reconnectTimeout);
290
315
  }
291
316
  if (this.ws) {
292
- this.ws.removeAllListeners("close");
317
+ this.ws.onclose = null; // Prevent reconnect on intentional close
293
318
  this.ws.close();
294
319
  }
295
320
  }
@@ -300,7 +325,7 @@ class RoomState {
300
325
  return this.isConnected;
301
326
  }
302
327
  }
303
- commander_1.program.version("0.0.13").description("Hapico CLI for project management");
328
+ commander_1.program.version("0.0.15").description("Hapico CLI for project management");
304
329
  commander_1.program
305
330
  .command("clone <id>")
306
331
  .description("Clone a project by ID")
@@ -373,19 +398,45 @@ commander_1.program
373
398
  devSpinner.fail("Source directory 'src' does not exist. Please clone a project first.");
374
399
  return;
375
400
  }
401
+ // Directory to store session config
402
+ const tmpDir = path.join(pwd, ".tmp");
403
+ const sessionConfigFile = path.join(tmpDir, "config.json");
404
+ // Ensure .tmp directory exists
405
+ if (!fs.existsSync(tmpDir)) {
406
+ fs.mkdirSync(tmpDir, { recursive: true });
407
+ }
408
+ // Function to get stored session ID
409
+ const getStoredSessionId = () => {
410
+ if (fs.existsSync(sessionConfigFile)) {
411
+ const data = fs.readFileSync(sessionConfigFile, { encoding: "utf8" });
412
+ const json = JSON.parse(data);
413
+ return json.sessionId || null;
414
+ }
415
+ return null;
416
+ };
417
+ // Function to save session ID
418
+ const saveSessionId = (sessionId) => {
419
+ fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2), { encoding: "utf8" });
420
+ };
421
+ // Get or generate session ID
422
+ let sessionId = getStoredSessionId();
423
+ if (!sessionId) {
424
+ sessionId = (0, crypto_1.randomUUID)();
425
+ saveSessionId(sessionId);
426
+ }
376
427
  const info = JSON.stringify({
377
- id: (0, crypto_1.randomUUID)(),
428
+ id: sessionId,
378
429
  createdAt: new Date().toISOString(),
379
430
  viewId: getStoredProjectId(pwd),
380
431
  });
381
- // convert info info to base64
432
+ // Convert info to base64
382
433
  const projectId = Buffer.from(info).toString("base64");
383
434
  if (!projectId) {
384
435
  devSpinner.fail("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
385
436
  return;
386
437
  }
387
438
  console.log(`Connecting to WebSocket server`);
388
- const room = new RoomState(`view_${projectId}`);
439
+ const room = new RoomState(`view_${projectId}`, []);
389
440
  room.connect(async () => {
390
441
  devSpinner.succeed("Project started in development mode!");
391
442
  const fileManager = new FileManager(srcDir);
@@ -394,7 +445,7 @@ commander_1.program
394
445
  fileManager.setOnFileChange((filePath, content) => {
395
446
  const es5 = (0, exports.compileES5)(content, filePath);
396
447
  console.log(`File changed: ${filePath === null || filePath === void 0 ? void 0 : filePath.replace(srcDir, ".")}`);
397
- const updatedView = room.files.map((file) => {
448
+ const updatedView = initialFiles.map((file) => {
398
449
  if (path.join(srcDir, file.path) === filePath) {
399
450
  return { ...file, content, es5 };
400
451
  }
@@ -402,7 +453,7 @@ commander_1.program
402
453
  });
403
454
  room.updateState("view", updatedView);
404
455
  });
405
- // LẤy thông tin về project
456
+ // Fetch project info
406
457
  const projectInfo = getStoredProjectId(pwd);
407
458
  if (!projectInfo) {
408
459
  console.error("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
@@ -419,7 +470,6 @@ commander_1.program
419
470
  return;
420
471
  }
421
472
  const projectType = project.data.type || "view";
422
- console.log("Project type:", projectType);
423
473
  if (projectType === "zalominiapp") {
424
474
  qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}`, { small: true }, (qrcode) => {
425
475
  console.log("Scan this QR code to connect to the project:");
@@ -428,7 +478,6 @@ commander_1.program
428
478
  return;
429
479
  }
430
480
  else {
431
- // show link https://com.ai.vn/dev_preview/{projectId}
432
481
  const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
433
482
  console.log(`Open this URL in your browser to preview the project: \n${previewUrl}`);
434
483
  await (0, open_1.default)(previewUrl);
package/bun.lock CHANGED
@@ -7,6 +7,7 @@
7
7
  "@babel/standalone": "^7.28.2",
8
8
  "axios": "^1.11.0",
9
9
  "commander": "^14.0.0",
10
+ "lodash": "^4.17.21",
10
11
  "open": "^10.2.0",
11
12
  "ora": "^8.2.0",
12
13
  "qrcode-terminal": "^0.12.0",
@@ -16,6 +17,7 @@
16
17
  "devDependencies": {
17
18
  "@types/babel__standalone": "^7.1.9",
18
19
  "@types/commander": "^2.12.5",
20
+ "@types/lodash": "^4.17.20",
19
21
  "@types/node": "^24.1.0",
20
22
  "@types/qrcode-terminal": "^0.12.2",
21
23
  "@types/unzipper": "^0.10.11",
@@ -47,6 +49,8 @@
47
49
 
48
50
  "@types/commander": ["@types/commander@2.12.5", "", { "dependencies": { "commander": "*" } }, "sha512-YXGZ/rz+s57VbzcvEV9fUoXeJlBt5HaKu5iUheiIWNsJs23bz6AnRuRiZBRVBLYyPnixNvVnuzM5pSaxr8Yp/g=="],
49
51
 
52
+ "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
53
+
50
54
  "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
51
55
 
52
56
  "@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
@@ -141,6 +145,8 @@
141
145
 
142
146
  "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
143
147
 
148
+ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
149
+
144
150
  "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
145
151
 
146
152
  "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
package/dist/index.js CHANGED
@@ -38,6 +38,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.compileES5 = void 0;
41
+ /* eslint-disable @typescript-eslint/no-explicit-any */
42
+ const lodash_1 = require("lodash");
41
43
  const commander_1 = require("commander");
42
44
  const axios_1 = __importDefault(require("axios"));
43
45
  const fs = __importStar(require("fs"));
@@ -226,7 +228,7 @@ class FileManager {
226
228
  }
227
229
  }
228
230
  class RoomState {
229
- constructor(roomId) {
231
+ constructor(roomId, onChangeListeners) {
230
232
  this.files = [];
231
233
  this.roomId = roomId;
232
234
  this.state = {};
@@ -234,6 +236,7 @@ class RoomState {
234
236
  this.ws = null;
235
237
  this.reconnectTimeout = null;
236
238
  this.reconnectAttempts = 0;
239
+ this.onChangeListeners = onChangeListeners;
237
240
  }
238
241
  connect(onConnected) {
239
242
  if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN)
@@ -242,25 +245,50 @@ class RoomState {
242
245
  clearTimeout(this.reconnectTimeout);
243
246
  }
244
247
  this.ws = new ws_1.WebSocket(`https://base.myworkbeast.com/ws?room=${this.roomId}`);
245
- this.ws.on("open", () => {
248
+ this.ws.onopen = () => {
249
+ console.log(`Connected to room: ${this.roomId}`);
246
250
  this.isConnected = true;
247
251
  this.reconnectAttempts = 0;
248
- onConnected === null || onConnected === void 0 ? void 0 : onConnected();
249
- connected.succeed(`Connected to WebSocket server`);
250
- });
251
- this.ws.on("close", () => {
252
+ onConnected === null || onConnected === void 0 ? void 0 : onConnected(); // Call the onConnected callback if provided
253
+ };
254
+ this.ws.onclose = () => {
255
+ console.log(`Disconnected from room: ${this.roomId}`);
252
256
  this.isConnected = false;
253
257
  this.reconnectAttempts++;
254
- connected.start(`Retry connection...`);
255
- this.connect();
256
- });
258
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
259
+ console.log(`Attempting to reconnect in ${delay / 1000}s...`);
260
+ this.reconnectTimeout = setTimeout(() => this.connect(onConnected), delay);
261
+ };
257
262
  this.ws.on("message", (data) => {
258
263
  try {
259
- const message = JSON.parse(data.toString());
260
- if (message.type === "state" && message.state) {
261
- this.state = message.state;
262
- const view = this.state.view;
263
- this.files = view;
264
+ const jsonStr = typeof data === "string" ? data : data.toString();
265
+ const parsedData = JSON.parse(jsonStr);
266
+ const includes = ["view", "coding", "refresh_key", "useActiveIdFocus", "activeId", "active_file", "figma"];
267
+ let newState;
268
+ let changedKeys;
269
+ if (parsedData.type === "state") {
270
+ // Full state (e.g., on initial connect)
271
+ newState = parsedData.state;
272
+ changedKeys = Object.keys(newState).filter((key) => !(0, lodash_1.isEqual)(newState[key], this.state[key]));
273
+ }
274
+ else if (parsedData.type === "update") {
275
+ // Delta update
276
+ const delta = parsedData.state;
277
+ newState = { ...this.state, ...delta };
278
+ changedKeys = Object.keys(delta).filter((key) => !(0, lodash_1.isEqual)(delta[key], this.state[key]));
279
+ }
280
+ else {
281
+ return;
282
+ }
283
+ const filteredChangedKeys = changedKeys.filter((key) => includes.includes(key));
284
+ if (filteredChangedKeys.length > 0) {
285
+ filteredChangedKeys.forEach((key) => {
286
+ const listener = (0, lodash_1.find)(this.onChangeListeners, (l) => l.key === key);
287
+ if (listener) {
288
+ listener.callback(newState[key]);
289
+ }
290
+ });
291
+ this.state = newState;
264
292
  }
265
293
  }
266
294
  catch (e) {
@@ -280,16 +308,13 @@ class RoomState {
280
308
  state: { [key]: value },
281
309
  }));
282
310
  }
283
- else {
284
- console.log("Cannot update state: Not connected");
285
- }
286
311
  }
287
312
  disconnect() {
288
313
  if (this.reconnectTimeout) {
289
314
  clearTimeout(this.reconnectTimeout);
290
315
  }
291
316
  if (this.ws) {
292
- this.ws.removeAllListeners("close");
317
+ this.ws.onclose = null; // Prevent reconnect on intentional close
293
318
  this.ws.close();
294
319
  }
295
320
  }
@@ -300,7 +325,7 @@ class RoomState {
300
325
  return this.isConnected;
301
326
  }
302
327
  }
303
- commander_1.program.version("0.0.13").description("Hapico CLI for project management");
328
+ commander_1.program.version("0.0.15").description("Hapico CLI for project management");
304
329
  commander_1.program
305
330
  .command("clone <id>")
306
331
  .description("Clone a project by ID")
@@ -373,19 +398,45 @@ commander_1.program
373
398
  devSpinner.fail("Source directory 'src' does not exist. Please clone a project first.");
374
399
  return;
375
400
  }
401
+ // Directory to store session config
402
+ const tmpDir = path.join(pwd, ".tmp");
403
+ const sessionConfigFile = path.join(tmpDir, "config.json");
404
+ // Ensure .tmp directory exists
405
+ if (!fs.existsSync(tmpDir)) {
406
+ fs.mkdirSync(tmpDir, { recursive: true });
407
+ }
408
+ // Function to get stored session ID
409
+ const getStoredSessionId = () => {
410
+ if (fs.existsSync(sessionConfigFile)) {
411
+ const data = fs.readFileSync(sessionConfigFile, { encoding: "utf8" });
412
+ const json = JSON.parse(data);
413
+ return json.sessionId || null;
414
+ }
415
+ return null;
416
+ };
417
+ // Function to save session ID
418
+ const saveSessionId = (sessionId) => {
419
+ fs.writeFileSync(sessionConfigFile, JSON.stringify({ sessionId }, null, 2), { encoding: "utf8" });
420
+ };
421
+ // Get or generate session ID
422
+ let sessionId = getStoredSessionId();
423
+ if (!sessionId) {
424
+ sessionId = (0, crypto_1.randomUUID)();
425
+ saveSessionId(sessionId);
426
+ }
376
427
  const info = JSON.stringify({
377
- id: (0, crypto_1.randomUUID)(),
428
+ id: sessionId,
378
429
  createdAt: new Date().toISOString(),
379
430
  viewId: getStoredProjectId(pwd),
380
431
  });
381
- // convert info info to base64
432
+ // Convert info to base64
382
433
  const projectId = Buffer.from(info).toString("base64");
383
434
  if (!projectId) {
384
435
  devSpinner.fail("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
385
436
  return;
386
437
  }
387
438
  console.log(`Connecting to WebSocket server`);
388
- const room = new RoomState(`view_${projectId}`);
439
+ const room = new RoomState(`view_${projectId}`, []);
389
440
  room.connect(async () => {
390
441
  devSpinner.succeed("Project started in development mode!");
391
442
  const fileManager = new FileManager(srcDir);
@@ -394,7 +445,7 @@ commander_1.program
394
445
  fileManager.setOnFileChange((filePath, content) => {
395
446
  const es5 = (0, exports.compileES5)(content, filePath);
396
447
  console.log(`File changed: ${filePath === null || filePath === void 0 ? void 0 : filePath.replace(srcDir, ".")}`);
397
- const updatedView = room.files.map((file) => {
448
+ const updatedView = initialFiles.map((file) => {
398
449
  if (path.join(srcDir, file.path) === filePath) {
399
450
  return { ...file, content, es5 };
400
451
  }
@@ -402,7 +453,7 @@ commander_1.program
402
453
  });
403
454
  room.updateState("view", updatedView);
404
455
  });
405
- // LẤy thông tin về project
456
+ // Fetch project info
406
457
  const projectInfo = getStoredProjectId(pwd);
407
458
  if (!projectInfo) {
408
459
  console.error("Project ID not found. Please ensure hapico.config.json exists in the project directory.");
@@ -419,7 +470,6 @@ commander_1.program
419
470
  return;
420
471
  }
421
472
  const projectType = project.data.type || "view";
422
- console.log("Project type:", projectType);
423
473
  if (projectType === "zalominiapp") {
424
474
  qrcode_terminal_1.default.generate(`https://zalo.me/s/3218692650896662017/player/${projectId}`, { small: true }, (qrcode) => {
425
475
  console.log("Scan this QR code to connect to the project:");
@@ -428,7 +478,6 @@ commander_1.program
428
478
  return;
429
479
  }
430
480
  else {
431
- // show link https://com.ai.vn/dev_preview/{projectId}
432
481
  const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
433
482
  console.log(`Open this URL in your browser to preview the project: \n${previewUrl}`);
434
483
  await (0, open_1.default)(previewUrl);
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
-
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { isEqual, find } from "lodash";
3
4
  import { program } from "commander";
4
5
  import axios from "axios";
5
6
  import * as fs from "fs";
@@ -249,6 +250,10 @@ class FileManager {
249
250
  }
250
251
  }
251
252
 
253
+ interface FileContent {
254
+ // Define the structure of FileContent if needed
255
+ }
256
+
252
257
  class RoomState {
253
258
  private roomId: string;
254
259
  private state: RoomStateData;
@@ -256,16 +261,21 @@ class RoomState {
256
261
  private ws: WebSocket | null;
257
262
  private reconnectTimeout: NodeJS.Timeout | null;
258
263
  private reconnectAttempts: number;
264
+ private onChangeListeners: Array<{ key: string; callback: (value: any) => void }>;
259
265
 
260
266
  public files: FileContent[] = [];
261
267
 
262
- constructor(roomId: string) {
268
+ constructor(
269
+ roomId: string,
270
+ onChangeListeners: Array<{ key: string; callback: (value: any) => void }>
271
+ ) {
263
272
  this.roomId = roomId;
264
273
  this.state = {};
265
274
  this.isConnected = false;
266
275
  this.ws = null;
267
276
  this.reconnectTimeout = null;
268
277
  this.reconnectAttempts = 0;
278
+ this.onChangeListeners = onChangeListeners;
269
279
  }
270
280
 
271
281
  connect(onConnected?: () => void): void {
@@ -279,36 +289,69 @@ class RoomState {
279
289
  `https://base.myworkbeast.com/ws?room=${this.roomId}`
280
290
  );
281
291
 
282
- this.ws.on("open", () => {
292
+ this.ws.onopen = () => {
293
+ console.log(`Connected to room: ${this.roomId}`);
283
294
  this.isConnected = true;
284
295
  this.reconnectAttempts = 0;
285
- onConnected?.();
286
- connected.succeed(`Connected to WebSocket server`);
287
- });
296
+ onConnected?.(); // Call the onConnected callback if provided
297
+ };
288
298
 
289
- this.ws.on("close", () => {
299
+ this.ws.onclose = () => {
300
+ console.log(`Disconnected from room: ${this.roomId}`);
290
301
  this.isConnected = false;
291
302
 
292
303
  this.reconnectAttempts++;
293
- connected.start(`Retry connection...`);
304
+ const delay = Math.min(
305
+ 1000 * Math.pow(2, this.reconnectAttempts),
306
+ 30000
307
+ );
308
+ console.log(`Attempting to reconnect in ${delay / 1000}s...`);
294
309
 
295
- this.connect();
296
- });
310
+ this.reconnectTimeout = setTimeout(() => this.connect(onConnected), delay);
311
+ };
297
312
 
298
- this.ws.on("message", (data: Buffer) => {
313
+ this.ws.on("message", (data: Buffer | string) => {
299
314
  try {
300
- const message: WebSocketMessage = JSON.parse(data.toString());
301
- if (message.type === "state" && message.state) {
302
- this.state = message.state;
303
- const view = this.state.view as Array<FileContent>;
304
- this.files = view;
315
+ const jsonStr = typeof data === "string" ? data : data.toString();
316
+ const parsedData = JSON.parse(jsonStr);
317
+ const includes = ["view", "coding", "refresh_key", "useActiveIdFocus", "activeId", "active_file", "figma"];
318
+
319
+ let newState: RoomStateData;
320
+ let changedKeys;
321
+
322
+ if (parsedData.type === "state") {
323
+ // Full state (e.g., on initial connect)
324
+ newState = parsedData.state;
325
+ changedKeys = Object.keys(newState).filter(
326
+ (key) => !isEqual(newState[key], this.state[key])
327
+ );
328
+ } else if (parsedData.type === "update") {
329
+ // Delta update
330
+ const delta = parsedData.state;
331
+ newState = { ...this.state, ...delta };
332
+ changedKeys = Object.keys(delta).filter(
333
+ (key) => !isEqual(delta[key], this.state[key])
334
+ );
335
+ } else {
336
+ return;
337
+ }
338
+
339
+ const filteredChangedKeys = changedKeys.filter((key) => includes.includes(key));
340
+ if (filteredChangedKeys.length > 0) {
341
+ filteredChangedKeys.forEach((key) => {
342
+ const listener = find(this.onChangeListeners, (l) => l.key === key);
343
+ if (listener) {
344
+ listener.callback(newState[key]);
345
+ }
346
+ });
347
+ this.state = newState;
305
348
  }
306
349
  } catch (e) {
307
350
  console.error("Error processing message:", e);
308
351
  }
309
352
  });
310
353
 
311
- this.ws.on("error", (err: Error) => {
354
+ this.ws.on("error", (err) => {
312
355
  console.error("WebSocket error:", err);
313
356
  this.ws?.close();
314
357
  });
@@ -322,8 +365,6 @@ class RoomState {
322
365
  state: { [key]: value },
323
366
  })
324
367
  );
325
- } else {
326
- console.log("Cannot update state: Not connected");
327
368
  }
328
369
  }
329
370
 
@@ -332,7 +373,7 @@ class RoomState {
332
373
  clearTimeout(this.reconnectTimeout);
333
374
  }
334
375
  if (this.ws) {
335
- this.ws.removeAllListeners("close");
376
+ this.ws.onclose = null; // Prevent reconnect on intentional close
336
377
  this.ws.close();
337
378
  }
338
379
  }
@@ -346,7 +387,7 @@ class RoomState {
346
387
  }
347
388
  }
348
389
 
349
- program.version("0.0.13").description("Hapico CLI for project management");
390
+ program.version("0.0.15").description("Hapico CLI for project management");
350
391
 
351
392
  program
352
393
  .command("clone <id>")
@@ -431,23 +472,60 @@ program
431
472
  console.error("You need to login first. Use 'hapico login' command.");
432
473
  return;
433
474
  }
434
- const devSpinner: Ora = ora(
475
+ const devSpinner = ora(
435
476
  "Starting the project in development mode..."
436
477
  ).start();
437
- const pwd: string = process.cwd();
438
- const srcDir: string = path.join(pwd, "src");
478
+ const pwd = process.cwd();
479
+ const srcDir = path.join(pwd, "src");
439
480
  if (!fs.existsSync(srcDir)) {
440
481
  devSpinner.fail(
441
482
  "Source directory 'src' does not exist. Please clone a project first."
442
483
  );
443
484
  return;
444
485
  }
486
+
487
+ // Directory to store session config
488
+ const tmpDir = path.join(pwd, ".tmp");
489
+ const sessionConfigFile = path.join(tmpDir, "config.json");
490
+
491
+ // Ensure .tmp directory exists
492
+ if (!fs.existsSync(tmpDir)) {
493
+ fs.mkdirSync(tmpDir, { recursive: true });
494
+ }
495
+
496
+ // Function to get stored session ID
497
+ const getStoredSessionId = () => {
498
+ if (fs.existsSync(sessionConfigFile)) {
499
+ const data = fs.readFileSync(sessionConfigFile, { encoding: "utf8" });
500
+ const json = JSON.parse(data);
501
+ return json.sessionId || null;
502
+ }
503
+ return null;
504
+ };
505
+
506
+ // Function to save session ID
507
+ const saveSessionId = (sessionId: string) => {
508
+ fs.writeFileSync(
509
+ sessionConfigFile,
510
+ JSON.stringify({ sessionId }, null, 2),
511
+ { encoding: "utf8" }
512
+ );
513
+ };
514
+
515
+ // Get or generate session ID
516
+ let sessionId = getStoredSessionId();
517
+ if (!sessionId) {
518
+ sessionId = randomUUID();
519
+ saveSessionId(sessionId);
520
+ }
521
+
445
522
  const info = JSON.stringify({
446
- id: randomUUID(),
523
+ id: sessionId,
447
524
  createdAt: new Date().toISOString(),
448
525
  viewId: getStoredProjectId(pwd),
449
526
  });
450
- // convert info info to base64
527
+
528
+ // Convert info to base64
451
529
  const projectId = Buffer.from(info).toString("base64");
452
530
  if (!projectId) {
453
531
  devSpinner.fail(
@@ -455,8 +533,9 @@ program
455
533
  );
456
534
  return;
457
535
  }
536
+
458
537
  console.log(`Connecting to WebSocket server`);
459
- const room = new RoomState(`view_${projectId}`);
538
+ const room = new RoomState(`view_${projectId}`, []);
460
539
 
461
540
  room.connect(async () => {
462
541
  devSpinner.succeed("Project started in development mode!");
@@ -466,10 +545,10 @@ program
466
545
  const initialFiles = fileManager.listFiles();
467
546
  room.updateState("view", initialFiles);
468
547
 
469
- fileManager.setOnFileChange((filePath: string, content: string) => {
548
+ fileManager.setOnFileChange((filePath, content) => {
470
549
  const es5 = compileES5(content, filePath);
471
550
  console.log(`File changed: ${filePath?.replace(srcDir, ".")}`);
472
- const updatedView = room.files.map((file) => {
551
+ const updatedView = initialFiles.map((file) => {
473
552
  if (path.join(srcDir, file.path) === filePath) {
474
553
  return { ...file, content, es5 };
475
554
  }
@@ -478,7 +557,7 @@ program
478
557
  room.updateState("view", updatedView);
479
558
  });
480
559
 
481
- // LẤy thông tin về project
560
+ // Fetch project info
482
561
  const projectInfo = getStoredProjectId(pwd);
483
562
  if (!projectInfo) {
484
563
  console.error(
@@ -503,20 +582,17 @@ program
503
582
 
504
583
  const projectType = project.data.type || "view";
505
584
 
506
- console.log("Project type:", projectType);
507
-
508
585
  if (projectType === "zalominiapp") {
509
586
  QRCode.generate(
510
587
  `https://zalo.me/s/3218692650896662017/player/${projectId}`,
511
588
  { small: true },
512
- (qrcode: string) => {
589
+ (qrcode) => {
513
590
  console.log("Scan this QR code to connect to the project:");
514
591
  console.log(qrcode);
515
592
  }
516
593
  );
517
594
  return;
518
595
  } else {
519
- // show link https://com.ai.vn/dev_preview/{projectId}
520
596
  const previewUrl = `https://com.ai.vn/dev_preview/${projectId}`;
521
597
  console.log(
522
598
  `Open this URL in your browser to preview the project: \n${previewUrl}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hapico/cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "A simple CLI tool for project management",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "@babel/standalone": "^7.28.2",
24
24
  "axios": "^1.11.0",
25
25
  "commander": "^14.0.0",
26
+ "lodash": "^4.17.21",
26
27
  "open": "^10.2.0",
27
28
  "ora": "^8.2.0",
28
29
  "qrcode-terminal": "^0.12.0",
@@ -32,6 +33,7 @@
32
33
  "devDependencies": {
33
34
  "@types/babel__standalone": "^7.1.9",
34
35
  "@types/commander": "^2.12.5",
36
+ "@types/lodash": "^4.17.20",
35
37
  "@types/node": "^24.1.0",
36
38
  "@types/qrcode-terminal": "^0.12.2",
37
39
  "@types/unzipper": "^0.10.11",