@anvil-works/anvil-cli 0.5.6 → 0.5.7

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 CHANGED
@@ -47629,12 +47629,14 @@ var __webpack_exports__ = {};
47629
47629
  editSession;
47630
47630
  username;
47631
47631
  reconnectAttempts = 0;
47632
+ connectAttempts = 0;
47632
47633
  reconnectTimer = null;
47633
47634
  heartbeatTimer = null;
47634
47635
  pongTimeoutTimer = null;
47635
47636
  reconnectDelayMs;
47636
47637
  isClosing = false;
47637
47638
  sessionId;
47639
+ lastDisconnectSummary = null;
47638
47640
  RECONNECT_DELAY_BASE_MS = 5000;
47639
47641
  RECONNECT_DELAY_MAX_MS = 60000;
47640
47642
  HEARTBEAT_INTERVAL_MS = 30000;
@@ -47652,6 +47654,7 @@ var __webpack_exports__ = {};
47652
47654
  }
47653
47655
  async connect() {
47654
47656
  this.isClosing = false;
47657
+ this.connectAttempts++;
47655
47658
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
47656
47659
  if (this.ws) {
47657
47660
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
@@ -47660,6 +47663,7 @@ var __webpack_exports__ = {};
47660
47663
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
47661
47664
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
47662
47665
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Connecting to: ${wsUrl.replace(this.authToken, "[TOKEN]")}`);
47666
+ logger_logger.verbose(chalk_source.gray(`WebSocket connecting to ${this.getConnectionTarget(wsUrl)} (app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}, connect attempt ${this.connectAttempts})`));
47663
47667
  this.ws = new ws_wrapper(wsUrl);
47664
47668
  this.ws.on("open", ()=>{
47665
47669
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
@@ -47676,6 +47680,21 @@ var __webpack_exports__ = {};
47676
47680
  this.ws?.send(JSON.stringify(subscribeMsg));
47677
47681
  this.emit("connected", void 0);
47678
47682
  });
47683
+ this.ws.on("unexpected-response", (request, response)=>{
47684
+ const summary = this.summarizeUnexpectedResponse(response, wsUrl);
47685
+ this.lastDisconnectSummary = summary;
47686
+ logger_logger.error(chalk_source.red(summary));
47687
+ response.resume();
47688
+ request.destroy();
47689
+ if (this.isClosing) return;
47690
+ this.stopHeartbeat();
47691
+ this.ws = null;
47692
+ this.emit("disconnected", {
47693
+ code: 1006,
47694
+ reason: summary
47695
+ });
47696
+ this.scheduleReconnect();
47697
+ });
47679
47698
  this.ws.on("message", (data)=>{
47680
47699
  this.markSocketResponsive();
47681
47700
  try {
@@ -47700,6 +47719,7 @@ var __webpack_exports__ = {};
47700
47719
  return;
47701
47720
  }
47702
47721
  if (this.isClosing) return;
47722
+ this.lastDisconnectSummary = this.lastDisconnectSummary ?? this.formatCloseSummary(code, reason.toString());
47703
47723
  this.emit("disconnected", {
47704
47724
  code,
47705
47725
  reason: reason.toString()
@@ -47709,8 +47729,13 @@ var __webpack_exports__ = {};
47709
47729
  this.ws.on("error", (error)=>{
47710
47730
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
47711
47731
  if (this.isClosing) return;
47712
- if (error.message.includes("502")) logger_logger.error(chalk_source.red("WebSocket connection failed (502 Bad Gateway) - likely a temporary server issue"));
47713
- else logger_logger.error(chalk_source.red(`WebSocket error: ${error.message}`));
47732
+ if (error.message.includes("Unexpected server response")) return;
47733
+ if (error.message.includes("502")) logger_logger.error(chalk_source.red(`WebSocket connection failed (502 Bad Gateway) - the server could not open the live update connection (${this.getConnectionContext()})`));
47734
+ else {
47735
+ const details = this.getConnectionContext();
47736
+ logger_logger.error(chalk_source.red(`WebSocket error: ${error.message} (${details})`));
47737
+ }
47738
+ this.lastDisconnectSummary = `WebSocket error: ${error.message}`;
47714
47739
  this.emit("error", {
47715
47740
  error
47716
47741
  });
@@ -47765,7 +47790,7 @@ var __webpack_exports__ = {};
47765
47790
  if (this.reconnectTimer || this.isClosing) return;
47766
47791
  this.reconnectAttempts++;
47767
47792
  const delayMs = this.reconnectDelayMs;
47768
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.reconnectAttempts})...`));
47793
+ logger_logger.verbose(chalk_source.gray(`WebSocket disconnected (${this.lastDisconnectSummary ?? "unknown cause"}), reconnecting in ${Math.round(delayMs / 1000)}s (${this.getConnectionContext()}, next connect attempt ${this.connectAttempts + 1})...`));
47769
47794
  this.reconnectTimer = setTimeout(()=>{
47770
47795
  this.reconnectTimer = null;
47771
47796
  this.connect().catch((error)=>{
@@ -47774,8 +47799,47 @@ var __webpack_exports__ = {};
47774
47799
  });
47775
47800
  });
47776
47801
  }, delayMs);
47802
+ this.lastDisconnectSummary = null;
47777
47803
  this.reconnectDelayMs = Math.min(Math.round(1.5 * this.reconnectDelayMs), this.RECONNECT_DELAY_MAX_MS);
47778
47804
  }
47805
+ getConnectionContext() {
47806
+ return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
47807
+ }
47808
+ getConnectionTarget(wsUrl) {
47809
+ const url = new URL(wsUrl);
47810
+ return `${url.origin}${url.pathname}`;
47811
+ }
47812
+ formatCloseSummary(code, reason) {
47813
+ const suffix = reason ? `, reason=${reason}` : "";
47814
+ return `close code=${code}${suffix}`;
47815
+ }
47816
+ summarizeUnexpectedResponse(response, wsUrl) {
47817
+ const statusCode = response.statusCode ?? "unknown";
47818
+ const statusMessage = response.statusMessage ?? "Unknown";
47819
+ const details = [
47820
+ `target ${this.getConnectionTarget(wsUrl)}`,
47821
+ this.getConnectionContext(),
47822
+ this.getHandshakeMetadata(response)
47823
+ ].filter(Boolean).join(", ");
47824
+ return `WebSocket handshake failed (${statusCode} ${statusMessage}; ${details})`;
47825
+ }
47826
+ getHandshakeMetadata(response) {
47827
+ const headers = response.headers;
47828
+ const detailParts = [];
47829
+ const requestId = this.getHeaderValue(headers["x-request-id"]) || this.getHeaderValue(headers["x-amzn-requestid"]) || this.getHeaderValue(headers["cf-ray"]);
47830
+ const retryAfter = this.getHeaderValue(headers["retry-after"]);
47831
+ const server = this.getHeaderValue(headers["server"]);
47832
+ const via = this.getHeaderValue(headers["via"]);
47833
+ if (requestId) detailParts.push(`request-id ${requestId}`);
47834
+ if (retryAfter) detailParts.push(`retry-after ${retryAfter}`);
47835
+ if (server) detailParts.push(`server ${server}`);
47836
+ if (via) detailParts.push(`via ${via}`);
47837
+ return detailParts.join(", ");
47838
+ }
47839
+ getHeaderValue(header) {
47840
+ if (Array.isArray(header)) return header[0] ?? null;
47841
+ return header ?? null;
47842
+ }
47779
47843
  startHeartbeat() {
47780
47844
  this.stopHeartbeat();
47781
47845
  this.heartbeatTimer = setInterval(()=>{
@@ -47917,9 +47981,9 @@ var __webpack_exports__ = {};
47917
47981
  result.localOnlyChanges.forEach((c)=>logger_logger.verbose(chalk_source.gray(` ${c.path}`)));
47918
47982
  }
47919
47983
  }
47920
- async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles) {
47984
+ async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
47921
47985
  const filesToRemove = [];
47922
- for (const file of unstagedFiles)try {
47986
+ for (const file of unstagedFiles)if (remotelyChangedFiles.has(file)) try {
47923
47987
  await gitService.show(`HEAD:${file}`);
47924
47988
  } catch (e) {
47925
47989
  filesToRemove.push(file);
@@ -49497,21 +49561,24 @@ var __webpack_exports__ = {};
49497
49561
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
49498
49562
  const currentBranch = this.config.getCurrentBranch();
49499
49563
  const tempRef = `anvil-sync-temp-${Date.now()}`;
49564
+ const oldCommitId = this.config.getCommitId();
49500
49565
  await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
49501
49566
  await this.config.gitService.reset(tempRef, "mixed");
49502
49567
  await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
49568
+ const newCommitId = await this.config.gitService.getCommitId();
49503
49569
  await this.config.gitService.checkout([
49504
49570
  ".anvil_editor.yaml",
49505
49571
  "anvil.yaml"
49506
49572
  ]);
49507
49573
  await this.config.editorYaml.reload();
49508
- await this.deleteFilesRemovedOnAnvilLocally();
49574
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
49509
49575
  await this.discardFormattingOnlyYamlChanges();
49510
49576
  }
49511
- async deleteFilesRemovedOnAnvilLocally() {
49577
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
49512
49578
  try {
49513
49579
  const status = await this.config.gitService.getStatus();
49514
- await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded);
49580
+ const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
49581
+ await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles);
49515
49582
  } catch (e) {
49516
49583
  logger_logger.verbose(chalk_source.gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
49517
49584
  }
package/dist/index.js CHANGED
@@ -22643,12 +22643,14 @@ var __webpack_exports__ = {};
22643
22643
  editSession;
22644
22644
  username;
22645
22645
  reconnectAttempts = 0;
22646
+ connectAttempts = 0;
22646
22647
  reconnectTimer = null;
22647
22648
  heartbeatTimer = null;
22648
22649
  pongTimeoutTimer = null;
22649
22650
  reconnectDelayMs;
22650
22651
  isClosing = false;
22651
22652
  sessionId;
22653
+ lastDisconnectSummary = null;
22652
22654
  RECONNECT_DELAY_BASE_MS = 5000;
22653
22655
  RECONNECT_DELAY_MAX_MS = 60000;
22654
22656
  HEARTBEAT_INTERVAL_MS = 30000;
@@ -22666,6 +22668,7 @@ var __webpack_exports__ = {};
22666
22668
  }
22667
22669
  async connect() {
22668
22670
  this.isClosing = false;
22671
+ this.connectAttempts++;
22669
22672
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
22670
22673
  if (this.ws) {
22671
22674
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
@@ -22674,6 +22677,7 @@ var __webpack_exports__ = {};
22674
22677
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
22675
22678
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
22676
22679
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Connecting to: ${wsUrl.replace(this.authToken, "[TOKEN]")}`);
22680
+ logger_logger.verbose(chalk_source.gray(`WebSocket connecting to ${this.getConnectionTarget(wsUrl)} (app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}, connect attempt ${this.connectAttempts})`));
22677
22681
  this.ws = new ws_wrapper(wsUrl);
22678
22682
  this.ws.on("open", ()=>{
22679
22683
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
@@ -22690,6 +22694,21 @@ var __webpack_exports__ = {};
22690
22694
  this.ws?.send(JSON.stringify(subscribeMsg));
22691
22695
  this.emit("connected", void 0);
22692
22696
  });
22697
+ this.ws.on("unexpected-response", (request, response)=>{
22698
+ const summary = this.summarizeUnexpectedResponse(response, wsUrl);
22699
+ this.lastDisconnectSummary = summary;
22700
+ logger_logger.error(chalk_source.red(summary));
22701
+ response.resume();
22702
+ request.destroy();
22703
+ if (this.isClosing) return;
22704
+ this.stopHeartbeat();
22705
+ this.ws = null;
22706
+ this.emit("disconnected", {
22707
+ code: 1006,
22708
+ reason: summary
22709
+ });
22710
+ this.scheduleReconnect();
22711
+ });
22693
22712
  this.ws.on("message", (data)=>{
22694
22713
  this.markSocketResponsive();
22695
22714
  try {
@@ -22714,6 +22733,7 @@ var __webpack_exports__ = {};
22714
22733
  return;
22715
22734
  }
22716
22735
  if (this.isClosing) return;
22736
+ this.lastDisconnectSummary = this.lastDisconnectSummary ?? this.formatCloseSummary(code, reason.toString());
22717
22737
  this.emit("disconnected", {
22718
22738
  code,
22719
22739
  reason: reason.toString()
@@ -22723,8 +22743,13 @@ var __webpack_exports__ = {};
22723
22743
  this.ws.on("error", (error)=>{
22724
22744
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
22725
22745
  if (this.isClosing) return;
22726
- if (error.message.includes("502")) logger_logger.error(chalk_source.red("WebSocket connection failed (502 Bad Gateway) - likely a temporary server issue"));
22727
- else logger_logger.error(chalk_source.red(`WebSocket error: ${error.message}`));
22746
+ if (error.message.includes("Unexpected server response")) return;
22747
+ if (error.message.includes("502")) logger_logger.error(chalk_source.red(`WebSocket connection failed (502 Bad Gateway) - the server could not open the live update connection (${this.getConnectionContext()})`));
22748
+ else {
22749
+ const details = this.getConnectionContext();
22750
+ logger_logger.error(chalk_source.red(`WebSocket error: ${error.message} (${details})`));
22751
+ }
22752
+ this.lastDisconnectSummary = `WebSocket error: ${error.message}`;
22728
22753
  this.emit("error", {
22729
22754
  error
22730
22755
  });
@@ -22779,7 +22804,7 @@ var __webpack_exports__ = {};
22779
22804
  if (this.reconnectTimer || this.isClosing) return;
22780
22805
  this.reconnectAttempts++;
22781
22806
  const delayMs = this.reconnectDelayMs;
22782
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.reconnectAttempts})...`));
22807
+ logger_logger.verbose(chalk_source.gray(`WebSocket disconnected (${this.lastDisconnectSummary ?? "unknown cause"}), reconnecting in ${Math.round(delayMs / 1000)}s (${this.getConnectionContext()}, next connect attempt ${this.connectAttempts + 1})...`));
22783
22808
  this.reconnectTimer = setTimeout(()=>{
22784
22809
  this.reconnectTimer = null;
22785
22810
  this.connect().catch((error)=>{
@@ -22788,8 +22813,47 @@ var __webpack_exports__ = {};
22788
22813
  });
22789
22814
  });
22790
22815
  }, delayMs);
22816
+ this.lastDisconnectSummary = null;
22791
22817
  this.reconnectDelayMs = Math.min(Math.round(1.5 * this.reconnectDelayMs), this.RECONNECT_DELAY_MAX_MS);
22792
22818
  }
22819
+ getConnectionContext() {
22820
+ return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
22821
+ }
22822
+ getConnectionTarget(wsUrl) {
22823
+ const url = new URL(wsUrl);
22824
+ return `${url.origin}${url.pathname}`;
22825
+ }
22826
+ formatCloseSummary(code, reason) {
22827
+ const suffix = reason ? `, reason=${reason}` : "";
22828
+ return `close code=${code}${suffix}`;
22829
+ }
22830
+ summarizeUnexpectedResponse(response, wsUrl) {
22831
+ const statusCode = response.statusCode ?? "unknown";
22832
+ const statusMessage = response.statusMessage ?? "Unknown";
22833
+ const details = [
22834
+ `target ${this.getConnectionTarget(wsUrl)}`,
22835
+ this.getConnectionContext(),
22836
+ this.getHandshakeMetadata(response)
22837
+ ].filter(Boolean).join(", ");
22838
+ return `WebSocket handshake failed (${statusCode} ${statusMessage}; ${details})`;
22839
+ }
22840
+ getHandshakeMetadata(response) {
22841
+ const headers = response.headers;
22842
+ const detailParts = [];
22843
+ const requestId = this.getHeaderValue(headers["x-request-id"]) || this.getHeaderValue(headers["x-amzn-requestid"]) || this.getHeaderValue(headers["cf-ray"]);
22844
+ const retryAfter = this.getHeaderValue(headers["retry-after"]);
22845
+ const server = this.getHeaderValue(headers["server"]);
22846
+ const via = this.getHeaderValue(headers["via"]);
22847
+ if (requestId) detailParts.push(`request-id ${requestId}`);
22848
+ if (retryAfter) detailParts.push(`retry-after ${retryAfter}`);
22849
+ if (server) detailParts.push(`server ${server}`);
22850
+ if (via) detailParts.push(`via ${via}`);
22851
+ return detailParts.join(", ");
22852
+ }
22853
+ getHeaderValue(header) {
22854
+ if (Array.isArray(header)) return header[0] ?? null;
22855
+ return header ?? null;
22856
+ }
22793
22857
  startHeartbeat() {
22794
22858
  this.stopHeartbeat();
22795
22859
  this.heartbeatTimer = setInterval(()=>{
@@ -22931,9 +22995,9 @@ var __webpack_exports__ = {};
22931
22995
  result.localOnlyChanges.forEach((c)=>logger_logger.verbose(chalk_source.gray(` ${c.path}`)));
22932
22996
  }
22933
22997
  }
22934
- async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles) {
22998
+ async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
22935
22999
  const filesToRemove = [];
22936
- for (const file of unstagedFiles)try {
23000
+ for (const file of unstagedFiles)if (remotelyChangedFiles.has(file)) try {
22937
23001
  await gitService.show(`HEAD:${file}`);
22938
23002
  } catch (e) {
22939
23003
  filesToRemove.push(file);
@@ -25010,21 +25074,24 @@ var __webpack_exports__ = {};
25010
25074
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
25011
25075
  const currentBranch = this.config.getCurrentBranch();
25012
25076
  const tempRef = `anvil-sync-temp-${Date.now()}`;
25077
+ const oldCommitId = this.config.getCommitId();
25013
25078
  await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
25014
25079
  await this.config.gitService.reset(tempRef, "mixed");
25015
25080
  await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
25081
+ const newCommitId = await this.config.gitService.getCommitId();
25016
25082
  await this.config.gitService.checkout([
25017
25083
  ".anvil_editor.yaml",
25018
25084
  "anvil.yaml"
25019
25085
  ]);
25020
25086
  await this.config.editorYaml.reload();
25021
- await this.deleteFilesRemovedOnAnvilLocally();
25087
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
25022
25088
  await this.discardFormattingOnlyYamlChanges();
25023
25089
  }
25024
- async deleteFilesRemovedOnAnvilLocally() {
25090
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
25025
25091
  try {
25026
25092
  const status = await this.config.gitService.getStatus();
25027
- await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded);
25093
+ const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
25094
+ await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles);
25028
25095
  } catch (e) {
25029
25096
  logger_logger.verbose(chalk_source.gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
25030
25097
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",