@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.
- package/dist/cli.js +149 -23
- package/dist/index.js +149 -23
- 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
|
-
|
|
47511
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47630
|
-
|
|
47631
|
-
|
|
47632
|
-
|
|
47633
|
-
|
|
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.
|
|
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
|
-
|
|
47639
|
-
|
|
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
|
-
|
|
22648
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
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.
|
|
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
|
-
|
|
22776
|
-
|
|
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));
|