@anvil-works/anvil-cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +280 -233
  2. package/dist/index.js +213 -81
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -42545,30 +42545,6 @@ var __webpack_exports__ = {};
42545
42545
  if (/^https?:\/\//.test(url)) return url;
42546
42546
  return `https://${url}`;
42547
42547
  }
42548
- async function detectAnvilUrlFromRemotes(repoPath) {
42549
- try {
42550
- const git = esm_default(repoPath);
42551
- const remotes = await git.getRemotes(true);
42552
- for (const remote of remotes){
42553
- const fetchUrl = remote.refs.fetch;
42554
- if (!fetchUrl) continue;
42555
- const httpMatch = fetchUrl.match(/(https?:\/\/[^\/]+)\/git\/[A-Z0-9]+\.git/);
42556
- if (httpMatch) return {
42557
- url: normalizeAnvilUrl(httpMatch[1])
42558
- };
42559
- const sshMatch = fetchUrl.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
42560
- if (sshMatch) {
42561
- const username = decodeURIComponent(sshMatch[1]);
42562
- const hostname = sshMatch[2];
42563
- return {
42564
- url: normalizeAnvilUrl(hostname),
42565
- username
42566
- };
42567
- }
42568
- }
42569
- } catch (_e) {}
42570
- return null;
42571
- }
42572
42548
  function resolveAnvilUrl() {
42573
42549
  const fromConfig = getConfig("anvilUrl");
42574
42550
  if ("string" == typeof fromConfig && fromConfig.trim()) return fromConfig.trim();
@@ -47410,38 +47386,36 @@ var __webpack_exports__ = {};
47410
47386
  return null;
47411
47387
  }
47412
47388
  }
47413
- async function detectAppIdsFromRemotes(repoPath, anvilUrl) {
47414
- const git = esm_default(repoPath);
47415
- const out = [];
47416
- try {
47417
- const remotes = await git.getRemotes(true);
47418
- const url = new URL(anvilUrl);
47419
- const expectedHost = url.hostname;
47420
- for (const remote of remotes){
47421
- const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
47422
- if (httpMatch) {
47423
- const [, host, detectedAppId] = httpMatch;
47424
- if (host === expectedHost) {
47425
- out.push({
47426
- appId: detectedAppId,
47427
- source: "remote",
47428
- description: `Git remote '${remote.name}'`
47429
- });
47430
- continue;
47431
- }
47432
- }
47433
- const sshMatch = remote.refs.fetch?.match(/ssh:\/\/(?:[^@]+@)?([^:\/]+):(\d+)\/([A-Z0-9]+)\.git/);
47434
- if (sshMatch) {
47435
- const [, host, , detectedAppId] = sshMatch;
47436
- if (host === expectedHost) out.push({
47437
- appId: detectedAppId,
47438
- source: "remote",
47439
- description: `Git remote '${remote.name}' (SSH)`
47440
- });
47441
- }
47442
- }
47443
- } catch (_e) {}
47444
- return out;
47389
+ function filterCandidates(candidates, explicitUrl, explicitUsername) {
47390
+ let filtered = candidates;
47391
+ if (explicitUrl) {
47392
+ const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
47393
+ filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
47394
+ }
47395
+ if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
47396
+ return filtered;
47397
+ }
47398
+ function formatCandidateLabel(candidate) {
47399
+ const parts = [
47400
+ candidate.appId
47401
+ ];
47402
+ if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
47403
+ else parts.push(`(${candidate.detectedUrl})`);
47404
+ parts.push(`- ${candidate.description}`);
47405
+ return parts.join(" ");
47406
+ }
47407
+ function lookupRemoteInfoForAppId(appId, detectedRemotes) {
47408
+ const matches = detectedRemotes.filter((c)=>c.appId === appId);
47409
+ if (0 === matches.length) return {};
47410
+ const withUsername = matches.find((c)=>c.detectedUsername);
47411
+ if (withUsername) return {
47412
+ detectedUrl: withUsername.detectedUrl,
47413
+ detectedUsername: withUsername.detectedUsername
47414
+ };
47415
+ return {
47416
+ detectedUrl: matches[0].detectedUrl,
47417
+ detectedUsername: matches[0].detectedUsername
47418
+ };
47445
47419
  }
47446
47420
  async function detectAppIdsFromAllRemotes(repoPath) {
47447
47421
  const git = esm_default(repoPath);
@@ -47477,11 +47451,13 @@ var __webpack_exports__ = {};
47477
47451
  } catch (_e) {}
47478
47452
  return out;
47479
47453
  }
47480
- async function detectAppIdsByCommitLookup(repoPath, anvilUrl) {
47454
+ async function detectAppIdsByCommitLookup(repoPath, options) {
47455
+ const anvilUrl = options.anvilUrl || resolveAnvilUrl();
47456
+ const username = options.username;
47481
47457
  const git = esm_default(repoPath);
47482
47458
  const out = [];
47483
47459
  try {
47484
- const authToken = await auth_getValidAuthToken(anvilUrl);
47460
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
47485
47461
  const branchRef = await git.revparse([
47486
47462
  "--abbrev-ref",
47487
47463
  "HEAD"
@@ -47520,16 +47496,6 @@ var __webpack_exports__ = {};
47520
47496
  } catch (_e) {}
47521
47497
  return out;
47522
47498
  }
47523
- async function detectAppIds(repoPath, options = {}) {
47524
- const anvilUrl = options.anvilUrl || resolveAnvilUrl();
47525
- const results = [];
47526
- const includeReverseLookup = options.includeReverseLookup ?? true;
47527
- results.push(...await detectAppIdsFromRemotes(repoPath, anvilUrl));
47528
- if (0 === results.length && includeReverseLookup) results.push(...await detectAppIdsByCommitLookup(repoPath, anvilUrl));
47529
- const seen = new Set();
47530
- const unique = results.filter((c)=>seen.has(c.appId) ? false : (seen.add(c.appId), true));
47531
- return unique;
47532
- }
47533
47499
  class WebSocketClient extends Emitter_Emitter {
47534
47500
  ws = null;
47535
47501
  appId;
@@ -47540,9 +47506,15 @@ var __webpack_exports__ = {};
47540
47506
  username;
47541
47507
  reconnectAttempts = 0;
47542
47508
  reconnectTimer = null;
47509
+ heartbeatTimer = null;
47510
+ pongTimeoutTimer = null;
47511
+ reconnectDelayMs;
47512
+ isClosing = false;
47543
47513
  sessionId;
47544
- MAX_RECONNECT_ATTEMPTS = 5;
47545
- RECONNECT_DELAY = 5000;
47514
+ RECONNECT_DELAY_BASE_MS = 5000;
47515
+ RECONNECT_DELAY_MAX_MS = 60000;
47516
+ HEARTBEAT_INTERVAL_MS = 30000;
47517
+ HEARTBEAT_TIMEOUT_MS = 10000;
47546
47518
  constructor(options){
47547
47519
  super();
47548
47520
  this.appId = options.appId;
@@ -47552,12 +47524,14 @@ var __webpack_exports__ = {};
47552
47524
  this.editSession = options.editSession;
47553
47525
  this.username = options.username;
47554
47526
  this.sessionId = Math.random().toString(36).substring(2, 10);
47527
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
47555
47528
  }
47556
47529
  async connect() {
47530
+ this.isClosing = false;
47557
47531
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
47558
47532
  if (this.ws) {
47559
47533
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
47560
- this.close();
47534
+ this.teardownSocket();
47561
47535
  }
47562
47536
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
47563
47537
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
@@ -47567,6 +47541,8 @@ var __webpack_exports__ = {};
47567
47541
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
47568
47542
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket opened");
47569
47543
  this.reconnectAttempts = 0;
47544
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
47545
+ this.startHeartbeat();
47570
47546
  const subscribeMsg = {
47571
47547
  cmd: "WATCH_APP",
47572
47548
  app: this.appId,
@@ -47577,6 +47553,7 @@ var __webpack_exports__ = {};
47577
47553
  this.emit("connected", void 0);
47578
47554
  });
47579
47555
  this.ws.on("message", (data)=>{
47556
+ this.markSocketResponsive();
47580
47557
  try {
47581
47558
  const msg = JSON.parse(data.toString());
47582
47559
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
@@ -47585,8 +47562,12 @@ var __webpack_exports__ = {};
47585
47562
  logger_logger.error(chalk_source.red(`Failed to parse WebSocket message: ${error.message}`));
47586
47563
  }
47587
47564
  });
47565
+ this.ws.on("pong", ()=>{
47566
+ this.markSocketResponsive();
47567
+ });
47588
47568
  this.ws.on("close", (code, reason)=>{
47589
47569
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Closed: code=${code}, reason=${reason.toString()}`);
47570
+ this.stopHeartbeat();
47590
47571
  this.ws = null;
47591
47572
  if (1008 === code || reason.toString().includes("unauthenticated")) {
47592
47573
  logger_logger.warn(chalk_source.yellow(" WebSocket authentication failed - changes from the Anvil Editor won't be detected"));
@@ -47594,21 +47575,12 @@ var __webpack_exports__ = {};
47594
47575
  this.emit("auth-failed", void 0);
47595
47576
  return;
47596
47577
  }
47578
+ if (this.isClosing) return;
47597
47579
  this.emit("disconnected", {
47598
47580
  code,
47599
47581
  reason: reason.toString()
47600
47582
  });
47601
- if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
47602
- this.reconnectAttempts++;
47603
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${this.RECONNECT_DELAY / 1000}s (attempt ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`));
47604
- this.reconnectTimer = setTimeout(()=>{
47605
- this.connect().catch((error)=>{
47606
- this.emit("error", {
47607
- error: error instanceof Error ? error : new Error(String(error))
47608
- });
47609
- });
47610
- }, this.RECONNECT_DELAY);
47611
- } 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();
47612
47584
  });
47613
47585
  this.ws.on("error", (error)=>{
47614
47586
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
@@ -47653,24 +47625,74 @@ var __webpack_exports__ = {};
47653
47625
  return this.editSession;
47654
47626
  }
47655
47627
  close() {
47628
+ this.isClosing = true;
47656
47629
  if (this.reconnectTimer) {
47657
47630
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Clearing reconnect timer");
47658
47631
  clearTimeout(this.reconnectTimer);
47659
47632
  this.reconnectTimer = null;
47660
47633
  }
47661
- if (this.ws) {
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;
47662
47659
  try {
47663
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
47664
- this.ws.removeAllListeners();
47665
- if (this.ws.readyState === ws_wrapper.OPEN) this.ws.close();
47666
- } catch (e) {
47667
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
47660
+ ws.ping();
47661
+ } catch (error) {
47662
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Failed to send ping: ${error.message}`);
47663
+ ws.terminate();
47664
+ return;
47668
47665
  }
47669
- this.ws = null;
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;
47670
47676
  }
47671
47677
  }
47672
- isConnected() {
47673
- return this.ws?.readyState === ws_wrapper.OPEN;
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;
47674
47696
  }
47675
47697
  }
47676
47698
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
@@ -49398,7 +49420,11 @@ var __webpack_exports__ = {};
49398
49420
  wsClient = null;
49399
49421
  saveProcessor = null;
49400
49422
  syncManager = null;
49423
+ localChangePollTimer = null;
49424
+ isPollingLocalChanges = false;
49425
+ lastLocalStatusFingerprint = null;
49401
49426
  BRANCH_CHANGE_SETTLE_MS = 2000;
49427
+ LOCAL_CHANGE_POLL_MS = 2000;
49402
49428
  stagedOnly;
49403
49429
  constructor(repoPath, appId, options){
49404
49430
  super();
@@ -49536,6 +49562,10 @@ var __webpack_exports__ = {};
49536
49562
  this.fileWatcher.cleanup();
49537
49563
  this.fileWatcher = null;
49538
49564
  }
49565
+ if (this.localChangePollTimer) {
49566
+ clearInterval(this.localChangePollTimer);
49567
+ this.localChangePollTimer = null;
49568
+ }
49539
49569
  logger_logger.debug(`[Session ${this.sessionId}]`, "Cleanup complete");
49540
49570
  }
49541
49571
  async startWatching() {
@@ -49569,8 +49599,70 @@ var __webpack_exports__ = {};
49569
49599
  }
49570
49600
  });
49571
49601
  await this.fileWatcher.start(this.currentBranch);
49602
+ await this.initializeLocalChangeFingerprint();
49603
+ this.startLocalChangeFallbackPolling();
49572
49604
  await new Promise(()=>{});
49573
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
+ }
49574
49666
  async handleFileChange(event, filePath, relativePath) {
49575
49667
  if (this.isCleanedUp || this.isPausedForUserInput) return;
49576
49668
  logger_logger.verbose(chalk_source.blue("File changed: ") + chalk_source.bold(relativePath));
@@ -49834,7 +49926,8 @@ var __webpack_exports__ = {};
49834
49926
  authToken,
49835
49927
  currentBranch,
49836
49928
  commitId,
49837
- stagedOnly
49929
+ stagedOnly,
49930
+ username
49838
49931
  });
49839
49932
  session.on("branch-changed", (data)=>{
49840
49933
  logger_logger.debug("Event: branch-changed", data);
@@ -49863,27 +49956,8 @@ var __webpack_exports__ = {};
49863
49956
  session.hasUncommittedChanges = hasUncommittedChanges;
49864
49957
  return session;
49865
49958
  }
49866
- async function resolveAnvilUrlWithPrompt(explicitUrl, repoPath, explicitUsername) {
49867
- let url;
49868
- let username = explicitUsername;
49869
- if (explicitUrl) url = normalizeAnvilUrl(explicitUrl);
49870
- else if (repoPath) {
49871
- const detected = await detectAnvilUrlFromRemotes(repoPath);
49872
- if (detected) {
49873
- url = detected.url;
49874
- if (!username && detected.username) username = detected.username;
49875
- } else url = resolveUrlFromAvailableOrConfig();
49876
- } else url = resolveUrlFromAvailableOrConfig();
49877
- if (!username) {
49878
- const accounts = auth_getAccountsForUrl(url);
49879
- if (1 === accounts.length) username = accounts[0];
49880
- }
49881
- return {
49882
- url,
49883
- username
49884
- };
49885
- }
49886
- function resolveUrlFromAvailableOrConfig() {
49959
+ function resolveUrlForFallback(explicitUrl) {
49960
+ if (explicitUrl) return normalizeAnvilUrl(explicitUrl);
49887
49961
  const availableUrls = getAvailableAnvilUrls();
49888
49962
  if (availableUrls.length > 0) return availableUrls[0];
49889
49963
  const fromConfig = getConfig("anvilUrl");
@@ -49911,11 +49985,16 @@ var __webpack_exports__ = {};
49911
49985
  trimmed ? resolve(trimmed) : resolve(null);
49912
49986
  });
49913
49987
  });
49914
- return manualAppId;
49988
+ if (manualAppId) return {
49989
+ appId: manualAppId,
49990
+ source: "config",
49991
+ description: "Manual entry"
49992
+ };
49993
+ return null;
49915
49994
  }
49916
- const choices = candidates.map((candidate)=>({
49917
- name: `${candidate.appId} (${candidate.description})`,
49918
- value: candidate.appId
49995
+ const choices = candidates.map((candidate, index)=>({
49996
+ name: formatCandidateLabel(candidate),
49997
+ value: index
49919
49998
  }));
49920
49999
  choices.push({
49921
50000
  name: "Cancel",
@@ -49944,9 +50023,14 @@ var __webpack_exports__ = {};
49944
50023
  trimmed ? resolve(trimmed) : resolve(null);
49945
50024
  });
49946
50025
  });
49947
- return manualAppId;
50026
+ if (manualAppId) return {
50027
+ appId: manualAppId,
50028
+ source: "config",
50029
+ description: "Manual entry"
50030
+ };
50031
+ return null;
49948
50032
  }
49949
- return answer.appId;
50033
+ return candidates[answer.appId];
49950
50034
  } catch (error) {
49951
50035
  const errorObj = error;
49952
50036
  if ("ExitPromptError" === errorObj.name || errorObj.message.includes("User force closed")) logger_logger.warn("Operation cancelled by user.");
@@ -50384,137 +50468,100 @@ var __webpack_exports__ = {};
50384
50468
  const validationResult = await validateAnvilApp(repoPath);
50385
50469
  if (validationResult.appName) logger_logger.info(chalk_source.green("Anvil app: ") + chalk_source.bold(validationResult.appName));
50386
50470
  const detectedFromAllRemotes = await detectAppIdsFromAllRemotes(repoPath);
50387
- const availableUrls = getAvailableAnvilUrls();
50388
- const resolved = await resolveAnvilUrlWithPrompt(explicitUrl, repoPath, explicitUsername);
50389
- let anvilUrl = resolved.url;
50390
- let username = resolved.username;
50391
- logger_logger.verbose(chalk_source.cyan("Resolved Anvil URL: ") + chalk_source.bold(anvilUrl));
50392
- if (username) logger_logger.verbose(chalk_source.cyan("Using username: ") + chalk_source.bold(username));
50393
- if (!explicitUrl) {
50394
- const detectedFromRemote = detectedFromAllRemotes.length > 0 && detectedFromAllRemotes[0].detectedUrl ? {
50395
- url: normalizeAnvilUrl(detectedFromAllRemotes[0].detectedUrl),
50396
- username: detectedFromAllRemotes[0].detectedUsername
50397
- } : null;
50398
- if (detectedFromRemote) {
50399
- logger_logger.verbose(chalk_source.cyan("Detected Anvil URL from git remote: ") + chalk_source.bold(detectedFromRemote.url));
50400
- if (!username && detectedFromRemote.username) {
50401
- username = detectedFromRemote.username;
50402
- logger_logger.verbose(chalk_source.cyan("Detected username from git remote: ") + chalk_source.bold(username));
50403
- }
50404
- anvilUrl = detectedFromRemote.url;
50405
- if (hasTokensForUrl(detectedFromRemote.url, username)) logger_logger.verbose(chalk_source.green(" Using detected URL (has authentication tokens)"));
50406
- else logger_logger.verbose(chalk_source.yellow("Using detected URL from remotes (auth will be checked)"));
50407
- } else if (1 === availableUrls.length) anvilUrl = normalizeAnvilUrl(availableUrls[0]);
50408
- else if (availableUrls.length > 1) {
50409
- const choices = availableUrls.map((url)=>({
50410
- name: url,
50411
- value: url
50412
- }));
50413
- choices.push({
50414
- name: "Cancel",
50415
- value: null
50416
- });
50417
- const answer = await logger_logger.select("Multiple Anvil installations found. Which one would you like to use?", choices, availableUrls[0]);
50418
- if (null === answer) {
50419
- logger_logger.warn("Operation cancelled. To login to a new Anvil installation, run:");
50420
- logger_logger.verbose(chalk_source.gray(" anvil login <anvil-url>"));
50421
- process.exit(0);
50471
+ let filteredCandidates = filterCandidates(detectedFromAllRemotes, explicitUrl, explicitUsername);
50472
+ logger_logger.verbose(chalk_source.cyan(`Detected ${detectedFromAllRemotes.length} app ID(s) from git remotes`));
50473
+ if (filteredCandidates.length !== detectedFromAllRemotes.length) logger_logger.verbose(chalk_source.cyan(`After filtering: ${filteredCandidates.length} candidate(s)`));
50474
+ let finalAppId;
50475
+ let anvilUrl;
50476
+ let username = explicitUsername;
50477
+ if (explicitAppId) {
50478
+ finalAppId = explicitAppId;
50479
+ const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
50480
+ if (remoteInfo.detectedUrl && !explicitUrl) {
50481
+ anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
50482
+ logger_logger.verbose(chalk_source.cyan("Resolved URL from remote for app ID: ") + chalk_source.bold(anvilUrl));
50483
+ }
50484
+ if (remoteInfo.detectedUsername && !explicitUsername) {
50485
+ username = remoteInfo.detectedUsername;
50486
+ logger_logger.verbose(chalk_source.cyan("Resolved username from remote for app ID: ") + chalk_source.bold(username));
50487
+ }
50488
+ } else {
50489
+ logger_logger.verbose(chalk_source.cyan("No app ID provided, attempting auto-detection..."));
50490
+ if (0 === filteredCandidates.length) {
50491
+ logger_logger.verbose(chalk_source.gray("No app IDs found in git remotes."));
50492
+ const fallbackUrl = resolveUrlForFallback(explicitUrl);
50493
+ const shouldContinue = await logger_logger.confirm(`Search ${fallbackUrl} for matching app IDs? (slower)`, true);
50494
+ if (shouldContinue) {
50495
+ logger_logger.progress("detect", `Searching ${fallbackUrl} ${explicitUsername ? `for ${explicitUsername}` : ''} for matching app IDs...`);
50496
+ const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
50497
+ anvilUrl: fallbackUrl,
50498
+ username: explicitUsername,
50499
+ includeRemotes: false
50500
+ });
50501
+ logger_logger.progressEnd("detect");
50502
+ for (const c of reverseLookupCandidates)filteredCandidates.push({
50503
+ ...c,
50504
+ detectedUrl: fallbackUrl
50505
+ });
50506
+ }
50507
+ }
50508
+ if (filteredCandidates.length > 0) {
50509
+ for (const c of filteredCandidates)logger_logger.verbose(chalk_source.gray(` Found: ${formatCandidateLabel(c)}`));
50510
+ if (filteredCandidates.length > 1) logger_logger.verbose(chalk_source.yellow(`Found ${filteredCandidates.length} potential app IDs`));
50511
+ }
50512
+ if (useFirst && filteredCandidates.length > 0) {
50513
+ const selected = filteredCandidates[0];
50514
+ finalAppId = selected.appId;
50515
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
50516
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
50517
+ logger_logger.success("Auto-selected first app ID: " + chalk_source.bold(finalAppId));
50518
+ } else {
50519
+ const selected = await selectAppId(filteredCandidates);
50520
+ if (!selected) {
50521
+ logger_logger.error("No app ID provided. Cannot continue without an app ID.");
50522
+ process.exit(1);
50422
50523
  }
50423
- anvilUrl = answer;
50524
+ finalAppId = selected.appId;
50525
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
50526
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
50424
50527
  }
50425
50528
  }
50529
+ if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
50530
+ else if (!anvilUrl) anvilUrl = resolveUrlForFallback();
50531
+ anvilUrl = normalizeAnvilUrl(anvilUrl);
50532
+ logger_logger.verbose(chalk_source.green("Using app ID: ") + chalk_source.bold(finalAppId));
50533
+ logger_logger.verbose(chalk_source.cyan("Using Anvil URL: ") + chalk_source.bold(anvilUrl));
50426
50534
  if (!username) {
50427
50535
  const accounts = auth_getAccountsForUrl(anvilUrl);
50428
50536
  if (1 === accounts.length) {
50429
50537
  username = accounts[0];
50430
- logger_logger.verbose(chalk_source.cyan("Auto-selected username: ") + chalk_source.bold(username));
50538
+ logger_logger.verbose(chalk_source.cyan("Auto-selected account: ") + chalk_source.bold(username));
50431
50539
  } else if (accounts.length > 1) {
50432
- logger_logger.error(`Multiple accounts found for ${anvilUrl}. Please specify which account to use with --user flag.`);
50433
- logger_logger.verbose(chalk_source.gray(`Available accounts: ${accounts.join(", ")}`));
50434
- process.exit(1);
50435
- }
50436
- }
50437
- anvilUrl = normalizeAnvilUrl(anvilUrl);
50438
- logger_logger.verbose(chalk_source.cyan("Using Anvil URL: ") + chalk_source.bold(anvilUrl));
50439
- const detectedUrls = new Set(detectedFromAllRemotes.map((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl)).filter(Boolean));
50440
- const normalizedSelected = normalizeAnvilUrl(anvilUrl);
50441
- if (explicitUrl && detectedFromAllRemotes.length > 0 && !detectedUrls.has(normalizedSelected)) {
50442
- const detectedUrl = Array.from(detectedUrls)[0];
50443
- const detectedAppIds = detectedFromAllRemotes.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === detectedUrl).map((c)=>c.appId);
50444
- logger_logger.warn(chalk_source.yellow("Git remotes point to ") + chalk_source.bold(detectedUrl) + chalk_source.yellow(", but you specified: ") + chalk_source.bold(normalizedSelected));
50445
- if (detectedAppIds.length > 0) logger_logger.info(chalk_source.gray(" Detected app IDs on remote URL: ") + chalk_source.bold(detectedAppIds.join(", ")));
50446
- logger_logger.info(chalk_source.gray(" To use the remote URL instead, run: ") + chalk_source.cyan(`anvil watch --url ${detectedUrl}`));
50447
- }
50448
- const candidatesForSelectedUrl = detectedFromAllRemotes.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedSelected);
50449
- if (candidatesForSelectedUrl.length > 0) for (const candidate of candidatesForSelectedUrl){
50450
- const isLoggedInAny = hasTokensForUrl(normalizedSelected);
50451
- const isLoggedInWithUsername = candidate.detectedUsername ? hasTokensForUrl(normalizedSelected, candidate.detectedUsername) : false;
50452
- const loggedInAccounts = auth_getAccountsForUrl(normalizedSelected);
50453
- if (isLoggedInAny) {
50454
- if (candidate.detectedUsername && !isLoggedInWithUsername) {
50455
- logger_logger.warn(chalk_source.yellow("Detected from git remote: ") + chalk_source.bold(`app ID ${candidate.appId}`) + chalk_source.yellow(" on ") + chalk_source.bold(normalizedSelected) + chalk_source.yellow(" with username ") + chalk_source.bold(candidate.detectedUsername) + chalk_source.yellow(", but you're logged in as ") + chalk_source.bold(loggedInAccounts.join(", ")) + chalk_source.yellow("."));
50456
- logger_logger.info(chalk_source.gray(" To log in, run: ") + chalk_source.cyan(`anvil login ${normalizedSelected}`));
50457
- logger_logger.info(chalk_source.gray(" Make sure you're logged in as ") + chalk_source.bold(candidate.detectedUsername) + chalk_source.gray(" in your browser."));
50540
+ const choices = accounts.map((acct)=>({
50541
+ name: acct,
50542
+ value: acct
50543
+ }));
50544
+ choices.push({
50545
+ name: "Cancel",
50546
+ value: null
50547
+ });
50548
+ const selected = await logger_logger.select("Multiple accounts found. Which account owns this app?", choices, accounts[0]);
50549
+ if (null === selected) {
50550
+ logger_logger.warn("Operation cancelled.");
50551
+ process.exit(0);
50458
50552
  }
50459
- } else {
50460
- logger_logger.warn(chalk_source.yellow("Detected from git remote: ") + chalk_source.bold(`app ID ${candidate.appId}`) + chalk_source.yellow(" on ") + chalk_source.bold(normalizedSelected) + chalk_source.yellow(", but you're not logged in to this URL."));
50461
- logger_logger.info(chalk_source.gray(" To log in, run: ") + chalk_source.cyan(`anvil login ${normalizedSelected}`));
50462
- if (candidate.detectedUsername) logger_logger.info(chalk_source.gray(" Make sure you're logged in as ") + chalk_source.bold(candidate.detectedUsername) + chalk_source.gray(" in your browser."));
50553
+ username = selected;
50463
50554
  }
50464
50555
  }
50556
+ if (username) logger_logger.verbose(chalk_source.cyan("Using account: ") + chalk_source.bold(username));
50465
50557
  if (!hasTokensForUrl(anvilUrl, username)) {
50466
50558
  if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
50467
50559
  else logger_logger.error(`Not logged in to ${anvilUrl}`);
50468
50560
  logger_logger.verbose(chalk_source.yellow("Please log in first:"));
50469
- console.log(chalk_source.cyan(` anvil login ${anvilUrl}`));
50561
+ console.log(chalk_source.cyan(` anvil login ${anvilUrl.replace(/^https?:\/\//, "")}`));
50470
50562
  process.exit(1);
50471
50563
  }
50472
- logger_logger.verbose(chalk_source.green("✓ Authentication tokens found for this URL"));
50473
- let finalAppId = explicitAppId;
50474
- if (!finalAppId) {
50475
- logger_logger.verbose(chalk_source.cyan("No app ID provided, attempting auto-detection..."));
50476
- logger_logger.progress("detect", "Auto-detecting app ID...");
50477
- let candidates = await detectAppIds(repoPath, {
50478
- includeReverseLookup: false,
50479
- anvilUrl
50480
- });
50481
- logger_logger.progressEnd("detect");
50482
- if (0 === candidates.length) {
50483
- logger_logger.verbose(chalk_source.gray("Fast lookup found no matching app IDs."));
50484
- const shouldContinue = await logger_logger.confirm(`Search ${anvilUrl} for matching app IDs? (slower)`, true);
50485
- if (shouldContinue) {
50486
- logger_logger.progress("detect", `Searching ${anvilUrl} for matching app IDs...`);
50487
- candidates = await detectAppIds(repoPath, {
50488
- anvilUrl
50489
- });
50490
- logger_logger.progressEnd("detect");
50491
- }
50492
- }
50493
- if (candidates.length > 0) {
50494
- logger_logger.verbose(chalk_source.gray("Checking git remotes:"));
50495
- candidates.filter((c)=>"remote" === c.source).forEach((c)=>{
50496
- logger_logger.verbose(chalk_source.gray(` Found: ${c.description}`));
50497
- logger_logger.verbose(chalk_source.green("Found app ID in remote: ") + chalk_source.bold(c.appId));
50498
- });
50499
- if (candidates.some((c)=>"config" === c.source)) candidates.filter((c)=>"config" === c.source).forEach((c)=>{
50500
- logger_logger.verbose(chalk_source.gray(`Checking ${c.description.split("'")[1]} config...`));
50501
- logger_logger.verbose(chalk_source.green("Found app ID in config: ") + chalk_source.bold(c.appId));
50502
- });
50503
- if (candidates.length > 1) logger_logger.verbose(chalk_source.yellow(`Found ${candidates.length} potential app IDs:`));
50504
- }
50505
- if (useFirst && candidates.length > 0) {
50506
- finalAppId = candidates[0].appId;
50507
- logger_logger.success("Auto-selected first app ID: " + chalk_source.bold(finalAppId));
50508
- } else {
50509
- const selectedAppId = await selectAppId(candidates);
50510
- if (!selectedAppId) {
50511
- logger_logger.error("No app ID provided. Cannot continue without an app ID.");
50512
- process.exit(1);
50513
- }
50514
- finalAppId = selectedAppId;
50515
- }
50516
- }
50517
- logger_logger.verbose(chalk_source.green("Using app ID: ") + chalk_source.bold(finalAppId));
50564
+ logger_logger.verbose(chalk_source.green("✓ Authentication tokens found"));
50518
50565
  logger_logger.progress("validate", `Validating app ID: ${finalAppId}`);
50519
50566
  const appIdValidation = await validateAppId(finalAppId, anvilUrl, username);
50520
50567
  logger_logger.progressEnd("validate");
package/dist/index.js CHANGED
@@ -11980,28 +11980,35 @@ var __webpack_exports__ = {};
11980
11980
  "use strict";
11981
11981
  __webpack_require__.r(__webpack_exports__);
11982
11982
  __webpack_require__.d(__webpack_exports__, {
11983
- syncToLatest: ()=>syncToLatest,
11984
- validateAnvilApp: ()=>validateAnvilApp,
11985
- validateBranchSyncStatus: ()=>validateBranchSyncStatus,
11986
11983
  verifyAuth: ()=>verifyAuth,
11987
- watch: ()=>api_watch,
11988
- detectAppIds: ()=>detectAppIds,
11989
- hasTokensForUrl: ()=>hasTokensForUrl,
11984
+ lookupRemoteInfoForAppId: ()=>lookupRemoteInfoForAppId,
11985
+ validateBranchSyncStatus: ()=>validateBranchSyncStatus,
11986
+ AppIdWithContext: ()=>anvil_api_namespaceObject.AppIdWithContext,
11990
11987
  checkUncommittedChanges: ()=>checkUncommittedChanges,
11991
- getValidAuthToken: ()=>auth_getValidAuthToken,
11992
- login: ()=>login,
11988
+ hasTokensForUrl: ()=>hasTokensForUrl,
11989
+ validateAnvilApp: ()=>validateAnvilApp,
11993
11990
  logout: ()=>logout,
11994
11991
  detectAppIdsFromAllRemotes: ()=>detectAppIdsFromAllRemotes,
11992
+ BranchSyncStatus: ()=>validation_namespaceObject.BranchSyncStatus,
11993
+ formatCandidateLabel: ()=>formatCandidateLabel,
11994
+ syncToLatest: ()=>syncToLatest,
11995
+ filterCandidates: ()=>filterCandidates,
11996
+ getValidAuthToken: ()=>auth_getValidAuthToken,
11997
+ login: ()=>login,
11998
+ watch: ()=>api_watch,
11995
11999
  AppIdCandidate: ()=>anvil_api_namespaceObject.AppIdCandidate,
11996
- BranchSyncStatus: ()=>validation_namespaceObject.BranchSyncStatus
12000
+ detectAppIdsByCommitLookup: ()=>detectAppIdsByCommitLookup
11997
12001
  });
11998
12002
  var anvil_api_namespaceObject = {};
11999
12003
  __webpack_require__.r(anvil_api_namespaceObject);
12000
12004
  __webpack_require__.d(anvil_api_namespaceObject, {
12001
- PH: ()=>detectAppIds,
12005
+ lx: ()=>detectAppIdsByCommitLookup,
12002
12006
  NZ: ()=>detectAppIdsFromAllRemotes,
12007
+ rZ: ()=>filterCandidates,
12008
+ T_: ()=>formatCandidateLabel,
12003
12009
  OI: ()=>getGitFetchUrl,
12004
- $0: ()=>getWebSocketUrl
12010
+ $0: ()=>getWebSocketUrl,
12011
+ Wj: ()=>lookupRemoteInfoForAppId
12005
12012
  });
12006
12013
  var validation_namespaceObject = {};
12007
12014
  __webpack_require__.r(validation_namespaceObject);
@@ -22516,38 +22523,36 @@ var __webpack_exports__ = {};
22516
22523
  function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
22517
22524
  return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
22518
22525
  }
22519
- async function detectAppIdsFromRemotes(repoPath, anvilUrl) {
22520
- const git = esm_default(repoPath);
22521
- const out = [];
22522
- try {
22523
- const remotes = await git.getRemotes(true);
22524
- const url = new URL(anvilUrl);
22525
- const expectedHost = url.hostname;
22526
- for (const remote of remotes){
22527
- const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
22528
- if (httpMatch) {
22529
- const [, host, detectedAppId] = httpMatch;
22530
- if (host === expectedHost) {
22531
- out.push({
22532
- appId: detectedAppId,
22533
- source: "remote",
22534
- description: `Git remote '${remote.name}'`
22535
- });
22536
- continue;
22537
- }
22538
- }
22539
- const sshMatch = remote.refs.fetch?.match(/ssh:\/\/(?:[^@]+@)?([^:\/]+):(\d+)\/([A-Z0-9]+)\.git/);
22540
- if (sshMatch) {
22541
- const [, host, , detectedAppId] = sshMatch;
22542
- if (host === expectedHost) out.push({
22543
- appId: detectedAppId,
22544
- source: "remote",
22545
- description: `Git remote '${remote.name}' (SSH)`
22546
- });
22547
- }
22548
- }
22549
- } catch (_e) {}
22550
- return out;
22526
+ function filterCandidates(candidates, explicitUrl, explicitUsername) {
22527
+ let filtered = candidates;
22528
+ if (explicitUrl) {
22529
+ const normalizedExplicit = config_normalizeAnvilUrl(explicitUrl);
22530
+ filtered = filtered.filter((c)=>c.detectedUrl && config_normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
22531
+ }
22532
+ if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
22533
+ return filtered;
22534
+ }
22535
+ function formatCandidateLabel(candidate) {
22536
+ const parts = [
22537
+ candidate.appId
22538
+ ];
22539
+ if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
22540
+ else parts.push(`(${candidate.detectedUrl})`);
22541
+ parts.push(`- ${candidate.description}`);
22542
+ return parts.join(" ");
22543
+ }
22544
+ function lookupRemoteInfoForAppId(appId, detectedRemotes) {
22545
+ const matches = detectedRemotes.filter((c)=>c.appId === appId);
22546
+ if (0 === matches.length) return {};
22547
+ const withUsername = matches.find((c)=>c.detectedUsername);
22548
+ if (withUsername) return {
22549
+ detectedUrl: withUsername.detectedUrl,
22550
+ detectedUsername: withUsername.detectedUsername
22551
+ };
22552
+ return {
22553
+ detectedUrl: matches[0].detectedUrl,
22554
+ detectedUsername: matches[0].detectedUsername
22555
+ };
22551
22556
  }
22552
22557
  async function detectAppIdsFromAllRemotes(repoPath) {
22553
22558
  const git = esm_default(repoPath);
@@ -22583,11 +22588,13 @@ var __webpack_exports__ = {};
22583
22588
  } catch (_e) {}
22584
22589
  return out;
22585
22590
  }
22586
- async function detectAppIdsByCommitLookup(repoPath, anvilUrl) {
22591
+ async function detectAppIdsByCommitLookup(repoPath, options) {
22592
+ const anvilUrl = options.anvilUrl || resolveAnvilUrl();
22593
+ const username = options.username;
22587
22594
  const git = esm_default(repoPath);
22588
22595
  const out = [];
22589
22596
  try {
22590
- const authToken = await auth_getValidAuthToken(anvilUrl);
22597
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
22591
22598
  const branchRef = await git.revparse([
22592
22599
  "--abbrev-ref",
22593
22600
  "HEAD"
@@ -22626,16 +22633,6 @@ var __webpack_exports__ = {};
22626
22633
  } catch (_e) {}
22627
22634
  return out;
22628
22635
  }
22629
- async function detectAppIds(repoPath, options = {}) {
22630
- const anvilUrl = options.anvilUrl || resolveAnvilUrl();
22631
- const results = [];
22632
- const includeReverseLookup = options.includeReverseLookup ?? true;
22633
- results.push(...await detectAppIdsFromRemotes(repoPath, anvilUrl));
22634
- if (0 === results.length && includeReverseLookup) results.push(...await detectAppIdsByCommitLookup(repoPath, anvilUrl));
22635
- const seen = new Set();
22636
- const unique = results.filter((c)=>seen.has(c.appId) ? false : (seen.add(c.appId), true));
22637
- return unique;
22638
- }
22639
22636
  class WebSocketClient extends Emitter {
22640
22637
  ws = null;
22641
22638
  appId;
@@ -22646,9 +22643,15 @@ var __webpack_exports__ = {};
22646
22643
  username;
22647
22644
  reconnectAttempts = 0;
22648
22645
  reconnectTimer = null;
22646
+ heartbeatTimer = null;
22647
+ pongTimeoutTimer = null;
22648
+ reconnectDelayMs;
22649
+ isClosing = false;
22649
22650
  sessionId;
22650
- MAX_RECONNECT_ATTEMPTS = 5;
22651
- RECONNECT_DELAY = 5000;
22651
+ RECONNECT_DELAY_BASE_MS = 5000;
22652
+ RECONNECT_DELAY_MAX_MS = 60000;
22653
+ HEARTBEAT_INTERVAL_MS = 30000;
22654
+ HEARTBEAT_TIMEOUT_MS = 10000;
22652
22655
  constructor(options){
22653
22656
  super();
22654
22657
  this.appId = options.appId;
@@ -22658,12 +22661,14 @@ var __webpack_exports__ = {};
22658
22661
  this.editSession = options.editSession;
22659
22662
  this.username = options.username;
22660
22663
  this.sessionId = Math.random().toString(36).substring(2, 10);
22664
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
22661
22665
  }
22662
22666
  async connect() {
22667
+ this.isClosing = false;
22663
22668
  if (this.ws?.readyState === ws_wrapper.OPEN) return void logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket already open, skipping connection");
22664
22669
  if (this.ws) {
22665
22670
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing existing WebSocket before reconnecting");
22666
- this.close();
22671
+ this.teardownSocket();
22667
22672
  }
22668
22673
  this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
22669
22674
  const wsUrl = getWebSocketUrl(this.appId, this.authToken, this.anvilUrl);
@@ -22673,6 +22678,8 @@ var __webpack_exports__ = {};
22673
22678
  logger_logger.verbose(chalk_source.green("🔌 Connected to Anvil WebSocket"));
22674
22679
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "WebSocket opened");
22675
22680
  this.reconnectAttempts = 0;
22681
+ this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
22682
+ this.startHeartbeat();
22676
22683
  const subscribeMsg = {
22677
22684
  cmd: "WATCH_APP",
22678
22685
  app: this.appId,
@@ -22683,6 +22690,7 @@ var __webpack_exports__ = {};
22683
22690
  this.emit("connected", void 0);
22684
22691
  });
22685
22692
  this.ws.on("message", (data)=>{
22693
+ this.markSocketResponsive();
22686
22694
  try {
22687
22695
  const msg = JSON.parse(data.toString());
22688
22696
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
@@ -22691,8 +22699,12 @@ var __webpack_exports__ = {};
22691
22699
  logger_logger.error(chalk_source.red(`Failed to parse WebSocket message: ${error.message}`));
22692
22700
  }
22693
22701
  });
22702
+ this.ws.on("pong", ()=>{
22703
+ this.markSocketResponsive();
22704
+ });
22694
22705
  this.ws.on("close", (code, reason)=>{
22695
22706
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Closed: code=${code}, reason=${reason.toString()}`);
22707
+ this.stopHeartbeat();
22696
22708
  this.ws = null;
22697
22709
  if (1008 === code || reason.toString().includes("unauthenticated")) {
22698
22710
  logger_logger.warn(chalk_source.yellow(" WebSocket authentication failed - changes from the Anvil Editor won't be detected"));
@@ -22700,21 +22712,12 @@ var __webpack_exports__ = {};
22700
22712
  this.emit("auth-failed", void 0);
22701
22713
  return;
22702
22714
  }
22715
+ if (this.isClosing) return;
22703
22716
  this.emit("disconnected", {
22704
22717
  code,
22705
22718
  reason: reason.toString()
22706
22719
  });
22707
- if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
22708
- this.reconnectAttempts++;
22709
- logger_logger.verbose(chalk_source.gray(`WebSocket disconnected, reconnecting in ${this.RECONNECT_DELAY / 1000}s (attempt ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})...`));
22710
- this.reconnectTimer = setTimeout(()=>{
22711
- this.connect().catch((error)=>{
22712
- this.emit("error", {
22713
- error: error instanceof Error ? error : new Error(String(error))
22714
- });
22715
- });
22716
- }, this.RECONNECT_DELAY);
22717
- } 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();
22718
22721
  });
22719
22722
  this.ws.on("error", (error)=>{
22720
22723
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Error: ${error.message}`);
@@ -22759,24 +22762,74 @@ var __webpack_exports__ = {};
22759
22762
  return this.editSession;
22760
22763
  }
22761
22764
  close() {
22765
+ this.isClosing = true;
22762
22766
  if (this.reconnectTimer) {
22763
22767
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Clearing reconnect timer");
22764
22768
  clearTimeout(this.reconnectTimer);
22765
22769
  this.reconnectTimer = null;
22766
22770
  }
22767
- if (this.ws) {
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;
22768
22796
  try {
22769
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Closing WebSocket");
22770
- this.ws.removeAllListeners();
22771
- if (this.ws.readyState === ws_wrapper.OPEN) this.ws.close();
22772
- } catch (e) {
22773
- logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
22797
+ ws.ping();
22798
+ } catch (error) {
22799
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Failed to send ping: ${error.message}`);
22800
+ ws.terminate();
22801
+ return;
22774
22802
  }
22775
- this.ws = null;
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;
22776
22813
  }
22777
22814
  }
22778
- isConnected() {
22779
- return this.ws?.readyState === ws_wrapper.OPEN;
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;
22780
22833
  }
22781
22834
  }
22782
22835
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
@@ -25003,7 +25056,11 @@ var __webpack_exports__ = {};
25003
25056
  wsClient = null;
25004
25057
  saveProcessor = null;
25005
25058
  syncManager = null;
25059
+ localChangePollTimer = null;
25060
+ isPollingLocalChanges = false;
25061
+ lastLocalStatusFingerprint = null;
25006
25062
  BRANCH_CHANGE_SETTLE_MS = 2000;
25063
+ LOCAL_CHANGE_POLL_MS = 2000;
25007
25064
  stagedOnly;
25008
25065
  constructor(repoPath, appId, options){
25009
25066
  super();
@@ -25141,6 +25198,10 @@ var __webpack_exports__ = {};
25141
25198
  this.fileWatcher.cleanup();
25142
25199
  this.fileWatcher = null;
25143
25200
  }
25201
+ if (this.localChangePollTimer) {
25202
+ clearInterval(this.localChangePollTimer);
25203
+ this.localChangePollTimer = null;
25204
+ }
25144
25205
  logger_logger.debug(`[Session ${this.sessionId}]`, "Cleanup complete");
25145
25206
  }
25146
25207
  async startWatching() {
@@ -25174,8 +25235,70 @@ var __webpack_exports__ = {};
25174
25235
  }
25175
25236
  });
25176
25237
  await this.fileWatcher.start(this.currentBranch);
25238
+ await this.initializeLocalChangeFingerprint();
25239
+ this.startLocalChangeFallbackPolling();
25177
25240
  await new Promise(()=>{});
25178
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
+ }
25179
25302
  async handleFileChange(event, filePath, relativePath) {
25180
25303
  if (this.isCleanedUp || this.isPausedForUserInput) return;
25181
25304
  logger_logger.verbose(chalk_source.blue("File changed: ") + chalk_source.bold(relativePath));
@@ -25459,7 +25582,8 @@ var __webpack_exports__ = {};
25459
25582
  authToken,
25460
25583
  currentBranch,
25461
25584
  commitId,
25462
- stagedOnly
25585
+ stagedOnly,
25586
+ username
25463
25587
  });
25464
25588
  session.on("branch-changed", (data)=>{
25465
25589
  logger_logger.debug("Event: branch-changed", data);
@@ -25490,14 +25614,18 @@ var __webpack_exports__ = {};
25490
25614
  }
25491
25615
  })();
25492
25616
  exports.AppIdCandidate = __webpack_exports__.AppIdCandidate;
25617
+ exports.AppIdWithContext = __webpack_exports__.AppIdWithContext;
25493
25618
  exports.BranchSyncStatus = __webpack_exports__.BranchSyncStatus;
25494
25619
  exports.checkUncommittedChanges = __webpack_exports__.checkUncommittedChanges;
25495
- exports.detectAppIds = __webpack_exports__.detectAppIds;
25620
+ exports.detectAppIdsByCommitLookup = __webpack_exports__.detectAppIdsByCommitLookup;
25496
25621
  exports.detectAppIdsFromAllRemotes = __webpack_exports__.detectAppIdsFromAllRemotes;
25622
+ exports.filterCandidates = __webpack_exports__.filterCandidates;
25623
+ exports.formatCandidateLabel = __webpack_exports__.formatCandidateLabel;
25497
25624
  exports.getValidAuthToken = __webpack_exports__.getValidAuthToken;
25498
25625
  exports.hasTokensForUrl = __webpack_exports__.hasTokensForUrl;
25499
25626
  exports.login = __webpack_exports__.login;
25500
25627
  exports.logout = __webpack_exports__.logout;
25628
+ exports.lookupRemoteInfoForAppId = __webpack_exports__.lookupRemoteInfoForAppId;
25501
25629
  exports.syncToLatest = __webpack_exports__.syncToLatest;
25502
25630
  exports.validateAnvilApp = __webpack_exports__.validateAnvilApp;
25503
25631
  exports.validateBranchSyncStatus = __webpack_exports__.validateBranchSyncStatus;
@@ -25505,14 +25633,18 @@ exports.verifyAuth = __webpack_exports__.verifyAuth;
25505
25633
  exports.watch = __webpack_exports__.watch;
25506
25634
  for(var __rspack_i in __webpack_exports__)if (-1 === [
25507
25635
  "AppIdCandidate",
25636
+ "AppIdWithContext",
25508
25637
  "BranchSyncStatus",
25509
25638
  "checkUncommittedChanges",
25510
- "detectAppIds",
25639
+ "detectAppIdsByCommitLookup",
25511
25640
  "detectAppIdsFromAllRemotes",
25641
+ "filterCandidates",
25642
+ "formatCandidateLabel",
25512
25643
  "getValidAuthToken",
25513
25644
  "hasTokensForUrl",
25514
25645
  "login",
25515
25646
  "logout",
25647
+ "lookupRemoteInfoForAppId",
25516
25648
  "syncToLatest",
25517
25649
  "validateAnvilApp",
25518
25650
  "validateBranchSyncStatus",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",