@anvil-works/anvil-cli 0.5.6 → 0.5.8

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 +140 -45
  2. package/dist/index.js +75 -8
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -33132,6 +33132,16 @@ function __webpack_require__(moduleId) {
33132
33132
  (()=>{
33133
33133
  __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
33134
33134
  })();
33135
+ (()=>{
33136
+ __webpack_require__.r = (exports1)=>{
33137
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
33138
+ value: 'Module'
33139
+ });
33140
+ Object.defineProperty(exports1, '__esModule', {
33141
+ value: true
33142
+ });
33143
+ };
33144
+ })();
33135
33145
  (()=>{
33136
33146
  __webpack_require__.nmd = (module)=>{
33137
33147
  module.paths = [];
@@ -33142,6 +33152,10 @@ function __webpack_require__(moduleId) {
33142
33152
  var __webpack_exports__ = {};
33143
33153
  (()=>{
33144
33154
  "use strict";
33155
+ __webpack_require__.r(__webpack_exports__);
33156
+ __webpack_require__.d(__webpack_exports__, {
33157
+ buildProgram: ()=>buildProgram
33158
+ });
33145
33159
  const ANSI_BACKGROUND_OFFSET = 10;
33146
33160
  const wrapAnsi16 = (offset = 0)=>(code)=>`\u001B[${code + offset}m`;
33147
33161
  const wrapAnsi256 = (offset = 0)=>(code)=>`\u001B[${38 + offset};5;${code}m`;
@@ -47629,12 +47643,14 @@ var __webpack_exports__ = {};
47629
47643
  editSession;
47630
47644
  username;
47631
47645
  reconnectAttempts = 0;
47646
+ connectAttempts = 0;
47632
47647
  reconnectTimer = null;
47633
47648
  heartbeatTimer = null;
47634
47649
  pongTimeoutTimer = null;
47635
47650
  reconnectDelayMs;
47636
47651
  isClosing = false;
47637
47652
  sessionId;
47653
+ lastDisconnectSummary = null;
47638
47654
  RECONNECT_DELAY_BASE_MS = 5000;
47639
47655
  RECONNECT_DELAY_MAX_MS = 60000;
47640
47656
  HEARTBEAT_INTERVAL_MS = 30000;
@@ -47652,6 +47668,7 @@ var __webpack_exports__ = {};
47652
47668
  }
47653
47669
  async connect() {
47654
47670
  this.isClosing = false;
47671
+ this.connectAttempts++;
47655
47672
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
47656
47673
  if (this.ws) {
47657
47674
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
@@ -47660,6 +47677,7 @@ var __webpack_exports__ = {};
47660
47677
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
47661
47678
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
47662
47679
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Connecting to: ${wsUrl.replace(this.authToken, "[TOKEN]")}`);
47680
+ 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
47681
  this.ws = new ws_wrapper(wsUrl);
47664
47682
  this.ws.on("open", ()=>{
47665
47683
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
@@ -47676,6 +47694,21 @@ var __webpack_exports__ = {};
47676
47694
  this.ws?.send(JSON.stringify(subscribeMsg));
47677
47695
  this.emit("connected", void 0);
47678
47696
  });
47697
+ this.ws.on("unexpected-response", (request, response)=>{
47698
+ const summary = this.summarizeUnexpectedResponse(response, wsUrl);
47699
+ this.lastDisconnectSummary = summary;
47700
+ logger_logger.error(chalk_source.red(summary));
47701
+ response.resume();
47702
+ request.destroy();
47703
+ if (this.isClosing) return;
47704
+ this.stopHeartbeat();
47705
+ this.ws = null;
47706
+ this.emit("disconnected", {
47707
+ code: 1006,
47708
+ reason: summary
47709
+ });
47710
+ this.scheduleReconnect();
47711
+ });
47679
47712
  this.ws.on("message", (data)=>{
47680
47713
  this.markSocketResponsive();
47681
47714
  try {
@@ -47700,6 +47733,7 @@ var __webpack_exports__ = {};
47700
47733
  return;
47701
47734
  }
47702
47735
  if (this.isClosing) return;
47736
+ this.lastDisconnectSummary = this.lastDisconnectSummary ?? this.formatCloseSummary(code, reason.toString());
47703
47737
  this.emit("disconnected", {
47704
47738
  code,
47705
47739
  reason: reason.toString()
@@ -47709,8 +47743,13 @@ var __webpack_exports__ = {};
47709
47743
  this.ws.on("error", (error)=>{
47710
47744
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
47711
47745
  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}`));
47746
+ if (error.message.includes("Unexpected server response")) return;
47747
+ 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()})`));
47748
+ else {
47749
+ const details = this.getConnectionContext();
47750
+ logger_logger.error(chalk_source.red(`WebSocket error: ${error.message} (${details})`));
47751
+ }
47752
+ this.lastDisconnectSummary = `WebSocket error: ${error.message}`;
47714
47753
  this.emit("error", {
47715
47754
  error
47716
47755
  });
@@ -47765,7 +47804,7 @@ var __webpack_exports__ = {};
47765
47804
  if (this.reconnectTimer || this.isClosing) return;
47766
47805
  this.reconnectAttempts++;
47767
47806
  const delayMs = this.reconnectDelayMs;
47768
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.reconnectAttempts})...`));
47807
+ 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
47808
  this.reconnectTimer = setTimeout(()=>{
47770
47809
  this.reconnectTimer = null;
47771
47810
  this.connect().catch((error)=>{
@@ -47774,8 +47813,47 @@ var __webpack_exports__ = {};
47774
47813
  });
47775
47814
  });
47776
47815
  }, delayMs);
47816
+ this.lastDisconnectSummary = null;
47777
47817
  this.reconnectDelayMs = Math.min(Math.round(1.5 * this.reconnectDelayMs), this.RECONNECT_DELAY_MAX_MS);
47778
47818
  }
47819
+ getConnectionContext() {
47820
+ return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
47821
+ }
47822
+ getConnectionTarget(wsUrl) {
47823
+ const url = new URL(wsUrl);
47824
+ return `${url.origin}${url.pathname}`;
47825
+ }
47826
+ formatCloseSummary(code, reason) {
47827
+ const suffix = reason ? `, reason=${reason}` : "";
47828
+ return `close code=${code}${suffix}`;
47829
+ }
47830
+ summarizeUnexpectedResponse(response, wsUrl) {
47831
+ const statusCode = response.statusCode ?? "unknown";
47832
+ const statusMessage = response.statusMessage ?? "Unknown";
47833
+ const details = [
47834
+ `target ${this.getConnectionTarget(wsUrl)}`,
47835
+ this.getConnectionContext(),
47836
+ this.getHandshakeMetadata(response)
47837
+ ].filter(Boolean).join(", ");
47838
+ return `WebSocket handshake failed (${statusCode} ${statusMessage}; ${details})`;
47839
+ }
47840
+ getHandshakeMetadata(response) {
47841
+ const headers = response.headers;
47842
+ const detailParts = [];
47843
+ const requestId = this.getHeaderValue(headers["x-request-id"]) || this.getHeaderValue(headers["x-amzn-requestid"]) || this.getHeaderValue(headers["cf-ray"]);
47844
+ const retryAfter = this.getHeaderValue(headers["retry-after"]);
47845
+ const server = this.getHeaderValue(headers["server"]);
47846
+ const via = this.getHeaderValue(headers["via"]);
47847
+ if (requestId) detailParts.push(`request-id ${requestId}`);
47848
+ if (retryAfter) detailParts.push(`retry-after ${retryAfter}`);
47849
+ if (server) detailParts.push(`server ${server}`);
47850
+ if (via) detailParts.push(`via ${via}`);
47851
+ return detailParts.join(", ");
47852
+ }
47853
+ getHeaderValue(header) {
47854
+ if (Array.isArray(header)) return header[0] ?? null;
47855
+ return header ?? null;
47856
+ }
47779
47857
  startHeartbeat() {
47780
47858
  this.stopHeartbeat();
47781
47859
  this.heartbeatTimer = setInterval(()=>{
@@ -47917,9 +47995,9 @@ var __webpack_exports__ = {};
47917
47995
  result.localOnlyChanges.forEach((c)=>logger_logger.verbose(chalk_source.gray(` ${c.path}`)));
47918
47996
  }
47919
47997
  }
47920
- async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles) {
47998
+ async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
47921
47999
  const filesToRemove = [];
47922
- for (const file of unstagedFiles)try {
48000
+ for (const file of unstagedFiles)if (remotelyChangedFiles.has(file)) try {
47923
48001
  await gitService.show(`HEAD:${file}`);
47924
48002
  } catch (e) {
47925
48003
  filesToRemove.push(file);
@@ -49497,21 +49575,24 @@ var __webpack_exports__ = {};
49497
49575
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
49498
49576
  const currentBranch = this.config.getCurrentBranch();
49499
49577
  const tempRef = `anvil-sync-temp-${Date.now()}`;
49578
+ const oldCommitId = this.config.getCommitId();
49500
49579
  await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
49501
49580
  await this.config.gitService.reset(tempRef, "mixed");
49502
49581
  await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
49582
+ const newCommitId = await this.config.gitService.getCommitId();
49503
49583
  await this.config.gitService.checkout([
49504
49584
  ".anvil_editor.yaml",
49505
49585
  "anvil.yaml"
49506
49586
  ]);
49507
49587
  await this.config.editorYaml.reload();
49508
- await this.deleteFilesRemovedOnAnvilLocally();
49588
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
49509
49589
  await this.discardFormattingOnlyYamlChanges();
49510
49590
  }
49511
- async deleteFilesRemovedOnAnvilLocally() {
49591
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
49512
49592
  try {
49513
49593
  const status = await this.config.gitService.getStatus();
49514
- await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded);
49594
+ const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
49595
+ await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles);
49515
49596
  } catch (e) {
49516
49597
  logger_logger.verbose(chalk_source.gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
49517
49598
  }
@@ -51571,15 +51652,16 @@ var __webpack_exports__ = {};
51571
51652
  }
51572
51653
  }
51573
51654
  function registerWatchCommand(program) {
51574
- const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-V, --verbose", "Show detailed output").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options)=>{
51575
- if (void 0 !== options.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(options.verbose);
51655
+ const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
51656
+ const globalOptions = command.optsWithGlobals();
51657
+ if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
51576
51658
  await handleWatchCommand({
51577
51659
  path,
51578
51660
  appid: options.appid,
51579
51661
  useFirst: options.first,
51580
51662
  stagedOnly: options.stagedOnly,
51581
51663
  autoMode: options.auto,
51582
- verbose: options.verbose,
51664
+ verbose: globalOptions.verbose,
51583
51665
  open: options.open,
51584
51666
  url: options.url,
51585
51667
  user: options.user
@@ -52507,8 +52589,9 @@ var __webpack_exports__ = {};
52507
52589
  await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
52508
52590
  }
52509
52591
  function registerCheckoutCommand(program) {
52510
- const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options)=>{
52592
+ const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
52511
52593
  try {
52594
+ const globalOptions = command?.optsWithGlobals() || options || {};
52512
52595
  if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
52513
52596
  await executeCheckout({
52514
52597
  input,
@@ -52519,7 +52602,7 @@ var __webpack_exports__ = {};
52519
52602
  singleBranch: options?.singleBranch,
52520
52603
  origin: options?.origin,
52521
52604
  quiet: options?.quiet,
52522
- verbose: options?.verbose,
52605
+ verbose: globalOptions.verbose,
52523
52606
  url: options?.url,
52524
52607
  user: options?.user,
52525
52608
  force: options?.force,
@@ -52987,39 +53070,51 @@ var __webpack_exports__ = {};
52987
53070
  process.exit(1);
52988
53071
  }
52989
53072
  }
52990
- const cli_program = new Command();
52991
- cli_program.name("anvil").description("CLI tool for developing Anvil apps locally").version(VERSION, "-v, --version", "Output the version number").option("--json", "Output in JSON format (NDJSON) for scripting/LLM consumption").helpOption('-h, --help', 'Display help for anvil command').hook("preAction", async (thisCommand, actionCommand)=>{
52992
- const opts = thisCommand.opts();
52993
- if (opts.json) setGlobalOutputConfig({
52994
- jsonMode: true
52995
- });
52996
- if (!opts.json) {
52997
- const commandName = actionCommand?.name();
52998
- if ("update" !== commandName && "git-credential" !== commandName) checkVersionAndWarn();
52999
- }
53000
- });
53001
- const cli_watchCommand = registerWatchCommand(cli_program);
53002
- registerCheckoutCommand(cli_program);
53003
- registerGitCredentialCommand(cli_program);
53004
- registerLoginCommand(cli_program);
53005
- registerLogoutCommand(cli_program);
53006
- registerConfigCommand(cli_program);
53007
- registerVersionCommand(cli_program, VERSION);
53008
- registerConfigureCommand(cli_program, VERSION);
53009
- cli_program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
53010
- await handleUpdateCommand();
53011
- });
53012
- if (cli_watchCommand) {
53013
- const watchOptions = cli_watchCommand.options.map((opt)=>{
53014
- const flags = opt.flags;
53015
- const description = opt.description || "";
53016
- return ` ${flags.padEnd(30)} ${description}`;
53017
- }).join("\n");
53018
- if (watchOptions) cli_program.addHelpText("after", "\n" + chalk_source.bold("Watch Command Options:") + "\n" + chalk_source.gray(" (These options apply to the 'watch' command)") + "\n" + watchOptions + "\n");
53019
- }
53020
- cli_program.parse();
53073
+ function addGlobalOptionsHelp(command) {
53074
+ if ("help" !== command.name()) command.addHelpText("after", "\n" + chalk_source.bold("Global Options:") + "\n -V, --verbose Show detailed output\n");
53075
+ command.commands.forEach((subcommand)=>addGlobalOptionsHelp(subcommand));
53076
+ }
53077
+ function buildProgram() {
53078
+ const program = new Command();
53079
+ program.name("anvil").description("CLI tool for developing Anvil apps locally").version(VERSION, "-v, --version", "Output the version number").option("--json", "Output in JSON format (NDJSON) for scripting/LLM consumption").option("-V, --verbose", "Show detailed output").helpOption('-h, --help', 'Display help for anvil command').hook("preAction", async (_thisCommand, actionCommand)=>{
53080
+ const opts = actionCommand.optsWithGlobals();
53081
+ setGlobalOutputConfig({
53082
+ jsonMode: !!opts.json
53083
+ });
53084
+ if (logger_logger instanceof CLILogger) logger_logger.setVerbose(!!opts.verbose);
53085
+ if (!opts.json) {
53086
+ const commandName = actionCommand?.name();
53087
+ if ("update" !== commandName && "git-credential" !== commandName) checkVersionAndWarn();
53088
+ }
53089
+ });
53090
+ const watchCommand = registerWatchCommand(program);
53091
+ registerCheckoutCommand(program);
53092
+ registerGitCredentialCommand(program);
53093
+ registerLoginCommand(program);
53094
+ registerLogoutCommand(program);
53095
+ registerConfigCommand(program);
53096
+ registerVersionCommand(program, VERSION);
53097
+ registerConfigureCommand(program, VERSION);
53098
+ program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
53099
+ await handleUpdateCommand();
53100
+ });
53101
+ if (watchCommand) {
53102
+ const watchOptions = watchCommand.options.map((opt)=>{
53103
+ const flags = opt.flags;
53104
+ const description = opt.description || "";
53105
+ return ` ${flags.padEnd(30)} ${description}`;
53106
+ }).join("\n");
53107
+ if (watchOptions) program.addHelpText("after", "\n" + chalk_source.bold("Watch Command Options:") + "\n" + chalk_source.gray(" (These options apply to the 'watch' command)") + "\n" + watchOptions + "\n");
53108
+ }
53109
+ program.commands.forEach((command)=>addGlobalOptionsHelp(command));
53110
+ return program;
53111
+ }
53112
+ buildProgram().parse();
53021
53113
  })();
53022
- for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
53114
+ exports.buildProgram = __webpack_exports__.buildProgram;
53115
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
53116
+ "buildProgram"
53117
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
53023
53118
  Object.defineProperty(exports, '__esModule', {
53024
53119
  value: true
53025
53120
  });
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.8",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",