@anvil-works/anvil-cli 0.8.0-canary.8 → 0.8.0-dev.0

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 (47) hide show
  1. package/dist/WatchSession.d.ts +2 -0
  2. package/dist/WatchSession.d.ts.map +1 -1
  3. package/dist/api.d.ts +5 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/cli.js +1583 -1068
  6. package/dist/commands/configure.d.ts.map +1 -1
  7. package/dist/commands/db.d.ts +3 -0
  8. package/dist/commands/db.d.ts.map +1 -0
  9. package/dist/commands/deps.d.ts.map +1 -1
  10. package/dist/commands/env.d.ts +8 -0
  11. package/dist/commands/env.d.ts.map +1 -0
  12. package/dist/commands/index.d.ts +3 -1
  13. package/dist/commands/index.d.ts.map +1 -1
  14. package/dist/commands/login.d.ts.map +1 -1
  15. package/dist/commands/modelToken.d.ts +33 -0
  16. package/dist/commands/modelToken.d.ts.map +1 -0
  17. package/dist/commands/repl.d.ts +3 -0
  18. package/dist/commands/repl.d.ts.map +1 -0
  19. package/dist/commands/watch.d.ts.map +1 -1
  20. package/dist/errors.d.ts +11 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/index.js +1257 -988
  23. package/dist/program.d.ts.map +1 -1
  24. package/dist/services/anvil-api.d.ts.map +1 -1
  25. package/dist/services/auth.d.ts +42 -3
  26. package/dist/services/auth.d.ts.map +1 -1
  27. package/dist/services/db.d.ts +7 -0
  28. package/dist/services/db.d.ts.map +1 -0
  29. package/dist/services/deps.d.ts.map +1 -1
  30. package/dist/services/environment.d.ts +16 -0
  31. package/dist/services/environment.d.ts.map +1 -0
  32. package/dist/services/git-auth.d.ts +35 -0
  33. package/dist/services/git-auth.d.ts.map +1 -1
  34. package/dist/services/git.d.ts +1 -0
  35. package/dist/services/git.d.ts.map +1 -1
  36. package/dist/services/http.d.ts +2 -0
  37. package/dist/services/http.d.ts.map +1 -0
  38. package/dist/services/repl.d.ts +18 -0
  39. package/dist/services/repl.d.ts.map +1 -0
  40. package/dist/validatePython.d.ts.map +1 -1
  41. package/dist/watch/SaveProcessor.d.ts +2 -0
  42. package/dist/watch/SaveProcessor.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/dist/commands/tables.d.ts +0 -7
  45. package/dist/commands/tables.d.ts.map +0 -1
  46. package/dist/services/tables.d.ts +0 -27
  47. package/dist/services/tables.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -13384,9 +13384,10 @@ var __webpack_exports__ = {};
13384
13384
  type: "auth_invalid",
13385
13385
  message
13386
13386
  }),
13387
- refreshFailed: (message)=>({
13387
+ refreshFailed: (message, status)=>({
13388
13388
  type: "token_refresh_failed",
13389
- message
13389
+ message,
13390
+ status
13390
13391
  })
13391
13392
  };
13392
13393
  const createAppError = {
@@ -13600,6 +13601,19 @@ var __webpack_exports__ = {};
13600
13601
  return "An unknown error occurred";
13601
13602
  }
13602
13603
  }
13604
+ function getErrorCauseMessage(error) {
13605
+ if ("object" != typeof error || null === error || !("cause" in error)) return null;
13606
+ const cause = error.cause;
13607
+ if (null == cause) return null;
13608
+ const message = errors_getErrorMessage(cause);
13609
+ const code = "object" == typeof cause && null !== cause && "code" in cause && "string" == typeof cause.code ? cause.code : null;
13610
+ if (code && !message.includes(code)) return `${message} (${code})`;
13611
+ return message;
13612
+ }
13613
+ function logErrorCause(error, ui) {
13614
+ const causeMessage = getErrorCauseMessage(error);
13615
+ if (causeMessage) ui.verbose(`Cause: ${causeMessage}`);
13616
+ }
13603
13617
  const external_module_namespaceObject = require("module");
13604
13618
  const requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
13605
13619
  const GIT_CREDENTIAL_HELPER_FILENAME = "anvil-credential-helper.cjs";
@@ -13820,6 +13834,76 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13820
13834
  function getAuthFilePath(gitDir) {
13821
13835
  return external_path_default().join(gitDir, "anvil-auth.json");
13822
13836
  }
13837
+ const AUTH_FILE_LOCK_DEFAULTS = {
13838
+ staleMs: 30000,
13839
+ timeoutMs: 60000,
13840
+ retryDelayMs: 100,
13841
+ heartbeatMs: 5000
13842
+ };
13843
+ function git_auth_delay(ms) {
13844
+ return new Promise((resolve)=>setTimeout(resolve, ms));
13845
+ }
13846
+ async function acquireAuthFileLock(lockPath, options) {
13847
+ const deadline = Date.now() + options.timeoutMs;
13848
+ while(true){
13849
+ try {
13850
+ const handle = await external_fs_.promises.open(lockPath, "wx", 384);
13851
+ try {
13852
+ await handle.writeFile(JSON.stringify({
13853
+ pid: process.pid,
13854
+ acquiredAt: new Date().toISOString()
13855
+ }));
13856
+ } finally{
13857
+ await handle.close();
13858
+ }
13859
+ return;
13860
+ } catch (e) {
13861
+ if ("EEXIST" !== e.code) throw e;
13862
+ }
13863
+ try {
13864
+ const lockStat = await external_fs_.promises.stat(lockPath);
13865
+ if (Date.now() - lockStat.mtimeMs > options.staleMs) {
13866
+ await external_fs_.promises.rm(lockPath, {
13867
+ force: true
13868
+ });
13869
+ continue;
13870
+ }
13871
+ } catch {
13872
+ continue;
13873
+ }
13874
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Anvil auth file lock at ${lockPath}`);
13875
+ await git_auth_delay(options.retryDelayMs);
13876
+ }
13877
+ }
13878
+ async function withRepoAuthFileLock(repoPath, fn, options) {
13879
+ const gitDir = await getRepositoryGitDir(repoPath);
13880
+ const lockPath = `${getAuthFilePath(gitDir)}.lock`;
13881
+ const lockOptions = {
13882
+ ...AUTH_FILE_LOCK_DEFAULTS,
13883
+ ...options
13884
+ };
13885
+ await external_fs_.promises.mkdir(external_path_default().dirname(lockPath), {
13886
+ recursive: true
13887
+ });
13888
+ await acquireAuthFileLock(lockPath, lockOptions);
13889
+ const heartbeat = startAuthFileLockHeartbeat(lockPath, lockOptions.heartbeatMs);
13890
+ try {
13891
+ return await fn();
13892
+ } finally{
13893
+ clearInterval(heartbeat);
13894
+ await external_fs_.promises.rm(lockPath, {
13895
+ force: true
13896
+ });
13897
+ }
13898
+ }
13899
+ function startAuthFileLockHeartbeat(lockPath, heartbeatMs) {
13900
+ const heartbeat = setInterval(()=>{
13901
+ const now = new Date();
13902
+ external_fs_.promises.utimes(lockPath, now, now).catch(()=>{});
13903
+ }, heartbeatMs);
13904
+ heartbeat.unref();
13905
+ return heartbeat;
13906
+ }
13823
13907
  function decodeKey(b64key) {
13824
13908
  const key = Buffer.from(b64key, "base64");
13825
13909
  if (32 !== key.length) throw new Error("Encryption key must be a base64-encoded 32-byte key");
@@ -13864,10 +13948,19 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13864
13948
  recursive: true
13865
13949
  });
13866
13950
  const authJson = JSON.stringify(auth, null, 2);
13867
- await external_fs_.promises.writeFile(authFilePath, encrypt(authJson, encryptionKey), {
13868
- mode: 384
13869
- });
13870
- await external_fs_.promises.chmod(authFilePath, 448);
13951
+ const tempFilePath = `${authFilePath}.tmp-${external_node_crypto_default().randomBytes(6).toString("hex")}`;
13952
+ try {
13953
+ await external_fs_.promises.writeFile(tempFilePath, encrypt(authJson, encryptionKey), {
13954
+ mode: 384
13955
+ });
13956
+ await external_fs_.promises.chmod(tempFilePath, 448);
13957
+ await external_fs_.promises.rename(tempFilePath, authFilePath);
13958
+ } catch (e) {
13959
+ await external_fs_.promises.rm(tempFilePath, {
13960
+ force: true
13961
+ });
13962
+ throw e;
13963
+ }
13871
13964
  await git.raw([
13872
13965
  "config",
13873
13966
  "--local",
@@ -13876,8 +13969,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13876
13969
  ]);
13877
13970
  return authFilePath;
13878
13971
  }
13879
- const readAuthFromRepo = async (repoPath)=>{
13972
+ const git_auth_readAuthFromRepo = async (repoPath)=>{
13880
13973
  const git = esm_default(repoPath);
13974
+ if (!await git.checkIsRepo()) return;
13881
13975
  const gitDir = await getRepositoryGitDir(repoPath, git);
13882
13976
  const authFilePath = getAuthFilePath(gitDir);
13883
13977
  if (!external_fs_default().existsSync(authFilePath)) return;
@@ -13889,14 +13983,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13889
13983
  return resolveAnvilUrl();
13890
13984
  }
13891
13985
  const ANVIL_SYNC_CLIENT_ID = "anvil-sync";
13986
+ const REFRESH_REQUEST_TIMEOUT_MS = 12000;
13892
13987
  let inMemoryAuth = null;
13893
13988
  function authMatches(auth, anvilUrl, username) {
13894
13989
  return auth.anvilUrl === normalizeAnvilUrl(anvilUrl) && (!username || auth.username === username);
13895
13990
  }
13896
13991
  let repoContext = null;
13897
- function setRepoContext(repoPath) {
13992
+ function auth_setRepoContext(repoPath) {
13898
13993
  repoContext = repoPath;
13899
13994
  }
13995
+ async function resolveAuthAnvilUrl() {
13996
+ if (inMemoryAuth) return inMemoryAuth.anvilUrl;
13997
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13998
+ return repoAuth?.anvilUrl ?? resolveAnvilUrl();
13999
+ }
13900
14000
  const inFlightRefreshes = new Map();
13901
14001
  async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13902
14002
  try {
@@ -13917,7 +14017,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13917
14017
  throw errors_createAuthError.invalid(`Network error: ${e.message}`);
13918
14018
  }
13919
14019
  }
13920
- async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID) {
14020
+ async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID, options) {
13921
14021
  try {
13922
14022
  const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
13923
14023
  method: "POST",
@@ -13928,11 +14028,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13928
14028
  grant_type: "refresh_token",
13929
14029
  refresh_token: refreshToken,
13930
14030
  client_id: clientId
13931
- })
14031
+ }),
14032
+ signal: AbortSignal.timeout(options?.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS)
13932
14033
  });
13933
14034
  if (!tokenResponse.ok) {
13934
14035
  const errorText = await tokenResponse.text();
13935
- throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`);
14036
+ throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`, tokenResponse.status);
13936
14037
  }
13937
14038
  const tokenData = await tokenResponse.json();
13938
14039
  return tokenData;
@@ -13944,22 +14045,21 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13944
14045
  async function hasTokensForUrl(anvilUrl, username) {
13945
14046
  if (inMemoryAuth) return authMatches(inMemoryAuth, anvilUrl, username);
13946
14047
  {
13947
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14048
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13948
14049
  if (repoAuth && authMatches(repoAuth, anvilUrl, username)) return true;
13949
14050
  return hasTokens(anvilUrl, username);
13950
14051
  }
13951
14052
  }
13952
- async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
14053
+ async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username, options) {
13953
14054
  const normalized = normalizeAnvilUrl(anvilUrl);
13954
14055
  const source = await resolveTokenSource(normalized, username);
13955
14056
  const tokens = getSourceTokens(source);
13956
14057
  if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
13957
- const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
13958
- if (tokens.refreshToken && isExpired) {
14058
+ if (tokens.refreshToken && isAccessTokenExpired(tokens)) {
13959
14059
  const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13960
14060
  const existingRefresh = inFlightRefreshes.get(refreshKey);
13961
14061
  if (existingRefresh) return existingRefresh;
13962
- const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
14062
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken, options).finally(()=>{
13963
14063
  inFlightRefreshes.delete(refreshKey);
13964
14064
  });
13965
14065
  inFlightRefreshes.set(refreshKey, refreshPromise);
@@ -13973,7 +14073,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13973
14073
  type: "memory",
13974
14074
  auth: inMemoryAuth
13975
14075
  };
13976
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14076
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13977
14077
  if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13978
14078
  type: "repo",
13979
14079
  auth: repoAuth,
@@ -13989,6 +14089,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13989
14089
  function getSourceTokens(source) {
13990
14090
  return "store" === source.type ? source.tokens : source.auth.tokens;
13991
14091
  }
14092
+ function isAccessTokenExpired(tokens) {
14093
+ return null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
14094
+ }
13992
14095
  function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13993
14096
  if (username) return username;
13994
14097
  const urlTokens = getUrlTokens(normalizedUrl);
@@ -14007,11 +14110,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14007
14110
  return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
14008
14111
  }
14009
14112
  }
14010
- async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
14113
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken, options) {
14114
+ if ("repo" === source.type) return refreshRepoAuthTokens(source.repoPath, {
14115
+ anvilUrl: normalizedUrl,
14116
+ username,
14117
+ auth: source.auth,
14118
+ refreshToken,
14119
+ lock: options?.lock,
14120
+ requestTimeoutMs: options?.requestTimeoutMs
14121
+ });
14122
+ return performTokenRefresh(normalizedUrl, username, source, refreshToken, options?.requestTimeoutMs);
14123
+ }
14124
+ async function refreshRepoAuthTokens(repoPath, options) {
14125
+ const normalizedUrl = normalizeAnvilUrl(options.anvilUrl);
14126
+ return withRepoAuthFileLock(repoPath, async ()=>{
14127
+ let auth = options.auth;
14128
+ let refreshToken = options.refreshToken;
14129
+ const current = await git_auth_readAuthFromRepo(repoPath);
14130
+ if (current && authMatches(current, normalizedUrl, options.username)) {
14131
+ if (current.tokens.authToken && !isAccessTokenExpired(current.tokens)) return current.tokens.authToken;
14132
+ auth = current;
14133
+ refreshToken = current.tokens.refreshToken ?? refreshToken;
14134
+ }
14135
+ if (!auth || !refreshToken) throw errors_createAuthError.required("No repo auth tokens available to refresh.");
14136
+ return performTokenRefresh(normalizedUrl, options.username, {
14137
+ type: "repo",
14138
+ auth,
14139
+ repoPath
14140
+ }, refreshToken, options.requestTimeoutMs);
14141
+ }, options.lock);
14142
+ }
14143
+ async function performTokenRefresh(normalizedUrl, username, source, refreshToken, requestTimeoutMs) {
14011
14144
  try {
14012
14145
  const tokens = getSourceTokens(source);
14013
14146
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
14014
- const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
14147
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId, {
14148
+ timeoutMs: requestTimeoutMs
14149
+ });
14015
14150
  const newTokens = {
14016
14151
  authToken: tokenData.access_token,
14017
14152
  refreshToken: tokenData.refresh_token,
@@ -14033,12 +14168,28 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14033
14168
  }
14034
14169
  return tokenData.access_token;
14035
14170
  } catch (e) {
14171
+ if ("repo" === source.type && isRefreshTokenRejection(e)) await clearRepoAuthTokens(source.repoPath, source.auth).catch(()=>{});
14036
14172
  await clearTokensForAccount(normalizedUrl, username, refreshToken);
14037
14173
  throw e;
14038
14174
  }
14039
14175
  }
14176
+ function isRefreshTokenRejection(e) {
14177
+ const error = e;
14178
+ return error?.type === "token_refresh_failed" && (400 === error.status || 401 === error.status);
14179
+ }
14180
+ async function clearRepoAuthTokens(repoPath, auth) {
14181
+ const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
14182
+ if (!encryptionKey) return;
14183
+ auth.tokens = {
14184
+ authToken: null,
14185
+ refreshToken: null,
14186
+ authTokenExpiresAt: null,
14187
+ clientId: auth.tokens.clientId
14188
+ };
14189
+ await writeAuthToFile(repoPath, auth, encryptionKey);
14190
+ }
14040
14191
  async function clearTokensForAccount(url, username, refreshToken) {
14041
- if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
14192
+ if (inMemoryAuth || repoContext && await git_auth_readAuthFromRepo(repoContext)) return;
14042
14193
  {
14043
14194
  const normalized = normalizeAnvilUrl(url);
14044
14195
  if (username) deleteAccountTokens(normalized, username);
@@ -14068,7 +14219,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14068
14219
  loggedOut: false,
14069
14220
  message: "Cannot log out when using in-memory auth tokens"
14070
14221
  };
14071
- if (repoContext && await readAuthFromRepo(repoContext)) return {
14222
+ if (repoContext && await git_auth_readAuthFromRepo(repoContext)) return {
14072
14223
  loggedOut: false,
14073
14224
  message: "Cannot log out when using auth tokens stored in the local git config."
14074
14225
  };
@@ -14099,7 +14250,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14099
14250
  inMemoryAuth.username
14100
14251
  ] : [];
14101
14252
  {
14102
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14253
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14103
14254
  if (repoAuth && authMatches(repoAuth, url)) return [
14104
14255
  repoAuth.username
14105
14256
  ];
@@ -14111,1030 +14262,1062 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14111
14262
  inMemoryAuth.anvilUrl
14112
14263
  ];
14113
14264
  {
14114
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14265
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14115
14266
  if (repoAuth) return [
14116
14267
  repoAuth.anvilUrl
14117
14268
  ];
14118
14269
  return getAvailableUrls();
14119
14270
  }
14120
14271
  }
14121
- function anvil_api_getDefaultAnvilUrl() {
14122
- return resolveAnvilUrl();
14272
+ const promises_namespaceObject = require("timers/promises");
14273
+ const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
14274
+ const FETCH_RETRY_DELAY_MS = 200;
14275
+ function isWantNotValidError(error) {
14276
+ return WANT_NOT_VALID_RE.test(error.message || "");
14123
14277
  }
14124
- async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14125
- const authToken = await auth_getValidAuthToken(anvilUrl, username);
14126
- try {
14127
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
14128
- method: "POST",
14129
- headers: {
14130
- Authorization: `Bearer ${authToken}`,
14131
- "Content-Type": "application/json"
14132
- },
14133
- body: JSON.stringify({
14134
- app_id: appId
14135
- })
14136
- });
14137
- const data = await resp.json();
14138
- if (resp.ok) return {
14139
- valid: data.valid ?? false,
14140
- app_name: data.app_name
14141
- };
14142
- if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14143
- if (403 === resp.status) throw createAppError.accessDenied(appId);
14144
- if (404 === resp.status) throw createAppError.notFound(appId);
14145
- logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
14146
- return {
14147
- valid: false,
14148
- error: data.error || `Server error: ${resp.status}`
14149
- };
14150
- } catch (error) {
14151
- if (error.type) throw error;
14152
- throw errors_createNetworkError.network(error.message);
14278
+ class GitService extends Emitter {
14279
+ git;
14280
+ repoPath;
14281
+ static INDEX_LOCK_RETRY_DELAYS_MS = [
14282
+ 25,
14283
+ 50,
14284
+ 100,
14285
+ 200
14286
+ ];
14287
+ constructor(repoPath){
14288
+ super();
14289
+ this.repoPath = external_path_default().resolve(repoPath);
14290
+ this.git = esm_default(this.repoPath);
14153
14291
  }
14154
- }
14155
- async function listAppsForCheckout(options = {}) {
14156
- const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
14157
- const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
14158
- const params = new URLSearchParams();
14159
- if ("number" == typeof options.limit) params.set("limit", String(options.limit));
14160
- if (options.cursor) params.set("cursor", options.cursor);
14161
- if (options.q && options.q.trim()) params.set("q", options.q.trim());
14162
- const query = params.toString();
14163
- const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
14164
- try {
14165
- const resp = await fetch(url, {
14166
- method: "GET",
14167
- headers: {
14168
- Authorization: `Bearer ${authToken}`
14169
- },
14170
- signal: options.signal
14171
- });
14172
- if (!resp.ok) {
14173
- if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14174
- throw errors_createNetworkError.server(resp.status, resp.statusText);
14175
- }
14176
- const data = await resp.json();
14177
- return {
14178
- apps: Array.isArray(data.apps) ? data.apps : [],
14179
- next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
14180
- };
14181
- } catch (error) {
14182
- if (error.type) throw error;
14183
- throw errors_createNetworkError.network(error.message);
14292
+ getGit() {
14293
+ return this.git;
14184
14294
  }
14185
- }
14186
- function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
14187
- const url = new URL(gitUrl);
14188
- const encodedToken = encodeURIComponent(authToken);
14189
- return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
14190
- }
14191
- function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14192
- const url = new URL(anvilUrl);
14193
- const encodedToken = encodeURIComponent(authToken);
14194
- return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
14195
- }
14196
- function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14197
- return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
14198
- }
14199
- async function getLatestVersion() {
14200
- try {
14201
- const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
14202
- if (!response.ok) return null;
14203
- const data = await response.json();
14204
- return data.version;
14205
- } catch (e) {
14206
- return null;
14295
+ getRepoPath() {
14296
+ return this.repoPath;
14207
14297
  }
14208
- }
14209
- function filterCandidates(candidates, explicitUrl, explicitUsername) {
14210
- let filtered = candidates;
14211
- if (explicitUrl) {
14212
- const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
14213
- filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
14298
+ async getGitDir() {
14299
+ try {
14300
+ const gitDir = (await this.git.revparse([
14301
+ "--git-dir"
14302
+ ])).trim();
14303
+ if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
14304
+ return gitDir;
14305
+ } catch (e) {
14306
+ throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14307
+ }
14214
14308
  }
14215
- if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
14216
- return filtered;
14217
- }
14218
- function formatCandidateLabel(candidate) {
14219
- const parts = [
14220
- candidate.appId
14221
- ];
14222
- if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
14223
- else parts.push(`(${candidate.detectedUrl})`);
14224
- parts.push(`- ${candidate.description}`);
14225
- return parts.join(" ");
14226
- }
14227
- function lookupRemoteInfoForAppId(appId, detectedRemotes) {
14228
- const matches = detectedRemotes.filter((c)=>c.appId === appId);
14229
- if (0 === matches.length) return {};
14230
- const withUsername = matches.find((c)=>c.detectedUsername);
14231
- if (withUsername) return {
14232
- detectedUrl: withUsername.detectedUrl,
14233
- detectedUsername: withUsername.detectedUsername
14234
- };
14235
- return {
14236
- detectedUrl: matches[0].detectedUrl,
14237
- detectedUsername: matches[0].detectedUsername
14238
- };
14239
- }
14240
- async function detectAppIdsFromAllRemotes(repoPath) {
14241
- const git = esm_default(repoPath);
14242
- const out = [];
14243
- try {
14244
- const remotes = await git.getRemotes(true);
14245
- for (const remote of remotes){
14246
- const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
14247
- if (httpMatch) {
14248
- const [, host, detectedAppId] = httpMatch;
14249
- out.push({
14250
- appId: detectedAppId,
14251
- source: "remote",
14252
- description: `Git remote '${remote.name}'`,
14253
- detectedUrl: normalizeAnvilUrl(host)
14254
- });
14255
- continue;
14256
- }
14257
- const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
14258
- if (sshMatch) {
14259
- const [, usernamePart, host, , detectedAppId] = sshMatch;
14260
- const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
14261
- const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
14262
- out.push({
14263
- appId: detectedAppId,
14264
- source: "remote",
14265
- description: `Git remote '${remote.name}' (SSH)`,
14266
- detectedUrl: normalizeAnvilUrl(host),
14267
- detectedUsername: finalUsername
14268
- });
14269
- }
14309
+ async getCurrentBranch() {
14310
+ try {
14311
+ const branchRef = await this.git.revparse([
14312
+ "--abbrev-ref",
14313
+ "HEAD"
14314
+ ]);
14315
+ if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14316
+ return branchRef;
14317
+ } catch (e) {
14318
+ if ("detached_head" === e.type) throw e;
14319
+ throw createSyncError.detachedHead();
14270
14320
  }
14271
- } catch (_e) {}
14272
- return out;
14273
- }
14274
- async function detectAppIdsByCommitLookup(repoPath, options) {
14275
- const anvilUrl = options.anvilUrl || resolveAnvilUrl();
14276
- const username = options.username;
14277
- const git = esm_default(repoPath);
14278
- const out = [];
14279
- try {
14280
- const authToken = await auth_getValidAuthToken(anvilUrl, username);
14281
- const branchRef = await git.revparse([
14282
- "--abbrev-ref",
14283
- "HEAD"
14284
- ]);
14285
- const currentBranch = "HEAD" === branchRef ? "master" : branchRef;
14286
- const commitId = (await git.revparse([
14287
- "HEAD"
14288
- ]))?.trim();
14289
- if (!commitId) return out;
14290
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
14291
- method: "POST",
14292
- headers: {
14293
- Authorization: `Bearer ${authToken}`,
14294
- "Content-Type": "application/json"
14295
- },
14296
- body: JSON.stringify({
14297
- commit_id: commitId,
14298
- branch_name: currentBranch
14299
- })
14300
- });
14301
- logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${currentBranch} returned ${resp.status}`);
14302
- if (!resp.ok) {
14303
- const errorBody = await resp.text();
14304
- logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
14321
+ }
14322
+ async getCommitId() {
14323
+ try {
14324
+ const commitId = (await this.git.revparse([
14325
+ "HEAD"
14326
+ ])).trim();
14327
+ return commitId;
14328
+ } catch (e) {
14329
+ throw errors_createGitError.commandFailed("revparse", e.message);
14305
14330
  }
14306
- if (resp.ok) {
14307
- const data = await resp.json();
14308
- logger_logger.debug(`data ${JSON.stringify(data)}`);
14309
- const apps = data?.apps ?? [];
14310
- for (const app of apps)if (app?.app_id) out.push({
14311
- appId: app.app_id,
14312
- source: "remote",
14313
- description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
14314
- });
14331
+ }
14332
+ async getCommitInfo() {
14333
+ try {
14334
+ const hash = (await this.git.revparse([
14335
+ "HEAD"
14336
+ ])).trim();
14337
+ const message = (await this.git.raw([
14338
+ "log",
14339
+ "-1",
14340
+ "--format=%s"
14341
+ ])).trim();
14342
+ return {
14343
+ hash,
14344
+ shortHash: hash.substring(0, 8),
14345
+ message
14346
+ };
14347
+ } catch (e) {
14348
+ throw errors_createGitError.commandFailed("log", e.message);
14315
14349
  }
14316
- } catch (_e) {}
14317
- return out;
14318
- }
14319
- const promises_namespaceObject = require("fs/promises");
14320
- var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
14321
- function normalizeLineEndings(content) {
14322
- return content.replace(/\r\n/g, "\n");
14323
- }
14324
- function pythonifyName(name) {
14325
- return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14326
- }
14327
- function extractPythonName(relativePath, skipParts = 1) {
14328
- const parts = relativePath.slice(0, -3).split("/");
14329
- const isPackage = relativePath.endsWith("__init__.py");
14330
- const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14331
- return relevantParts.map(pythonifyName).join(".");
14332
- }
14333
- function deepEqual(a, b) {
14334
- if (a === b) return true;
14335
- if (null == a || null == b) return false;
14336
- if (typeof a !== typeof b) return false;
14337
- if ("object" != typeof a) return a === b;
14338
- if (Array.isArray(a) !== Array.isArray(b)) return false;
14339
- if (Array.isArray(a)) {
14340
- if (a.length !== b.length) return false;
14341
- return a.every((item, index)=>deepEqual(item, b[index]));
14342
14350
  }
14343
- const keysA = Object.keys(a);
14344
- const keysB = Object.keys(b);
14345
- if (keysA.length !== keysB.length) return false;
14346
- return keysA.every((key)=>deepEqual(a[key], b[key]));
14347
- }
14348
- function parseHtmlWithFrontmatter(htmlContent) {
14349
- const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14350
- const match = htmlContent.match(frontmatterPattern);
14351
- if (match) {
14352
- const yamlStr = match[1];
14353
- const html = match[2];
14354
- const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14355
- return {
14356
- frontmatter,
14357
- html
14358
- };
14351
+ async getStatus() {
14352
+ try {
14353
+ const status = await this.git.status();
14354
+ const staged = status.files.filter((file)=>{
14355
+ const indexStatus = file.index.trim();
14356
+ return indexStatus.length > 0 && "?" !== indexStatus;
14357
+ }).map((file)=>file.path);
14358
+ return {
14359
+ isClean: status.isClean(),
14360
+ modified: status.modified,
14361
+ notAdded: status.not_added,
14362
+ created: status.created,
14363
+ deleted: status.deleted,
14364
+ staged,
14365
+ renamed: status.renamed || []
14366
+ };
14367
+ } catch (e) {
14368
+ throw errors_createGitError.commandFailed("status", e.message);
14369
+ }
14359
14370
  }
14360
- return {
14361
- frontmatter: {},
14362
- html: htmlContent
14363
- };
14364
- }
14365
- const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
14366
- async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
14367
- const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
14368
- try {
14369
- const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
14370
- if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
14371
- const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
14372
- await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14373
- return true;
14374
- } catch (error) {
14375
- if ("ENOENT" !== error.code) throw error;
14376
- await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14377
- return true;
14371
+ async discardModeOnlyChanges(paths) {
14372
+ const discarded = [];
14373
+ for (const relativePath of paths)try {
14374
+ const headMode = await this.getHeadFileMode(relativePath);
14375
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14376
+ const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14377
+ if (!contentUnchanged) continue;
14378
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14379
+ discarded.push(relativePath);
14380
+ } catch (e) {
14381
+ throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14382
+ }
14383
+ return discarded;
14378
14384
  }
14379
- }
14380
- const ANVIL_DEPS_DIR = ".anvil/deps";
14381
- const ANVIL_DEPS_MANIFEST = "manifest.json";
14382
- const DEPS_MANIFEST_VERSION = 2;
14383
- const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
14384
- function formatFetchError(prefix, status, errorBody) {
14385
- if (!errorBody) return `${prefix}: ${status}`;
14386
- try {
14387
- const parsed = JSON.parse(errorBody);
14388
- if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14389
- } catch {}
14390
- return `${prefix}: ${status} ${errorBody}`;
14391
- }
14392
- async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14393
- const { commit, includeDocs = true, knownDependencies = [] } = options;
14394
- const token = await auth_getValidAuthToken(anvilUrl);
14395
- const body = {
14396
- include_docs: includeDocs,
14397
- known_dependencies: knownDependencies
14398
- };
14399
- if (commit) body.commit = commit;
14400
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
14401
- method: "POST",
14402
- headers: {
14403
- Authorization: `Bearer ${token}`,
14404
- "Content-Type": "application/json"
14405
- },
14406
- body: JSON.stringify(body)
14407
- });
14408
- if (!resp.ok) {
14409
- const errorBody = await resp.text();
14410
- throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
14385
+ async discardStagedModeOnlyChanges(paths) {
14386
+ const discarded = [];
14387
+ for (const relativePath of paths)try {
14388
+ const headMode = await this.getHeadFileMode(relativePath);
14389
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14390
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14391
+ if (!indexContentUnchanged) continue;
14392
+ await this.git.raw([
14393
+ "reset",
14394
+ "-q",
14395
+ "HEAD",
14396
+ "--",
14397
+ relativePath
14398
+ ]);
14399
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14400
+ discarded.push(relativePath);
14401
+ } catch (e) {
14402
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14403
+ }
14404
+ return discarded;
14411
14405
  }
14412
- return await resp.json();
14413
- }
14414
- async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14415
- const includeDocs = options.includeDocs ?? true;
14416
- const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
14417
- const response = await fetchAllDependencies(appId, anvilUrl, {
14418
- commit: options.commit,
14419
- includeDocs,
14420
- knownDependencies
14421
- });
14422
- if (!response.resolved_dependencies.length) {
14423
- await clearDepsCache(projectRoot);
14424
- return {
14425
- response,
14426
- addedGitignoreEntry: false
14427
- };
14406
+ async getHeadFileMode(relativePath) {
14407
+ const output = await this.git.raw([
14408
+ "ls-tree",
14409
+ "HEAD",
14410
+ "--",
14411
+ relativePath
14412
+ ]);
14413
+ const match = output.match(/^(\d{6})\s/);
14414
+ return match?.[1] ?? null;
14428
14415
  }
14429
- const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
14430
- await writeAllDepsToCache(projectRoot, response, {
14431
- includeDocs
14432
- });
14433
- return {
14434
- response,
14435
- addedGitignoreEntry
14436
- };
14437
- }
14438
- function depsDir(projectRoot) {
14439
- return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
14440
- }
14441
- function depsManifestPath(projectRoot) {
14442
- return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
14443
- }
14444
- function isResolvedDependencyManifestEntry(value) {
14445
- if (!value || "object" != typeof value) return false;
14446
- const entry = value;
14447
- return "string" == typeof entry.app_id && entry.app_id.length > 0 && "string" == typeof entry.commit_id && entry.commit_id.length > 0 && (void 0 === entry.branch || "string" == typeof entry.branch);
14448
- }
14449
- function isCachedDependencyManifestEntry(value) {
14450
- if (!isResolvedDependencyManifestEntry(value)) return false;
14451
- const entry = value;
14452
- return "string" == typeof entry.package_name && entry.package_name.length > 0;
14453
- }
14454
- function isDepsCacheManifest(value) {
14455
- if (!value || "object" != typeof value) return false;
14456
- const manifest = value;
14457
- return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.source_dependencies) && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
14458
- }
14459
- async function readDepsCacheManifest(projectRoot) {
14460
- try {
14461
- const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
14462
- const parsed = JSON.parse(raw);
14463
- return isDepsCacheManifest(parsed) ? parsed : void 0;
14464
- } catch {
14465
- return;
14416
+ async hasSameContentAsHead(relativePath) {
14417
+ const headHash = (await this.git.revparse([
14418
+ `HEAD:${relativePath}`
14419
+ ])).trim();
14420
+ const worktreeHash = (await this.git.raw([
14421
+ "hash-object",
14422
+ "--",
14423
+ relativePath
14424
+ ])).trim();
14425
+ return headHash === worktreeHash;
14466
14426
  }
14467
- }
14468
- async function getKnownDependenciesForCache(projectRoot, includeDocs) {
14469
- const manifest = await readDepsCacheManifest(projectRoot);
14470
- if (!manifest || manifest.includeDocs !== includeDocs) return [];
14471
- return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
14472
- app_id,
14473
- commit_id
14474
- }));
14475
- }
14476
- function getDependencyWatchSubscriptionsFromManifest(manifest) {
14477
- if (!manifest) return [];
14478
- const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
14479
- dep.app_id,
14480
- dep
14481
- ]));
14482
- return manifest.resolved_dependencies.flatMap((dep)=>{
14483
- if (!dep.branch) return [];
14484
- return [
14485
- {
14486
- appId: dep.app_id,
14487
- branch: dep.branch,
14488
- packageName: cachedByAppId.get(dep.app_id)?.package_name
14489
- }
14490
- ];
14491
- });
14492
- }
14493
- function normalizeAnvilYamlDependenciesFromContent(content) {
14494
- const parsed = external_js_yaml_default().load(content);
14495
- if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
14496
- return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
14497
- }
14498
- function normalizeAnvilYamlDependenciesValue(value) {
14499
- if (!Array.isArray(value)) return [];
14500
- return value.map(normalizeForComparison);
14501
- }
14502
- async function readAnvilYamlDependencies(projectRoot) {
14503
- try {
14504
- const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
14505
- return normalizeAnvilYamlDependenciesFromContent(content);
14506
- } catch {
14507
- return [];
14427
+ async hasSameIndexContentAsHead(relativePath) {
14428
+ const headHash = (await this.git.revparse([
14429
+ `HEAD:${relativePath}`
14430
+ ])).trim();
14431
+ const indexHash = (await this.git.revparse([
14432
+ `:${relativePath}`
14433
+ ])).trim();
14434
+ return headHash === indexHash;
14508
14435
  }
14509
- }
14510
- async function getDependencyCacheWarning(projectRoot) {
14511
- const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
14512
- if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
14513
- const manifest = await readDepsCacheManifest(projectRoot);
14514
- if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
14515
- if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
14516
- const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
14517
- dependency.app_id,
14518
- dependency
14519
- ]));
14520
- for (const dependency of manifest.resolved_dependencies){
14521
- const cached = cachedByAppId.get(dependency.app_id);
14522
- if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
14523
- try {
14524
- await promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
14525
- } catch {
14526
- return DEPENDENCY_CACHE_WARNING;
14527
- }
14436
+ async restoreExecutableBit(relativePath, executable) {
14437
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14438
+ const stat = await external_fs_.promises.stat(filePath);
14439
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14440
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14528
14441
  }
14529
- }
14530
- function anvilYamlDependenciesEqual(left, right) {
14531
- return deepEqual(left, right);
14532
- }
14533
- function normalizeForComparison(value) {
14534
- if (Array.isArray(value)) return value.map(normalizeForComparison);
14535
- if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
14536
- key,
14537
- normalizeForComparison(entryValue)
14538
- ]));
14539
- return value;
14540
- }
14541
- function cachedManifestEntry(dep, packageName) {
14542
- return {
14543
- app_id: dep.app_id,
14544
- commit_id: dep.commit_id,
14545
- ...dep.branch ? {
14546
- branch: dep.branch
14547
- } : {},
14548
- package_name: packageName
14549
- };
14550
- }
14551
- function safeDependencyPath(filePath, kind) {
14552
- const normalizedPath = external_path_default().normalize(filePath);
14553
- if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
14554
- return normalizedPath;
14555
- }
14556
- function dependencyPythonPath(name, kind) {
14557
- const parts = name.split(".");
14558
- if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
14559
- return external_path_default().join(...parts);
14560
- }
14561
- async function writeDepToCache(projectRoot, dep) {
14562
- const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
14563
- await promises_default().rm(depDir, {
14564
- recursive: true,
14565
- force: true
14566
- });
14567
- await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
14568
- recursive: true
14569
- });
14570
- await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
14571
- recursive: true
14572
- });
14573
- await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
14574
- package_name: dep.package_name
14575
- }));
14576
- for (const mod of dep.modules ?? []){
14577
- const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
14578
- if (mod.is_package) {
14579
- await promises_default().mkdir(modulePath, {
14580
- recursive: true
14442
+ async hasUncommittedChanges() {
14443
+ const status = await this.getStatus();
14444
+ return !status.isClean;
14445
+ }
14446
+ async fetch(url, refSpec) {
14447
+ let attempt = 0;
14448
+ while(true)try {
14449
+ await this.git.fetch([
14450
+ url,
14451
+ refSpec
14452
+ ]);
14453
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14454
+ attempts: attempt,
14455
+ outcome: "succeeded"
14581
14456
  });
14582
- await promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
14583
- } else {
14584
- const moduleFile = `${modulePath}.py`;
14585
- await promises_default().mkdir(external_path_default().dirname(moduleFile), {
14586
- recursive: true
14457
+ return;
14458
+ } catch (e) {
14459
+ const err = e;
14460
+ if (!isWantNotValidError(err)) {
14461
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14462
+ attempts: attempt,
14463
+ outcome: "failed"
14464
+ });
14465
+ throw errors_createGitError.fetchFailed(err.message);
14466
+ }
14467
+ attempt++;
14468
+ this.emit("fetch-retry", {
14469
+ attempt,
14470
+ delayMs: FETCH_RETRY_DELAY_MS,
14471
+ error: err.message
14587
14472
  });
14588
- await promises_default().writeFile(moduleFile, mod.code);
14473
+ logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
14474
+ await (0, promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
14589
14475
  }
14590
14476
  }
14591
- for (const form of dep.forms ?? []){
14592
- const { code, class_name, is_package, ...formTemplate } = form;
14593
- const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
14594
- if (is_package) {
14595
- await promises_default().mkdir(formPath, {
14596
- recursive: true
14597
- });
14598
- await promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
14599
- await promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
14600
- } else {
14601
- const formCodeFile = `${formPath}.py`;
14602
- await promises_default().mkdir(external_path_default().dirname(formCodeFile), {
14603
- recursive: true
14604
- });
14605
- await promises_default().writeFile(formCodeFile, code);
14606
- await promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
14477
+ async reset(ref, mode = "mixed") {
14478
+ await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14479
+ `--${mode}`,
14480
+ ref
14481
+ ]));
14482
+ }
14483
+ async checkout(paths) {
14484
+ await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14485
+ }
14486
+ async runGitCommandWithIndexLockRetry(command, operation) {
14487
+ let lastError;
14488
+ for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
14489
+ await operation();
14490
+ return;
14491
+ } catch (e) {
14492
+ lastError = e;
14493
+ if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
14494
+ await (0, promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
14607
14495
  }
14496
+ throw errors_createGitError.commandFailed(command, lastError.message);
14608
14497
  }
14609
- for (const mod of dep.server_modules ?? []){
14610
- const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
14611
- if (mod.is_package) {
14612
- await promises_default().mkdir(modulePath, {
14613
- recursive: true
14614
- });
14615
- await promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
14616
- } else {
14617
- const moduleFile = `${modulePath}.py`;
14618
- await promises_default().mkdir(external_path_default().dirname(moduleFile), {
14619
- recursive: true
14620
- });
14621
- await promises_default().writeFile(moduleFile, mod.code);
14498
+ isIndexLockError(error) {
14499
+ const message = error.message ?? "";
14500
+ return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
14501
+ }
14502
+ async stash(message) {
14503
+ try {
14504
+ const args = [
14505
+ "stash",
14506
+ "push",
14507
+ "--include-untracked"
14508
+ ];
14509
+ if (message) args.push("-m", message);
14510
+ const result = await this.git.raw(args);
14511
+ return !result.includes("No local changes to save");
14512
+ } catch (e) {
14513
+ throw errors_createGitError.commandFailed("stash", e.message);
14622
14514
  }
14623
14515
  }
14624
- for (const asset of dep.assets ?? []){
14625
- const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
14626
- await promises_default().mkdir(external_path_default().dirname(targetPath), {
14627
- recursive: true
14628
- });
14629
- await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
14516
+ async stashPop() {
14517
+ try {
14518
+ await this.git.raw([
14519
+ "stash",
14520
+ "pop"
14521
+ ]);
14522
+ } catch (e) {
14523
+ throw errors_createGitError.commandFailed("stash pop", e.message);
14524
+ }
14630
14525
  }
14631
- for (const doc of dep.docs ?? []){
14632
- const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
14633
- await promises_default().mkdir(external_path_default().dirname(targetPath), {
14634
- recursive: true
14635
- });
14636
- await promises_default().writeFile(targetPath, doc.content);
14526
+ async clean(files) {
14527
+ try {
14528
+ await this.git.clean(CleanOptions.FORCE, files);
14529
+ } catch (e) {
14530
+ throw errors_createGitError.commandFailed("clean", e.message);
14531
+ }
14637
14532
  }
14638
- }
14639
- async function writeAllDepsToCache(projectRoot, response, options = {}) {
14640
- const includeDocs = options.includeDocs ?? true;
14641
- const manifest = await readDepsCacheManifest(projectRoot);
14642
- const canReuseExistingCache = manifest?.includeDocs === includeDocs;
14643
- if (!canReuseExistingCache) await clearDepsCache(projectRoot);
14644
- const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
14645
- const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
14646
- for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
14647
- recursive: true,
14648
- force: true
14649
- });
14650
- for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
14651
- const returnedByAppId = new Map(response.dependencies.map((dep)=>[
14652
- dep.app_id,
14653
- dep
14654
- ]));
14655
- const previousByAppId = new Map(previousCached.map((dep)=>[
14656
- dep.app_id,
14657
- dep
14658
- ]));
14659
- const cachedDependencies = response.resolved_dependencies.map((dep)=>{
14660
- const returned = returnedByAppId.get(dep.app_id);
14661
- if (returned) return cachedManifestEntry(returned, returned.package_name);
14662
- const previous = previousByAppId.get(dep.app_id);
14663
- if (!previous) return;
14664
- return cachedManifestEntry(dep, previous.package_name);
14665
- }).filter((dep)=>void 0 !== dep);
14666
- const newManifest = {
14667
- version: DEPS_MANIFEST_VERSION,
14668
- includeDocs,
14669
- source_dependencies: await readAnvilYamlDependencies(projectRoot),
14670
- resolved_dependencies: response.resolved_dependencies,
14671
- cached_dependencies: cachedDependencies
14672
- };
14673
- await promises_default().mkdir(depsDir(projectRoot), {
14674
- recursive: true
14675
- });
14676
- await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
14677
- }
14678
- async function clearDepsCache(projectRoot) {
14679
- await promises_default().rm(depsDir(projectRoot), {
14680
- recursive: true,
14681
- force: true
14682
- });
14683
- }
14684
- async function getCachedPackages(projectRoot) {
14685
- try {
14686
- const entries = await promises_default().readdir(depsDir(projectRoot), {
14687
- withFileTypes: true
14688
- });
14689
- return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
14690
- } catch {
14691
- return [];
14692
- }
14693
- }
14694
- const external_timers_promises_namespaceObject = require("timers/promises");
14695
- const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
14696
- const FETCH_RETRY_DELAY_MS = 200;
14697
- function isWantNotValidError(error) {
14698
- return WANT_NOT_VALID_RE.test(error.message || "");
14699
- }
14700
- class GitService extends Emitter {
14701
- git;
14702
- repoPath;
14703
- static INDEX_LOCK_RETRY_DELAYS_MS = [
14704
- 25,
14705
- 50,
14706
- 100,
14707
- 200
14708
- ];
14709
- constructor(repoPath){
14710
- super();
14711
- this.repoPath = external_path_default().resolve(repoPath);
14712
- this.git = esm_default(this.repoPath);
14713
- }
14714
- getGit() {
14715
- return this.git;
14716
- }
14717
- getRepoPath() {
14718
- return this.repoPath;
14719
- }
14720
- async getGitDir() {
14533
+ async diffNames(fromCommit, toCommit) {
14721
14534
  try {
14722
- const gitDir = (await this.git.revparse([
14723
- "--git-dir"
14724
- ])).trim();
14725
- if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
14726
- return gitDir;
14535
+ const diffResult = await this.git.diff([
14536
+ fromCommit,
14537
+ toCommit,
14538
+ "--name-only"
14539
+ ]);
14540
+ const files = diffResult.split("\n").filter((f)=>f.trim());
14541
+ return files;
14727
14542
  } catch (e) {
14728
- throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14543
+ throw errors_createGitError.commandFailed("diff", e.message);
14729
14544
  }
14730
14545
  }
14731
- async getCurrentBranch() {
14546
+ async getAheadBehind(localRef, remoteRef) {
14732
14547
  try {
14733
- const branchRef = await this.git.revparse([
14734
- "--abbrev-ref",
14735
- "HEAD"
14548
+ const result = await this.git.raw([
14549
+ "rev-list",
14550
+ "--left-right",
14551
+ "--count",
14552
+ `${remoteRef}...${localRef}`
14736
14553
  ]);
14737
- if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14738
- return branchRef;
14554
+ const [behind, ahead] = result.trim().split("\t").map(Number);
14555
+ return {
14556
+ ahead,
14557
+ behind
14558
+ };
14739
14559
  } catch (e) {
14740
- if ("detached_head" === e.type) throw e;
14741
- throw createSyncError.detachedHead();
14560
+ throw errors_createGitError.commandFailed("rev-list", e.message);
14742
14561
  }
14743
14562
  }
14744
- async getCommitId() {
14563
+ async show(refPath) {
14745
14564
  try {
14746
- const commitId = (await this.git.revparse([
14747
- "HEAD"
14748
- ])).trim();
14749
- return commitId;
14565
+ const content = await this.git.show([
14566
+ refPath
14567
+ ]);
14568
+ return content;
14750
14569
  } catch (e) {
14751
- throw errors_createGitError.commandFailed("revparse", e.message);
14570
+ throw errors_createGitError.commandFailed("show", e.message);
14752
14571
  }
14753
14572
  }
14754
- async getCommitInfo() {
14573
+ async push(url, refSpec, force = false) {
14755
14574
  try {
14756
- const hash = (await this.git.revparse([
14757
- "HEAD"
14758
- ])).trim();
14759
- const message = (await this.git.raw([
14760
- "log",
14761
- "-1",
14762
- "--format=%s"
14763
- ])).trim();
14764
- return {
14765
- hash,
14766
- shortHash: hash.substring(0, 8),
14767
- message
14768
- };
14575
+ const args = force ? [
14576
+ "push",
14577
+ "--force",
14578
+ url,
14579
+ refSpec
14580
+ ] : [
14581
+ "push",
14582
+ url,
14583
+ refSpec
14584
+ ];
14585
+ await this.git.raw(args);
14769
14586
  } catch (e) {
14770
- throw errors_createGitError.commandFailed("log", e.message);
14587
+ throw errors_createGitError.commandFailed("push", e.message);
14771
14588
  }
14772
14589
  }
14773
- async getStatus() {
14590
+ async rebase(onto) {
14774
14591
  try {
14775
- const status = await this.git.status();
14776
- const staged = status.files.filter((file)=>{
14777
- const indexStatus = file.index.trim();
14778
- return indexStatus.length > 0 && "?" !== indexStatus;
14779
- }).map((file)=>file.path);
14780
- return {
14781
- isClean: status.isClean(),
14782
- modified: status.modified,
14783
- notAdded: status.not_added,
14784
- created: status.created,
14785
- deleted: status.deleted,
14786
- staged,
14787
- renamed: status.renamed || []
14788
- };
14592
+ await this.git.raw([
14593
+ "rebase",
14594
+ onto
14595
+ ]);
14789
14596
  } catch (e) {
14790
- throw errors_createGitError.commandFailed("status", e.message);
14597
+ throw errors_createGitError.commandFailed("rebase", e.message);
14791
14598
  }
14792
14599
  }
14793
- async discardModeOnlyChanges(paths) {
14794
- const discarded = [];
14795
- for (const relativePath of paths)try {
14796
- const headMode = await this.getHeadFileMode(relativePath);
14797
- if ("100644" !== headMode && "100755" !== headMode) continue;
14798
- const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14799
- if (!contentUnchanged) continue;
14800
- await this.restoreExecutableBit(relativePath, "100755" === headMode);
14801
- discarded.push(relativePath);
14600
+ async rebaseAbort() {
14601
+ try {
14602
+ await this.git.raw([
14603
+ "rebase",
14604
+ "--abort"
14605
+ ]);
14802
14606
  } catch (e) {
14803
- throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14607
+ throw errors_createGitError.commandFailed("rebase --abort", e.message);
14804
14608
  }
14805
- return discarded;
14806
14609
  }
14807
- async discardStagedModeOnlyChanges(paths) {
14808
- const discarded = [];
14809
- for (const relativePath of paths)try {
14810
- const headMode = await this.getHeadFileMode(relativePath);
14811
- if ("100644" !== headMode && "100755" !== headMode) continue;
14812
- const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14813
- if (!indexContentUnchanged) continue;
14610
+ async isAncestor(ancestorRef, descendantRef) {
14611
+ try {
14612
+ const mergeBase = (await this.git.raw([
14613
+ "merge-base",
14614
+ ancestorRef,
14615
+ descendantRef
14616
+ ])).trim();
14617
+ const ancestorCommit = (await this.git.revparse([
14618
+ ancestorRef
14619
+ ])).trim();
14620
+ return mergeBase === ancestorCommit;
14621
+ } catch {
14622
+ return false;
14623
+ }
14624
+ }
14625
+ async mergeFastForward(ref) {
14626
+ await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
14627
+ "merge",
14628
+ "--ff-only",
14629
+ ref
14630
+ ]));
14631
+ }
14632
+ async deleteRef(ref) {
14633
+ try {
14814
14634
  await this.git.raw([
14815
- "reset",
14816
- "-q",
14817
- "HEAD",
14818
- "--",
14819
- relativePath
14635
+ "update-ref",
14636
+ "-d",
14637
+ ref
14820
14638
  ]);
14821
- await this.restoreExecutableBit(relativePath, "100755" === headMode);
14822
- discarded.push(relativePath);
14639
+ } catch (e) {}
14640
+ }
14641
+ async getRemotes() {
14642
+ try {
14643
+ const remotes = await this.git.getRemotes(true);
14644
+ return remotes.map((r)=>({
14645
+ name: r.name,
14646
+ fetchUrl: r.refs.fetch
14647
+ }));
14823
14648
  } catch (e) {
14824
- throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14649
+ throw errors_createGitError.commandFailed("remote", e.message);
14825
14650
  }
14826
- return discarded;
14827
14651
  }
14828
- async getHeadFileMode(relativePath) {
14829
- const output = await this.git.raw([
14830
- "ls-tree",
14831
- "HEAD",
14832
- "--",
14833
- relativePath
14834
- ]);
14835
- const match = output.match(/^(\d{6})\s/);
14836
- return match?.[1] ?? null;
14652
+ async isGitInitialized() {
14653
+ const gitPath = external_path_default().join(this.repoPath, ".git");
14654
+ return external_fs_.existsSync(gitPath);
14837
14655
  }
14838
- async hasSameContentAsHead(relativePath) {
14839
- const headHash = (await this.git.revparse([
14840
- `HEAD:${relativePath}`
14841
- ])).trim();
14842
- const worktreeHash = (await this.git.raw([
14843
- "hash-object",
14844
- "--",
14845
- relativePath
14846
- ])).trim();
14847
- return headHash === worktreeHash;
14656
+ async removeEmptyDirectories(removedFiles) {
14657
+ const directoriesToCheck = new Set();
14658
+ for (const file of removedFiles){
14659
+ let dir = external_path_default().dirname(file);
14660
+ while(dir && "." !== dir){
14661
+ directoriesToCheck.add(dir);
14662
+ const parentDir = external_path_default().dirname(dir);
14663
+ if (parentDir === dir) break;
14664
+ dir = parentDir;
14665
+ }
14666
+ }
14667
+ const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
14668
+ for (const dir of sortedDirs)try {
14669
+ const dirPath = external_path_default().join(this.repoPath, dir);
14670
+ if (!external_fs_.existsSync(dirPath)) continue;
14671
+ const entries = await external_fs_.promises.readdir(dirPath);
14672
+ if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
14673
+ } catch (error) {}
14848
14674
  }
14849
- async hasSameIndexContentAsHead(relativePath) {
14850
- const headHash = (await this.git.revparse([
14851
- `HEAD:${relativePath}`
14852
- ])).trim();
14853
- const indexHash = (await this.git.revparse([
14854
- `:${relativePath}`
14855
- ])).trim();
14856
- return headHash === indexHash;
14675
+ }
14676
+ async function getStagedFileChanges(git, gitService) {
14677
+ try {
14678
+ const status = await git.status();
14679
+ const renames = status.renamed || [];
14680
+ const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
14681
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14682
+ const stagedRenames = [];
14683
+ for (const f of stagedFiles)if ("R" === f.index) {
14684
+ const rename = renames.find((r)=>r.to === f.path);
14685
+ if (rename) stagedRenames.push({
14686
+ path: rename.to,
14687
+ type: "rename",
14688
+ from: rename.from
14689
+ });
14690
+ }
14691
+ const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14692
+ const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14693
+ const changes = [];
14694
+ for (const f of stagedFiles){
14695
+ if ("R" !== f.index) {
14696
+ if (!stagedModeOnlyPaths.has(f.path)) {
14697
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14698
+ path: f.path,
14699
+ type: "unlink"
14700
+ });
14701
+ else if ("A" === f.index || "?" === f.index) changes.push({
14702
+ path: f.path,
14703
+ type: "add"
14704
+ });
14705
+ else changes.push({
14706
+ path: f.path,
14707
+ type: "change"
14708
+ });
14709
+ }
14710
+ }
14711
+ }
14712
+ changes.push(...stagedRenames);
14713
+ return changes;
14714
+ } catch (e) {
14715
+ throw errors_createGitError.commandFailed("status", e.message);
14857
14716
  }
14858
- async restoreExecutableBit(relativePath, executable) {
14859
- const filePath = external_path_default().join(this.repoPath, relativePath);
14860
- const stat = await external_fs_.promises.stat(filePath);
14861
- const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14862
- if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14717
+ }
14718
+ async function getCurrentOrFallbackBranchName(git) {
14719
+ const branchRef = (await git.revparse([
14720
+ "--abbrev-ref",
14721
+ "HEAD"
14722
+ ])).trim();
14723
+ if ("HEAD" !== branchRef) return branchRef;
14724
+ const [localBranches, remoteBranches] = await Promise.all([
14725
+ git.raw([
14726
+ "branch",
14727
+ "--format=%(refname:short)",
14728
+ "--contains",
14729
+ "HEAD"
14730
+ ]),
14731
+ git.raw([
14732
+ "branch",
14733
+ "-r",
14734
+ "--format=%(refname:short)",
14735
+ "--contains",
14736
+ "HEAD"
14737
+ ])
14738
+ ]);
14739
+ const branchNames = [
14740
+ ...parseBranchList(localBranches),
14741
+ ...parseBranchList(remoteBranches).filter((branchName)=>!branchName.endsWith("/HEAD")).map((branchName)=>branchName.replace(/^[^/]+\//, ""))
14742
+ ];
14743
+ const branchSet = new Set(branchNames);
14744
+ if (branchSet.has("master")) return "master";
14745
+ if (branchSet.has("main")) return "main";
14746
+ return "master";
14747
+ }
14748
+ function parseBranchList(output) {
14749
+ return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
14750
+ }
14751
+ function anvil_api_getDefaultAnvilUrl() {
14752
+ return resolveAnvilUrl();
14753
+ }
14754
+ async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14755
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14756
+ try {
14757
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
14758
+ method: "POST",
14759
+ headers: {
14760
+ Authorization: `Bearer ${authToken}`,
14761
+ "Content-Type": "application/json"
14762
+ },
14763
+ body: JSON.stringify({
14764
+ app_id: appId
14765
+ })
14766
+ });
14767
+ const data = await resp.json();
14768
+ if (resp.ok) return {
14769
+ valid: data.valid ?? false,
14770
+ app_name: data.app_name
14771
+ };
14772
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14773
+ if (403 === resp.status) throw createAppError.accessDenied(appId);
14774
+ if (404 === resp.status) throw createAppError.notFound(appId);
14775
+ logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
14776
+ return {
14777
+ valid: false,
14778
+ error: data.error || `Server error: ${resp.status}`
14779
+ };
14780
+ } catch (error) {
14781
+ if (error.type) throw error;
14782
+ throw errors_createNetworkError.network(error.message);
14863
14783
  }
14864
- async hasUncommittedChanges() {
14865
- const status = await this.getStatus();
14866
- return !status.isClean;
14784
+ }
14785
+ async function listAppsForCheckout(options = {}) {
14786
+ const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
14787
+ const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
14788
+ const params = new URLSearchParams();
14789
+ if ("number" == typeof options.limit) params.set("limit", String(options.limit));
14790
+ if (options.cursor) params.set("cursor", options.cursor);
14791
+ if (options.q && options.q.trim()) params.set("q", options.q.trim());
14792
+ const query = params.toString();
14793
+ const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
14794
+ try {
14795
+ const resp = await fetch(url, {
14796
+ method: "GET",
14797
+ headers: {
14798
+ Authorization: `Bearer ${authToken}`
14799
+ },
14800
+ signal: options.signal
14801
+ });
14802
+ if (!resp.ok) {
14803
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14804
+ throw errors_createNetworkError.server(resp.status, resp.statusText);
14805
+ }
14806
+ const data = await resp.json();
14807
+ return {
14808
+ apps: Array.isArray(data.apps) ? data.apps : [],
14809
+ next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
14810
+ };
14811
+ } catch (error) {
14812
+ if (error.type) throw error;
14813
+ throw errors_createNetworkError.network(error.message);
14867
14814
  }
14868
- async fetch(url, refSpec) {
14869
- let attempt = 0;
14870
- while(true)try {
14871
- await this.git.fetch([
14872
- url,
14873
- refSpec
14874
- ]);
14875
- if (attempt > 0) this.emit("fetch-retry-resolved", {
14876
- attempts: attempt,
14877
- outcome: "succeeded"
14878
- });
14879
- return;
14880
- } catch (e) {
14881
- const err = e;
14882
- if (!isWantNotValidError(err)) {
14883
- if (attempt > 0) this.emit("fetch-retry-resolved", {
14884
- attempts: attempt,
14885
- outcome: "failed"
14815
+ }
14816
+ function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
14817
+ const url = new URL(gitUrl);
14818
+ const encodedToken = encodeURIComponent(authToken);
14819
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
14820
+ }
14821
+ function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14822
+ const url = new URL(anvilUrl);
14823
+ const encodedToken = encodeURIComponent(authToken);
14824
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
14825
+ }
14826
+ function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14827
+ return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
14828
+ }
14829
+ async function getLatestVersion() {
14830
+ try {
14831
+ const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
14832
+ if (!response.ok) return null;
14833
+ const data = await response.json();
14834
+ return data.version;
14835
+ } catch (e) {
14836
+ return null;
14837
+ }
14838
+ }
14839
+ function filterCandidates(candidates, explicitUrl, explicitUsername) {
14840
+ let filtered = candidates;
14841
+ if (explicitUrl) {
14842
+ const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
14843
+ filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
14844
+ }
14845
+ if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
14846
+ return filtered;
14847
+ }
14848
+ function formatCandidateLabel(candidate) {
14849
+ const parts = [
14850
+ candidate.appId
14851
+ ];
14852
+ if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
14853
+ else parts.push(`(${candidate.detectedUrl})`);
14854
+ parts.push(`- ${candidate.description}`);
14855
+ return parts.join(" ");
14856
+ }
14857
+ function lookupRemoteInfoForAppId(appId, detectedRemotes) {
14858
+ const matches = detectedRemotes.filter((c)=>c.appId === appId);
14859
+ if (0 === matches.length) return {};
14860
+ const withUsername = matches.find((c)=>c.detectedUsername);
14861
+ if (withUsername) return {
14862
+ detectedUrl: withUsername.detectedUrl,
14863
+ detectedUsername: withUsername.detectedUsername
14864
+ };
14865
+ return {
14866
+ detectedUrl: matches[0].detectedUrl,
14867
+ detectedUsername: matches[0].detectedUsername
14868
+ };
14869
+ }
14870
+ async function detectAppIdsFromAllRemotes(repoPath) {
14871
+ const git = esm_default(repoPath);
14872
+ const out = [];
14873
+ try {
14874
+ const remotes = await git.getRemotes(true);
14875
+ for (const remote of remotes){
14876
+ const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
14877
+ if (httpMatch) {
14878
+ const [, host, detectedAppId] = httpMatch;
14879
+ out.push({
14880
+ appId: detectedAppId,
14881
+ source: "remote",
14882
+ description: `Git remote '${remote.name}'`,
14883
+ detectedUrl: normalizeAnvilUrl(host)
14884
+ });
14885
+ continue;
14886
+ }
14887
+ const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
14888
+ if (sshMatch) {
14889
+ const [, usernamePart, host, , detectedAppId] = sshMatch;
14890
+ const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
14891
+ const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
14892
+ out.push({
14893
+ appId: detectedAppId,
14894
+ source: "remote",
14895
+ description: `Git remote '${remote.name}' (SSH)`,
14896
+ detectedUrl: normalizeAnvilUrl(host),
14897
+ detectedUsername: finalUsername
14886
14898
  });
14887
- throw errors_createGitError.fetchFailed(err.message);
14888
14899
  }
14889
- attempt++;
14890
- this.emit("fetch-retry", {
14891
- attempt,
14892
- delayMs: FETCH_RETRY_DELAY_MS,
14893
- error: err.message
14894
- });
14895
- logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
14896
- await (0, external_timers_promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
14897
14900
  }
14898
- }
14899
- async reset(ref, mode = "mixed") {
14900
- await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14901
- `--${mode}`,
14902
- ref
14903
- ]));
14904
- }
14905
- async checkout(paths) {
14906
- await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14907
- }
14908
- async runGitCommandWithIndexLockRetry(command, operation) {
14909
- let lastError;
14910
- for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
14911
- await operation();
14912
- return;
14913
- } catch (e) {
14914
- lastError = e;
14915
- if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
14916
- await (0, external_timers_promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
14901
+ } catch (_e) {}
14902
+ return out;
14903
+ }
14904
+ async function detectAppIdsByCommitLookup(repoPath, options) {
14905
+ const anvilUrl = options.anvilUrl || resolveAnvilUrl();
14906
+ const username = options.username;
14907
+ const git = esm_default(repoPath);
14908
+ const out = [];
14909
+ try {
14910
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14911
+ const commitId = (await git.revparse([
14912
+ "HEAD"
14913
+ ]))?.trim();
14914
+ if (!commitId) return out;
14915
+ const branchName = await getCurrentOrFallbackBranchName(git);
14916
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
14917
+ method: "POST",
14918
+ headers: {
14919
+ Authorization: `Bearer ${authToken}`,
14920
+ "Content-Type": "application/json"
14921
+ },
14922
+ body: JSON.stringify({
14923
+ commit_id: commitId,
14924
+ branch_name: branchName
14925
+ })
14926
+ });
14927
+ logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${branchName} returned ${resp.status}`);
14928
+ if (!resp.ok) {
14929
+ const errorBody = await resp.text();
14930
+ logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
14917
14931
  }
14918
- throw errors_createGitError.commandFailed(command, lastError.message);
14919
- }
14920
- isIndexLockError(error) {
14921
- const message = error.message ?? "";
14922
- return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
14923
- }
14924
- async stash(message) {
14925
- try {
14926
- const args = [
14927
- "stash",
14928
- "push",
14929
- "--include-untracked"
14930
- ];
14931
- if (message) args.push("-m", message);
14932
- const result = await this.git.raw(args);
14933
- return !result.includes("No local changes to save");
14934
- } catch (e) {
14935
- throw errors_createGitError.commandFailed("stash", e.message);
14932
+ if (resp.ok) {
14933
+ const data = await resp.json();
14934
+ logger_logger.debug(`data ${JSON.stringify(data)}`);
14935
+ const apps = data?.apps ?? [];
14936
+ for (const app of apps)if (app?.app_id) out.push({
14937
+ appId: app.app_id,
14938
+ source: "remote",
14939
+ description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
14940
+ });
14936
14941
  }
14942
+ } catch (_e) {}
14943
+ return out;
14944
+ }
14945
+ const external_fs_promises_namespaceObject = require("fs/promises");
14946
+ var external_fs_promises_default = /*#__PURE__*/ __webpack_require__.n(external_fs_promises_namespaceObject);
14947
+ function normalizeLineEndings(content) {
14948
+ return content.replace(/\r\n/g, "\n");
14949
+ }
14950
+ function pythonifyName(name) {
14951
+ return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14952
+ }
14953
+ function extractPythonName(relativePath, skipParts = 1) {
14954
+ const parts = relativePath.slice(0, -3).split("/");
14955
+ const isPackage = relativePath.endsWith("__init__.py");
14956
+ const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14957
+ return relevantParts.map(pythonifyName).join(".");
14958
+ }
14959
+ function deepEqual(a, b) {
14960
+ if (a === b) return true;
14961
+ if (null == a || null == b) return false;
14962
+ if (typeof a !== typeof b) return false;
14963
+ if ("object" != typeof a) return a === b;
14964
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
14965
+ if (Array.isArray(a)) {
14966
+ if (a.length !== b.length) return false;
14967
+ return a.every((item, index)=>deepEqual(item, b[index]));
14937
14968
  }
14938
- async stashPop() {
14939
- try {
14940
- await this.git.raw([
14941
- "stash",
14942
- "pop"
14943
- ]);
14944
- } catch (e) {
14945
- throw errors_createGitError.commandFailed("stash pop", e.message);
14946
- }
14969
+ const keysA = Object.keys(a);
14970
+ const keysB = Object.keys(b);
14971
+ if (keysA.length !== keysB.length) return false;
14972
+ return keysA.every((key)=>deepEqual(a[key], b[key]));
14973
+ }
14974
+ function parseHtmlWithFrontmatter(htmlContent) {
14975
+ const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14976
+ const match = htmlContent.match(frontmatterPattern);
14977
+ if (match) {
14978
+ const yamlStr = match[1];
14979
+ const html = match[2];
14980
+ const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14981
+ return {
14982
+ frontmatter,
14983
+ html
14984
+ };
14947
14985
  }
14948
- async clean(files) {
14949
- try {
14950
- await this.git.clean(CleanOptions.FORCE, files);
14951
- } catch (e) {
14952
- throw errors_createGitError.commandFailed("clean", e.message);
14953
- }
14986
+ return {
14987
+ frontmatter: {},
14988
+ html: htmlContent
14989
+ };
14990
+ }
14991
+ function formatHttpError(prefix, status, body) {
14992
+ if (!body) return `${prefix}: ${status}`;
14993
+ try {
14994
+ const parsed = JSON.parse(body);
14995
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14996
+ } catch {}
14997
+ return `${prefix}: ${status} ${body}`;
14998
+ }
14999
+ const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
15000
+ async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
15001
+ const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
15002
+ try {
15003
+ const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
15004
+ if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
15005
+ const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
15006
+ await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15007
+ return true;
15008
+ } catch (error) {
15009
+ if ("ENOENT" !== error.code) throw error;
15010
+ await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15011
+ return true;
14954
15012
  }
14955
- async diffNames(fromCommit, toCommit) {
14956
- try {
14957
- const diffResult = await this.git.diff([
14958
- fromCommit,
14959
- toCommit,
14960
- "--name-only"
14961
- ]);
14962
- const files = diffResult.split("\n").filter((f)=>f.trim());
14963
- return files;
14964
- } catch (e) {
14965
- throw errors_createGitError.commandFailed("diff", e.message);
14966
- }
15013
+ }
15014
+ const ANVIL_DEPS_DIR = ".anvil/deps";
15015
+ const ANVIL_DEPS_MANIFEST = "manifest.json";
15016
+ const DEPS_MANIFEST_VERSION = 2;
15017
+ const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
15018
+ async function fetchAllDependencies(appId, anvilUrl, options = {}) {
15019
+ const { commit, includeDocs = true, knownDependencies = [] } = options;
15020
+ anvilUrl ??= await resolveAuthAnvilUrl();
15021
+ const token = await auth_getValidAuthToken(anvilUrl);
15022
+ const body = {
15023
+ include_docs: includeDocs,
15024
+ known_dependencies: knownDependencies
15025
+ };
15026
+ if (commit) body.commit = commit;
15027
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
15028
+ method: "POST",
15029
+ headers: {
15030
+ Authorization: `Bearer ${token}`,
15031
+ "Content-Type": "application/json"
15032
+ },
15033
+ body: JSON.stringify(body)
15034
+ });
15035
+ if (!resp.ok) {
15036
+ const errorBody = await resp.text();
15037
+ throw new Error(formatHttpError("Failed to fetch dependencies", resp.status, errorBody));
14967
15038
  }
14968
- async getAheadBehind(localRef, remoteRef) {
14969
- try {
14970
- const result = await this.git.raw([
14971
- "rev-list",
14972
- "--left-right",
14973
- "--count",
14974
- `${remoteRef}...${localRef}`
14975
- ]);
14976
- const [behind, ahead] = result.trim().split("\t").map(Number);
14977
- return {
14978
- ahead,
14979
- behind
14980
- };
14981
- } catch (e) {
14982
- throw errors_createGitError.commandFailed("rev-list", e.message);
14983
- }
15039
+ return await resp.json();
15040
+ }
15041
+ async function refreshDependencyCache(projectRoot, appId, anvilUrl, options = {}) {
15042
+ auth_setRepoContext(projectRoot);
15043
+ anvilUrl ??= await resolveAuthAnvilUrl();
15044
+ const includeDocs = options.includeDocs ?? true;
15045
+ const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
15046
+ const response = await fetchAllDependencies(appId, anvilUrl, {
15047
+ commit: options.commit,
15048
+ includeDocs,
15049
+ knownDependencies
15050
+ });
15051
+ if (!response.resolved_dependencies.length) {
15052
+ await clearDepsCache(projectRoot);
15053
+ return {
15054
+ response,
15055
+ addedGitignoreEntry: false
15056
+ };
14984
15057
  }
14985
- async show(refPath) {
14986
- try {
14987
- const content = await this.git.show([
14988
- refPath
14989
- ]);
14990
- return content;
14991
- } catch (e) {
14992
- throw errors_createGitError.commandFailed("show", e.message);
14993
- }
15058
+ const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
15059
+ await writeAllDepsToCache(projectRoot, response, {
15060
+ includeDocs
15061
+ });
15062
+ return {
15063
+ response,
15064
+ addedGitignoreEntry
15065
+ };
15066
+ }
15067
+ function depsDir(projectRoot) {
15068
+ return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
15069
+ }
15070
+ function depsManifestPath(projectRoot) {
15071
+ return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
15072
+ }
15073
+ function isResolvedDependencyManifestEntry(value) {
15074
+ if (!value || "object" != typeof value) return false;
15075
+ const entry = value;
15076
+ return "string" == typeof entry.app_id && entry.app_id.length > 0 && "string" == typeof entry.commit_id && entry.commit_id.length > 0 && (void 0 === entry.branch || "string" == typeof entry.branch);
15077
+ }
15078
+ function isCachedDependencyManifestEntry(value) {
15079
+ if (!isResolvedDependencyManifestEntry(value)) return false;
15080
+ const entry = value;
15081
+ return "string" == typeof entry.package_name && entry.package_name.length > 0;
15082
+ }
15083
+ function isDepsCacheManifest(value) {
15084
+ if (!value || "object" != typeof value) return false;
15085
+ const manifest = value;
15086
+ return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.source_dependencies) && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
15087
+ }
15088
+ async function readDepsCacheManifest(projectRoot) {
15089
+ try {
15090
+ const raw = await external_fs_promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
15091
+ const parsed = JSON.parse(raw);
15092
+ return isDepsCacheManifest(parsed) ? parsed : void 0;
15093
+ } catch {
15094
+ return;
14994
15095
  }
14995
- async push(url, refSpec, force = false) {
14996
- try {
14997
- const args = force ? [
14998
- "push",
14999
- "--force",
15000
- url,
15001
- refSpec
15002
- ] : [
15003
- "push",
15004
- url,
15005
- refSpec
15006
- ];
15007
- await this.git.raw(args);
15008
- } catch (e) {
15009
- throw errors_createGitError.commandFailed("push", e.message);
15010
- }
15096
+ }
15097
+ async function getKnownDependenciesForCache(projectRoot, includeDocs) {
15098
+ const manifest = await readDepsCacheManifest(projectRoot);
15099
+ if (!manifest || manifest.includeDocs !== includeDocs) return [];
15100
+ return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
15101
+ app_id,
15102
+ commit_id
15103
+ }));
15104
+ }
15105
+ function getDependencyWatchSubscriptionsFromManifest(manifest) {
15106
+ if (!manifest) return [];
15107
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
15108
+ dep.app_id,
15109
+ dep
15110
+ ]));
15111
+ return manifest.resolved_dependencies.flatMap((dep)=>{
15112
+ if (!dep.branch) return [];
15113
+ return [
15114
+ {
15115
+ appId: dep.app_id,
15116
+ branch: dep.branch,
15117
+ packageName: cachedByAppId.get(dep.app_id)?.package_name
15118
+ }
15119
+ ];
15120
+ });
15121
+ }
15122
+ function normalizeAnvilYamlDependenciesFromContent(content) {
15123
+ const parsed = external_js_yaml_default().load(content);
15124
+ if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
15125
+ return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
15126
+ }
15127
+ function normalizeAnvilYamlDependenciesValue(value) {
15128
+ if (!Array.isArray(value)) return [];
15129
+ return value.map(normalizeForComparison);
15130
+ }
15131
+ async function readAnvilYamlDependencies(projectRoot) {
15132
+ try {
15133
+ const content = await external_fs_promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
15134
+ return normalizeAnvilYamlDependenciesFromContent(content);
15135
+ } catch {
15136
+ return [];
15011
15137
  }
15012
- async rebase(onto) {
15138
+ }
15139
+ async function getDependencyCacheWarning(projectRoot) {
15140
+ const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
15141
+ if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
15142
+ const manifest = await readDepsCacheManifest(projectRoot);
15143
+ if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
15144
+ if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
15145
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
15146
+ dependency.app_id,
15147
+ dependency
15148
+ ]));
15149
+ for (const dependency of manifest.resolved_dependencies){
15150
+ const cached = cachedByAppId.get(dependency.app_id);
15151
+ if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
15013
15152
  try {
15014
- await this.git.raw([
15015
- "rebase",
15016
- onto
15017
- ]);
15018
- } catch (e) {
15019
- throw errors_createGitError.commandFailed("rebase", e.message);
15153
+ await external_fs_promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
15154
+ } catch {
15155
+ return DEPENDENCY_CACHE_WARNING;
15020
15156
  }
15021
15157
  }
15022
- async rebaseAbort() {
15023
- try {
15024
- await this.git.raw([
15025
- "rebase",
15026
- "--abort"
15027
- ]);
15028
- } catch (e) {
15029
- throw errors_createGitError.commandFailed("rebase --abort", e.message);
15158
+ }
15159
+ function anvilYamlDependenciesEqual(left, right) {
15160
+ return deepEqual(left, right);
15161
+ }
15162
+ function normalizeForComparison(value) {
15163
+ if (Array.isArray(value)) return value.map(normalizeForComparison);
15164
+ if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
15165
+ key,
15166
+ normalizeForComparison(entryValue)
15167
+ ]));
15168
+ return value;
15169
+ }
15170
+ function cachedManifestEntry(dep, packageName) {
15171
+ return {
15172
+ app_id: dep.app_id,
15173
+ commit_id: dep.commit_id,
15174
+ ...dep.branch ? {
15175
+ branch: dep.branch
15176
+ } : {},
15177
+ package_name: packageName
15178
+ };
15179
+ }
15180
+ function safeDependencyPath(filePath, kind) {
15181
+ const normalizedPath = external_path_default().normalize(filePath);
15182
+ if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
15183
+ return normalizedPath;
15184
+ }
15185
+ function dependencyPythonPath(name, kind) {
15186
+ const parts = name.split(".");
15187
+ if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
15188
+ return external_path_default().join(...parts);
15189
+ }
15190
+ async function writeDepToCache(projectRoot, dep) {
15191
+ const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
15192
+ await external_fs_promises_default().rm(depDir, {
15193
+ recursive: true,
15194
+ force: true
15195
+ });
15196
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
15197
+ recursive: true
15198
+ });
15199
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
15200
+ recursive: true
15201
+ });
15202
+ await external_fs_promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
15203
+ package_name: dep.package_name
15204
+ }));
15205
+ for (const mod of dep.modules ?? []){
15206
+ const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
15207
+ if (mod.is_package) {
15208
+ await external_fs_promises_default().mkdir(modulePath, {
15209
+ recursive: true
15210
+ });
15211
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15212
+ } else {
15213
+ const moduleFile = `${modulePath}.py`;
15214
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15215
+ recursive: true
15216
+ });
15217
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15030
15218
  }
15031
15219
  }
15032
- async isAncestor(ancestorRef, descendantRef) {
15033
- try {
15034
- const mergeBase = (await this.git.raw([
15035
- "merge-base",
15036
- ancestorRef,
15037
- descendantRef
15038
- ])).trim();
15039
- const ancestorCommit = (await this.git.revparse([
15040
- ancestorRef
15041
- ])).trim();
15042
- return mergeBase === ancestorCommit;
15043
- } catch {
15044
- return false;
15220
+ for (const form of dep.forms ?? []){
15221
+ const { code, class_name, is_package, ...formTemplate } = form;
15222
+ const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
15223
+ if (is_package) {
15224
+ await external_fs_promises_default().mkdir(formPath, {
15225
+ recursive: true
15226
+ });
15227
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
15228
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
15229
+ } else {
15230
+ const formCodeFile = `${formPath}.py`;
15231
+ await external_fs_promises_default().mkdir(external_path_default().dirname(formCodeFile), {
15232
+ recursive: true
15233
+ });
15234
+ await external_fs_promises_default().writeFile(formCodeFile, code);
15235
+ await external_fs_promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
15045
15236
  }
15046
15237
  }
15047
- async mergeFastForward(ref) {
15048
- await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
15049
- "merge",
15050
- "--ff-only",
15051
- ref
15052
- ]));
15053
- }
15054
- async deleteRef(ref) {
15055
- try {
15056
- await this.git.raw([
15057
- "update-ref",
15058
- "-d",
15059
- ref
15060
- ]);
15061
- } catch (e) {}
15062
- }
15063
- async getRemotes() {
15064
- try {
15065
- const remotes = await this.git.getRemotes(true);
15066
- return remotes.map((r)=>({
15067
- name: r.name,
15068
- fetchUrl: r.refs.fetch
15069
- }));
15070
- } catch (e) {
15071
- throw errors_createGitError.commandFailed("remote", e.message);
15238
+ for (const mod of dep.server_modules ?? []){
15239
+ const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
15240
+ if (mod.is_package) {
15241
+ await external_fs_promises_default().mkdir(modulePath, {
15242
+ recursive: true
15243
+ });
15244
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15245
+ } else {
15246
+ const moduleFile = `${modulePath}.py`;
15247
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15248
+ recursive: true
15249
+ });
15250
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15072
15251
  }
15073
15252
  }
15074
- async isGitInitialized() {
15075
- const gitPath = external_path_default().join(this.repoPath, ".git");
15076
- return external_fs_.existsSync(gitPath);
15253
+ for (const asset of dep.assets ?? []){
15254
+ const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
15255
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15256
+ recursive: true
15257
+ });
15258
+ await external_fs_promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
15077
15259
  }
15078
- async removeEmptyDirectories(removedFiles) {
15079
- const directoriesToCheck = new Set();
15080
- for (const file of removedFiles){
15081
- let dir = external_path_default().dirname(file);
15082
- while(dir && "." !== dir){
15083
- directoriesToCheck.add(dir);
15084
- const parentDir = external_path_default().dirname(dir);
15085
- if (parentDir === dir) break;
15086
- dir = parentDir;
15087
- }
15088
- }
15089
- const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
15090
- for (const dir of sortedDirs)try {
15091
- const dirPath = external_path_default().join(this.repoPath, dir);
15092
- if (!external_fs_.existsSync(dirPath)) continue;
15093
- const entries = await external_fs_.promises.readdir(dirPath);
15094
- if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
15095
- } catch (error) {}
15260
+ for (const doc of dep.docs ?? []){
15261
+ const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
15262
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15263
+ recursive: true
15264
+ });
15265
+ await external_fs_promises_default().writeFile(targetPath, doc.content);
15096
15266
  }
15097
15267
  }
15098
- async function getStagedFileChanges(git, gitService) {
15268
+ async function writeAllDepsToCache(projectRoot, response, options = {}) {
15269
+ const includeDocs = options.includeDocs ?? true;
15270
+ const manifest = await readDepsCacheManifest(projectRoot);
15271
+ const canReuseExistingCache = manifest?.includeDocs === includeDocs;
15272
+ if (!canReuseExistingCache) await clearDepsCache(projectRoot);
15273
+ const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
15274
+ const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
15275
+ for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await external_fs_promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
15276
+ recursive: true,
15277
+ force: true
15278
+ });
15279
+ for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
15280
+ const returnedByAppId = new Map(response.dependencies.map((dep)=>[
15281
+ dep.app_id,
15282
+ dep
15283
+ ]));
15284
+ const previousByAppId = new Map(previousCached.map((dep)=>[
15285
+ dep.app_id,
15286
+ dep
15287
+ ]));
15288
+ const cachedDependencies = response.resolved_dependencies.map((dep)=>{
15289
+ const returned = returnedByAppId.get(dep.app_id);
15290
+ if (returned) return cachedManifestEntry(returned, returned.package_name);
15291
+ const previous = previousByAppId.get(dep.app_id);
15292
+ if (!previous) return;
15293
+ return cachedManifestEntry(dep, previous.package_name);
15294
+ }).filter((dep)=>void 0 !== dep);
15295
+ const newManifest = {
15296
+ version: DEPS_MANIFEST_VERSION,
15297
+ includeDocs,
15298
+ source_dependencies: await readAnvilYamlDependencies(projectRoot),
15299
+ resolved_dependencies: response.resolved_dependencies,
15300
+ cached_dependencies: cachedDependencies
15301
+ };
15302
+ await external_fs_promises_default().mkdir(depsDir(projectRoot), {
15303
+ recursive: true
15304
+ });
15305
+ await external_fs_promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
15306
+ }
15307
+ async function clearDepsCache(projectRoot) {
15308
+ await external_fs_promises_default().rm(depsDir(projectRoot), {
15309
+ recursive: true,
15310
+ force: true
15311
+ });
15312
+ }
15313
+ async function getCachedPackages(projectRoot) {
15099
15314
  try {
15100
- const status = await git.status();
15101
- const renames = status.renamed || [];
15102
- const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
15103
- const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
15104
- const stagedRenames = [];
15105
- for (const f of stagedFiles)if ("R" === f.index) {
15106
- const rename = renames.find((r)=>r.to === f.path);
15107
- if (rename) stagedRenames.push({
15108
- path: rename.to,
15109
- type: "rename",
15110
- from: rename.from
15111
- });
15112
- }
15113
- const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
15114
- const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
15115
- const changes = [];
15116
- for (const f of stagedFiles){
15117
- if ("R" !== f.index) {
15118
- if (!stagedModeOnlyPaths.has(f.path)) {
15119
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
15120
- path: f.path,
15121
- type: "unlink"
15122
- });
15123
- else if ("A" === f.index || "?" === f.index) changes.push({
15124
- path: f.path,
15125
- type: "add"
15126
- });
15127
- else changes.push({
15128
- path: f.path,
15129
- type: "change"
15130
- });
15131
- }
15132
- }
15133
- }
15134
- changes.push(...stagedRenames);
15135
- return changes;
15136
- } catch (e) {
15137
- throw errors_createGitError.commandFailed("status", e.message);
15315
+ const entries = await external_fs_promises_default().readdir(depsDir(projectRoot), {
15316
+ withFileTypes: true
15317
+ });
15318
+ return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
15319
+ } catch {
15320
+ return [];
15138
15321
  }
15139
15322
  }
15140
15323
  const external_chokidar_namespaceObject = require("chokidar");
@@ -16440,6 +16623,45 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16440
16623
  };
16441
16624
  }
16442
16625
  const validatePython_requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
16626
+ const DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB = 768;
16627
+ const DEFAULT_PYRIGHT_TIMEOUT_MS = 300000;
16628
+ function parsePositiveIntegerEnv(env, name, defaultValue) {
16629
+ const rawValue = env[name];
16630
+ if (void 0 === rawValue || "" === rawValue) return {
16631
+ value: defaultValue
16632
+ };
16633
+ if (!/^\d+$/.test(rawValue) || Number(rawValue) <= 0 || !Number.isSafeInteger(Number(rawValue))) return {
16634
+ error: `${name} must be a positive integer`
16635
+ };
16636
+ return {
16637
+ value: Number(rawValue)
16638
+ };
16639
+ }
16640
+ function pyrightChildNodeOptions(nodeOptions, maxOldSpaceMb) {
16641
+ const maxOldSpaceFlag = "--max[-_]old[-_]space[-_]size";
16642
+ const withoutEqualsForm = (nodeOptions ?? "").replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}=\\S+`, "g"), "$1");
16643
+ const withoutSeparateForm = withoutEqualsForm.replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}\\s+\\S+`, "g"), "$1");
16644
+ return [
16645
+ withoutSeparateForm.trim(),
16646
+ `--max-old-space-size=${maxOldSpaceMb}`
16647
+ ].filter(Boolean).join(" ");
16648
+ }
16649
+ function semanticProcessFailure(message) {
16650
+ return {
16651
+ ok: false,
16652
+ message: "Python semantic validation could not be run",
16653
+ issues: [
16654
+ {
16655
+ path: "root",
16656
+ message,
16657
+ severity: "error"
16658
+ }
16659
+ ]
16660
+ };
16661
+ }
16662
+ function isV8HeapOutOfMemory(stderr) {
16663
+ return /(?:JavaScript heap out of memory|Reached heap limit|Ineffective mark-compacts near heap limit)/i.test(stderr);
16664
+ }
16443
16665
  function resolveBundledAnvilPyright() {
16444
16666
  try {
16445
16667
  return {
@@ -16707,8 +16929,13 @@ print(json.dumps({"issues": issues}))
16707
16929
  };
16708
16930
  }
16709
16931
  function validatePythonWithPyright(appRoot, options = {}) {
16932
+ const env = options.env ?? process.env;
16933
+ const maxOldSpace = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_MAX_OLD_SPACE_MB", DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB);
16934
+ if ("error" in maxOldSpace) return semanticProcessFailure(maxOldSpace.error);
16935
+ const timeout = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_TIMEOUT_MS", DEFAULT_PYRIGHT_TIMEOUT_MS);
16936
+ if ("error" in timeout) return semanticProcessFailure(timeout.error);
16710
16937
  const pyright = void 0 === options.pyright ? findAnvilPyright({
16711
- env: options.env
16938
+ env
16712
16939
  }) : options.pyright;
16713
16940
  if (!pyright) return {
16714
16941
  ok: false,
@@ -16731,30 +16958,19 @@ print(json.dumps({"issues": issues}))
16731
16958
  ...options.files ?? []
16732
16959
  ], {
16733
16960
  encoding: "utf8",
16734
- maxBuffer: 10485760
16961
+ env: {
16962
+ ...env,
16963
+ NODE_OPTIONS: pyrightChildNodeOptions(env.NODE_OPTIONS, maxOldSpace.value)
16964
+ },
16965
+ maxBuffer: 10485760,
16966
+ timeout: timeout.value
16735
16967
  });
16736
- if (result.error) return {
16737
- ok: false,
16738
- message: "Python semantic validation could not be run",
16739
- issues: [
16740
- {
16741
- path: "root",
16742
- message: result.error.message,
16743
- severity: "error"
16744
- }
16745
- ]
16746
- };
16747
- if (0 !== result.status && 1 !== result.status) return {
16748
- ok: false,
16749
- message: "Python semantic validation could not be run",
16750
- issues: [
16751
- {
16752
- path: "root",
16753
- message: result.stderr.trim() || `anvil-pyright exited with status ${result.status}`,
16754
- severity: "error"
16755
- }
16756
- ]
16757
- };
16968
+ if (result.error) {
16969
+ if ("ETIMEDOUT" === result.error.code) return semanticProcessFailure(`anvil-pyright exceeded the ${timeout.value} ms timeout. Increase ANVIL_PYRIGHT_TIMEOUT_MS or validate a smaller target.`);
16970
+ return semanticProcessFailure(result.error.message);
16971
+ }
16972
+ if (isV8HeapOutOfMemory(result.stderr)) return semanticProcessFailure(`anvil-pyright ran out of memory with a ${maxOldSpace.value} MB heap. Increase ANVIL_PYRIGHT_MAX_OLD_SPACE_MB or validate a smaller target.`);
16973
+ if (0 !== result.status && 1 !== result.status) return semanticProcessFailure(result.stderr.trim() || `anvil-pyright exited with status ${result.status}`);
16758
16974
  let parsed;
16759
16975
  try {
16760
16976
  parsed = JSON.parse(result.stdout.trim());
@@ -17071,7 +17287,7 @@ print(json.dumps({"issues": issues}))
17071
17287
  }
17072
17288
  function validateScheduledTasks(value, path) {
17073
17289
  const issues = [];
17074
- if (void 0 === value) return issues;
17290
+ if (null == value) return issues;
17075
17291
  if (!Array.isArray(value)) {
17076
17292
  validators_pushIssue(issues, path, "must be an array");
17077
17293
  return issues;
@@ -18570,6 +18786,15 @@ print(json.dumps({"issues": issues}))
18570
18786
  this.hasPendingChanges = true;
18571
18787
  await this.processSaveBatchWithEvents();
18572
18788
  }
18789
+ async flush() {
18790
+ const requireReady = ()=>{
18791
+ if (this.config.isPaused() || this.config.isCleanedUp() || !this.config.isSettlePeriodOver()) throw new Error("Cannot flush saves while watching is paused, closed, or changing branch");
18792
+ };
18793
+ requireReady();
18794
+ await this.forceSave();
18795
+ requireReady();
18796
+ if (this.hasPendingChanges) throw new Error("Saves remain pending");
18797
+ }
18573
18798
  cleanup() {
18574
18799
  if (this.hasPendingChanges) {
18575
18800
  logger_logger.progressEnd("sync");
@@ -18666,10 +18891,7 @@ print(json.dumps({"issues": issues}))
18666
18891
  status = await this.config.gitService.getStatus();
18667
18892
  } catch (e) {
18668
18893
  logger_logger.error(external_chalk_default().red(`Error reading git status: ${errors_getErrorMessage(e)}`));
18669
- return {
18670
- skipped: true,
18671
- reason: "Git status error"
18672
- };
18894
+ throw e;
18673
18895
  }
18674
18896
  let allChanges;
18675
18897
  if (this.config.stagedOnly) {
@@ -19826,6 +20048,12 @@ print(json.dumps({"issues": issues}))
19826
20048
  if (!this.saveProcessor) return;
19827
20049
  return this.saveProcessor.forceSave();
19828
20050
  }
20051
+ async flushSaves() {
20052
+ if (!this.saveProcessor || this.isSwitchingForEnvironment) throw new Error("App synchronization is not ready");
20053
+ const branch = this.currentBranch;
20054
+ await this.saveProcessor.flush();
20055
+ if (this.isSwitchingForEnvironment || this.currentBranch !== branch || await this.gitService.getCurrentBranch() !== branch) throw new Error("App branch changed while flushing saves");
20056
+ }
19829
20057
  async getSuccessMessage() {
19830
20058
  try {
19831
20059
  const info = await this.gitService.getCommitInfo();
@@ -21577,7 +21805,7 @@ print(json.dumps({"issues": issues}))
21577
21805
  ])).trim();
21578
21806
  const resolvedGitCommonDir = external_path_default().isAbsolute(rawGitCommonDir) ? rawGitCommonDir : external_path_default().resolve(inputPath, rawGitCommonDir);
21579
21807
  const gitCommonDir = external_fs_default().realpathSync(resolvedGitCommonDir);
21580
- setRepoContext(repoPath);
21808
+ auth_setRepoContext(repoPath);
21581
21809
  const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
21582
21810
  const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
21583
21811
  const findings = [];
@@ -21703,7 +21931,7 @@ print(json.dumps({"issues": issues}))
21703
21931
  const repoPath = options.repoPath ?? deps.cwd();
21704
21932
  if ("get" !== operation) return null;
21705
21933
  if (!request.protocol || !request.host) return null;
21706
- setRepoContext(repoPath);
21934
+ auth_setRepoContext(repoPath);
21707
21935
  const appId = parseAppIdFromGitPath(request.path);
21708
21936
  if (!appId) return null;
21709
21937
  const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
@@ -21754,7 +21982,6 @@ print(json.dumps({"issues": issues}))
21754
21982
  }
21755
21983
  });
21756
21984
  }
21757
- const DEFAULT_ANVIL_URL = resolveAnvilUrl();
21758
21985
  async function syncToLatest(repoPath, appId, options) {
21759
21986
  const { anvilUrl, gitUrl = anvilUrl, authToken, currentBranch, username } = options;
21760
21987
  try {
@@ -21783,9 +22010,10 @@ print(json.dumps({"issues": issues}))
21783
22010
  }
21784
22011
  }
21785
22012
  async function watch(repoPath, appId, options = {}) {
21786
- const { anvilUrl = DEFAULT_ANVIL_URL, gitUrl = anvilUrl, stagedOnly = false, username } = options;
21787
22013
  repoPath = external_path_default().resolve(repoPath);
21788
- setRepoContext(repoPath);
22014
+ auth_setRepoContext(repoPath);
22015
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
22016
+ const { gitUrl = anvilUrl, stagedOnly = false, username } = options;
21789
22017
  const storedEnvPid = options.environment?.envPid ? void 0 : await readEnvironmentPid(repoPath);
21790
22018
  const environment = options.environment ?? (storedEnvPid ? {
21791
22019
  envPid: storedEnvPid
@@ -22213,7 +22441,7 @@ print(json.dumps({"issues": issues}))
22213
22441
  });
22214
22442
  logger_logger.progressEnd("verify");
22215
22443
  const currentCategory = getSyncStateCategory(freshSession.syncStatus);
22216
- const currentBranch = freshSession.getBranchName() || "master";
22444
+ const currentBranch = freshSession.getBranchName() || await getFallbackBranchName(options.repoPath);
22217
22445
  if (currentCategory !== previousCategory || currentBranch !== previousBranch) {
22218
22446
  logger_logger.warn("Repository status has changed. Re-evaluating...");
22219
22447
  return freshSession;
@@ -22246,7 +22474,7 @@ print(json.dumps({"issues": issues}))
22246
22474
  }
22247
22475
  async function checkSyncStatusAndStart(session, options, deps = defaultSyncStartDeps) {
22248
22476
  const syncStatus = session.syncStatus;
22249
- const branchName = session.getBranchName() || "master";
22477
+ const branchName = session.getBranchName() || await getFallbackBranchName(options.repoPath);
22250
22478
  const hasUncommitted = session.hasUncommittedChanges;
22251
22479
  const stateCategory = getSyncStateCategory(syncStatus);
22252
22480
  if (syncStatus?.branchMissing) {
@@ -22566,6 +22794,9 @@ print(json.dumps({"issues": issues}))
22566
22794
  const suggestedAction = error.suggestedAction;
22567
22795
  if ("string" == typeof suggestedAction && suggestedAction.length > 0) logger_logger.warn(suggestedAction);
22568
22796
  }
22797
+ async function getFallbackBranchName(repoPath) {
22798
+ return getCurrentOrFallbackBranchName(esm_default(repoPath));
22799
+ }
22569
22800
  async function handleWatchCommand(options) {
22570
22801
  const invoked = process.argv[2];
22571
22802
  if ("sync" === invoked) {
@@ -22573,7 +22804,7 @@ print(json.dumps({"issues": issues}))
22573
22804
  process.exit(1);
22574
22805
  }
22575
22806
  const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
22576
- setRepoContext(repoPath);
22807
+ auth_setRepoContext(repoPath);
22577
22808
  try {
22578
22809
  const validationResult = await validateAnvilApp(repoPath);
22579
22810
  if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
@@ -22780,6 +23011,7 @@ print(json.dumps({"issues": issues}))
22780
23011
  logger_logger.success("Logged in as " + external_chalk_default().bold(result.email));
22781
23012
  } catch (e) {
22782
23013
  logger_logger.error("Error: " + e.message);
23014
+ logErrorCause(e, logger_logger);
22783
23015
  process.exit(1);
22784
23016
  }
22785
23017
  });
@@ -23358,6 +23590,7 @@ print(json.dumps({"issues": issues}))
23358
23590
  await runConfigureFlow(version, configure_defaultDeps);
23359
23591
  } catch (e) {
23360
23592
  logger_logger.error("Error: " + errors_getErrorMessage(e));
23593
+ logErrorCause(e, logger_logger);
23361
23594
  process.exit(1);
23362
23595
  }
23363
23596
  });
@@ -23376,7 +23609,8 @@ print(json.dumps({"issues": issues}))
23376
23609
  deps.command("fetch").description("Fetch dependencies into .anvil/deps/").option("--commit <commit>", "Fetch deps at specific commit").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
23377
23610
  try {
23378
23611
  const projectRoot = process.cwd();
23379
- const anvilUrl = resolveAnvilUrl();
23612
+ auth_setRepoContext(projectRoot);
23613
+ const anvilUrl = await resolveAuthAnvilUrl();
23380
23614
  const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23381
23615
  if (!mainAppId) {
23382
23616
  logger_logger.error("No Anvil app found in current directory");
@@ -23419,100 +23653,379 @@ print(json.dumps({"issues": issues}))
23419
23653
  }
23420
23654
  });
23421
23655
  }
23422
- const TABLE_MAPPINGS_PATH = ".anvil/table-mappings.json";
23423
- function tables_formatFetchError(prefix, status, errorBody) {
23424
- if (!errorBody) return `${prefix}: ${status}`;
23425
- try {
23426
- const parsed = JSON.parse(errorBody);
23427
- if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
23428
- } catch {}
23429
- return `${prefix}: ${status} ${errorBody}`;
23656
+ async function applyDatabaseSchema(appId, options = {}) {
23657
+ const projectRoot = options.projectRoot ?? process.cwd();
23658
+ auth_setRepoContext(projectRoot);
23659
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23660
+ const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
23661
+ const token = await auth_getValidAuthToken(anvilUrl);
23662
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
23663
+ method: "PUT",
23664
+ headers: {
23665
+ Authorization: `Bearer ${token}`,
23666
+ "Content-Type": "application/json"
23667
+ },
23668
+ body: JSON.stringify({
23669
+ schema: appYaml?.db_schema ?? {},
23670
+ table_id_hints: appYaml?.table_id_hints
23671
+ })
23672
+ });
23673
+ if (!response.ok) throw new Error(formatHttpError("Failed to apply database schema", response.status, await response.text()));
23674
+ return await response.json();
23675
+ }
23676
+ async function confirmSchemaApplication(force = false) {
23677
+ if (force) return true;
23678
+ assertCanPrompt("Applying database schema", "Pass --force to apply without prompting.");
23679
+ return logger_logger.confirm("Apply the app environment's schema to its database? This may delete tables, columns, or data.", true);
23680
+ }
23681
+ function registerDbCommand(program) {
23682
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23683
+ const db = program.command("db").description("Manage app databases");
23684
+ db.command("apply-schema").description("Apply the app schema to the database for the current environment").option("-f, --force", "Apply the schema without asking for confirmation").action(async (options)=>{
23685
+ try {
23686
+ const projectRoot = process.cwd();
23687
+ auth_setRepoContext(projectRoot);
23688
+ const anvilUrl = await resolveAuthAnvilUrl();
23689
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23690
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
23691
+ if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
23692
+ const response = await applyDatabaseSchema(appId, {
23693
+ anvilUrl,
23694
+ projectRoot
23695
+ });
23696
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23697
+ data: response
23698
+ });
23699
+ else logger_logger.success("Database schema applied.");
23700
+ } catch (error) {
23701
+ const message = errors_getErrorMessage(error);
23702
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23703
+ error: message
23704
+ });
23705
+ else logger_logger.error(message);
23706
+ process.exit(1);
23707
+ }
23708
+ });
23430
23709
  }
23431
- async function fetchTableMappings(appId, anvilUrl = resolveAnvilUrl()) {
23710
+ async function ensureEnvironmentUrl(appId, envPid, options = {}) {
23711
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23432
23712
  const token = await auth_getValidAuthToken(anvilUrl);
23433
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db`, {
23713
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
23714
+ method: "POST",
23434
23715
  headers: {
23435
23716
  Authorization: `Bearer ${token}`
23436
23717
  }
23437
23718
  });
23438
- if (!resp.ok) {
23439
- const errorBody = await resp.text();
23440
- throw new Error(tables_formatFetchError("Failed to fetch table mappings", resp.status, errorBody));
23441
- }
23442
- const databases = await resp.json();
23443
- const allTables = [];
23444
- for (const db of databases)for (const table of db.tables || [])allTables.push({
23445
- table_id: table.id,
23446
- python_name: table.python_name,
23447
- name: table.name
23448
- });
23719
+ if (!response.ok) throw new Error(formatHttpError("Failed to get environment URL", response.status, await response.text()));
23720
+ const { url, expires_at } = await response.json();
23449
23721
  return {
23450
- tables: allTables
23722
+ url,
23723
+ expires_at
23451
23724
  };
23452
23725
  }
23453
- async function writeTableMappings(projectRoot, mappings) {
23454
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23455
- await promises_default().mkdir(external_path_default().dirname(filePath), {
23456
- recursive: true
23457
- });
23458
- await promises_default().writeFile(filePath, JSON.stringify(mappings, null, 2));
23459
- }
23460
- async function readTableMappings(projectRoot) {
23461
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23462
- try {
23463
- const content = await promises_default().readFile(filePath, "utf-8");
23464
- return JSON.parse(content);
23465
- } catch {
23466
- return null;
23467
- }
23468
- }
23469
- async function clearTableMappings(projectRoot) {
23470
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23471
- await promises_default().rm(filePath, {
23472
- force: true
23726
+ async function getTemporaryUplinkKey(appId, envPid, options = {}) {
23727
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23728
+ const token = await auth_getValidAuthToken(anvilUrl);
23729
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
23730
+ method: "POST",
23731
+ headers: {
23732
+ Authorization: `Bearer ${token}`
23733
+ }
23473
23734
  });
23735
+ if (!response.ok) throw new Error(formatHttpError("Failed to get temporary uplink key", response.status, await response.text()));
23736
+ const { key, url, expires_at } = await response.json();
23737
+ return {
23738
+ key,
23739
+ url,
23740
+ expires_at
23741
+ };
23742
+ }
23743
+ async function resolveEnvironmentContext() {
23744
+ const projectRoot = process.cwd();
23745
+ const envPid = await readEnvironmentPid(projectRoot);
23746
+ if (!envPid) throw new Error("No app environment associated with the current directory.");
23747
+ auth_setRepoContext(projectRoot);
23748
+ const anvilUrl = await resolveAuthAnvilUrl();
23749
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23750
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
23751
+ return {
23752
+ envPid,
23753
+ anvilUrl,
23754
+ appId
23755
+ };
23474
23756
  }
23475
- function registerTablesCommand(program) {
23476
- const tables = program.command("tables").description("Manage table mappings");
23477
- tables.command("fetch").description("Fetch table mappings into .anvil/table-mappings.json").action(async ()=>{
23757
+ function registerEnvCommand(program) {
23758
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23759
+ const env = program.command("env").description("Manage app environments");
23760
+ env.command("url").description("Get a temporary private app URL for the current environment").action(async ()=>{
23478
23761
  try {
23479
- const projectRoot = process.cwd();
23480
- const anvilUrl = resolveAnvilUrl();
23481
- const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23482
- if (!mainAppId) {
23483
- logger_logger.error("No Anvil app found in current directory");
23484
- logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23485
- process.exit(1);
23486
- }
23487
- logger_logger.info("Fetching table mappings...");
23488
- const response = await fetchTableMappings(mainAppId, anvilUrl);
23489
- if (!response.tables.length) return void logger_logger.info("No tables found");
23490
- if (await ensureProjectGitignoreIgnoresAnvilDir(projectRoot)) logger_logger.info("Added /.anvil/ to .gitignore");
23491
- await writeTableMappings(projectRoot, response);
23492
- logger_logger.success(`Fetched ${response.tables.length} table mappings`);
23493
- } catch (e) {
23494
- logger_logger.error(e.message);
23762
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
23763
+ const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
23764
+ anvilUrl
23765
+ });
23766
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23767
+ data: {
23768
+ url,
23769
+ expires_at
23770
+ }
23771
+ });
23772
+ else logger_logger.info(`URL: ${url}\nExpires at: ${expires_at}`);
23773
+ } catch (error) {
23774
+ const message = errors_getErrorMessage(error);
23775
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23776
+ error: message
23777
+ });
23778
+ else logger_logger.error(message);
23495
23779
  process.exit(1);
23496
23780
  }
23497
23781
  });
23498
- tables.command("status").description("Show cached table mappings").action(async ()=>{
23782
+ env.command("uplink").description("Get a temporary uplink key for the current environment").action(async ()=>{
23499
23783
  try {
23500
- const mappings = await readTableMappings(process.cwd());
23501
- if (!mappings || !mappings.tables.length) return void logger_logger.info("No cached table mappings (run 'anvil tables fetch')");
23502
- logger_logger.info("Cached table mappings:");
23503
- for (const t of mappings.tables)logger_logger.info(` ${t.python_name} (${t.name}) - ID: ${t.table_id}`);
23504
- } catch (e) {
23505
- logger_logger.error(e.message);
23784
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
23785
+ const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
23786
+ anvilUrl
23787
+ });
23788
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23789
+ data: {
23790
+ key,
23791
+ url,
23792
+ expires_at
23793
+ }
23794
+ });
23795
+ else logger_logger.info(`Key: ${key}\nURL: ${url}\nExpires at: ${expires_at}`);
23796
+ } catch (error) {
23797
+ const message = errors_getErrorMessage(error);
23798
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23799
+ error: message
23800
+ });
23801
+ else logger_logger.error(message);
23506
23802
  process.exit(1);
23507
23803
  }
23508
23804
  });
23509
- tables.command("clear").description("Clear cached table mappings").action(async ()=>{
23805
+ }
23806
+ const external_events_namespaceObject = require("events");
23807
+ function replError(error) {
23808
+ if ("string" == typeof error) return {
23809
+ message: error
23810
+ };
23811
+ if (error && "object" == typeof error) {
23812
+ const value = error;
23813
+ return {
23814
+ message: String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value)),
23815
+ ..."string" == typeof value.type ? {
23816
+ type: value.type
23817
+ } : {},
23818
+ ...Array.isArray(value.trace) ? {
23819
+ trace: value.trace.filter((frame)=>Array.isArray(frame) && "string" == typeof frame[0] && "number" == typeof frame[1])
23820
+ } : {}
23821
+ };
23822
+ }
23823
+ return {
23824
+ message: String(error)
23825
+ };
23826
+ }
23827
+ class ReplServerError extends Error {
23828
+ detail;
23829
+ constructor(detail){
23830
+ super(detail.message), this.detail = detail;
23831
+ }
23832
+ }
23833
+ async function runRepl(appId, envPid, options) {
23834
+ const interrupted = ()=>new Error("REPL interrupted.");
23835
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23836
+ const token = await auth_getValidAuthToken(anvilUrl);
23837
+ const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
23838
+ url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
23839
+ if (options.signal?.aborted) throw interrupted();
23840
+ const ws = new (external_ws_default())(url, {
23841
+ headers: {
23842
+ Authorization: `Bearer ${token}`
23843
+ },
23844
+ handshakeTimeout: 30000
23845
+ });
23846
+ const controller = new AbortController();
23847
+ let repl;
23848
+ let nextId = 1;
23849
+ let output = "";
23850
+ let keepalive;
23851
+ let pongTimeout;
23852
+ let closeTimeout;
23853
+ const startupTimeout = setTimeout(()=>{
23854
+ controller.abort(new Error("Timed out waiting for the REPL to start."));
23855
+ }, 30000);
23856
+ const onAbort = ()=>controller.abort(interrupted());
23857
+ const markResponsive = ()=>{
23858
+ clearTimeout(pongTimeout);
23859
+ pongTimeout = void 0;
23860
+ };
23861
+ options.signal?.addEventListener("abort", onAbort, {
23862
+ once: true
23863
+ });
23864
+ ws.on("pong", markResponsive);
23865
+ ws.on("unexpected-response", (request, response)=>{
23866
+ response.resume();
23867
+ controller.abort(new Error(`REPL websocket handshake failed (${response.statusCode} ${response.statusMessage}).`));
23868
+ request.destroy();
23869
+ });
23870
+ ws.on("error", (error)=>controller.abort(new Error(`REPL websocket connection failed: ${error.message}`)));
23871
+ ws.on("close", (code, reason)=>{
23872
+ clearTimeout(closeTimeout);
23873
+ controller.abort(new Error(`REPL websocket disconnected unexpectedly (code ${code}${reason.length ? `: ${reason}` : ""}).`));
23874
+ });
23875
+ const messages = (0, external_events_namespaceObject.on)(ws, "message", {
23876
+ signal: controller.signal
23877
+ });
23878
+ function send(message) {
23879
+ const id = nextId++;
23880
+ ws.send(JSON.stringify({
23881
+ ...message,
23882
+ id
23883
+ }), (error)=>{
23884
+ if (error) controller.abort(new Error(`REPL websocket send failed: ${error.message}`));
23885
+ });
23886
+ return id;
23887
+ }
23888
+ async function readUntil(matches) {
23889
+ while(true){
23890
+ const { value, done } = await messages.next();
23891
+ options.signal?.throwIfAborted();
23892
+ if (done) throw new Error("REPL websocket message stream ended unexpectedly.");
23893
+ markResponsive();
23894
+ let message;
23895
+ try {
23896
+ message = JSON.parse(value[0].toString());
23897
+ if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
23898
+ } catch {
23899
+ throw new Error("Invalid REPL websocket message.");
23900
+ }
23901
+ if ("REPL_UPDATE" === message.event) {
23902
+ if (!repl || message.repl !== repl) continue;
23903
+ if ("string" == typeof message.output) {
23904
+ output += message.output;
23905
+ options.onOutput?.(message.output);
23906
+ options.signal?.throwIfAborted();
23907
+ }
23908
+ }
23909
+ if (message.error) throw new ReplServerError(replError(message.error));
23910
+ if ("REPL_UPDATE" === message.event && message.terminated) throw new Error("REPL terminated unexpectedly.");
23911
+ if (matches(message)) return message;
23912
+ }
23913
+ }
23914
+ try {
23915
+ await (0, external_events_namespaceObject.once)(ws, "open", {
23916
+ signal: controller.signal
23917
+ });
23918
+ const launchId = send({
23919
+ cmd: "LAUNCH_REPL"
23920
+ });
23921
+ const launched = await readUntil((message)=>message.id === launchId);
23922
+ if ("string" != typeof launched.repl || !launched.repl) throw new Error("Invalid REPL launch response: missing REPL ID.");
23923
+ repl = launched.repl;
23924
+ keepalive = setInterval(()=>{
23925
+ try {
23926
+ send({
23927
+ cmd: "REPL_KEEPALIVE",
23928
+ repl
23929
+ });
23930
+ pongTimeout = setTimeout(()=>controller.abort(new Error("REPL websocket heartbeat timed out.")), 10000);
23931
+ ws.ping();
23932
+ } catch (error) {
23933
+ controller.abort(error);
23934
+ }
23935
+ }, 20000);
23936
+ const isReady = (message)=>"REPL_UPDATE" === message.event && true === message.ready;
23937
+ await readUntil(isReady);
23938
+ clearTimeout(startupTimeout);
23939
+ send({
23940
+ cmd: "REPL_COMMAND",
23941
+ repl,
23942
+ command: options.code
23943
+ });
23944
+ await readUntil(isReady);
23945
+ return {
23946
+ output
23947
+ };
23948
+ } catch (error) {
23949
+ if (error instanceof ReplServerError) return {
23950
+ output,
23951
+ error: error.detail
23952
+ };
23953
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
23954
+ throw failure instanceof Error ? failure : new Error(String(failure));
23955
+ } finally{
23956
+ clearTimeout(startupTimeout);
23957
+ clearInterval(keepalive);
23958
+ clearTimeout(pongTimeout);
23959
+ options.signal?.removeEventListener("abort", onAbort);
23960
+ controller.abort();
23961
+ await messages.return?.();
23962
+ if (ws.readyState === external_ws_default().OPEN) {
23963
+ if (repl) ws.send(JSON.stringify({
23964
+ id: nextId++,
23965
+ cmd: "TERMINATE_REPL",
23966
+ repl
23967
+ }), ()=>{});
23968
+ ws.close();
23969
+ } else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
23970
+ if (ws.readyState !== external_ws_default().CLOSED) {
23971
+ closeTimeout = setTimeout(()=>ws.terminate(), 1000);
23972
+ closeTimeout.unref();
23973
+ }
23974
+ }
23975
+ }
23976
+ function writeJsonReplOutput(output) {
23977
+ process.stdout.write(JSON.stringify({
23978
+ type: "repl_output",
23979
+ output,
23980
+ timestamp: new Date().toISOString()
23981
+ }) + "\n");
23982
+ }
23983
+ function registerReplCommand(program) {
23984
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23985
+ program.command("repl <code>").description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
23986
+ Examples:
23987
+ anvil repl '1 + 2'
23988
+ anvil --json repl '1 + 2'
23989
+ anvil repl - < script.py
23990
+ `).action(async (code)=>{
23991
+ const controller = new AbortController();
23992
+ const onInterrupt = ()=>controller.abort();
23510
23993
  try {
23511
- await clearTableMappings(process.cwd());
23512
- logger_logger.success("Cleared .anvil/table-mappings.json");
23513
- } catch (e) {
23514
- logger_logger.error(e.message);
23515
- process.exit(1);
23994
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
23995
+ if ("-" === code) {
23996
+ const chunks = [];
23997
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
23998
+ code = Buffer.concat(chunks).toString("utf8");
23999
+ }
24000
+ process.on("SIGINT", onInterrupt);
24001
+ const response = await runRepl(appId, envPid, {
24002
+ code,
24003
+ anvilUrl,
24004
+ signal: controller.signal,
24005
+ onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
24006
+ process.stdout.write(output);
24007
+ }
24008
+ });
24009
+ const error = response.error;
24010
+ const message = error ? `${error.type ? `${error.type}: ` : ""}${error.message}` : void 0;
24011
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(!error, {
24012
+ data: response,
24013
+ error: message
24014
+ });
24015
+ else if (error) {
24016
+ process.stderr.write(`${message}\n`);
24017
+ for (const [file, line] of error.trace ?? [])process.stderr.write(` at ${file}:${line}\n`);
24018
+ }
24019
+ if (error) process.exitCode = 1;
24020
+ } catch (error) {
24021
+ const message = errors_getErrorMessage(error);
24022
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24023
+ error: message
24024
+ });
24025
+ else logger_logger.error(message);
24026
+ process.exitCode = controller.signal.aborted ? 130 : 1;
24027
+ } finally{
24028
+ process.removeListener("SIGINT", onInterrupt);
23516
24029
  }
23517
24030
  });
23518
24031
  }
@@ -23936,7 +24449,9 @@ print(json.dumps({"issues": issues}))
23936
24449
  registerVersionCommand(program, VERSION);
23937
24450
  registerConfigureCommand(program, VERSION);
23938
24451
  registerDepsCommand(program);
23939
- registerTablesCommand(program);
24452
+ registerDbCommand(program);
24453
+ registerEnvCommand(program);
24454
+ registerReplCommand(program);
23940
24455
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
23941
24456
  await handleUpdateCommand();
23942
24457
  });