@anvil-works/anvil-cli 0.5.5 → 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.
Files changed (3) hide show
  1. package/dist/cli.js +151 -12
  2. package/dist/index.js +144 -9
  3. package/package.json +1 -1
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);
@@ -48023,6 +48087,16 @@ var __webpack_exports__ = {};
48023
48087
  function normalizeClientCodePath(relativePath) {
48024
48088
  return relativePath.replace(/\\/g, "/");
48025
48089
  }
48090
+ function isServerRequirementsPath(relativePath) {
48091
+ return "server_code/requirements.txt" === relativePath.replace(/\\/g, "/");
48092
+ }
48093
+ async function readServerRequirements(repoPath) {
48094
+ try {
48095
+ return await external_fs_.promises.readFile(external_path_default().join(repoPath, "server_code", "requirements.txt"), "utf8");
48096
+ } catch (error) {
48097
+ return;
48098
+ }
48099
+ }
48026
48100
  function detectFormTemplate(relativePath) {
48027
48101
  const normalized = normalizeClientCodePath(relativePath);
48028
48102
  if (!normalized.startsWith("client_code/")) return null;
@@ -48486,6 +48560,44 @@ var __webpack_exports__ = {};
48486
48560
  type: "ignore",
48487
48561
  reason: "anvil.yaml handled specially (multiple save paths)"
48488
48562
  };
48563
+ if (isServerRequirementsPath(relativePath)) {
48564
+ if ("unlink" === changeType) {
48565
+ const anvilYamlContent = await readFileContent(repoPath, "anvil.yaml", stagedOnly);
48566
+ const anvilConfig = jsYaml.load(anvilYamlContent) ?? {};
48567
+ const runtimeOptions = {
48568
+ ...anvilConfig.runtime_options ?? {}
48569
+ };
48570
+ const serverSpec = {
48571
+ ...runtimeOptions.server_spec ?? {}
48572
+ };
48573
+ delete serverSpec.requirements;
48574
+ if (Object.keys(serverSpec).length > 0) return {
48575
+ type: "save",
48576
+ savePath: [
48577
+ "runtime_options",
48578
+ "server_spec"
48579
+ ],
48580
+ content: serverSpec
48581
+ };
48582
+ delete runtimeOptions.server_spec;
48583
+ return {
48584
+ type: "save",
48585
+ savePath: [
48586
+ "runtime_options"
48587
+ ],
48588
+ content: runtimeOptions
48589
+ };
48590
+ }
48591
+ return {
48592
+ type: "save",
48593
+ savePath: [
48594
+ "runtime_options",
48595
+ "server_spec",
48596
+ "requirements"
48597
+ ],
48598
+ content: await readFileContent(repoPath, relativePath, stagedOnly)
48599
+ };
48600
+ }
48489
48601
  if ("unlink" === changeType) return await handleFileDeletion(repoPath, relativePath, editorYaml);
48490
48602
  for (const route of ROUTES)if (route.matches(relativePath)) return await route.handle(repoPath, relativePath, changeType, editorYaml, stagedOnly);
48491
48603
  return {
@@ -48892,6 +49004,14 @@ var __webpack_exports__ = {};
48892
49004
  throw new Error(`Failed to parse anvil.yaml: ${error.message}. File may be in an invalid state.`);
48893
49005
  }
48894
49006
  if (!parsedYaml || "object" != typeof parsedYaml) throw new Error("anvil.yaml is not a valid YAML object");
49007
+ const currentRequirements = await readServerRequirements(repoPath);
49008
+ if (void 0 !== currentRequirements) parsedYaml.runtime_options = {
49009
+ ...parsedYaml.runtime_options ?? {},
49010
+ server_spec: {
49011
+ ...parsedYaml.runtime_options?.server_spec ?? {},
49012
+ requirements: currentRequirements
49013
+ }
49014
+ };
48895
49015
  if (!validateAnvilYaml(yamlContent)) throw new Error("anvil.yaml validation failed - see errors above");
48896
49016
  const changes = [];
48897
49017
  for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
@@ -49135,7 +49255,19 @@ var __webpack_exports__ = {};
49135
49255
  async getHeadAnvilYaml() {
49136
49256
  try {
49137
49257
  const content = await this.config.gitService.show("HEAD:anvil.yaml");
49138
- return js_yaml_load(content);
49258
+ const parsedYaml = js_yaml_load(content);
49259
+ if (!parsedYaml || "object" != typeof parsedYaml) return null;
49260
+ try {
49261
+ const requirements = await this.config.gitService.show("HEAD:server_code/requirements.txt");
49262
+ parsedYaml.runtime_options = {
49263
+ ...parsedYaml.runtime_options ?? {},
49264
+ server_spec: {
49265
+ ...parsedYaml.runtime_options?.server_spec ?? {},
49266
+ requirements
49267
+ }
49268
+ };
49269
+ } catch (e) {}
49270
+ return parsedYaml;
49139
49271
  } catch (e) {
49140
49272
  return null;
49141
49273
  }
@@ -49429,21 +49561,24 @@ var __webpack_exports__ = {};
49429
49561
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
49430
49562
  const currentBranch = this.config.getCurrentBranch();
49431
49563
  const tempRef = `anvil-sync-temp-${Date.now()}`;
49564
+ const oldCommitId = this.config.getCommitId();
49432
49565
  await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
49433
49566
  await this.config.gitService.reset(tempRef, "mixed");
49434
49567
  await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
49568
+ const newCommitId = await this.config.gitService.getCommitId();
49435
49569
  await this.config.gitService.checkout([
49436
49570
  ".anvil_editor.yaml",
49437
49571
  "anvil.yaml"
49438
49572
  ]);
49439
49573
  await this.config.editorYaml.reload();
49440
- await this.deleteFilesRemovedOnAnvilLocally();
49574
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
49441
49575
  await this.discardFormattingOnlyYamlChanges();
49442
49576
  }
49443
- async deleteFilesRemovedOnAnvilLocally() {
49577
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
49444
49578
  try {
49445
49579
  const status = await this.config.gitService.getStatus();
49446
- 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);
49447
49582
  } catch (e) {
49448
49583
  logger_logger.verbose(chalk_source.gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
49449
49584
  }
@@ -50807,7 +50942,7 @@ var __webpack_exports__ = {};
50807
50942
  async function confirmReverseLookupWithResolvedUser(anvilUrl, username) {
50808
50943
  const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username, `Multiple accounts found for ${anvilUrl}. Which account should be used for app lookup?`);
50809
50944
  if (null === resolvedUsername) return null;
50810
- const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} ${resolvedUsername ? `for ${resolvedUsername}` : ""} for matching app IDs? (slower)`, true);
50945
+ const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} ${resolvedUsername ? `for ${resolvedUsername}` : ""} for matching app IDs? This is slower than detecting the app ID from local git remotes.`, true);
50811
50946
  return {
50812
50947
  username: resolvedUsername,
50813
50948
  shouldContinue
@@ -50864,6 +50999,11 @@ var __webpack_exports__ = {};
50864
50999
  };
50865
51000
  return null;
50866
51001
  }
51002
+ if (1 === candidates.length) {
51003
+ const [candidate] = candidates;
51004
+ logger_logger.success("Auto-selected detected app ID: " + chalk_source.bold(candidate.appId));
51005
+ return candidate;
51006
+ }
50867
51007
  const choices = candidates.map((candidate, index)=>({
50868
51008
  name: formatCandidateLabel(candidate),
50869
51009
  value: index
@@ -50873,12 +51013,11 @@ var __webpack_exports__ = {};
50873
51013
  value: null
50874
51014
  });
50875
51015
  try {
50876
- const promptText = 1 === candidates.length ? "Confirm the detected app ID:" : "Select an app ID:";
50877
51016
  const answer = await logger_logger.prompt([
50878
51017
  {
50879
51018
  type: "list",
50880
51019
  name: "appId",
50881
- message: promptText,
51020
+ message: "Select an app ID:",
50882
51021
  choices: choices,
50883
51022
  pageSize: 10
50884
51023
  }
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);
@@ -23536,6 +23600,16 @@ var __webpack_exports__ = {};
23536
23600
  function normalizeClientCodePath(relativePath) {
23537
23601
  return relativePath.replace(/\\/g, "/");
23538
23602
  }
23603
+ function isServerRequirementsPath(relativePath) {
23604
+ return "server_code/requirements.txt" === relativePath.replace(/\\/g, "/");
23605
+ }
23606
+ async function readServerRequirements(repoPath) {
23607
+ try {
23608
+ return await external_fs_.promises.readFile(external_path_default().join(repoPath, "server_code", "requirements.txt"), "utf8");
23609
+ } catch (error) {
23610
+ return;
23611
+ }
23612
+ }
23539
23613
  function detectFormTemplate(relativePath) {
23540
23614
  const normalized = normalizeClientCodePath(relativePath);
23541
23615
  if (!normalized.startsWith("client_code/")) return null;
@@ -23999,6 +24073,44 @@ var __webpack_exports__ = {};
23999
24073
  type: "ignore",
24000
24074
  reason: "anvil.yaml handled specially (multiple save paths)"
24001
24075
  };
24076
+ if (isServerRequirementsPath(relativePath)) {
24077
+ if ("unlink" === changeType) {
24078
+ const anvilYamlContent = await readFileContent(repoPath, "anvil.yaml", stagedOnly);
24079
+ const anvilConfig = jsYaml.load(anvilYamlContent) ?? {};
24080
+ const runtimeOptions = {
24081
+ ...anvilConfig.runtime_options ?? {}
24082
+ };
24083
+ const serverSpec = {
24084
+ ...runtimeOptions.server_spec ?? {}
24085
+ };
24086
+ delete serverSpec.requirements;
24087
+ if (Object.keys(serverSpec).length > 0) return {
24088
+ type: "save",
24089
+ savePath: [
24090
+ "runtime_options",
24091
+ "server_spec"
24092
+ ],
24093
+ content: serverSpec
24094
+ };
24095
+ delete runtimeOptions.server_spec;
24096
+ return {
24097
+ type: "save",
24098
+ savePath: [
24099
+ "runtime_options"
24100
+ ],
24101
+ content: runtimeOptions
24102
+ };
24103
+ }
24104
+ return {
24105
+ type: "save",
24106
+ savePath: [
24107
+ "runtime_options",
24108
+ "server_spec",
24109
+ "requirements"
24110
+ ],
24111
+ content: await readFileContent(repoPath, relativePath, stagedOnly)
24112
+ };
24113
+ }
24002
24114
  if ("unlink" === changeType) return await handleFileDeletion(repoPath, relativePath, editorYaml);
24003
24115
  for (const route of ROUTES)if (route.matches(relativePath)) return await route.handle(repoPath, relativePath, changeType, editorYaml, stagedOnly);
24004
24116
  return {
@@ -24405,6 +24517,14 @@ var __webpack_exports__ = {};
24405
24517
  throw new Error(`Failed to parse anvil.yaml: ${error.message}. File may be in an invalid state.`);
24406
24518
  }
24407
24519
  if (!parsedYaml || "object" != typeof parsedYaml) throw new Error("anvil.yaml is not a valid YAML object");
24520
+ const currentRequirements = await readServerRequirements(repoPath);
24521
+ if (void 0 !== currentRequirements) parsedYaml.runtime_options = {
24522
+ ...parsedYaml.runtime_options ?? {},
24523
+ server_spec: {
24524
+ ...parsedYaml.runtime_options?.server_spec ?? {},
24525
+ requirements: currentRequirements
24526
+ }
24527
+ };
24408
24528
  if (!validateAnvilYaml(yamlContent)) throw new Error("anvil.yaml validation failed - see errors above");
24409
24529
  const changes = [];
24410
24530
  for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
@@ -24648,7 +24768,19 @@ var __webpack_exports__ = {};
24648
24768
  async getHeadAnvilYaml() {
24649
24769
  try {
24650
24770
  const content = await this.config.gitService.show("HEAD:anvil.yaml");
24651
- return load(content);
24771
+ const parsedYaml = load(content);
24772
+ if (!parsedYaml || "object" != typeof parsedYaml) return null;
24773
+ try {
24774
+ const requirements = await this.config.gitService.show("HEAD:server_code/requirements.txt");
24775
+ parsedYaml.runtime_options = {
24776
+ ...parsedYaml.runtime_options ?? {},
24777
+ server_spec: {
24778
+ ...parsedYaml.runtime_options?.server_spec ?? {},
24779
+ requirements
24780
+ }
24781
+ };
24782
+ } catch (e) {}
24783
+ return parsedYaml;
24652
24784
  } catch (e) {
24653
24785
  return null;
24654
24786
  }
@@ -24942,21 +25074,24 @@ var __webpack_exports__ = {};
24942
25074
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
24943
25075
  const currentBranch = this.config.getCurrentBranch();
24944
25076
  const tempRef = `anvil-sync-temp-${Date.now()}`;
25077
+ const oldCommitId = this.config.getCommitId();
24945
25078
  await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
24946
25079
  await this.config.gitService.reset(tempRef, "mixed");
24947
25080
  await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
25081
+ const newCommitId = await this.config.gitService.getCommitId();
24948
25082
  await this.config.gitService.checkout([
24949
25083
  ".anvil_editor.yaml",
24950
25084
  "anvil.yaml"
24951
25085
  ]);
24952
25086
  await this.config.editorYaml.reload();
24953
- await this.deleteFilesRemovedOnAnvilLocally();
25087
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
24954
25088
  await this.discardFormattingOnlyYamlChanges();
24955
25089
  }
24956
- async deleteFilesRemovedOnAnvilLocally() {
25090
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
24957
25091
  try {
24958
25092
  const status = await this.config.gitService.getStatus();
24959
- 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);
24960
25095
  } catch (e) {
24961
25096
  logger_logger.verbose(chalk_source.gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
24962
25097
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.5.5",
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",