@anvil-works/anvil-cli 0.4.1 → 0.4.2

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.
Files changed (3) hide show
  1. package/dist/cli.js +149 -23
  2. package/dist/index.js +149 -23
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -47506,9 +47506,15 @@ var __webpack_exports__ = {};
47506
47506
  username;
47507
47507
  reconnectAttempts = 0;
47508
47508
  reconnectTimer = null;
47509
+ heartbeatTimer = null;
47510
+ pongTimeoutTimer = null;
47511
+ reconnectDelayMs;
47512
+ isClosing = false;
47509
47513
  sessionId;
47510
- MAX_RECONNECT_ATTEMPTS = 5;
47511
- RECONNECT_DELAY = 5000;
47514
+ RECONNECT_DELAY_BASE_MS = 5000;
47515
+ RECONNECT_DELAY_MAX_MS = 60000;
47516
+ HEARTBEAT_INTERVAL_MS = 30000;
47517
+ HEARTBEAT_TIMEOUT_MS = 10000;
47512
47518
  constructor(options){
47513
47519
  super();
47514
47520
  this.appId = options.appId;
@@ -47518,12 +47524,14 @@ var __webpack_exports__ = {};
47518
47524
  this.editSession = options.editSession;
47519
47525
  this.username = options.username;
47520
47526
  this.sessionId = Math.random().toString(36).substring(2, 10);
47527
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
47521
47528
  }
47522
47529
  async connect() {
47530
+ this.isClosing = false;
47523
47531
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
47524
47532
  if (this.ws) {
47525
47533
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
47526
- this.close();
47534
+ this.teardownSocket();
47527
47535
  }
47528
47536
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
47529
47537
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
@@ -47533,6 +47541,8 @@ var __webpack_exports__ = {};
47533
47541
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
47534
47542
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket opened");
47535
47543
  this.reconnectAttempts = 0;
47544
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
47545
+ this.startHeartbeat();
47536
47546
  const subscribeMsg = {
47537
47547
  cmd: "WATCH_APP",
47538
47548
  app: this.appId,
@@ -47543,6 +47553,7 @@ var __webpack_exports__ = {};
47543
47553
  this.emit("connected", void 0);
47544
47554
  });
47545
47555
  this.ws.on("message", (data)=>{
47556
+ this.markSocketResponsive();
47546
47557
  try {
47547
47558
  const msg = JSON.parse(data.toString());
47548
47559
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
@@ -47551,8 +47562,12 @@ var __webpack_exports__ = {};
47551
47562
  logger_logger.error(chalk_source.red(`Failed to parse WebSocket message: ${error.message}`));
47552
47563
  }
47553
47564
  });
47565
+ this.ws.on("pong", ()=>{
47566
+ this.markSocketResponsive();
47567
+ });
47554
47568
  this.ws.on("close", (code, reason)=>{
47555
47569
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Closed: code=${code}, reason=${reason.toString()}`);
47570
+ this.stopHeartbeat();
47556
47571
  this.ws = null;
47557
47572
  if (1008 === code || reason.toString().includes("unauthenticated")) {
47558
47573
  logger_logger.warn(chalk_source.yellow(" WebSocket authentication failed - changes from the Anvil Editor won't be detected"));
@@ -47560,21 +47575,12 @@ var __webpack_exports__ = {};
47560
47575
  this.emit("auth-failed", void 0);
47561
47576
  return;
47562
47577
  }
47578
+ if (this.isClosing) return;
47563
47579
  this.emit("disconnected", {
47564
47580
  code,
47565
47581
  reason: reason.toString()
47566
47582
  });
47567
- if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
47568
- this.reconnectAttempts++;
47569
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${this.RECONNECT_DELAY / 1000}s (attempt ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`));
47570
- this.reconnectTimer = setTimeout(()=>{
47571
- this.connect().catch((error)=>{
47572
- this.emit("error", {
47573
- error: error instanceof Error ? error : new Error(String(error))
47574
- });
47575
- });
47576
- }, this.RECONNECT_DELAY);
47577
- } else logger_logger.warn(chalk_source.yellow(` WebSocket reconnection failed after ${this.MAX_RECONNECT_ATTEMPTS} attempts - changes from the Anvil Editor won't be detected`));
47583
+ this.scheduleReconnect();
47578
47584
  });
47579
47585
  this.ws.on("error", (error)=>{
47580
47586
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
@@ -47619,24 +47625,74 @@ var __webpack_exports__ = {};
47619
47625
  return this.editSession;
47620
47626
  }
47621
47627
  close() {
47628
+ this.isClosing = true;
47622
47629
  if (this.reconnectTimer) {
47623
47630
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Clearing reconnect timer");
47624
47631
  clearTimeout(this.reconnectTimer);
47625
47632
  this.reconnectTimer = null;
47626
47633
  }
47627
- if (this.ws) {
47634
+ this.teardownSocket();
47635
+ }
47636
+ isConnected() {
47637
+ return this.ws?.readyState === ws_wrapper.OPEN;
47638
+ }
47639
+ scheduleReconnect() {
47640
+ if (this.reconnectTimer || this.isClosing) return;
47641
+ this.reconnectAttempts++;
47642
+ const delayMs = this.reconnectDelayMs;
47643
+ logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.reconnectAttempts})...`));
47644
+ this.reconnectTimer = setTimeout(()=>{
47645
+ this.reconnectTimer = null;
47646
+ this.connect().catch((error)=>{
47647
+ this.emit("error", {
47648
+ error: error instanceof Error ? error : new Error(String(error))
47649
+ });
47650
+ });
47651
+ }, delayMs);
47652
+ this.reconnectDelayMs = Math.min(Math.round(1.5 * this.reconnectDelayMs), this.RECONNECT_DELAY_MAX_MS);
47653
+ }
47654
+ startHeartbeat() {
47655
+ this.stopHeartbeat();
47656
+ this.heartbeatTimer = setInterval(()=>{
47657
+ const ws = this.ws;
47658
+ if (!ws || ws.readyState !== ws_wrapper.OPEN) return;
47628
47659
  try {
47629
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
47630
- this.ws.removeAllListeners();
47631
- if (this.ws.readyState === ws_wrapper.OPEN) this.ws.close();
47632
- } catch (e) {
47633
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
47660
+ ws.ping();
47661
+ } catch (error) {
47662
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Failed to send ping: ${error.message}`);
47663
+ ws.terminate();
47664
+ return;
47634
47665
  }
47635
- this.ws = null;
47666
+ this.pongTimeoutTimer = setTimeout(()=>{
47667
+ logger_logger.warn(chalk_source.yellow(" WebSocket heartbeat timed out - reconnecting"));
47668
+ ws.terminate();
47669
+ }, this.HEARTBEAT_TIMEOUT_MS);
47670
+ }, this.HEARTBEAT_INTERVAL_MS);
47671
+ }
47672
+ markSocketResponsive() {
47673
+ if (this.pongTimeoutTimer) {
47674
+ clearTimeout(this.pongTimeoutTimer);
47675
+ this.pongTimeoutTimer = null;
47636
47676
  }
47637
47677
  }
47638
- isConnected() {
47639
- return this.ws?.readyState === ws_wrapper.OPEN;
47678
+ stopHeartbeat() {
47679
+ if (this.heartbeatTimer) {
47680
+ clearInterval(this.heartbeatTimer);
47681
+ this.heartbeatTimer = null;
47682
+ }
47683
+ this.markSocketResponsive();
47684
+ }
47685
+ teardownSocket() {
47686
+ this.stopHeartbeat();
47687
+ if (!this.ws) return;
47688
+ try {
47689
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
47690
+ this.ws.removeAllListeners();
47691
+ if (this.ws.readyState === ws_wrapper.OPEN || this.ws.readyState === ws_wrapper.CONNECTING) this.ws.terminate();
47692
+ } catch (e) {
47693
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
47694
+ }
47695
+ this.ws = null;
47640
47696
  }
47641
47697
  }
47642
47698
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
@@ -49364,7 +49420,11 @@ var __webpack_exports__ = {};
49364
49420
  wsClient = null;
49365
49421
  saveProcessor = null;
49366
49422
  syncManager = null;
49423
+ localChangePollTimer = null;
49424
+ isPollingLocalChanges = false;
49425
+ lastLocalStatusFingerprint = null;
49367
49426
  BRANCH_CHANGE_SETTLE_MS = 2000;
49427
+ LOCAL_CHANGE_POLL_MS = 2000;
49368
49428
  stagedOnly;
49369
49429
  constructor(repoPath, appId, options){
49370
49430
  super();
@@ -49502,6 +49562,10 @@ var __webpack_exports__ = {};
49502
49562
  this.fileWatcher.cleanup();
49503
49563
  this.fileWatcher = null;
49504
49564
  }
49565
+ if (this.localChangePollTimer) {
49566
+ clearInterval(this.localChangePollTimer);
49567
+ this.localChangePollTimer = null;
49568
+ }
49505
49569
  logger_logger.debug(`[Session ${this.sessionId}]`, "Cleanup complete");
49506
49570
  }
49507
49571
  async startWatching() {
@@ -49535,8 +49599,70 @@ var __webpack_exports__ = {};
49535
49599
  }
49536
49600
  });
49537
49601
  await this.fileWatcher.start(this.currentBranch);
49602
+ await this.initializeLocalChangeFingerprint();
49603
+ this.startLocalChangeFallbackPolling();
49538
49604
  await new Promise(()=>{});
49539
49605
  }
49606
+ async initializeLocalChangeFingerprint() {
49607
+ if (this.stagedOnly) return;
49608
+ try {
49609
+ const status = await this.gitService.getStatus();
49610
+ this.lastLocalStatusFingerprint = this.getLocalStatusFingerprint(status);
49611
+ } catch (error) {
49612
+ logger_logger.debug(`[Session ${this.sessionId}]`, `Failed to initialize local change fingerprint: ${error.message}`);
49613
+ }
49614
+ }
49615
+ startLocalChangeFallbackPolling() {
49616
+ if (this.stagedOnly || this.localChangePollTimer) return;
49617
+ this.localChangePollTimer = setInterval(()=>{
49618
+ this.pollForMissedLocalChanges();
49619
+ }, this.LOCAL_CHANGE_POLL_MS);
49620
+ }
49621
+ async pollForMissedLocalChanges() {
49622
+ if (this.isCleanedUp || this.isPausedForUserInput || this.isPollingLocalChanges) return;
49623
+ this.isPollingLocalChanges = true;
49624
+ try {
49625
+ const status = await this.gitService.getStatus();
49626
+ const fingerprint = this.getLocalStatusFingerprint(status);
49627
+ if (null === this.lastLocalStatusFingerprint) {
49628
+ this.lastLocalStatusFingerprint = fingerprint;
49629
+ return;
49630
+ }
49631
+ if (fingerprint !== this.lastLocalStatusFingerprint) {
49632
+ this.lastLocalStatusFingerprint = fingerprint;
49633
+ if (fingerprint) {
49634
+ logger_logger.debug(`[Session ${this.sessionId}]`, "Detected local change via fallback status poll");
49635
+ this.saveProcessor?.queueSave();
49636
+ }
49637
+ }
49638
+ } catch (error) {
49639
+ logger_logger.debug(`[Session ${this.sessionId}]`, `Fallback status poll failed: ${error.message}`);
49640
+ } finally{
49641
+ this.isPollingLocalChanges = false;
49642
+ }
49643
+ }
49644
+ getLocalStatusFingerprint(status) {
49645
+ const renamed = status.renamed.map((r)=>`${r.from}->${r.to}`).sort();
49646
+ const modified = [
49647
+ ...status.modified
49648
+ ].sort();
49649
+ const notAdded = [
49650
+ ...status.notAdded
49651
+ ].sort();
49652
+ const created = [
49653
+ ...status.created
49654
+ ].sort();
49655
+ const deleted = [
49656
+ ...status.deleted
49657
+ ].sort();
49658
+ return [
49659
+ ...renamed.map((r)=>`R:${r}`),
49660
+ ...modified.map((m)=>`M:${m}`),
49661
+ ...notAdded.map((n)=>`N:${n}`),
49662
+ ...created.map((c)=>`C:${c}`),
49663
+ ...deleted.map((d)=>`D:${d}`)
49664
+ ].join("|");
49665
+ }
49540
49666
  async handleFileChange(event, filePath, relativePath) {
49541
49667
  if (this.isCleanedUp || this.isPausedForUserInput) return;
49542
49668
  logger_logger.verbose(chalk_source.blue("File changed: ") + chalk_source.bold(relativePath));
package/dist/index.js CHANGED
@@ -22643,9 +22643,15 @@ var __webpack_exports__ = {};
22643
22643
  username;
22644
22644
  reconnectAttempts = 0;
22645
22645
  reconnectTimer = null;
22646
+ heartbeatTimer = null;
22647
+ pongTimeoutTimer = null;
22648
+ reconnectDelayMs;
22649
+ isClosing = false;
22646
22650
  sessionId;
22647
- MAX_RECONNECT_ATTEMPTS = 5;
22648
- RECONNECT_DELAY = 5000;
22651
+ RECONNECT_DELAY_BASE_MS = 5000;
22652
+ RECONNECT_DELAY_MAX_MS = 60000;
22653
+ HEARTBEAT_INTERVAL_MS = 30000;
22654
+ HEARTBEAT_TIMEOUT_MS = 10000;
22649
22655
  constructor(options){
22650
22656
  super();
22651
22657
  this.appId = options.appId;
@@ -22655,12 +22661,14 @@ var __webpack_exports__ = {};
22655
22661
  this.editSession = options.editSession;
22656
22662
  this.username = options.username;
22657
22663
  this.sessionId = Math.random().toString(36).substring(2, 10);
22664
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
22658
22665
  }
22659
22666
  async connect() {
22667
+ this.isClosing = false;
22660
22668
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
22661
22669
  if (this.ws) {
22662
22670
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
22663
- this.close();
22671
+ this.teardownSocket();
22664
22672
  }
22665
22673
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
22666
22674
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
@@ -22670,6 +22678,8 @@ var __webpack_exports__ = {};
22670
22678
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
22671
22679
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket opened");
22672
22680
  this.reconnectAttempts = 0;
22681
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
22682
+ this.startHeartbeat();
22673
22683
  const subscribeMsg = {
22674
22684
  cmd: "WATCH_APP",
22675
22685
  app: this.appId,
@@ -22680,6 +22690,7 @@ var __webpack_exports__ = {};
22680
22690
  this.emit("connected", void 0);
22681
22691
  });
22682
22692
  this.ws.on("message", (data)=>{
22693
+ this.markSocketResponsive();
22683
22694
  try {
22684
22695
  const msg = JSON.parse(data.toString());
22685
22696
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
@@ -22688,8 +22699,12 @@ var __webpack_exports__ = {};
22688
22699
  logger_logger.error(chalk_source.red(`Failed to parse WebSocket message: ${error.message}`));
22689
22700
  }
22690
22701
  });
22702
+ this.ws.on("pong", ()=>{
22703
+ this.markSocketResponsive();
22704
+ });
22691
22705
  this.ws.on("close", (code, reason)=>{
22692
22706
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Closed: code=${code}, reason=${reason.toString()}`);
22707
+ this.stopHeartbeat();
22693
22708
  this.ws = null;
22694
22709
  if (1008 === code || reason.toString().includes("unauthenticated")) {
22695
22710
  logger_logger.warn(chalk_source.yellow(" WebSocket authentication failed - changes from the Anvil Editor won't be detected"));
@@ -22697,21 +22712,12 @@ var __webpack_exports__ = {};
22697
22712
  this.emit("auth-failed", void 0);
22698
22713
  return;
22699
22714
  }
22715
+ if (this.isClosing) return;
22700
22716
  this.emit("disconnected", {
22701
22717
  code,
22702
22718
  reason: reason.toString()
22703
22719
  });
22704
- if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
22705
- this.reconnectAttempts++;
22706
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${this.RECONNECT_DELAY / 1000}s (attempt ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`));
22707
- this.reconnectTimer = setTimeout(()=>{
22708
- this.connect().catch((error)=>{
22709
- this.emit("error", {
22710
- error: error instanceof Error ? error : new Error(String(error))
22711
- });
22712
- });
22713
- }, this.RECONNECT_DELAY);
22714
- } else logger_logger.warn(chalk_source.yellow(` WebSocket reconnection failed after ${this.MAX_RECONNECT_ATTEMPTS} attempts - changes from the Anvil Editor won't be detected`));
22720
+ this.scheduleReconnect();
22715
22721
  });
22716
22722
  this.ws.on("error", (error)=>{
22717
22723
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
@@ -22756,24 +22762,74 @@ var __webpack_exports__ = {};
22756
22762
  return this.editSession;
22757
22763
  }
22758
22764
  close() {
22765
+ this.isClosing = true;
22759
22766
  if (this.reconnectTimer) {
22760
22767
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Clearing reconnect timer");
22761
22768
  clearTimeout(this.reconnectTimer);
22762
22769
  this.reconnectTimer = null;
22763
22770
  }
22764
- if (this.ws) {
22771
+ this.teardownSocket();
22772
+ }
22773
+ isConnected() {
22774
+ return this.ws?.readyState === ws_wrapper.OPEN;
22775
+ }
22776
+ scheduleReconnect() {
22777
+ if (this.reconnectTimer || this.isClosing) return;
22778
+ this.reconnectAttempts++;
22779
+ const delayMs = this.reconnectDelayMs;
22780
+ logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.reconnectAttempts})...`));
22781
+ this.reconnectTimer = setTimeout(()=>{
22782
+ this.reconnectTimer = null;
22783
+ this.connect().catch((error)=>{
22784
+ this.emit("error", {
22785
+ error: error instanceof Error ? error : new Error(String(error))
22786
+ });
22787
+ });
22788
+ }, delayMs);
22789
+ this.reconnectDelayMs = Math.min(Math.round(1.5 * this.reconnectDelayMs), this.RECONNECT_DELAY_MAX_MS);
22790
+ }
22791
+ startHeartbeat() {
22792
+ this.stopHeartbeat();
22793
+ this.heartbeatTimer = setInterval(()=>{
22794
+ const ws = this.ws;
22795
+ if (!ws || ws.readyState !== ws_wrapper.OPEN) return;
22765
22796
  try {
22766
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
22767
- this.ws.removeAllListeners();
22768
- if (this.ws.readyState === ws_wrapper.OPEN) this.ws.close();
22769
- } catch (e) {
22770
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
22797
+ ws.ping();
22798
+ } catch (error) {
22799
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Failed to send ping: ${error.message}`);
22800
+ ws.terminate();
22801
+ return;
22771
22802
  }
22772
- this.ws = null;
22803
+ this.pongTimeoutTimer = setTimeout(()=>{
22804
+ logger_logger.warn(chalk_source.yellow(" WebSocket heartbeat timed out - reconnecting"));
22805
+ ws.terminate();
22806
+ }, this.HEARTBEAT_TIMEOUT_MS);
22807
+ }, this.HEARTBEAT_INTERVAL_MS);
22808
+ }
22809
+ markSocketResponsive() {
22810
+ if (this.pongTimeoutTimer) {
22811
+ clearTimeout(this.pongTimeoutTimer);
22812
+ this.pongTimeoutTimer = null;
22773
22813
  }
22774
22814
  }
22775
- isConnected() {
22776
- return this.ws?.readyState === ws_wrapper.OPEN;
22815
+ stopHeartbeat() {
22816
+ if (this.heartbeatTimer) {
22817
+ clearInterval(this.heartbeatTimer);
22818
+ this.heartbeatTimer = null;
22819
+ }
22820
+ this.markSocketResponsive();
22821
+ }
22822
+ teardownSocket() {
22823
+ this.stopHeartbeat();
22824
+ if (!this.ws) return;
22825
+ try {
22826
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
22827
+ this.ws.removeAllListeners();
22828
+ if (this.ws.readyState === ws_wrapper.OPEN || this.ws.readyState === ws_wrapper.CONNECTING) this.ws.terminate();
22829
+ } catch (e) {
22830
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
22831
+ }
22832
+ this.ws = null;
22777
22833
  }
22778
22834
  }
22779
22835
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
@@ -25000,7 +25056,11 @@ var __webpack_exports__ = {};
25000
25056
  wsClient = null;
25001
25057
  saveProcessor = null;
25002
25058
  syncManager = null;
25059
+ localChangePollTimer = null;
25060
+ isPollingLocalChanges = false;
25061
+ lastLocalStatusFingerprint = null;
25003
25062
  BRANCH_CHANGE_SETTLE_MS = 2000;
25063
+ LOCAL_CHANGE_POLL_MS = 2000;
25004
25064
  stagedOnly;
25005
25065
  constructor(repoPath, appId, options){
25006
25066
  super();
@@ -25138,6 +25198,10 @@ var __webpack_exports__ = {};
25138
25198
  this.fileWatcher.cleanup();
25139
25199
  this.fileWatcher = null;
25140
25200
  }
25201
+ if (this.localChangePollTimer) {
25202
+ clearInterval(this.localChangePollTimer);
25203
+ this.localChangePollTimer = null;
25204
+ }
25141
25205
  logger_logger.debug(`[Session ${this.sessionId}]`, "Cleanup complete");
25142
25206
  }
25143
25207
  async startWatching() {
@@ -25171,8 +25235,70 @@ var __webpack_exports__ = {};
25171
25235
  }
25172
25236
  });
25173
25237
  await this.fileWatcher.start(this.currentBranch);
25238
+ await this.initializeLocalChangeFingerprint();
25239
+ this.startLocalChangeFallbackPolling();
25174
25240
  await new Promise(()=>{});
25175
25241
  }
25242
+ async initializeLocalChangeFingerprint() {
25243
+ if (this.stagedOnly) return;
25244
+ try {
25245
+ const status = await this.gitService.getStatus();
25246
+ this.lastLocalStatusFingerprint = this.getLocalStatusFingerprint(status);
25247
+ } catch (error) {
25248
+ logger_logger.debug(`[Session ${this.sessionId}]`, `Failed to initialize local change fingerprint: ${error.message}`);
25249
+ }
25250
+ }
25251
+ startLocalChangeFallbackPolling() {
25252
+ if (this.stagedOnly || this.localChangePollTimer) return;
25253
+ this.localChangePollTimer = setInterval(()=>{
25254
+ this.pollForMissedLocalChanges();
25255
+ }, this.LOCAL_CHANGE_POLL_MS);
25256
+ }
25257
+ async pollForMissedLocalChanges() {
25258
+ if (this.isCleanedUp || this.isPausedForUserInput || this.isPollingLocalChanges) return;
25259
+ this.isPollingLocalChanges = true;
25260
+ try {
25261
+ const status = await this.gitService.getStatus();
25262
+ const fingerprint = this.getLocalStatusFingerprint(status);
25263
+ if (null === this.lastLocalStatusFingerprint) {
25264
+ this.lastLocalStatusFingerprint = fingerprint;
25265
+ return;
25266
+ }
25267
+ if (fingerprint !== this.lastLocalStatusFingerprint) {
25268
+ this.lastLocalStatusFingerprint = fingerprint;
25269
+ if (fingerprint) {
25270
+ logger_logger.debug(`[Session ${this.sessionId}]`, "Detected local change via fallback status poll");
25271
+ this.saveProcessor?.queueSave();
25272
+ }
25273
+ }
25274
+ } catch (error) {
25275
+ logger_logger.debug(`[Session ${this.sessionId}]`, `Fallback status poll failed: ${error.message}`);
25276
+ } finally{
25277
+ this.isPollingLocalChanges = false;
25278
+ }
25279
+ }
25280
+ getLocalStatusFingerprint(status) {
25281
+ const renamed = status.renamed.map((r)=>`${r.from}->${r.to}`).sort();
25282
+ const modified = [
25283
+ ...status.modified
25284
+ ].sort();
25285
+ const notAdded = [
25286
+ ...status.notAdded
25287
+ ].sort();
25288
+ const created = [
25289
+ ...status.created
25290
+ ].sort();
25291
+ const deleted = [
25292
+ ...status.deleted
25293
+ ].sort();
25294
+ return [
25295
+ ...renamed.map((r)=>`R:${r}`),
25296
+ ...modified.map((m)=>`M:${m}`),
25297
+ ...notAdded.map((n)=>`N:${n}`),
25298
+ ...created.map((c)=>`C:${c}`),
25299
+ ...deleted.map((d)=>`D:${d}`)
25300
+ ].join("|");
25301
+ }
25176
25302
  async handleFileChange(event, filePath, relativePath) {
25177
25303
  if (this.isCleanedUp || this.isPausedForUserInput) return;
25178
25304
  logger_logger.verbose(chalk_source.blue("File changed: ") + chalk_source.bold(relativePath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",