@anvil-works/anvil-cli 0.4.1 → 0.4.3
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/README.md +35 -30
- package/dist/cli.js +275 -63
- package/dist/index.js +149 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,9 +85,7 @@ Or use the short form:
|
|
|
85
85
|
anvil w
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
You can specify the app ID explicitly:
|
|
88
|
+
You can specify the app ID explicitly:
|
|
91
89
|
|
|
92
90
|
```bash
|
|
93
91
|
anvil watch -A YOUR_APP_ID
|
|
@@ -126,7 +124,6 @@ The diagram shows bidirectional sync:
|
|
|
126
124
|
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
127
125
|
| `anvil watch [path]` | Watch directory for changes and sync to Anvil |
|
|
128
126
|
| `anvil watch -V` or `--verbose` | Watch with verbose logging (detailed output) |
|
|
129
|
-
| `anvil sync [path]` | Deprecated alias for `watch` |
|
|
130
127
|
| `anvil w [path]` | Short form for watch |
|
|
131
128
|
| `anvil login [anvil-server-url]` | Authenticate with Anvil using OAuth (supports smart URL handling) |
|
|
132
129
|
| `anvil l [anvil-server-url]` | Short form for login |
|
|
@@ -153,13 +150,15 @@ anvil watch -A YOUR_APP_ID
|
|
|
153
150
|
|
|
154
151
|
### Anvil URL Detection
|
|
155
152
|
|
|
156
|
-
anvil-cli automatically detects which Anvil server to use:
|
|
157
|
-
|
|
158
|
-
1. **Explicit URL**: If you specify `--url` or `-u` flag
|
|
159
|
-
2. **Git Remotes**: Automatically extracts URL from git remotes in your repository
|
|
160
|
-
3. **
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
anvil-cli automatically detects which Anvil server to use:
|
|
154
|
+
|
|
155
|
+
1. **Explicit URL**: If you specify `--url` or `-u` flag
|
|
156
|
+
2. **Git Remotes**: Automatically extracts URL from git remotes in your repository
|
|
157
|
+
3. **Logged-in URLs**:
|
|
158
|
+
- If one logged-in URL is available, uses it automatically
|
|
159
|
+
- If multiple logged-in URLs are available, prompts you to select one
|
|
160
|
+
4. **Global Config**: Falls back to `anvilUrl` in config file
|
|
161
|
+
5. **Default**: Uses `https://anvil.works` (or `http://localhost:3000` in dev mode)
|
|
163
162
|
|
|
164
163
|
**Examples:**
|
|
165
164
|
|
|
@@ -172,12 +171,12 @@ anvil watch --url anvil.works
|
|
|
172
171
|
anvil watch --url localhost:3000
|
|
173
172
|
anvil watch --url anvil.mycompany.com
|
|
174
173
|
|
|
175
|
-
# If multiple
|
|
176
|
-
anvil watch
|
|
177
|
-
# ? Multiple Anvil installations found. Which one would you like to use?
|
|
178
|
-
# ❯ https://anvil.works
|
|
179
|
-
# https://anvil.company.com
|
|
180
|
-
# Cancel
|
|
174
|
+
# If multiple logged-in URLs, you'll be prompted to select
|
|
175
|
+
anvil watch
|
|
176
|
+
# ? Multiple Anvil installations found. Which one would you like to use?
|
|
177
|
+
# ❯ https://anvil.works
|
|
178
|
+
# https://anvil.company.com
|
|
179
|
+
# Cancel
|
|
181
180
|
```
|
|
182
181
|
|
|
183
182
|
## Configuration
|
|
@@ -269,18 +268,21 @@ anvil login anvil.works
|
|
|
269
268
|
anvil login anvil.company-a.com
|
|
270
269
|
anvil login anvil.company-b.com
|
|
271
270
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
# If URL is not resolved from git remotes and multiple logged-in URLs exist,
|
|
272
|
+
# you'll be prompted to select which one to use
|
|
273
|
+
# Or specify explicitly:
|
|
274
|
+
anvil watch --url anvil.company-a.com
|
|
275
|
+
```
|
|
276
276
|
|
|
277
277
|
**URL Resolution Priority:**
|
|
278
278
|
|
|
279
|
-
1. Explicit `--url` flag
|
|
280
|
-
2. Git remote detection (from current repository)
|
|
281
|
-
3.
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
1. Explicit `--url` flag
|
|
280
|
+
2. Git remote detection (from current repository)
|
|
281
|
+
3. Logged-in URL selection:
|
|
282
|
+
- One URL: auto-select
|
|
283
|
+
- Multiple URLs: prompt to select
|
|
284
|
+
4. `anvilUrl` in config file
|
|
285
|
+
5. Default based on `NODE_ENV`:
|
|
284
286
|
- Production: `https://anvil.works`
|
|
285
287
|
- Development: `http://localhost:3000`
|
|
286
288
|
|
|
@@ -308,10 +310,13 @@ anvil logout anvil.mycompany.com
|
|
|
308
310
|
anvil logout --url localhost:3000
|
|
309
311
|
```
|
|
310
312
|
|
|
311
|
-
**
|
|
312
|
-
|
|
313
|
-
-
|
|
314
|
-
-
|
|
313
|
+
**Without URL:** `anvil logout` uses total logged-in account count across all URLs:
|
|
314
|
+
|
|
315
|
+
- If you have **one account total**, it logs out immediately
|
|
316
|
+
- If you have **multiple accounts total**, you'll be prompted to:
|
|
317
|
+
|
|
318
|
+
- Logout from one account (then select which one)
|
|
319
|
+
- Logout from all accounts
|
|
315
320
|
- Cancel
|
|
316
321
|
|
|
317
322
|
## Troubleshooting
|
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));
|
|
@@ -49830,13 +49956,84 @@ var __webpack_exports__ = {};
|
|
|
49830
49956
|
session.hasUncommittedChanges = hasUncommittedChanges;
|
|
49831
49957
|
return session;
|
|
49832
49958
|
}
|
|
49833
|
-
function
|
|
49834
|
-
if (explicitUrl) return
|
|
49835
|
-
|
|
49836
|
-
|
|
49959
|
+
function decideFallbackUrl(explicitUrl, availableUrls = getAvailableAnvilUrls()) {
|
|
49960
|
+
if (explicitUrl) return {
|
|
49961
|
+
source: "explicit",
|
|
49962
|
+
url: normalizeAnvilUrl(explicitUrl)
|
|
49963
|
+
};
|
|
49964
|
+
if (availableUrls.length > 1) return {
|
|
49965
|
+
source: "available-multiple",
|
|
49966
|
+
urls: availableUrls
|
|
49967
|
+
};
|
|
49968
|
+
if (1 === availableUrls.length) return {
|
|
49969
|
+
source: "available-single",
|
|
49970
|
+
url: availableUrls[0]
|
|
49971
|
+
};
|
|
49837
49972
|
const fromConfig = getConfig("anvilUrl");
|
|
49838
|
-
if ("string" == typeof fromConfig && fromConfig.trim()) return
|
|
49839
|
-
|
|
49973
|
+
if ("string" == typeof fromConfig && fromConfig.trim()) return {
|
|
49974
|
+
source: "config",
|
|
49975
|
+
url: normalizeAnvilUrl(fromConfig.trim())
|
|
49976
|
+
};
|
|
49977
|
+
return {
|
|
49978
|
+
source: "default",
|
|
49979
|
+
url: isDevMode() ? "http://localhost:3000" : "https://anvil.works"
|
|
49980
|
+
};
|
|
49981
|
+
}
|
|
49982
|
+
function decideUsernameForUrl(accounts) {
|
|
49983
|
+
if (0 === accounts.length) return {
|
|
49984
|
+
source: "none"
|
|
49985
|
+
};
|
|
49986
|
+
if (1 === accounts.length) return {
|
|
49987
|
+
source: "single",
|
|
49988
|
+
username: accounts[0]
|
|
49989
|
+
};
|
|
49990
|
+
return {
|
|
49991
|
+
source: "multiple",
|
|
49992
|
+
usernames: [
|
|
49993
|
+
...accounts
|
|
49994
|
+
]
|
|
49995
|
+
};
|
|
49996
|
+
}
|
|
49997
|
+
async function resolveUsernameForUrl(anvilUrl, explicitUsername, promptMessage = "Multiple accounts found. Which account owns this app?") {
|
|
49998
|
+
if (explicitUsername) return explicitUsername;
|
|
49999
|
+
const decision = decideUsernameForUrl(auth_getAccountsForUrl(anvilUrl));
|
|
50000
|
+
if ("none" === decision.source) return;
|
|
50001
|
+
if ("single" === decision.source) {
|
|
50002
|
+
logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(decision.username));
|
|
50003
|
+
return decision.username;
|
|
50004
|
+
}
|
|
50005
|
+
const choices = decision.usernames.map((acct)=>({
|
|
50006
|
+
name: acct,
|
|
50007
|
+
value: acct
|
|
50008
|
+
}));
|
|
50009
|
+
choices.push({
|
|
50010
|
+
name: "Cancel",
|
|
50011
|
+
value: null
|
|
50012
|
+
});
|
|
50013
|
+
return logger_logger.select(promptMessage, choices, decision.usernames[0]);
|
|
50014
|
+
}
|
|
50015
|
+
async function confirmReverseLookupWithResolvedUser(anvilUrl, username) {
|
|
50016
|
+
const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username, `Multiple accounts found for ${anvilUrl}. Which account should be used for app lookup?`);
|
|
50017
|
+
if (null === resolvedUsername) return null;
|
|
50018
|
+
const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} ${resolvedUsername ? `for ${resolvedUsername}` : ""} for matching app IDs? (slower)`, true);
|
|
50019
|
+
return {
|
|
50020
|
+
username: resolvedUsername,
|
|
50021
|
+
shouldContinue
|
|
50022
|
+
};
|
|
50023
|
+
}
|
|
50024
|
+
async function resolveUrlForFallback(explicitUrl) {
|
|
50025
|
+
const decision = decideFallbackUrl(explicitUrl);
|
|
50026
|
+
if ("available-multiple" !== decision.source) return decision.url;
|
|
50027
|
+
const choices = decision.urls.map((url)=>({
|
|
50028
|
+
name: url,
|
|
50029
|
+
value: url
|
|
50030
|
+
}));
|
|
50031
|
+
choices.push({
|
|
50032
|
+
name: "Cancel",
|
|
50033
|
+
value: null
|
|
50034
|
+
});
|
|
50035
|
+
const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
|
|
50036
|
+
return selectedUrl;
|
|
49840
50037
|
}
|
|
49841
50038
|
async function selectAppId(candidates) {
|
|
49842
50039
|
if (0 === candidates.length) {
|
|
@@ -50348,6 +50545,7 @@ var __webpack_exports__ = {};
|
|
|
50348
50545
|
let finalAppId;
|
|
50349
50546
|
let anvilUrl;
|
|
50350
50547
|
let username = explicitUsername;
|
|
50548
|
+
let fallbackUrl;
|
|
50351
50549
|
if (explicitAppId) {
|
|
50352
50550
|
finalAppId = explicitAppId;
|
|
50353
50551
|
const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
|
|
@@ -50363,13 +50561,23 @@ var __webpack_exports__ = {};
|
|
|
50363
50561
|
logger_logger.verbose(chalk_source.cyan("No app ID provided, attempting auto-detection..."));
|
|
50364
50562
|
if (0 === filteredCandidates.length) {
|
|
50365
50563
|
logger_logger.verbose(chalk_source.gray("No app IDs found in git remotes."));
|
|
50366
|
-
const
|
|
50367
|
-
|
|
50368
|
-
|
|
50369
|
-
|
|
50564
|
+
const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
|
|
50565
|
+
if (null === resolvedFallbackUrl) {
|
|
50566
|
+
logger_logger.warn("Operation cancelled.");
|
|
50567
|
+
process.exit(0);
|
|
50568
|
+
}
|
|
50569
|
+
fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
|
|
50570
|
+
const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
|
|
50571
|
+
if (null === lookupDecision) {
|
|
50572
|
+
logger_logger.warn("Operation cancelled.");
|
|
50573
|
+
process.exit(0);
|
|
50574
|
+
}
|
|
50575
|
+
username = lookupDecision.username;
|
|
50576
|
+
if (lookupDecision.shouldContinue) {
|
|
50577
|
+
logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
|
|
50370
50578
|
const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
|
|
50371
50579
|
anvilUrl: fallbackUrl,
|
|
50372
|
-
username
|
|
50580
|
+
username,
|
|
50373
50581
|
includeRemotes: false
|
|
50374
50582
|
});
|
|
50375
50583
|
logger_logger.progressEnd("detect");
|
|
@@ -50401,32 +50609,24 @@ var __webpack_exports__ = {};
|
|
|
50401
50609
|
}
|
|
50402
50610
|
}
|
|
50403
50611
|
if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
|
|
50404
|
-
else if (!anvilUrl) anvilUrl =
|
|
50612
|
+
else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
|
|
50613
|
+
else {
|
|
50614
|
+
const resolvedFallbackUrl = await resolveUrlForFallback();
|
|
50615
|
+
if (null === resolvedFallbackUrl) {
|
|
50616
|
+
logger_logger.warn("Operation cancelled.");
|
|
50617
|
+
process.exit(0);
|
|
50618
|
+
}
|
|
50619
|
+
anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
|
|
50620
|
+
}
|
|
50405
50621
|
anvilUrl = normalizeAnvilUrl(anvilUrl);
|
|
50406
50622
|
logger_logger.verbose(chalk_source.green("Using app ID: ") + chalk_source.bold(finalAppId));
|
|
50407
50623
|
logger_logger.verbose(chalk_source.cyan("Using Anvil URL: ") + chalk_source.bold(anvilUrl));
|
|
50408
|
-
|
|
50409
|
-
|
|
50410
|
-
|
|
50411
|
-
|
|
50412
|
-
logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(username));
|
|
50413
|
-
} else if (accounts.length > 1) {
|
|
50414
|
-
const choices = accounts.map((acct)=>({
|
|
50415
|
-
name: acct,
|
|
50416
|
-
value: acct
|
|
50417
|
-
}));
|
|
50418
|
-
choices.push({
|
|
50419
|
-
name: "Cancel",
|
|
50420
|
-
value: null
|
|
50421
|
-
});
|
|
50422
|
-
const selected = await logger_logger.select("Multiple accounts found. Which account owns this app?", choices, accounts[0]);
|
|
50423
|
-
if (null === selected) {
|
|
50424
|
-
logger_logger.warn("Operation cancelled.");
|
|
50425
|
-
process.exit(0);
|
|
50426
|
-
}
|
|
50427
|
-
username = selected;
|
|
50428
|
-
}
|
|
50624
|
+
const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
|
|
50625
|
+
if (null === resolvedUsername) {
|
|
50626
|
+
logger_logger.warn("Operation cancelled.");
|
|
50627
|
+
process.exit(0);
|
|
50429
50628
|
}
|
|
50629
|
+
username = resolvedUsername;
|
|
50430
50630
|
if (username) logger_logger.verbose(chalk_source.cyan("Using account: ") + chalk_source.bold(username));
|
|
50431
50631
|
if (!hasTokensForUrl(anvilUrl, username)) {
|
|
50432
50632
|
if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
|
|
@@ -51233,6 +51433,13 @@ var __webpack_exports__ = {};
|
|
|
51233
51433
|
});
|
|
51234
51434
|
loginCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil login Log in to default Anvil server\n anvil login anvil.works Log in to anvil.works\n anvil login localhost Log in to local Anvil server\n");
|
|
51235
51435
|
}
|
|
51436
|
+
function getTotalLoggedInAccounts(urls) {
|
|
51437
|
+
return urls.reduce((total, url)=>total + auth_getAccountsForUrl(url).length, 0);
|
|
51438
|
+
}
|
|
51439
|
+
function formatMultiAccountLogoutPrompt(totalAccounts, urlCount) {
|
|
51440
|
+
if (urlCount <= 1) return `You have ${totalAccounts} logged-in accounts. What would you like to do?`;
|
|
51441
|
+
return `You have ${totalAccounts} logged-in accounts across ${urlCount} URLs. What would you like to do?`;
|
|
51442
|
+
}
|
|
51236
51443
|
function registerLogoutCommand(program) {
|
|
51237
51444
|
const logoutCommand = program.command("logout [anvil-server-url]").description("Log out from Anvil (optionally specify URL to logout from specific installation)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL to logout from").option("-U, --user <USERNAME>", "Specify which user account to logout from").alias("lo").action(async (anvilUrl, options)=>{
|
|
51238
51445
|
try {
|
|
@@ -51246,8 +51453,9 @@ var __webpack_exports__ = {};
|
|
|
51246
51453
|
else logger_logger.info(result.message || `Not logged in to ${normalized}. No action needed.`);
|
|
51247
51454
|
} else {
|
|
51248
51455
|
const availableUrls = getAvailableAnvilUrls();
|
|
51249
|
-
|
|
51250
|
-
|
|
51456
|
+
const totalAccounts = getTotalLoggedInAccounts(availableUrls);
|
|
51457
|
+
if (0 === totalAccounts) logger_logger.warn("No logged-in accounts found.");
|
|
51458
|
+
else if (1 === totalAccounts) {
|
|
51251
51459
|
const result = await logout();
|
|
51252
51460
|
if (result.loggedOut) logger_logger.success("Logged out.");
|
|
51253
51461
|
} else {
|
|
@@ -51265,7 +51473,7 @@ var __webpack_exports__ = {};
|
|
|
51265
51473
|
value: "cancel"
|
|
51266
51474
|
}
|
|
51267
51475
|
];
|
|
51268
|
-
const action = await logger_logger.select(
|
|
51476
|
+
const action = await logger_logger.select(formatMultiAccountLogoutPrompt(totalAccounts, availableUrls.length), choices, "one");
|
|
51269
51477
|
if ("cancel" === action) return void logger_logger.info("Logout cancelled.");
|
|
51270
51478
|
if ("all" === action) {
|
|
51271
51479
|
const result = await logout();
|
|
@@ -51364,17 +51572,21 @@ var __webpack_exports__ = {};
|
|
|
51364
51572
|
let totalAccounts = 0;
|
|
51365
51573
|
for (const url of urls){
|
|
51366
51574
|
const urlData = authTokens[url];
|
|
51367
|
-
if (urlData && "object" == typeof urlData && !Array.isArray(urlData))
|
|
51575
|
+
if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
|
|
51576
|
+
const usernames = Object.keys(urlData);
|
|
51577
|
+
totalAccounts += usernames.filter((username)=>hasTokensForUrl(url, username)).length;
|
|
51578
|
+
}
|
|
51368
51579
|
}
|
|
51369
|
-
console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} account(s) across ${urls.length} URL(s)`)}`);
|
|
51580
|
+
console.log(chalk_source.cyan("authTokens") + ` = ${chalk_source.gray(`${totalAccounts} logged-in account(s) across ${urls.length} URL(s)`)}`);
|
|
51370
51581
|
for (const url of urls){
|
|
51371
51582
|
const urlData = authTokens[url];
|
|
51372
51583
|
if (urlData && "object" == typeof urlData && !Array.isArray(urlData)) {
|
|
51373
51584
|
const usernames = Object.keys(urlData).sort();
|
|
51374
51585
|
if (usernames.length > 0) for (const username of usernames){
|
|
51375
51586
|
const accountData = urlData[username];
|
|
51376
|
-
const
|
|
51377
|
-
const
|
|
51587
|
+
const hasConfigTokens = !!(accountData?.authToken || accountData?.refreshToken);
|
|
51588
|
+
const hasAnyTokens = hasTokensForUrl(url, username);
|
|
51589
|
+
const status = hasConfigTokens ? chalk_source.green("✓ logged in (config)") : hasAnyTokens ? chalk_source.green("✓ logged in (keychain)") : chalk_source.gray("(no tokens)");
|
|
51378
51590
|
console.log(chalk_source.gray(` ${url}: ${username} ${status}`));
|
|
51379
51591
|
}
|
|
51380
51592
|
}
|
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));
|