@anvil-works/anvil-cli 0.8.0-canary.9 → 0.8.0-dev.1

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 (71) hide show
  1. package/README.md +54 -0
  2. package/dist/WatchSession.d.ts +2 -0
  3. package/dist/WatchSession.d.ts.map +1 -1
  4. package/dist/api.d.ts +8 -2
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/cli.js +2799 -1249
  7. package/dist/commands/appDetection.d.ts +68 -1
  8. package/dist/commands/appDetection.d.ts.map +1 -1
  9. package/dist/commands/checkout.d.ts +1 -0
  10. package/dist/commands/checkout.d.ts.map +1 -1
  11. package/dist/commands/configure.d.ts.map +1 -1
  12. package/dist/commands/create.d.ts +52 -0
  13. package/dist/commands/create.d.ts.map +1 -0
  14. package/dist/commands/db.d.ts +3 -0
  15. package/dist/commands/db.d.ts.map +1 -0
  16. package/dist/commands/deps.d.ts.map +1 -1
  17. package/dist/commands/env.d.ts +10 -0
  18. package/dist/commands/env.d.ts.map +1 -0
  19. package/dist/commands/index.d.ts +6 -1
  20. package/dist/commands/index.d.ts.map +1 -1
  21. package/dist/commands/login.d.ts.map +1 -1
  22. package/dist/commands/modelToken.d.ts +33 -0
  23. package/dist/commands/modelToken.d.ts.map +1 -0
  24. package/dist/commands/repl.d.ts +3 -0
  25. package/dist/commands/repl.d.ts.map +1 -0
  26. package/dist/commands/secret.d.ts +3 -0
  27. package/dist/commands/secret.d.ts.map +1 -0
  28. package/dist/commands/test.d.ts +26 -0
  29. package/dist/commands/test.d.ts.map +1 -0
  30. package/dist/commands/watch.d.ts +1 -25
  31. package/dist/commands/watch.d.ts.map +1 -1
  32. package/dist/config.d.ts +3 -1
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/errors.d.ts +11 -1
  35. package/dist/errors.d.ts.map +1 -1
  36. package/dist/formTemplateValidation.d.ts.map +1 -1
  37. package/dist/index.js +1466 -994
  38. package/dist/program.d.ts.map +1 -1
  39. package/dist/services/anvil-api.d.ts +20 -0
  40. package/dist/services/anvil-api.d.ts.map +1 -1
  41. package/dist/services/auth.d.ts +42 -3
  42. package/dist/services/auth.d.ts.map +1 -1
  43. package/dist/services/db.d.ts +9 -0
  44. package/dist/services/db.d.ts.map +1 -0
  45. package/dist/services/deps.d.ts +5 -2
  46. package/dist/services/deps.d.ts.map +1 -1
  47. package/dist/services/environment.d.ts +20 -0
  48. package/dist/services/environment.d.ts.map +1 -0
  49. package/dist/services/git-auth.d.ts +35 -0
  50. package/dist/services/git-auth.d.ts.map +1 -1
  51. package/dist/services/git.d.ts +1 -0
  52. package/dist/services/git.d.ts.map +1 -1
  53. package/dist/services/http.d.ts +2 -0
  54. package/dist/services/http.d.ts.map +1 -0
  55. package/dist/services/path-open.d.ts +1 -0
  56. package/dist/services/path-open.d.ts.map +1 -1
  57. package/dist/services/repl.d.ts +20 -0
  58. package/dist/services/repl.d.ts.map +1 -0
  59. package/dist/services/secret.d.ts +43 -0
  60. package/dist/services/secret.d.ts.map +1 -0
  61. package/dist/services/server-tests.d.ts +47 -0
  62. package/dist/services/server-tests.d.ts.map +1 -0
  63. package/dist/validateFormTemplateHtml.d.ts.map +1 -1
  64. package/dist/validatePython.d.ts.map +1 -1
  65. package/dist/watch/SaveProcessor.d.ts +2 -0
  66. package/dist/watch/SaveProcessor.d.ts.map +1 -1
  67. package/package.json +2 -2
  68. package/dist/commands/tables.d.ts +0 -7
  69. package/dist/commands/tables.d.ts.map +0 -1
  70. package/dist/services/tables.d.ts +0 -27
  71. package/dist/services/tables.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -12875,12 +12875,25 @@ var __webpack_exports__ = {};
12875
12875
  if (!urlInput || "string" != typeof urlInput) return urlInput;
12876
12876
  let url = urlInput.trim();
12877
12877
  url = url.replace(/\/+$/, "");
12878
- if ("localhost" === url || url.startsWith("localhost:")) return "http://localhost:3000";
12879
- if ("http://localhost" === url || url.startsWith("http://localhost:")) return "http://localhost:3000";
12880
- if ("https://localhost" === url || url.startsWith("https://localhost:")) return "http://localhost:3000";
12881
- if (/^https?:\/\//.test(url)) return url;
12878
+ if ("localhost" === url) return "http://localhost:3000";
12879
+ if (url.startsWith("localhost:")) return `http://${url}`;
12880
+ if (/^https?:\/\//i.test(url)) return url;
12882
12881
  return `https://${url}`;
12883
12882
  }
12883
+ function normalizeAnvilOrigin(input) {
12884
+ try {
12885
+ const raw = input.trim();
12886
+ const normalized = normalizeAnvilUrl(raw);
12887
+ const url = new URL(raw.includes("://") ? raw : normalized);
12888
+ if (![
12889
+ "http:",
12890
+ "https:"
12891
+ ].includes(url.protocol) || url.username || url.password || "/" !== url.pathname || url.search || url.hash) throw new Error("Invalid origin");
12892
+ return normalized;
12893
+ } catch {
12894
+ throw new Error("Server URL must be an HTTP(S) origin without credentials, a path, query, or fragment.");
12895
+ }
12896
+ }
12884
12897
  function resolveAnvilUrl() {
12885
12898
  const fromConfig = getConfig("anvilUrl");
12886
12899
  if ("string" == typeof fromConfig && fromConfig.trim()) return fromConfig.trim();
@@ -12904,6 +12917,9 @@ var __webpack_exports__ = {};
12904
12917
  function getGlobalOutputConfig() {
12905
12918
  return globalOutputConfig;
12906
12919
  }
12920
+ function canPrompt() {
12921
+ return !globalOutputConfig.jsonMode && !!(process.stdin.isTTY && process.stdout.isTTY);
12922
+ }
12907
12923
  function assertCanPrompt(action = "This command", explicitAlternative = "Pass explicit flags instead.") {
12908
12924
  if (globalOutputConfig.jsonMode) throw new Error(`${action} requires interactive input, but --json disables prompts. ${explicitAlternative}`);
12909
12925
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error(`${action} requires interactive input, but stdin/stdout is not a TTY. ${explicitAlternative}`);
@@ -13214,8 +13230,6 @@ var __webpack_exports__ = {};
13214
13230
  return answer.value;
13215
13231
  }
13216
13232
  }
13217
- const external_readline_namespaceObject = require("readline");
13218
- var external_readline_default = /*#__PURE__*/ __webpack_require__.n(external_readline_namespaceObject);
13219
13233
  const external_js_yaml_namespaceObject = require("js-yaml");
13220
13234
  var external_js_yaml_default = /*#__PURE__*/ __webpack_require__.n(external_js_yaml_namespaceObject);
13221
13235
  class EditorYamlManager {
@@ -13384,9 +13398,10 @@ var __webpack_exports__ = {};
13384
13398
  type: "auth_invalid",
13385
13399
  message
13386
13400
  }),
13387
- refreshFailed: (message)=>({
13401
+ refreshFailed: (message, status)=>({
13388
13402
  type: "token_refresh_failed",
13389
- message
13403
+ message,
13404
+ status
13390
13405
  })
13391
13406
  };
13392
13407
  const createAppError = {
@@ -13600,6 +13615,19 @@ var __webpack_exports__ = {};
13600
13615
  return "An unknown error occurred";
13601
13616
  }
13602
13617
  }
13618
+ function getErrorCauseMessage(error) {
13619
+ if ("object" != typeof error || null === error || !("cause" in error)) return null;
13620
+ const cause = error.cause;
13621
+ if (null == cause) return null;
13622
+ const message = errors_getErrorMessage(cause);
13623
+ const code = "object" == typeof cause && null !== cause && "code" in cause && "string" == typeof cause.code ? cause.code : null;
13624
+ if (code && !message.includes(code)) return `${message} (${code})`;
13625
+ return message;
13626
+ }
13627
+ function logErrorCause(error, ui) {
13628
+ const causeMessage = getErrorCauseMessage(error);
13629
+ if (causeMessage) ui.verbose(`Cause: ${causeMessage}`);
13630
+ }
13603
13631
  const external_module_namespaceObject = require("module");
13604
13632
  const requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
13605
13633
  const GIT_CREDENTIAL_HELPER_FILENAME = "anvil-credential-helper.cjs";
@@ -13820,6 +13848,76 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13820
13848
  function getAuthFilePath(gitDir) {
13821
13849
  return external_path_default().join(gitDir, "anvil-auth.json");
13822
13850
  }
13851
+ const AUTH_FILE_LOCK_DEFAULTS = {
13852
+ staleMs: 30000,
13853
+ timeoutMs: 60000,
13854
+ retryDelayMs: 100,
13855
+ heartbeatMs: 5000
13856
+ };
13857
+ function git_auth_delay(ms) {
13858
+ return new Promise((resolve)=>setTimeout(resolve, ms));
13859
+ }
13860
+ async function acquireAuthFileLock(lockPath, options) {
13861
+ const deadline = Date.now() + options.timeoutMs;
13862
+ while(true){
13863
+ try {
13864
+ const handle = await external_fs_.promises.open(lockPath, "wx", 384);
13865
+ try {
13866
+ await handle.writeFile(JSON.stringify({
13867
+ pid: process.pid,
13868
+ acquiredAt: new Date().toISOString()
13869
+ }));
13870
+ } finally{
13871
+ await handle.close();
13872
+ }
13873
+ return;
13874
+ } catch (e) {
13875
+ if ("EEXIST" !== e.code) throw e;
13876
+ }
13877
+ try {
13878
+ const lockStat = await external_fs_.promises.stat(lockPath);
13879
+ if (Date.now() - lockStat.mtimeMs > options.staleMs) {
13880
+ await external_fs_.promises.rm(lockPath, {
13881
+ force: true
13882
+ });
13883
+ continue;
13884
+ }
13885
+ } catch {
13886
+ continue;
13887
+ }
13888
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Anvil auth file lock at ${lockPath}`);
13889
+ await git_auth_delay(options.retryDelayMs);
13890
+ }
13891
+ }
13892
+ async function withRepoAuthFileLock(repoPath, fn, options) {
13893
+ const gitDir = await getRepositoryGitDir(repoPath);
13894
+ const lockPath = `${getAuthFilePath(gitDir)}.lock`;
13895
+ const lockOptions = {
13896
+ ...AUTH_FILE_LOCK_DEFAULTS,
13897
+ ...options
13898
+ };
13899
+ await external_fs_.promises.mkdir(external_path_default().dirname(lockPath), {
13900
+ recursive: true
13901
+ });
13902
+ await acquireAuthFileLock(lockPath, lockOptions);
13903
+ const heartbeat = startAuthFileLockHeartbeat(lockPath, lockOptions.heartbeatMs);
13904
+ try {
13905
+ return await fn();
13906
+ } finally{
13907
+ clearInterval(heartbeat);
13908
+ await external_fs_.promises.rm(lockPath, {
13909
+ force: true
13910
+ });
13911
+ }
13912
+ }
13913
+ function startAuthFileLockHeartbeat(lockPath, heartbeatMs) {
13914
+ const heartbeat = setInterval(()=>{
13915
+ const now = new Date();
13916
+ external_fs_.promises.utimes(lockPath, now, now).catch(()=>{});
13917
+ }, heartbeatMs);
13918
+ heartbeat.unref();
13919
+ return heartbeat;
13920
+ }
13823
13921
  function decodeKey(b64key) {
13824
13922
  const key = Buffer.from(b64key, "base64");
13825
13923
  if (32 !== key.length) throw new Error("Encryption key must be a base64-encoded 32-byte key");
@@ -13864,10 +13962,19 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13864
13962
  recursive: true
13865
13963
  });
13866
13964
  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);
13965
+ const tempFilePath = `${authFilePath}.tmp-${external_node_crypto_default().randomBytes(6).toString("hex")}`;
13966
+ try {
13967
+ await external_fs_.promises.writeFile(tempFilePath, encrypt(authJson, encryptionKey), {
13968
+ mode: 384
13969
+ });
13970
+ await external_fs_.promises.chmod(tempFilePath, 448);
13971
+ await external_fs_.promises.rename(tempFilePath, authFilePath);
13972
+ } catch (e) {
13973
+ await external_fs_.promises.rm(tempFilePath, {
13974
+ force: true
13975
+ });
13976
+ throw e;
13977
+ }
13871
13978
  await git.raw([
13872
13979
  "config",
13873
13980
  "--local",
@@ -13876,8 +13983,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13876
13983
  ]);
13877
13984
  return authFilePath;
13878
13985
  }
13879
- const readAuthFromRepo = async (repoPath)=>{
13986
+ const git_auth_readAuthFromRepo = async (repoPath)=>{
13880
13987
  const git = esm_default(repoPath);
13988
+ if (!await git.checkIsRepo()) return;
13881
13989
  const gitDir = await getRepositoryGitDir(repoPath, git);
13882
13990
  const authFilePath = getAuthFilePath(gitDir);
13883
13991
  if (!external_fs_default().existsSync(authFilePath)) return;
@@ -13889,14 +13997,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13889
13997
  return resolveAnvilUrl();
13890
13998
  }
13891
13999
  const ANVIL_SYNC_CLIENT_ID = "anvil-sync";
14000
+ const REFRESH_REQUEST_TIMEOUT_MS = 12000;
13892
14001
  let inMemoryAuth = null;
13893
14002
  function authMatches(auth, anvilUrl, username) {
13894
14003
  return auth.anvilUrl === normalizeAnvilUrl(anvilUrl) && (!username || auth.username === username);
13895
14004
  }
13896
14005
  let repoContext = null;
13897
- function setRepoContext(repoPath) {
14006
+ function auth_setRepoContext(repoPath) {
13898
14007
  repoContext = repoPath;
13899
14008
  }
14009
+ async function resolveAuthAnvilUrl() {
14010
+ if (inMemoryAuth) return inMemoryAuth.anvilUrl;
14011
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14012
+ return repoAuth?.anvilUrl ?? resolveAnvilUrl();
14013
+ }
13900
14014
  const inFlightRefreshes = new Map();
13901
14015
  async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13902
14016
  try {
@@ -13917,7 +14031,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13917
14031
  throw errors_createAuthError.invalid(`Network error: ${e.message}`);
13918
14032
  }
13919
14033
  }
13920
- async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID) {
14034
+ async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID, options) {
13921
14035
  try {
13922
14036
  const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
13923
14037
  method: "POST",
@@ -13928,11 +14042,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13928
14042
  grant_type: "refresh_token",
13929
14043
  refresh_token: refreshToken,
13930
14044
  client_id: clientId
13931
- })
14045
+ }),
14046
+ signal: AbortSignal.timeout(options?.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS)
13932
14047
  });
13933
14048
  if (!tokenResponse.ok) {
13934
14049
  const errorText = await tokenResponse.text();
13935
- throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`);
14050
+ throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`, tokenResponse.status);
13936
14051
  }
13937
14052
  const tokenData = await tokenResponse.json();
13938
14053
  return tokenData;
@@ -13944,22 +14059,21 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13944
14059
  async function hasTokensForUrl(anvilUrl, username) {
13945
14060
  if (inMemoryAuth) return authMatches(inMemoryAuth, anvilUrl, username);
13946
14061
  {
13947
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14062
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13948
14063
  if (repoAuth && authMatches(repoAuth, anvilUrl, username)) return true;
13949
14064
  return hasTokens(anvilUrl, username);
13950
14065
  }
13951
14066
  }
13952
- async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
14067
+ async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username, options) {
13953
14068
  const normalized = normalizeAnvilUrl(anvilUrl);
13954
14069
  const source = await resolveTokenSource(normalized, username);
13955
14070
  const tokens = getSourceTokens(source);
13956
14071
  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) {
14072
+ if (tokens.refreshToken && isAccessTokenExpired(tokens)) {
13959
14073
  const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13960
14074
  const existingRefresh = inFlightRefreshes.get(refreshKey);
13961
14075
  if (existingRefresh) return existingRefresh;
13962
- const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
14076
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken, options).finally(()=>{
13963
14077
  inFlightRefreshes.delete(refreshKey);
13964
14078
  });
13965
14079
  inFlightRefreshes.set(refreshKey, refreshPromise);
@@ -13973,7 +14087,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13973
14087
  type: "memory",
13974
14088
  auth: inMemoryAuth
13975
14089
  };
13976
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14090
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13977
14091
  if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13978
14092
  type: "repo",
13979
14093
  auth: repoAuth,
@@ -13989,6 +14103,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13989
14103
  function getSourceTokens(source) {
13990
14104
  return "store" === source.type ? source.tokens : source.auth.tokens;
13991
14105
  }
14106
+ function isAccessTokenExpired(tokens) {
14107
+ return null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
14108
+ }
13992
14109
  function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13993
14110
  if (username) return username;
13994
14111
  const urlTokens = getUrlTokens(normalizedUrl);
@@ -14007,11 +14124,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14007
14124
  return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
14008
14125
  }
14009
14126
  }
14010
- async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
14127
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken, options) {
14128
+ if ("repo" === source.type) return refreshRepoAuthTokens(source.repoPath, {
14129
+ anvilUrl: normalizedUrl,
14130
+ username,
14131
+ auth: source.auth,
14132
+ refreshToken,
14133
+ lock: options?.lock,
14134
+ requestTimeoutMs: options?.requestTimeoutMs
14135
+ });
14136
+ return performTokenRefresh(normalizedUrl, username, source, refreshToken, options?.requestTimeoutMs);
14137
+ }
14138
+ async function refreshRepoAuthTokens(repoPath, options) {
14139
+ const normalizedUrl = normalizeAnvilUrl(options.anvilUrl);
14140
+ return withRepoAuthFileLock(repoPath, async ()=>{
14141
+ let auth = options.auth;
14142
+ let refreshToken = options.refreshToken;
14143
+ const current = await git_auth_readAuthFromRepo(repoPath);
14144
+ if (current && authMatches(current, normalizedUrl, options.username)) {
14145
+ if (current.tokens.authToken && !isAccessTokenExpired(current.tokens)) return current.tokens.authToken;
14146
+ auth = current;
14147
+ refreshToken = current.tokens.refreshToken ?? refreshToken;
14148
+ }
14149
+ if (!auth || !refreshToken) throw errors_createAuthError.required("No repo auth tokens available to refresh.");
14150
+ return performTokenRefresh(normalizedUrl, options.username, {
14151
+ type: "repo",
14152
+ auth,
14153
+ repoPath
14154
+ }, refreshToken, options.requestTimeoutMs);
14155
+ }, options.lock);
14156
+ }
14157
+ async function performTokenRefresh(normalizedUrl, username, source, refreshToken, requestTimeoutMs) {
14011
14158
  try {
14012
14159
  const tokens = getSourceTokens(source);
14013
14160
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
14014
- const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
14161
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId, {
14162
+ timeoutMs: requestTimeoutMs
14163
+ });
14015
14164
  const newTokens = {
14016
14165
  authToken: tokenData.access_token,
14017
14166
  refreshToken: tokenData.refresh_token,
@@ -14033,12 +14182,28 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14033
14182
  }
14034
14183
  return tokenData.access_token;
14035
14184
  } catch (e) {
14185
+ if ("repo" === source.type && isRefreshTokenRejection(e)) await clearRepoAuthTokens(source.repoPath, source.auth).catch(()=>{});
14036
14186
  await clearTokensForAccount(normalizedUrl, username, refreshToken);
14037
14187
  throw e;
14038
14188
  }
14039
14189
  }
14190
+ function isRefreshTokenRejection(e) {
14191
+ const error = e;
14192
+ return error?.type === "token_refresh_failed" && (400 === error.status || 401 === error.status);
14193
+ }
14194
+ async function clearRepoAuthTokens(repoPath, auth) {
14195
+ const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
14196
+ if (!encryptionKey) return;
14197
+ auth.tokens = {
14198
+ authToken: null,
14199
+ refreshToken: null,
14200
+ authTokenExpiresAt: null,
14201
+ clientId: auth.tokens.clientId
14202
+ };
14203
+ await writeAuthToFile(repoPath, auth, encryptionKey);
14204
+ }
14040
14205
  async function clearTokensForAccount(url, username, refreshToken) {
14041
- if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
14206
+ if (inMemoryAuth || repoContext && await git_auth_readAuthFromRepo(repoContext)) return;
14042
14207
  {
14043
14208
  const normalized = normalizeAnvilUrl(url);
14044
14209
  if (username) deleteAccountTokens(normalized, username);
@@ -14068,7 +14233,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14068
14233
  loggedOut: false,
14069
14234
  message: "Cannot log out when using in-memory auth tokens"
14070
14235
  };
14071
- if (repoContext && await readAuthFromRepo(repoContext)) return {
14236
+ if (repoContext && await git_auth_readAuthFromRepo(repoContext)) return {
14072
14237
  loggedOut: false,
14073
14238
  message: "Cannot log out when using auth tokens stored in the local git config."
14074
14239
  };
@@ -14099,7 +14264,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14099
14264
  inMemoryAuth.username
14100
14265
  ] : [];
14101
14266
  {
14102
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14267
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14103
14268
  if (repoAuth && authMatches(repoAuth, url)) return [
14104
14269
  repoAuth.username
14105
14270
  ];
@@ -14111,634 +14276,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14111
14276
  inMemoryAuth.anvilUrl
14112
14277
  ];
14113
14278
  {
14114
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14279
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14115
14280
  if (repoAuth) return [
14116
14281
  repoAuth.anvilUrl
14117
14282
  ];
14118
14283
  return getAvailableUrls();
14119
14284
  }
14120
14285
  }
14121
- function anvil_api_getDefaultAnvilUrl() {
14122
- return resolveAnvilUrl();
14286
+ const promises_namespaceObject = require("timers/promises");
14287
+ const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
14288
+ const FETCH_RETRY_DELAY_MS = 200;
14289
+ function isWantNotValidError(error) {
14290
+ return WANT_NOT_VALID_RE.test(error.message || "");
14123
14291
  }
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);
14292
+ class GitService extends Emitter {
14293
+ git;
14294
+ repoPath;
14295
+ static INDEX_LOCK_RETRY_DELAYS_MS = [
14296
+ 25,
14297
+ 50,
14298
+ 100,
14299
+ 200
14300
+ ];
14301
+ constructor(repoPath){
14302
+ super();
14303
+ this.repoPath = external_path_default().resolve(repoPath);
14304
+ this.git = esm_default(this.repoPath);
14153
14305
  }
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);
14306
+ getGit() {
14307
+ return this.git;
14184
14308
  }
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;
14309
+ getRepoPath() {
14310
+ return this.repoPath;
14207
14311
  }
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);
14312
+ async getGitDir() {
14313
+ try {
14314
+ const gitDir = (await this.git.revparse([
14315
+ "--git-dir"
14316
+ ])).trim();
14317
+ if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
14318
+ return gitDir;
14319
+ } catch (e) {
14320
+ throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14321
+ }
14214
14322
  }
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
- }
14270
- }
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}`);
14305
- }
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
- });
14315
- }
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
- }
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
- };
14359
- }
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;
14378
- }
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));
14411
- }
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
- };
14428
- }
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;
14466
- }
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 [];
14508
- }
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
- }
14528
- }
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
14581
- });
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
14587
- });
14588
- await promises_default().writeFile(moduleFile, mod.code);
14589
- }
14590
- }
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));
14607
- }
14608
- }
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);
14622
- }
14623
- }
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"));
14630
- }
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);
14637
- }
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() {
14721
- 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;
14727
- } catch (e) {
14728
- throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14729
- }
14730
- }
14731
- async getCurrentBranch() {
14732
- try {
14733
- const branchRef = await this.git.revparse([
14734
- "--abbrev-ref",
14735
- "HEAD"
14736
- ]);
14737
- if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14738
- return branchRef;
14739
- } catch (e) {
14740
- if ("detached_head" === e.type) throw e;
14741
- throw createSyncError.detachedHead();
14323
+ async getCurrentBranch() {
14324
+ try {
14325
+ const branchRef = await this.git.revparse([
14326
+ "--abbrev-ref",
14327
+ "HEAD"
14328
+ ]);
14329
+ if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14330
+ return branchRef;
14331
+ } catch (e) {
14332
+ if ("detached_head" === e.type) throw e;
14333
+ throw createSyncError.detachedHead();
14742
14334
  }
14743
14335
  }
14744
14336
  async getCommitId() {
@@ -14818,323 +14410,982 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14818
14410
  "--",
14819
14411
  relativePath
14820
14412
  ]);
14821
- await this.restoreExecutableBit(relativePath, "100755" === headMode);
14822
- discarded.push(relativePath);
14413
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14414
+ discarded.push(relativePath);
14415
+ } catch (e) {
14416
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14417
+ }
14418
+ return discarded;
14419
+ }
14420
+ async getHeadFileMode(relativePath) {
14421
+ const output = await this.git.raw([
14422
+ "ls-tree",
14423
+ "HEAD",
14424
+ "--",
14425
+ relativePath
14426
+ ]);
14427
+ const match = output.match(/^(\d{6})\s/);
14428
+ return match?.[1] ?? null;
14429
+ }
14430
+ async hasSameContentAsHead(relativePath) {
14431
+ const headHash = (await this.git.revparse([
14432
+ `HEAD:${relativePath}`
14433
+ ])).trim();
14434
+ const worktreeHash = (await this.git.raw([
14435
+ "hash-object",
14436
+ "--",
14437
+ relativePath
14438
+ ])).trim();
14439
+ return headHash === worktreeHash;
14440
+ }
14441
+ async hasSameIndexContentAsHead(relativePath) {
14442
+ const headHash = (await this.git.revparse([
14443
+ `HEAD:${relativePath}`
14444
+ ])).trim();
14445
+ const indexHash = (await this.git.revparse([
14446
+ `:${relativePath}`
14447
+ ])).trim();
14448
+ return headHash === indexHash;
14449
+ }
14450
+ async restoreExecutableBit(relativePath, executable) {
14451
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14452
+ const stat = await external_fs_.promises.stat(filePath);
14453
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14454
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14455
+ }
14456
+ async hasUncommittedChanges() {
14457
+ const status = await this.getStatus();
14458
+ return !status.isClean;
14459
+ }
14460
+ async fetch(url, refSpec) {
14461
+ let attempt = 0;
14462
+ while(true)try {
14463
+ await this.git.fetch([
14464
+ url,
14465
+ refSpec
14466
+ ]);
14467
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14468
+ attempts: attempt,
14469
+ outcome: "succeeded"
14470
+ });
14471
+ return;
14472
+ } catch (e) {
14473
+ const err = e;
14474
+ if (!isWantNotValidError(err)) {
14475
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14476
+ attempts: attempt,
14477
+ outcome: "failed"
14478
+ });
14479
+ throw errors_createGitError.fetchFailed(err.message);
14480
+ }
14481
+ attempt++;
14482
+ this.emit("fetch-retry", {
14483
+ attempt,
14484
+ delayMs: FETCH_RETRY_DELAY_MS,
14485
+ error: err.message
14486
+ });
14487
+ logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
14488
+ await (0, promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
14489
+ }
14490
+ }
14491
+ async reset(ref, mode = "mixed") {
14492
+ await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14493
+ `--${mode}`,
14494
+ ref
14495
+ ]));
14496
+ }
14497
+ async checkout(paths) {
14498
+ await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14499
+ }
14500
+ async runGitCommandWithIndexLockRetry(command, operation) {
14501
+ let lastError;
14502
+ for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
14503
+ await operation();
14504
+ return;
14505
+ } catch (e) {
14506
+ lastError = e;
14507
+ if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
14508
+ await (0, promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
14509
+ }
14510
+ throw errors_createGitError.commandFailed(command, lastError.message);
14511
+ }
14512
+ isIndexLockError(error) {
14513
+ const message = error.message ?? "";
14514
+ return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
14515
+ }
14516
+ async stash(message) {
14517
+ try {
14518
+ const args = [
14519
+ "stash",
14520
+ "push",
14521
+ "--include-untracked"
14522
+ ];
14523
+ if (message) args.push("-m", message);
14524
+ const result = await this.git.raw(args);
14525
+ return !result.includes("No local changes to save");
14526
+ } catch (e) {
14527
+ throw errors_createGitError.commandFailed("stash", e.message);
14528
+ }
14529
+ }
14530
+ async stashPop() {
14531
+ try {
14532
+ await this.git.raw([
14533
+ "stash",
14534
+ "pop"
14535
+ ]);
14536
+ } catch (e) {
14537
+ throw errors_createGitError.commandFailed("stash pop", e.message);
14538
+ }
14539
+ }
14540
+ async clean(files) {
14541
+ try {
14542
+ await this.git.clean(CleanOptions.FORCE, files);
14543
+ } catch (e) {
14544
+ throw errors_createGitError.commandFailed("clean", e.message);
14545
+ }
14546
+ }
14547
+ async diffNames(fromCommit, toCommit) {
14548
+ try {
14549
+ const diffResult = await this.git.diff([
14550
+ fromCommit,
14551
+ toCommit,
14552
+ "--name-only"
14553
+ ]);
14554
+ const files = diffResult.split("\n").filter((f)=>f.trim());
14555
+ return files;
14556
+ } catch (e) {
14557
+ throw errors_createGitError.commandFailed("diff", e.message);
14558
+ }
14559
+ }
14560
+ async getAheadBehind(localRef, remoteRef) {
14561
+ try {
14562
+ const result = await this.git.raw([
14563
+ "rev-list",
14564
+ "--left-right",
14565
+ "--count",
14566
+ `${remoteRef}...${localRef}`
14567
+ ]);
14568
+ const [behind, ahead] = result.trim().split("\t").map(Number);
14569
+ return {
14570
+ ahead,
14571
+ behind
14572
+ };
14573
+ } catch (e) {
14574
+ throw errors_createGitError.commandFailed("rev-list", e.message);
14575
+ }
14576
+ }
14577
+ async show(refPath) {
14578
+ try {
14579
+ const content = await this.git.show([
14580
+ refPath
14581
+ ]);
14582
+ return content;
14583
+ } catch (e) {
14584
+ throw errors_createGitError.commandFailed("show", e.message);
14585
+ }
14586
+ }
14587
+ async push(url, refSpec, force = false) {
14588
+ try {
14589
+ const args = force ? [
14590
+ "push",
14591
+ "--force",
14592
+ url,
14593
+ refSpec
14594
+ ] : [
14595
+ "push",
14596
+ url,
14597
+ refSpec
14598
+ ];
14599
+ await this.git.raw(args);
14600
+ } catch (e) {
14601
+ throw errors_createGitError.commandFailed("push", e.message);
14602
+ }
14603
+ }
14604
+ async rebase(onto) {
14605
+ try {
14606
+ await this.git.raw([
14607
+ "rebase",
14608
+ onto
14609
+ ]);
14610
+ } catch (e) {
14611
+ throw errors_createGitError.commandFailed("rebase", e.message);
14612
+ }
14613
+ }
14614
+ async rebaseAbort() {
14615
+ try {
14616
+ await this.git.raw([
14617
+ "rebase",
14618
+ "--abort"
14619
+ ]);
14620
+ } catch (e) {
14621
+ throw errors_createGitError.commandFailed("rebase --abort", e.message);
14622
+ }
14623
+ }
14624
+ async isAncestor(ancestorRef, descendantRef) {
14625
+ try {
14626
+ const mergeBase = (await this.git.raw([
14627
+ "merge-base",
14628
+ ancestorRef,
14629
+ descendantRef
14630
+ ])).trim();
14631
+ const ancestorCommit = (await this.git.revparse([
14632
+ ancestorRef
14633
+ ])).trim();
14634
+ return mergeBase === ancestorCommit;
14635
+ } catch {
14636
+ return false;
14637
+ }
14638
+ }
14639
+ async mergeFastForward(ref) {
14640
+ await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
14641
+ "merge",
14642
+ "--ff-only",
14643
+ ref
14644
+ ]));
14645
+ }
14646
+ async deleteRef(ref) {
14647
+ try {
14648
+ await this.git.raw([
14649
+ "update-ref",
14650
+ "-d",
14651
+ ref
14652
+ ]);
14653
+ } catch (e) {}
14654
+ }
14655
+ async getRemotes() {
14656
+ try {
14657
+ const remotes = await this.git.getRemotes(true);
14658
+ return remotes.map((r)=>({
14659
+ name: r.name,
14660
+ fetchUrl: r.refs.fetch
14661
+ }));
14823
14662
  } catch (e) {
14824
- throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14663
+ throw errors_createGitError.commandFailed("remote", e.message);
14825
14664
  }
14826
- return discarded;
14827
- }
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;
14837
- }
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;
14848
- }
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;
14857
14665
  }
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);
14666
+ async isGitInitialized() {
14667
+ const gitPath = external_path_default().join(this.repoPath, ".git");
14668
+ return external_fs_.existsSync(gitPath);
14863
14669
  }
14864
- async hasUncommittedChanges() {
14865
- const status = await this.getStatus();
14866
- return !status.isClean;
14670
+ async removeEmptyDirectories(removedFiles) {
14671
+ const directoriesToCheck = new Set();
14672
+ for (const file of removedFiles){
14673
+ let dir = external_path_default().dirname(file);
14674
+ while(dir && "." !== dir){
14675
+ directoriesToCheck.add(dir);
14676
+ const parentDir = external_path_default().dirname(dir);
14677
+ if (parentDir === dir) break;
14678
+ dir = parentDir;
14679
+ }
14680
+ }
14681
+ const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
14682
+ for (const dir of sortedDirs)try {
14683
+ const dirPath = external_path_default().join(this.repoPath, dir);
14684
+ if (!external_fs_.existsSync(dirPath)) continue;
14685
+ const entries = await external_fs_.promises.readdir(dirPath);
14686
+ if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
14687
+ } catch (error) {}
14867
14688
  }
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"
14689
+ }
14690
+ async function getStagedFileChanges(git, gitService) {
14691
+ try {
14692
+ const status = await git.status();
14693
+ const renames = status.renamed || [];
14694
+ const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
14695
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14696
+ const stagedRenames = [];
14697
+ for (const f of stagedFiles)if ("R" === f.index) {
14698
+ const rename = renames.find((r)=>r.to === f.path);
14699
+ if (rename) stagedRenames.push({
14700
+ path: rename.to,
14701
+ type: "rename",
14702
+ from: rename.from
14878
14703
  });
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"
14886
- });
14887
- throw errors_createGitError.fetchFailed(err.message);
14704
+ }
14705
+ const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14706
+ const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14707
+ const changes = [];
14708
+ for (const f of stagedFiles){
14709
+ if ("R" !== f.index) {
14710
+ if (!stagedModeOnlyPaths.has(f.path)) {
14711
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14712
+ path: f.path,
14713
+ type: "unlink"
14714
+ });
14715
+ else if ("A" === f.index || "?" === f.index) changes.push({
14716
+ path: f.path,
14717
+ type: "add"
14718
+ });
14719
+ else changes.push({
14720
+ path: f.path,
14721
+ type: "change"
14722
+ });
14723
+ }
14888
14724
  }
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
14725
  }
14726
+ changes.push(...stagedRenames);
14727
+ return changes;
14728
+ } catch (e) {
14729
+ throw errors_createGitError.commandFailed("status", e.message);
14898
14730
  }
14899
- async reset(ref, mode = "mixed") {
14900
- await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14901
- `--${mode}`,
14902
- ref
14903
- ]));
14731
+ }
14732
+ async function getCurrentOrFallbackBranchName(git) {
14733
+ const branchRef = (await git.revparse([
14734
+ "--abbrev-ref",
14735
+ "HEAD"
14736
+ ])).trim();
14737
+ if ("HEAD" !== branchRef) return branchRef;
14738
+ const [localBranches, remoteBranches] = await Promise.all([
14739
+ git.raw([
14740
+ "branch",
14741
+ "--format=%(refname:short)",
14742
+ "--contains",
14743
+ "HEAD"
14744
+ ]),
14745
+ git.raw([
14746
+ "branch",
14747
+ "-r",
14748
+ "--format=%(refname:short)",
14749
+ "--contains",
14750
+ "HEAD"
14751
+ ])
14752
+ ]);
14753
+ const branchNames = [
14754
+ ...parseBranchList(localBranches),
14755
+ ...parseBranchList(remoteBranches).filter((branchName)=>!branchName.endsWith("/HEAD")).map((branchName)=>branchName.replace(/^[^/]+\//, ""))
14756
+ ];
14757
+ const branchSet = new Set(branchNames);
14758
+ if (branchSet.has("master")) return "master";
14759
+ if (branchSet.has("main")) return "main";
14760
+ return "master";
14761
+ }
14762
+ function parseBranchList(output) {
14763
+ return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
14764
+ }
14765
+ function formatHttpError(prefix, status, body) {
14766
+ if (!body) return `${prefix}: ${status}`;
14767
+ try {
14768
+ const parsed = JSON.parse(body);
14769
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14770
+ } catch {}
14771
+ return `${prefix}: ${status} ${body}`;
14772
+ }
14773
+ function anvil_api_getDefaultAnvilUrl() {
14774
+ return resolveAnvilUrl();
14775
+ }
14776
+ async function createAnvilApp(options) {
14777
+ let response;
14778
+ try {
14779
+ response = await fetch(`${options.anvilUrl}/ide/api/_/apps`, {
14780
+ method: "POST",
14781
+ headers: {
14782
+ Authorization: `Bearer ${options.authToken}`,
14783
+ "Content-Type": "application/json"
14784
+ },
14785
+ body: JSON.stringify({
14786
+ name: options.name,
14787
+ tags: options.tags
14788
+ })
14789
+ });
14790
+ } catch (error) {
14791
+ throw errors_createNetworkError.network(error.message);
14904
14792
  }
14905
- async checkout(paths) {
14906
- await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14793
+ const data = await response.json().catch(()=>({}));
14794
+ if (!response.ok) {
14795
+ if (401 === response.status || 403 === response.status) throw errors_createAuthError.invalid(data.error || "Not authorized to create an app");
14796
+ throw new Error(data.error || `Failed to create app: ${response.status} ${response.statusText}`);
14797
+ }
14798
+ if ("string" != typeof data.id || !data.id) throw new Error("App creation response did not contain an app ID.");
14799
+ return {
14800
+ id: data.id,
14801
+ ..."string" == typeof data.name ? {
14802
+ name: data.name
14803
+ } : {},
14804
+ ...data.tags ? {
14805
+ tags: data.tags
14806
+ } : {}
14807
+ };
14808
+ }
14809
+ async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14810
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14811
+ try {
14812
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
14813
+ method: "POST",
14814
+ headers: {
14815
+ Authorization: `Bearer ${authToken}`,
14816
+ "Content-Type": "application/json"
14817
+ },
14818
+ body: JSON.stringify({
14819
+ app_id: appId
14820
+ })
14821
+ });
14822
+ const data = await resp.json();
14823
+ if (resp.ok) return {
14824
+ valid: data.valid ?? false,
14825
+ app_name: data.app_name
14826
+ };
14827
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14828
+ if (403 === resp.status) throw createAppError.accessDenied(appId);
14829
+ if (404 === resp.status) throw createAppError.notFound(appId);
14830
+ logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
14831
+ return {
14832
+ valid: false,
14833
+ error: data.error || `Server error: ${resp.status}`
14834
+ };
14835
+ } catch (error) {
14836
+ if (error.type) throw error;
14837
+ throw errors_createNetworkError.network(error.message);
14838
+ }
14839
+ }
14840
+ async function listAppsForCheckout(options = {}) {
14841
+ const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
14842
+ const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
14843
+ const params = new URLSearchParams();
14844
+ if ("number" == typeof options.limit) params.set("limit", String(options.limit));
14845
+ if (options.cursor) params.set("cursor", options.cursor);
14846
+ if (options.q && options.q.trim()) params.set("q", options.q.trim());
14847
+ const query = params.toString();
14848
+ const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
14849
+ try {
14850
+ const resp = await fetch(url, {
14851
+ method: "GET",
14852
+ headers: {
14853
+ Authorization: `Bearer ${authToken}`
14854
+ },
14855
+ signal: options.signal
14856
+ });
14857
+ if (!resp.ok) {
14858
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14859
+ throw errors_createNetworkError.server(resp.status, resp.statusText);
14860
+ }
14861
+ const data = await resp.json();
14862
+ return {
14863
+ apps: Array.isArray(data.apps) ? data.apps : [],
14864
+ next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
14865
+ };
14866
+ } catch (error) {
14867
+ if (error.type) throw error;
14868
+ throw errors_createNetworkError.network(error.message);
14869
+ }
14870
+ }
14871
+ function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
14872
+ const url = new URL(gitUrl);
14873
+ const encodedToken = encodeURIComponent(authToken);
14874
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
14875
+ }
14876
+ function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14877
+ const url = new URL(anvilUrl);
14878
+ const encodedToken = encodeURIComponent(authToken);
14879
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
14880
+ }
14881
+ function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14882
+ return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
14883
+ }
14884
+ async function getLatestVersion() {
14885
+ try {
14886
+ const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
14887
+ if (!response.ok) return null;
14888
+ const data = await response.json();
14889
+ return data.version;
14890
+ } catch (e) {
14891
+ return null;
14907
14892
  }
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]);
14893
+ }
14894
+ async function getTestRunSnapshot(appId, runId, options = {}) {
14895
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
14896
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
14897
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/test-runs/${encodeURIComponent(runId)}`, {
14898
+ headers: {
14899
+ Authorization: `Bearer ${token}`
14917
14900
  }
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");
14901
+ });
14902
+ if (!response.ok) throw new Error(formatHttpError("Failed to get the test run's results", response.status, await response.text()));
14903
+ return response.json();
14904
+ }
14905
+ function filterCandidates(candidates, explicitUrl, explicitUsername) {
14906
+ let filtered = candidates;
14907
+ if (explicitUrl) {
14908
+ const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
14909
+ filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
14923
14910
  }
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);
14911
+ if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
14912
+ return filtered;
14913
+ }
14914
+ function formatCandidateLabel(candidate) {
14915
+ const parts = [
14916
+ candidate.appId
14917
+ ];
14918
+ if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
14919
+ else parts.push(`(${candidate.detectedUrl})`);
14920
+ parts.push(`- ${candidate.description}`);
14921
+ return parts.join(" ");
14922
+ }
14923
+ function lookupRemoteInfoForAppId(appId, detectedRemotes) {
14924
+ const matches = detectedRemotes.filter((c)=>c.appId === appId);
14925
+ if (0 === matches.length) return {};
14926
+ const withUsername = matches.find((c)=>c.detectedUsername);
14927
+ if (withUsername) return {
14928
+ detectedUrl: withUsername.detectedUrl,
14929
+ detectedUsername: withUsername.detectedUsername
14930
+ };
14931
+ return {
14932
+ detectedUrl: matches[0].detectedUrl,
14933
+ detectedUsername: matches[0].detectedUsername
14934
+ };
14935
+ }
14936
+ async function detectAppIdsFromAllRemotes(repoPath) {
14937
+ const git = esm_default(repoPath);
14938
+ const out = [];
14939
+ try {
14940
+ const remotes = await git.getRemotes(true);
14941
+ for (const remote of remotes){
14942
+ const httpMatch = remote.refs.fetch?.match(/(https?):\/\/(?:[^@\/]+@)?([^\/]+)\/git\/([A-Z0-9]+)\.git/);
14943
+ if (httpMatch) {
14944
+ const [, scheme, host, detectedAppId] = httpMatch;
14945
+ const origin = new URL(`${scheme}://${host}`).origin;
14946
+ out.push({
14947
+ appId: detectedAppId,
14948
+ source: "remote",
14949
+ description: `Git remote '${remote.name}'`,
14950
+ detectedUrl: normalizeAnvilUrl(origin)
14951
+ });
14952
+ continue;
14953
+ }
14954
+ const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
14955
+ if (sshMatch) {
14956
+ const [, usernamePart, host, , detectedAppId] = sshMatch;
14957
+ const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
14958
+ const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
14959
+ out.push({
14960
+ appId: detectedAppId,
14961
+ source: "remote",
14962
+ description: `Git remote '${remote.name}' (SSH)`,
14963
+ detectedUrl: normalizeAnvilUrl(host),
14964
+ detectedUsername: finalUsername
14965
+ });
14966
+ }
14936
14967
  }
14937
- }
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);
14968
+ } catch (_e) {}
14969
+ return out;
14970
+ }
14971
+ async function detectAppIdsByCommitLookup(repoPath, options) {
14972
+ const anvilUrl = options.anvilUrl || resolveAnvilUrl();
14973
+ const username = options.username;
14974
+ const git = esm_default(repoPath);
14975
+ const out = [];
14976
+ try {
14977
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14978
+ const commitId = (await git.revparse([
14979
+ "HEAD"
14980
+ ]))?.trim();
14981
+ if (!commitId) return out;
14982
+ const branchName = await getCurrentOrFallbackBranchName(git);
14983
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
14984
+ method: "POST",
14985
+ headers: {
14986
+ Authorization: `Bearer ${authToken}`,
14987
+ "Content-Type": "application/json"
14988
+ },
14989
+ body: JSON.stringify({
14990
+ commit_id: commitId,
14991
+ branch_name: branchName
14992
+ })
14993
+ });
14994
+ logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${branchName} returned ${resp.status}`);
14995
+ if (!resp.ok) {
14996
+ const errorBody = await resp.text();
14997
+ logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
14946
14998
  }
14947
- }
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);
14999
+ if (resp.ok) {
15000
+ const data = await resp.json();
15001
+ logger_logger.debug(`data ${JSON.stringify(data)}`);
15002
+ const apps = data?.apps ?? [];
15003
+ for (const app of apps)if (app?.app_id) out.push({
15004
+ appId: app.app_id,
15005
+ source: "remote",
15006
+ description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
15007
+ });
14953
15008
  }
15009
+ } catch (_e) {}
15010
+ return out;
15011
+ }
15012
+ const external_fs_promises_namespaceObject = require("fs/promises");
15013
+ var external_fs_promises_default = /*#__PURE__*/ __webpack_require__.n(external_fs_promises_namespaceObject);
15014
+ function normalizeLineEndings(content) {
15015
+ return content.replace(/\r\n/g, "\n");
15016
+ }
15017
+ function pythonifyName(name) {
15018
+ return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
15019
+ }
15020
+ function extractPythonName(relativePath, skipParts = 1) {
15021
+ const parts = relativePath.slice(0, -3).split("/");
15022
+ const isPackage = relativePath.endsWith("__init__.py");
15023
+ const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
15024
+ return relevantParts.map(pythonifyName).join(".");
15025
+ }
15026
+ function deepEqual(a, b) {
15027
+ if (a === b) return true;
15028
+ if (null == a || null == b) return false;
15029
+ if (typeof a !== typeof b) return false;
15030
+ if ("object" != typeof a) return a === b;
15031
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
15032
+ if (Array.isArray(a)) {
15033
+ if (a.length !== b.length) return false;
15034
+ return a.every((item, index)=>deepEqual(item, b[index]));
14954
15035
  }
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
- }
15036
+ const keysA = Object.keys(a);
15037
+ const keysB = Object.keys(b);
15038
+ if (keysA.length !== keysB.length) return false;
15039
+ return keysA.every((key)=>deepEqual(a[key], b[key]));
15040
+ }
15041
+ function parseHtmlWithFrontmatter(htmlContent) {
15042
+ const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
15043
+ const match = htmlContent.match(frontmatterPattern);
15044
+ if (match) {
15045
+ const yamlStr = match[1];
15046
+ const html = match[2];
15047
+ const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
15048
+ return {
15049
+ frontmatter,
15050
+ html
15051
+ };
14967
15052
  }
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
- }
15053
+ return {
15054
+ frontmatter: {},
15055
+ html: htmlContent
15056
+ };
15057
+ }
15058
+ const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
15059
+ async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
15060
+ const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
15061
+ try {
15062
+ const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
15063
+ if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
15064
+ const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
15065
+ await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15066
+ return true;
15067
+ } catch (error) {
15068
+ if ("ENOENT" !== error.code) throw error;
15069
+ await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15070
+ return true;
14984
15071
  }
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
- }
15072
+ }
15073
+ const ANVIL_DEPS_DIR = ".anvil/deps";
15074
+ const ANVIL_DEPS_MANIFEST = "manifest.json";
15075
+ const DEPS_MANIFEST_VERSION = 3;
15076
+ const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
15077
+ async function fetchAllDependencies(appId, anvilUrl, options = {}) {
15078
+ const { commit, includeDocs = true, knownDependencies = [] } = options;
15079
+ anvilUrl ??= await resolveAuthAnvilUrl();
15080
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
15081
+ const body = {
15082
+ include_docs: includeDocs,
15083
+ known_dependencies: knownDependencies
15084
+ };
15085
+ if (commit) body.commit = commit;
15086
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
15087
+ method: "POST",
15088
+ headers: {
15089
+ Authorization: `Bearer ${token}`,
15090
+ "Content-Type": "application/json"
15091
+ },
15092
+ body: JSON.stringify(body)
15093
+ });
15094
+ if (!resp.ok) {
15095
+ const errorBody = await resp.text();
15096
+ throw new Error(formatHttpError("Failed to fetch dependencies", resp.status, errorBody));
14994
15097
  }
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
- }
15098
+ return await resp.json();
15099
+ }
15100
+ async function refreshDependencyCache(projectRoot, appId, anvilUrl, options = {}) {
15101
+ auth_setRepoContext(projectRoot);
15102
+ anvilUrl ??= await resolveAuthAnvilUrl();
15103
+ const includeDocs = options.includeDocs ?? true;
15104
+ const git = new GitService(projectRoot);
15105
+ const commit = options.commit ?? await git.getCommitId();
15106
+ const sourceYaml = await git.show(`${commit}:anvil.yaml`).catch((error)=>{
15107
+ if (options.commit) return null;
15108
+ throw error;
15109
+ });
15110
+ const sourceDependencies = null === sourceYaml ? null : normalizeAnvilYamlDependenciesFromContent(sourceYaml);
15111
+ const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
15112
+ const response = await fetchAllDependencies(appId, anvilUrl, {
15113
+ username: options.username,
15114
+ commit,
15115
+ includeDocs,
15116
+ knownDependencies
15117
+ });
15118
+ if (!response.resolved_dependencies.length) {
15119
+ await clearDepsCache(projectRoot);
15120
+ return {
15121
+ response,
15122
+ addedGitignoreEntry: false
15123
+ };
15011
15124
  }
15012
- async rebase(onto) {
15013
- try {
15014
- await this.git.raw([
15015
- "rebase",
15016
- onto
15017
- ]);
15018
- } catch (e) {
15019
- throw errors_createGitError.commandFailed("rebase", e.message);
15020
- }
15125
+ const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
15126
+ await writeAllDepsToCache(projectRoot, response, {
15127
+ includeDocs,
15128
+ sourceDependencies
15129
+ });
15130
+ return {
15131
+ response,
15132
+ addedGitignoreEntry
15133
+ };
15134
+ }
15135
+ function depsDir(projectRoot) {
15136
+ return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
15137
+ }
15138
+ function depsManifestPath(projectRoot) {
15139
+ return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
15140
+ }
15141
+ function isResolvedDependencyManifestEntry(value) {
15142
+ if (!value || "object" != typeof value) return false;
15143
+ const entry = value;
15144
+ 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);
15145
+ }
15146
+ function isCachedDependencyManifestEntry(value) {
15147
+ if (!isResolvedDependencyManifestEntry(value)) return false;
15148
+ const entry = value;
15149
+ return "string" == typeof entry.package_name && entry.package_name.length > 0;
15150
+ }
15151
+ function isDepsCacheManifest(value) {
15152
+ if (!value || "object" != typeof value) return false;
15153
+ const manifest = value;
15154
+ return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && (null === manifest.source_dependencies || 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);
15155
+ }
15156
+ async function readDepsCacheManifest(projectRoot) {
15157
+ try {
15158
+ const raw = await external_fs_promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
15159
+ const parsed = JSON.parse(raw);
15160
+ return isDepsCacheManifest(parsed) ? parsed : void 0;
15161
+ } catch {
15162
+ return;
15021
15163
  }
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);
15030
- }
15164
+ }
15165
+ async function getKnownDependenciesForCache(projectRoot, includeDocs) {
15166
+ const manifest = await readDepsCacheManifest(projectRoot);
15167
+ if (!manifest || manifest.includeDocs !== includeDocs) return [];
15168
+ return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
15169
+ app_id,
15170
+ commit_id
15171
+ }));
15172
+ }
15173
+ function getDependencyWatchSubscriptionsFromManifest(manifest) {
15174
+ if (!manifest) return [];
15175
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
15176
+ dep.app_id,
15177
+ dep
15178
+ ]));
15179
+ return manifest.resolved_dependencies.flatMap((dep)=>{
15180
+ if (!dep.branch) return [];
15181
+ return [
15182
+ {
15183
+ appId: dep.app_id,
15184
+ branch: dep.branch,
15185
+ packageName: cachedByAppId.get(dep.app_id)?.package_name
15186
+ }
15187
+ ];
15188
+ });
15189
+ }
15190
+ function normalizeAnvilYamlDependenciesFromContent(content) {
15191
+ const parsed = external_js_yaml_default().load(content);
15192
+ if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
15193
+ return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
15194
+ }
15195
+ function normalizeAnvilYamlDependenciesValue(value) {
15196
+ if (!Array.isArray(value)) return [];
15197
+ return value.map(normalizeForComparison);
15198
+ }
15199
+ async function readAnvilYamlDependencies(projectRoot) {
15200
+ try {
15201
+ const content = await external_fs_promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
15202
+ return normalizeAnvilYamlDependenciesFromContent(content);
15203
+ } catch {
15204
+ return [];
15031
15205
  }
15032
- async isAncestor(ancestorRef, descendantRef) {
15206
+ }
15207
+ async function getDependencyCacheWarning(projectRoot) {
15208
+ const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
15209
+ if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
15210
+ const manifest = await readDepsCacheManifest(projectRoot);
15211
+ if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
15212
+ if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
15213
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
15214
+ dependency.app_id,
15215
+ dependency
15216
+ ]));
15217
+ for (const dependency of manifest.resolved_dependencies){
15218
+ const cached = cachedByAppId.get(dependency.app_id);
15219
+ if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
15033
15220
  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;
15221
+ await external_fs_promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
15043
15222
  } catch {
15044
- return false;
15223
+ return DEPENDENCY_CACHE_WARNING;
15045
15224
  }
15046
15225
  }
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);
15226
+ }
15227
+ function anvilYamlDependenciesEqual(left, right) {
15228
+ return deepEqual(left, right);
15229
+ }
15230
+ function normalizeForComparison(value) {
15231
+ if (Array.isArray(value)) return value.map(normalizeForComparison);
15232
+ if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
15233
+ key,
15234
+ normalizeForComparison(entryValue)
15235
+ ]));
15236
+ return value;
15237
+ }
15238
+ function cachedManifestEntry(dep, packageName) {
15239
+ return {
15240
+ app_id: dep.app_id,
15241
+ commit_id: dep.commit_id,
15242
+ ...dep.branch ? {
15243
+ branch: dep.branch
15244
+ } : {},
15245
+ package_name: packageName
15246
+ };
15247
+ }
15248
+ function safeDependencyPath(filePath, kind) {
15249
+ const normalizedPath = external_path_default().normalize(filePath);
15250
+ if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
15251
+ return normalizedPath;
15252
+ }
15253
+ function dependencyPythonPath(name, kind) {
15254
+ const parts = name.split(".");
15255
+ if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
15256
+ return external_path_default().join(...parts);
15257
+ }
15258
+ async function writeDepToCache(projectRoot, dep) {
15259
+ const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
15260
+ await external_fs_promises_default().rm(depDir, {
15261
+ recursive: true,
15262
+ force: true
15263
+ });
15264
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
15265
+ recursive: true
15266
+ });
15267
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
15268
+ recursive: true
15269
+ });
15270
+ await external_fs_promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
15271
+ package_name: dep.package_name
15272
+ }));
15273
+ for (const mod of dep.modules ?? []){
15274
+ const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
15275
+ if (mod.is_package) {
15276
+ await external_fs_promises_default().mkdir(modulePath, {
15277
+ recursive: true
15278
+ });
15279
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15280
+ } else {
15281
+ const moduleFile = `${modulePath}.py`;
15282
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15283
+ recursive: true
15284
+ });
15285
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15072
15286
  }
15073
15287
  }
15074
- async isGitInitialized() {
15075
- const gitPath = external_path_default().join(this.repoPath, ".git");
15076
- return external_fs_.existsSync(gitPath);
15077
- }
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
- }
15288
+ for (const form of dep.forms ?? []){
15289
+ const { code, class_name, is_package, ...formTemplate } = form;
15290
+ const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
15291
+ if (is_package) {
15292
+ await external_fs_promises_default().mkdir(formPath, {
15293
+ recursive: true
15294
+ });
15295
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
15296
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
15297
+ } else {
15298
+ const formCodeFile = `${formPath}.py`;
15299
+ await external_fs_promises_default().mkdir(external_path_default().dirname(formCodeFile), {
15300
+ recursive: true
15301
+ });
15302
+ await external_fs_promises_default().writeFile(formCodeFile, code);
15303
+ await external_fs_promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
15304
+ }
15305
+ }
15306
+ for (const mod of dep.server_modules ?? []){
15307
+ const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
15308
+ if (mod.is_package) {
15309
+ await external_fs_promises_default().mkdir(modulePath, {
15310
+ recursive: true
15311
+ });
15312
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15313
+ } else {
15314
+ const moduleFile = `${modulePath}.py`;
15315
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15316
+ recursive: true
15317
+ });
15318
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15088
15319
  }
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) {}
15320
+ }
15321
+ for (const asset of dep.assets ?? []){
15322
+ const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
15323
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15324
+ recursive: true
15325
+ });
15326
+ await external_fs_promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
15327
+ }
15328
+ for (const doc of dep.docs ?? []){
15329
+ const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
15330
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15331
+ recursive: true
15332
+ });
15333
+ await external_fs_promises_default().writeFile(targetPath, doc.content);
15096
15334
  }
15097
15335
  }
15098
- async function getStagedFileChanges(git, gitService) {
15336
+ async function writeAllDepsToCache(projectRoot, response, options = {}) {
15337
+ const includeDocs = options.includeDocs ?? true;
15338
+ const manifest = await readDepsCacheManifest(projectRoot);
15339
+ const canReuseExistingCache = manifest?.includeDocs === includeDocs;
15340
+ if (!canReuseExistingCache) await clearDepsCache(projectRoot);
15341
+ const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
15342
+ const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
15343
+ 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), {
15344
+ recursive: true,
15345
+ force: true
15346
+ });
15347
+ for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
15348
+ const returnedByAppId = new Map(response.dependencies.map((dep)=>[
15349
+ dep.app_id,
15350
+ dep
15351
+ ]));
15352
+ const previousByAppId = new Map(previousCached.map((dep)=>[
15353
+ dep.app_id,
15354
+ dep
15355
+ ]));
15356
+ const cachedDependencies = response.resolved_dependencies.map((dep)=>{
15357
+ const returned = returnedByAppId.get(dep.app_id);
15358
+ if (returned) return cachedManifestEntry(returned, returned.package_name);
15359
+ const previous = previousByAppId.get(dep.app_id);
15360
+ if (!previous) return;
15361
+ return cachedManifestEntry(dep, previous.package_name);
15362
+ }).filter((dep)=>void 0 !== dep);
15363
+ const newManifest = {
15364
+ version: DEPS_MANIFEST_VERSION,
15365
+ includeDocs,
15366
+ source_dependencies: void 0 === options.sourceDependencies ? await readAnvilYamlDependencies(projectRoot) : options.sourceDependencies,
15367
+ resolved_dependencies: response.resolved_dependencies,
15368
+ cached_dependencies: cachedDependencies
15369
+ };
15370
+ await external_fs_promises_default().mkdir(depsDir(projectRoot), {
15371
+ recursive: true
15372
+ });
15373
+ await external_fs_promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
15374
+ }
15375
+ async function clearDepsCache(projectRoot) {
15376
+ await external_fs_promises_default().rm(depsDir(projectRoot), {
15377
+ recursive: true,
15378
+ force: true
15379
+ });
15380
+ }
15381
+ async function getCachedPackages(projectRoot) {
15099
15382
  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);
15383
+ const entries = await external_fs_promises_default().readdir(depsDir(projectRoot), {
15384
+ withFileTypes: true
15385
+ });
15386
+ return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
15387
+ } catch {
15388
+ return [];
15138
15389
  }
15139
15390
  }
15140
15391
  const external_chokidar_namespaceObject = require("chokidar");
@@ -16199,8 +16450,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16199
16450
  }
16200
16451
  if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
16201
16452
  if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
16453
+ if (void 0 !== value.help_text && "string" != typeof value.help_text) formTemplateValidation_pushIssue(issues, path("help_text"), "must be a string");
16202
16454
  if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
16203
- if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16455
+ if (void 0 !== value.item_type.table_name) validateNonEmptyString(value.item_type.table_name, path("item_type.table_name"), issues);
16456
+ else if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16204
16457
  } else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
16205
16458
  return issues;
16206
16459
  }
@@ -16273,7 +16526,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16273
16526
  "toolbox_item",
16274
16527
  "layout_metadata",
16275
16528
  "item_type",
16276
- "slots"
16529
+ "slots",
16530
+ "help_text"
16277
16531
  ]);
16278
16532
  const FRONTMATTER_DISALLOWED_KEYS = new Set([
16279
16533
  "container",
@@ -16440,6 +16694,45 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16440
16694
  };
16441
16695
  }
16442
16696
  const validatePython_requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
16697
+ const DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB = 768;
16698
+ const DEFAULT_PYRIGHT_TIMEOUT_MS = 300000;
16699
+ function parsePositiveIntegerEnv(env, name, defaultValue) {
16700
+ const rawValue = env[name];
16701
+ if (void 0 === rawValue || "" === rawValue) return {
16702
+ value: defaultValue
16703
+ };
16704
+ if (!/^\d+$/.test(rawValue) || Number(rawValue) <= 0 || !Number.isSafeInteger(Number(rawValue))) return {
16705
+ error: `${name} must be a positive integer`
16706
+ };
16707
+ return {
16708
+ value: Number(rawValue)
16709
+ };
16710
+ }
16711
+ function pyrightChildNodeOptions(nodeOptions, maxOldSpaceMb) {
16712
+ const maxOldSpaceFlag = "--max[-_]old[-_]space[-_]size";
16713
+ const withoutEqualsForm = (nodeOptions ?? "").replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}=\\S+`, "g"), "$1");
16714
+ const withoutSeparateForm = withoutEqualsForm.replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}\\s+\\S+`, "g"), "$1");
16715
+ return [
16716
+ withoutSeparateForm.trim(),
16717
+ `--max-old-space-size=${maxOldSpaceMb}`
16718
+ ].filter(Boolean).join(" ");
16719
+ }
16720
+ function semanticProcessFailure(message) {
16721
+ return {
16722
+ ok: false,
16723
+ message: "Python semantic validation could not be run",
16724
+ issues: [
16725
+ {
16726
+ path: "root",
16727
+ message,
16728
+ severity: "error"
16729
+ }
16730
+ ]
16731
+ };
16732
+ }
16733
+ function isV8HeapOutOfMemory(stderr) {
16734
+ return /(?:JavaScript heap out of memory|Reached heap limit|Ineffective mark-compacts near heap limit)/i.test(stderr);
16735
+ }
16443
16736
  function resolveBundledAnvilPyright() {
16444
16737
  try {
16445
16738
  return {
@@ -16707,8 +17000,13 @@ print(json.dumps({"issues": issues}))
16707
17000
  };
16708
17001
  }
16709
17002
  function validatePythonWithPyright(appRoot, options = {}) {
17003
+ const env = options.env ?? process.env;
17004
+ const maxOldSpace = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_MAX_OLD_SPACE_MB", DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB);
17005
+ if ("error" in maxOldSpace) return semanticProcessFailure(maxOldSpace.error);
17006
+ const timeout = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_TIMEOUT_MS", DEFAULT_PYRIGHT_TIMEOUT_MS);
17007
+ if ("error" in timeout) return semanticProcessFailure(timeout.error);
16710
17008
  const pyright = void 0 === options.pyright ? findAnvilPyright({
16711
- env: options.env
17009
+ env
16712
17010
  }) : options.pyright;
16713
17011
  if (!pyright) return {
16714
17012
  ok: false,
@@ -16731,30 +17029,19 @@ print(json.dumps({"issues": issues}))
16731
17029
  ...options.files ?? []
16732
17030
  ], {
16733
17031
  encoding: "utf8",
16734
- maxBuffer: 10485760
17032
+ env: {
17033
+ ...env,
17034
+ NODE_OPTIONS: pyrightChildNodeOptions(env.NODE_OPTIONS, maxOldSpace.value)
17035
+ },
17036
+ maxBuffer: 10485760,
17037
+ timeout: timeout.value
16735
17038
  });
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
- };
17039
+ if (result.error) {
17040
+ 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.`);
17041
+ return semanticProcessFailure(result.error.message);
17042
+ }
17043
+ 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.`);
17044
+ if (0 !== result.status && 1 !== result.status) return semanticProcessFailure(result.stderr.trim() || `anvil-pyright exited with status ${result.status}`);
16758
17045
  let parsed;
16759
17046
  try {
16760
17047
  parsed = JSON.parse(result.stdout.trim());
@@ -17071,7 +17358,7 @@ print(json.dumps({"issues": issues}))
17071
17358
  }
17072
17359
  function validateScheduledTasks(value, path) {
17073
17360
  const issues = [];
17074
- if (void 0 === value) return issues;
17361
+ if (null == value) return issues;
17075
17362
  if (!Array.isArray(value)) {
17076
17363
  validators_pushIssue(issues, path, "must be an array");
17077
17364
  return issues;
@@ -18570,6 +18857,15 @@ print(json.dumps({"issues": issues}))
18570
18857
  this.hasPendingChanges = true;
18571
18858
  await this.processSaveBatchWithEvents();
18572
18859
  }
18860
+ async flush() {
18861
+ const requireReady = ()=>{
18862
+ if (this.config.isPaused() || this.config.isCleanedUp() || !this.config.isSettlePeriodOver()) throw new Error("Cannot flush saves while watching is paused, closed, or changing branch");
18863
+ };
18864
+ requireReady();
18865
+ await this.forceSave();
18866
+ requireReady();
18867
+ if (this.hasPendingChanges) throw new Error("Saves remain pending");
18868
+ }
18573
18869
  cleanup() {
18574
18870
  if (this.hasPendingChanges) {
18575
18871
  logger_logger.progressEnd("sync");
@@ -18666,10 +18962,7 @@ print(json.dumps({"issues": issues}))
18666
18962
  status = await this.config.gitService.getStatus();
18667
18963
  } catch (e) {
18668
18964
  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
- };
18965
+ throw e;
18673
18966
  }
18674
18967
  let allChanges;
18675
18968
  if (this.config.stagedOnly) {
@@ -19515,6 +19808,7 @@ print(json.dumps({"issues": issues}))
19515
19808
  try {
19516
19809
  const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
19517
19810
  const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
19811
+ username: this.username,
19518
19812
  includeDocs: true
19519
19813
  });
19520
19814
  if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
@@ -19826,6 +20120,12 @@ print(json.dumps({"issues": issues}))
19826
20120
  if (!this.saveProcessor) return;
19827
20121
  return this.saveProcessor.forceSave();
19828
20122
  }
20123
+ async flushSaves() {
20124
+ if (!this.saveProcessor || this.isSwitchingForEnvironment) throw new Error("App synchronization is not ready");
20125
+ const branch = this.currentBranch;
20126
+ await this.saveProcessor.flush();
20127
+ if (this.isSwitchingForEnvironment || this.currentBranch !== branch || await this.gitService.getCurrentBranch() !== branch) throw new Error("App branch changed while flushing saves");
20128
+ }
19829
20129
  async getSuccessMessage() {
19830
20130
  try {
19831
20131
  const info = await this.gitService.getCommitInfo();
@@ -20017,6 +20317,8 @@ print(json.dumps({"issues": issues}))
20017
20317
  }
20018
20318
  }
20019
20319
  const external_http_namespaceObject = require("http");
20320
+ const external_readline_namespaceObject = require("readline");
20321
+ var external_readline_default = /*#__PURE__*/ __webpack_require__.n(external_readline_namespaceObject);
20020
20322
  const external_open_namespaceObject = require("open");
20021
20323
  var external_open_default = /*#__PURE__*/ __webpack_require__.n(external_open_namespaceObject);
20022
20324
  const successPage = `<!DOCTYPE html>
@@ -21048,7 +21350,7 @@ print(json.dumps({"issues": issues}))
21048
21350
  value: null
21049
21351
  });
21050
21352
  return logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0], {
21051
- action: "`anvil checkout` URL selection",
21353
+ action: "Anvil server selection",
21052
21354
  nonInteractiveHint: `Use --url <ANVIL_URL>. Available URLs: ${decision.urls.join(", ")}.`
21053
21355
  });
21054
21356
  }
@@ -21127,8 +21429,8 @@ print(json.dumps({"issues": issues}))
21127
21429
  name: "Cancel",
21128
21430
  value: null
21129
21431
  });
21130
- return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used for checkout?`, choices, accounts[0], {
21131
- action: "`anvil checkout` account selection",
21432
+ return logger_logger.select(`Multiple accounts found for ${anvilUrl}. Which account should be used?`, choices, accounts[0], {
21433
+ action: "Anvil account selection",
21132
21434
  nonInteractiveHint: `Use --user <USERNAME>. Available accounts: ${accounts.join(", ")}.`
21133
21435
  });
21134
21436
  }
@@ -21147,7 +21449,7 @@ print(json.dumps({"issues": issues}))
21147
21449
  logger_logger.verbose(external_chalk_default().cyan("Resolved app ID: ") + external_chalk_default().bold(parsed.appId));
21148
21450
  const anvilUrl = await resolveCheckoutUrlForApi(options.anvilUrl, parsed.detectedUrl);
21149
21451
  if (!anvilUrl) throw new Error("Anvil URL is required. Use --url <ANVIL_URL> or pass an input URL containing the host.");
21150
- const gitUrl = options.gitUrl ?? anvilUrl;
21452
+ const gitUrl = void 0 === options.gitUrl ? anvilUrl : normalizeAnvilOrigin(options.gitUrl);
21151
21453
  const resolvedUsername = await resolveCheckoutUsernameForApi(anvilUrl, options.user);
21152
21454
  if (resolvedUsername) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(resolvedUsername));
21153
21455
  else logger_logger.verbose(external_chalk_default().cyan("No account preselected; resolving after auth token lookup."));
@@ -21258,7 +21560,7 @@ print(json.dumps({"issues": issues}))
21258
21560
  await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
21259
21561
  }
21260
21562
  function registerCheckoutCommand(program) {
21261
- const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
21563
+ const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("--git-url <GIT_URL>", "Git server URL, if separate from the Anvil server").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
21262
21564
  try {
21263
21565
  const globalOptions = command?.optsWithGlobals() || options || {};
21264
21566
  if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
@@ -21273,6 +21575,7 @@ print(json.dumps({"issues": issues}))
21273
21575
  quiet: options?.quiet,
21274
21576
  verbose: globalOptions.verbose,
21275
21577
  anvilUrl: options?.url,
21578
+ gitUrl: options?.gitUrl,
21276
21579
  user: options?.user,
21277
21580
  force: options?.force,
21278
21581
  query: options?.query
@@ -21284,6 +21587,232 @@ print(json.dumps({"issues": issues}))
21284
21587
  });
21285
21588
  checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
21286
21589
  }
21590
+ class CreateCheckoutError extends Error {
21591
+ created;
21592
+ constructor(message, created, options){
21593
+ super(message, options), this.created = created;
21594
+ this.name = "CreateCheckoutError";
21595
+ }
21596
+ }
21597
+ const create_defaultDeps = {
21598
+ getAccountsForUrl: auth_getAccountsForUrl,
21599
+ getValidAuthToken: auth_getValidAuthToken,
21600
+ createApp: createAnvilApp,
21601
+ checkout: executeCheckout
21602
+ };
21603
+ function validateCheckoutOptions(options) {
21604
+ if (false === options.checkout && (void 0 !== options.directory || void 0 !== options.gitUrl || options.open)) throw new Error("--no-checkout cannot be combined with a destination argument, --directory, --git-url, or --open.");
21605
+ }
21606
+ async function validateCreateInput(options) {
21607
+ validateCheckoutOptions(options);
21608
+ const name = options.name.trim();
21609
+ if (!name) throw new Error("App name is required.");
21610
+ const tags = void 0 === options.tags ? void 0 : [
21611
+ ...new Set(options.tags.map((tag)=>tag.trim().toLowerCase()))
21612
+ ];
21613
+ if (tags && (tags.length > 20 || tags.some((tag)=>tag.length > 50 || !/^[a-z0-9_-]+$/.test(tag)))) throw new Error("Use at most 20 unique tags, each 1–50 characters: letters, numbers, hyphens, or underscores.");
21614
+ if (false === options.checkout) return {
21615
+ name,
21616
+ tags,
21617
+ destinationPath: void 0
21618
+ };
21619
+ const directory = options.directory ?? sanitizeDirectoryName(name);
21620
+ if (!directory) throw new Error("App name cannot be used as a directory. Pass --directory <PATH>.");
21621
+ const destinationPath = external_path_default().resolve(directory);
21622
+ await validateCheckoutDestination(destinationPath);
21623
+ return {
21624
+ name,
21625
+ tags,
21626
+ destinationPath
21627
+ };
21628
+ }
21629
+ async function executeCreate(options, deps = create_defaultDeps) {
21630
+ const { name, tags, destinationPath } = await validateCreateInput(options);
21631
+ const anvilUrl = normalizeAnvilOrigin(options.anvilUrl ?? await resolveCheckoutUrlForApi() ?? "");
21632
+ const gitUrl = void 0 === options.gitUrl ? void 0 : normalizeAnvilOrigin(options.gitUrl);
21633
+ const accounts = options.user ? [] : await deps.getAccountsForUrl(anvilUrl);
21634
+ const username = options.user ?? accounts[0];
21635
+ if (!username) throw new Error(`Not logged in to ${anvilUrl}. Run 'anvil login ${anvilUrl}' and retry.`);
21636
+ if (!options.user && accounts.length > 1) throw new Error(`Multiple accounts found for ${anvilUrl}: ${accounts.join(", ")}. Use --user <USERNAME>.`);
21637
+ const authToken = await deps.getValidAuthToken(anvilUrl, username);
21638
+ const created = await deps.createApp({
21639
+ name,
21640
+ anvilUrl,
21641
+ authToken,
21642
+ ...tags ? {
21643
+ tags
21644
+ } : {}
21645
+ });
21646
+ if (void 0 === destinationPath) return {
21647
+ appId: created.id,
21648
+ appName: created.name,
21649
+ anvilUrl,
21650
+ username,
21651
+ tags: created.tags ?? tags ?? [],
21652
+ editorUrl: `${anvilUrl}/build/apps/${encodeURIComponent(created.id)}`
21653
+ };
21654
+ try {
21655
+ const checkout = await deps.checkout({
21656
+ input: created.id,
21657
+ directory: destinationPath,
21658
+ anvilUrl,
21659
+ ...gitUrl ? {
21660
+ gitUrl
21661
+ } : {},
21662
+ user: username
21663
+ });
21664
+ return {
21665
+ ...checkout,
21666
+ tags: created.tags ?? tags ?? []
21667
+ };
21668
+ } catch (error) {
21669
+ const details = {
21670
+ appId: created.id,
21671
+ appName: created.name,
21672
+ anvilUrl,
21673
+ gitUrl,
21674
+ destinationPath,
21675
+ username,
21676
+ tags: created.tags ?? tags ?? []
21677
+ };
21678
+ const retry = buildShellCommandLine([
21679
+ "anvil",
21680
+ "checkout",
21681
+ created.id,
21682
+ destinationPath,
21683
+ "--url",
21684
+ anvilUrl,
21685
+ "--user",
21686
+ username,
21687
+ ...gitUrl ? [
21688
+ "--git-url",
21689
+ gitUrl
21690
+ ] : []
21691
+ ]);
21692
+ const remainingFiles = await isDirectoryNonEmpty(destinationPath).catch(()=>true);
21693
+ const recovery = remainingFiles ? `Inspect the existing checkout at ${destinationPath} before retrying. For Git authentication problems, run ${buildShellCommandLine([
21694
+ "anvil",
21695
+ "git-auth",
21696
+ "doctor",
21697
+ "--repo",
21698
+ destinationPath
21699
+ ])}.` : `Retry checkout with: ${retry}`;
21700
+ const message = errors_getErrorMessage(error).replaceAll(authToken, "[REDACTED]").replaceAll(encodeURIComponent(authToken), "[REDACTED]");
21701
+ throw new CreateCheckoutError(`Created app ${created.id} at ${anvilUrl}, but checkout failed: ${message}. Do not rerun create. ${recovery}`, details, {
21702
+ cause: error
21703
+ });
21704
+ }
21705
+ }
21706
+ async function resolveCreateAccountInteractive(anvilUrl, options) {
21707
+ const user = await resolveCheckoutUsername(anvilUrl, options.user);
21708
+ if (null === user) return null;
21709
+ try {
21710
+ await auth_getValidAuthToken(anvilUrl, user);
21711
+ return user;
21712
+ } catch (error) {
21713
+ const loginNeeded = isAnvilError(error) && ([
21714
+ "auth_required",
21715
+ "auth_expired",
21716
+ "auth_invalid"
21717
+ ].includes(error.type) || "token_refresh_failed" === error.type && [
21718
+ 400,
21719
+ 401,
21720
+ 403
21721
+ ].includes(error.status ?? 0));
21722
+ if (!loginNeeded) throw error;
21723
+ if (!canPrompt()) throw new Error(`${errors_getErrorMessage(error)} Run 'anvil login ${anvilUrl}' and retry.`, {
21724
+ cause: error
21725
+ });
21726
+ if (!await logger_logger.confirm(`${errors_getErrorMessage(error)} Log in to ${anvilUrl} now?`, true)) return null;
21727
+ const login = await (options.login ?? runInteractiveLoginFlow)(anvilUrl);
21728
+ if (user && login.email !== user) throw new Error(`Logged in as ${login.email}, but the selected account is ${user}. No app was created.`);
21729
+ return login.email;
21730
+ }
21731
+ }
21732
+ function registerCreateCommand(program, deps = {
21733
+ executeCreate
21734
+ }) {
21735
+ program.command("create [name] [directory]").description("Create a new Anvil app and check it out locally").option("--no-checkout", "Create the app without a local checkout").option("-d, --directory <PATH>", "Local directory for the new app").option("--tag <TAG...>", "App tags (space-separated or repeated; place after name and directory)", (tag, tags)=>[
21736
+ ...tags,
21737
+ tag
21738
+ ], []).option("-u, --url <ANVIL_URL>", "Anvil server URL").option("-U, --user <USERNAME>", "Logged-in account to create the app").option("--git-url <GIT_URL>", "Git server URL, if separate from the Anvil server").option("-O, --open", "Open the new app directory after checkout").action(async (name, directory, options)=>{
21739
+ try {
21740
+ if (void 0 !== directory && void 0 !== options.directory) throw new Error("Pass the directory once, either as an argument or with --directory.");
21741
+ validateCheckoutOptions({
21742
+ ...options,
21743
+ directory: directory ?? options.directory
21744
+ });
21745
+ if (void 0 === name) {
21746
+ assertCanPrompt("`anvil create` without a name", "Pass a name: anvil create <NAME>.");
21747
+ const answer = await logger_logger.prompt([
21748
+ {
21749
+ type: "input",
21750
+ name: "name",
21751
+ message: "App name",
21752
+ validate: (value)=>value.trim() ? true : "App name is required."
21753
+ }
21754
+ ]);
21755
+ name = answer.name;
21756
+ }
21757
+ const input = await validateCreateInput({
21758
+ name,
21759
+ directory: directory ?? options.directory,
21760
+ tags: options.tag,
21761
+ checkout: options.checkout
21762
+ });
21763
+ const explicitUrl = void 0 === options.url ? void 0 : normalizeAnvilOrigin(options.url);
21764
+ const gitUrl = void 0 === options.gitUrl ? void 0 : normalizeAnvilOrigin(options.gitUrl);
21765
+ const selectedUrl = await resolveCheckoutUrl(explicitUrl);
21766
+ if (!selectedUrl) return void logger_logger.info("Create cancelled.");
21767
+ const anvilUrl = normalizeAnvilOrigin(selectedUrl);
21768
+ const user = await resolveCreateAccountInteractive(anvilUrl, {
21769
+ user: options.user,
21770
+ login: deps.login
21771
+ });
21772
+ if (null === user) return void logger_logger.info("Create cancelled.");
21773
+ const result = await deps.executeCreate({
21774
+ name: input.name,
21775
+ directory: input.destinationPath,
21776
+ tags: input.tags,
21777
+ anvilUrl,
21778
+ ...false === options.checkout ? {
21779
+ checkout: false
21780
+ } : {},
21781
+ ...gitUrl ? {
21782
+ gitUrl
21783
+ } : {},
21784
+ user
21785
+ });
21786
+ if (options.open && "destinationPath" in result) {
21787
+ const preferredEditor = String(getConfig("preferredEditor") || "").trim();
21788
+ try {
21789
+ await (deps.openPath ?? checkout_openPathInEditorOrDefault)(result.destinationPath, preferredEditor ? getPreferredEditorCommand(preferredEditor) : void 0);
21790
+ } catch (error) {
21791
+ logger_logger.warn(`App created and checked out at ${result.destinationPath}, but opening it failed: ${errors_getErrorMessage(error)}`);
21792
+ }
21793
+ }
21794
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
21795
+ data: result
21796
+ });
21797
+ else {
21798
+ logger_logger.success("destinationDisplay" in result ? `Created ${result.appName ?? result.appId} (${result.appId}) in ${result.destinationDisplay}` : `Created ${result.appName ?? result.appId} (${result.appId}) at ${result.editorUrl}`);
21799
+ if ("destinationDisplay" in result) logger_logger.info(`${result.anvilUrl}/build/apps/${encodeURIComponent(result.appId)}`);
21800
+ }
21801
+ } catch (error) {
21802
+ const message = errors_getErrorMessage(error);
21803
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
21804
+ error: message,
21805
+ ...error instanceof CreateCheckoutError ? {
21806
+ data: {
21807
+ created: error.created
21808
+ }
21809
+ } : {}
21810
+ });
21811
+ else logger_logger.error(`Error: ${message}`);
21812
+ process.exitCode = 1;
21813
+ }
21814
+ });
21815
+ }
21287
21816
  const defaultGitAuthDoctorDeps = {
21288
21817
  getAppAuthBinding: getAppAuthBinding,
21289
21818
  getAccountsForUrl: auth_getAccountsForUrl,
@@ -21577,7 +22106,7 @@ print(json.dumps({"issues": issues}))
21577
22106
  ])).trim();
21578
22107
  const resolvedGitCommonDir = external_path_default().isAbsolute(rawGitCommonDir) ? rawGitCommonDir : external_path_default().resolve(inputPath, rawGitCommonDir);
21579
22108
  const gitCommonDir = external_fs_default().realpathSync(resolvedGitCommonDir);
21580
- setRepoContext(repoPath);
22109
+ auth_setRepoContext(repoPath);
21581
22110
  const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
21582
22111
  const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
21583
22112
  const findings = [];
@@ -21703,7 +22232,7 @@ print(json.dumps({"issues": issues}))
21703
22232
  const repoPath = options.repoPath ?? deps.cwd();
21704
22233
  if ("get" !== operation) return null;
21705
22234
  if (!request.protocol || !request.host) return null;
21706
- setRepoContext(repoPath);
22235
+ auth_setRepoContext(repoPath);
21707
22236
  const appId = parseAppIdFromGitPath(request.path);
21708
22237
  if (!appId) return null;
21709
22238
  const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
@@ -21754,7 +22283,6 @@ print(json.dumps({"issues": issues}))
21754
22283
  }
21755
22284
  });
21756
22285
  }
21757
- const DEFAULT_ANVIL_URL = resolveAnvilUrl();
21758
22286
  async function syncToLatest(repoPath, appId, options) {
21759
22287
  const { anvilUrl, gitUrl = anvilUrl, authToken, currentBranch, username } = options;
21760
22288
  try {
@@ -21783,9 +22311,10 @@ print(json.dumps({"issues": issues}))
21783
22311
  }
21784
22312
  }
21785
22313
  async function watch(repoPath, appId, options = {}) {
21786
- const { anvilUrl = DEFAULT_ANVIL_URL, gitUrl = anvilUrl, stagedOnly = false, username } = options;
21787
22314
  repoPath = external_path_default().resolve(repoPath);
21788
- setRepoContext(repoPath);
22315
+ auth_setRepoContext(repoPath);
22316
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
22317
+ const { gitUrl = anvilUrl, stagedOnly = false, username } = options;
21789
22318
  const storedEnvPid = options.environment?.envPid ? void 0 : await readEnvironmentPid(repoPath);
21790
22319
  const environment = options.environment ?? (storedEnvPid ? {
21791
22320
  envPid: storedEnvPid
@@ -21973,40 +22502,10 @@ print(json.dumps({"issues": issues}))
21973
22502
  }));
21974
22503
  choices.push({
21975
22504
  name: "Cancel",
21976
- value: null
21977
- });
21978
- const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
21979
- return selectedUrl;
21980
- }
21981
- const defaultConfigureWatchGitAuthDeps = {
21982
- configureCredentialHelperForUrl: configureCredentialHelperForUrl,
21983
- setAppAuthBinding: setAppAuthBinding
21984
- };
21985
- const defaultSyncStartDeps = {
21986
- pushToAnvil,
21987
- recreateSessionAndValidate,
21988
- recheckSyncStatus,
21989
- startWatchingWithEventHandlers
21990
- };
21991
- function resolveWatchOpenPath(repoPath) {
21992
- return external_path_default().resolve(repoPath);
21993
- }
21994
- async function openWatchPath(targetPath, deps) {
21995
- const preferredEditor = String(getConfig("preferredEditor") || "").trim();
21996
- const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
21997
- await openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps);
21998
- logger_logger.info(external_chalk_default().gray(`Opened ${targetPath}`));
21999
- }
22000
- async function configureWatchGitAuth(options, deps = defaultConfigureWatchGitAuthDeps) {
22001
- await deps.configureCredentialHelperForUrl(options.repoPath, options.anvilUrl);
22002
- await deps.setAppAuthBinding(options.repoPath, options.appId, {
22003
- url: options.anvilUrl,
22004
- username: options.username
22505
+ value: null
22005
22506
  });
22006
- }
22007
- function isStaleRemotePushError(message) {
22008
- const normalized = message.toLowerCase();
22009
- return normalized.includes("fetch first") || normalized.includes("non-fast-forward") || normalized.includes("failed to push some refs") || normalized.includes("remote contains work that you do") || normalized.includes("[rejected]");
22507
+ const selectedUrl = await logger_logger.select("Multiple logged-in Anvil URLs found. Which one would you like to use?", choices, decision.urls[0]);
22508
+ return selectedUrl;
22010
22509
  }
22011
22510
  async function selectAppId(candidates) {
22012
22511
  if (0 === candidates.length) {
@@ -22085,6 +22584,169 @@ print(json.dumps({"issues": issues}))
22085
22584
  return null;
22086
22585
  }
22087
22586
  }
22587
+ class AppAccountCancelledError extends Error {
22588
+ constructor(){
22589
+ super("Operation cancelled.");
22590
+ }
22591
+ }
22592
+ class NoAppIdError extends Error {
22593
+ constructor(){
22594
+ super("No app ID provided. Cannot continue without an app ID.");
22595
+ }
22596
+ }
22597
+ async function resolveAppAccount(repoPath, options = {}) {
22598
+ const { appId: explicitAppId, url: explicitUrl, user: explicitUsername, useFirst = false } = options;
22599
+ auth_setRepoContext(repoPath);
22600
+ const detectedFromAllRemotes = await detectAppIdsFromAllRemotes(repoPath);
22601
+ let filteredCandidates = filterCandidates(detectedFromAllRemotes, explicitUrl, explicitUsername);
22602
+ logger_logger.verbose(external_chalk_default().cyan(`Detected ${detectedFromAllRemotes.length} app ID(s) from git remotes`));
22603
+ if (filteredCandidates.length !== detectedFromAllRemotes.length) logger_logger.verbose(external_chalk_default().cyan(`After filtering: ${filteredCandidates.length} candidate(s)`));
22604
+ let finalAppId;
22605
+ let anvilUrl;
22606
+ let username = explicitUsername;
22607
+ let fallbackUrl;
22608
+ let shouldPersistUsernameBinding = false;
22609
+ if (explicitAppId) {
22610
+ finalAppId = explicitAppId;
22611
+ const binding = await getAppAuthBinding(repoPath, explicitAppId);
22612
+ if (binding.url && !explicitUrl) {
22613
+ anvilUrl = normalizeAnvilUrl(binding.url);
22614
+ logger_logger.verbose(external_chalk_default().cyan("Resolved URL from binding for app ID: ") + external_chalk_default().bold(anvilUrl));
22615
+ }
22616
+ if (binding.username && !explicitUsername) {
22617
+ username = binding.username;
22618
+ logger_logger.verbose(external_chalk_default().cyan("Resolved username from binding for app ID: ") + external_chalk_default().bold(username));
22619
+ }
22620
+ const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
22621
+ if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
22622
+ anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
22623
+ logger_logger.verbose(external_chalk_default().cyan("Resolved URL from remote for app ID: ") + external_chalk_default().bold(anvilUrl));
22624
+ }
22625
+ if (remoteInfo.detectedUsername && !explicitUsername && !username) {
22626
+ username = remoteInfo.detectedUsername;
22627
+ logger_logger.verbose(external_chalk_default().cyan("Resolved username from remote for app ID: ") + external_chalk_default().bold(username));
22628
+ }
22629
+ } else {
22630
+ logger_logger.verbose(external_chalk_default().cyan("No app ID provided, attempting auto-detection..."));
22631
+ if (0 === filteredCandidates.length) {
22632
+ logger_logger.verbose(external_chalk_default().gray("No app IDs found in git remotes."));
22633
+ const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
22634
+ if (null === resolvedFallbackUrl) throw new AppAccountCancelledError();
22635
+ fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22636
+ const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
22637
+ if (null === lookupDecision) throw new AppAccountCancelledError();
22638
+ username = lookupDecision.username;
22639
+ if (lookupDecision.shouldContinue) {
22640
+ logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
22641
+ const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
22642
+ anvilUrl: fallbackUrl,
22643
+ username,
22644
+ includeRemotes: false
22645
+ });
22646
+ logger_logger.progressEnd("detect");
22647
+ for (const c of reverseLookupCandidates)filteredCandidates.push({
22648
+ ...c,
22649
+ detectedUrl: fallbackUrl
22650
+ });
22651
+ }
22652
+ }
22653
+ if (filteredCandidates.length > 0) {
22654
+ for (const c of filteredCandidates)logger_logger.verbose(external_chalk_default().gray(` Found: ${formatCandidateLabel(c)}`));
22655
+ if (filteredCandidates.length > 1) logger_logger.verbose(external_chalk_default().yellow(`Found ${filteredCandidates.length} potential app IDs`));
22656
+ }
22657
+ if (useFirst && filteredCandidates.length > 0) {
22658
+ const selected = filteredCandidates[0];
22659
+ finalAppId = selected.appId;
22660
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22661
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
22662
+ logger_logger.success("Auto-selected first app ID: " + external_chalk_default().bold(finalAppId));
22663
+ } else {
22664
+ const selected = await selectAppId(filteredCandidates);
22665
+ if (!selected) throw new NoAppIdError();
22666
+ finalAppId = selected.appId;
22667
+ if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22668
+ if (selected.detectedUsername && !username) username = selected.detectedUsername;
22669
+ }
22670
+ }
22671
+ const binding = await getAppAuthBinding(repoPath, finalAppId);
22672
+ if (binding.url && !explicitUrl) {
22673
+ anvilUrl = normalizeAnvilUrl(binding.url);
22674
+ logger_logger.verbose(external_chalk_default().cyan("Using app binding URL: ") + external_chalk_default().bold(anvilUrl));
22675
+ }
22676
+ if (binding.username && !explicitUsername) {
22677
+ username = binding.username;
22678
+ logger_logger.verbose(external_chalk_default().cyan("Using app binding username: ") + external_chalk_default().bold(username));
22679
+ }
22680
+ shouldPersistUsernameBinding = !binding.username && !explicitUsername;
22681
+ if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
22682
+ else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
22683
+ else {
22684
+ const resolvedFallbackUrl = await resolveUrlForFallback();
22685
+ if (null === resolvedFallbackUrl) throw new AppAccountCancelledError();
22686
+ anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22687
+ }
22688
+ anvilUrl = normalizeAnvilUrl(anvilUrl);
22689
+ logger_logger.verbose(external_chalk_default().green("Using app ID: ") + external_chalk_default().bold(finalAppId));
22690
+ logger_logger.verbose(external_chalk_default().cyan("Using Anvil URL: ") + external_chalk_default().bold(anvilUrl));
22691
+ const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
22692
+ if (null === resolvedUsername) throw new AppAccountCancelledError();
22693
+ username = resolvedUsername;
22694
+ if (shouldPersistUsernameBinding && username && (await auth_getAccountsForUrl(anvilUrl)).length > 1) {
22695
+ await setAppAuthBinding(repoPath, finalAppId, {
22696
+ url: anvilUrl,
22697
+ username
22698
+ });
22699
+ logger_logger.verbose(external_chalk_default().cyan("Saved app account binding for future non-interactive use."));
22700
+ }
22701
+ if (username) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(username));
22702
+ return {
22703
+ appId: finalAppId,
22704
+ anvilUrl,
22705
+ ...username ? {
22706
+ username
22707
+ } : {}
22708
+ };
22709
+ }
22710
+ function addAppAccountOptions(command) {
22711
+ return command.option("-A, --appid <APP_ID>", "Specify app ID directly").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use");
22712
+ }
22713
+ function appAccountOptions(options) {
22714
+ return {
22715
+ appId: options.appid,
22716
+ url: options.url,
22717
+ user: options.user
22718
+ };
22719
+ }
22720
+ const defaultConfigureWatchGitAuthDeps = {
22721
+ configureCredentialHelperForUrl: configureCredentialHelperForUrl,
22722
+ setAppAuthBinding: setAppAuthBinding
22723
+ };
22724
+ const defaultSyncStartDeps = {
22725
+ pushToAnvil,
22726
+ recreateSessionAndValidate,
22727
+ recheckSyncStatus,
22728
+ startWatchingWithEventHandlers
22729
+ };
22730
+ function resolveWatchOpenPath(repoPath) {
22731
+ return external_path_default().resolve(repoPath);
22732
+ }
22733
+ async function openWatchPath(targetPath, deps) {
22734
+ const preferredEditor = String(getConfig("preferredEditor") || "").trim();
22735
+ const preferredEditorCommand = preferredEditor ? getPreferredEditorCommand(preferredEditor) : "";
22736
+ await openPathInEditorOrDefault(targetPath, preferredEditorCommand, deps);
22737
+ logger_logger.info(external_chalk_default().gray(`Opened ${targetPath}`));
22738
+ }
22739
+ async function configureWatchGitAuth(options, deps = defaultConfigureWatchGitAuthDeps) {
22740
+ await deps.configureCredentialHelperForUrl(options.repoPath, options.anvilUrl);
22741
+ await deps.setAppAuthBinding(options.repoPath, options.appId, {
22742
+ url: options.anvilUrl,
22743
+ username: options.username
22744
+ });
22745
+ }
22746
+ function isStaleRemotePushError(message) {
22747
+ const normalized = message.toLowerCase();
22748
+ return normalized.includes("fetch first") || normalized.includes("non-fast-forward") || normalized.includes("failed to push some refs") || normalized.includes("remote contains work that you do") || normalized.includes("[rejected]");
22749
+ }
22088
22750
  async function pushToAnvil(options) {
22089
22751
  try {
22090
22752
  const authToken = await auth_getValidAuthToken(options.anvilUrl, options.username);
@@ -22213,7 +22875,7 @@ print(json.dumps({"issues": issues}))
22213
22875
  });
22214
22876
  logger_logger.progressEnd("verify");
22215
22877
  const currentCategory = getSyncStateCategory(freshSession.syncStatus);
22216
- const currentBranch = freshSession.getBranchName() || "master";
22878
+ const currentBranch = freshSession.getBranchName() || await getFallbackBranchName(options.repoPath);
22217
22879
  if (currentCategory !== previousCategory || currentBranch !== previousBranch) {
22218
22880
  logger_logger.warn("Repository status has changed. Re-evaluating...");
22219
22881
  return freshSession;
@@ -22246,7 +22908,7 @@ print(json.dumps({"issues": issues}))
22246
22908
  }
22247
22909
  async function checkSyncStatusAndStart(session, options, deps = defaultSyncStartDeps) {
22248
22910
  const syncStatus = session.syncStatus;
22249
- const branchName = session.getBranchName() || "master";
22911
+ const branchName = session.getBranchName() || await getFallbackBranchName(options.repoPath);
22250
22912
  const hasUncommitted = session.hasUncommittedChanges;
22251
22913
  const stateCategory = getSyncStateCategory(syncStatus);
22252
22914
  if (syncStatus?.branchMissing) {
@@ -22566,6 +23228,9 @@ print(json.dumps({"issues": issues}))
22566
23228
  const suggestedAction = error.suggestedAction;
22567
23229
  if ("string" == typeof suggestedAction && suggestedAction.length > 0) logger_logger.warn(suggestedAction);
22568
23230
  }
23231
+ async function getFallbackBranchName(repoPath) {
23232
+ return getCurrentOrFallbackBranchName(esm_default(repoPath));
23233
+ }
22569
23234
  async function handleWatchCommand(options) {
22570
23235
  const invoked = process.argv[2];
22571
23236
  if ("sync" === invoked) {
@@ -22573,127 +23238,30 @@ print(json.dumps({"issues": issues}))
22573
23238
  process.exit(1);
22574
23239
  }
22575
23240
  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);
23241
+ auth_setRepoContext(repoPath);
22577
23242
  try {
22578
23243
  const validationResult = await validateAnvilApp(repoPath);
22579
23244
  if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
22580
- const detectedFromAllRemotes = await detectAppIdsFromAllRemotes(repoPath);
22581
- let filteredCandidates = filterCandidates(detectedFromAllRemotes, explicitUrl, explicitUsername);
22582
- logger_logger.verbose(external_chalk_default().cyan(`Detected ${detectedFromAllRemotes.length} app ID(s) from git remotes`));
22583
- if (filteredCandidates.length !== detectedFromAllRemotes.length) logger_logger.verbose(external_chalk_default().cyan(`After filtering: ${filteredCandidates.length} candidate(s)`));
22584
- let finalAppId;
22585
- let anvilUrl;
22586
- let username = explicitUsername;
22587
- let fallbackUrl;
22588
- let shouldPersistUsernameBinding = false;
22589
- if (explicitAppId) {
22590
- finalAppId = explicitAppId;
22591
- const binding = await getAppAuthBinding(repoPath, explicitAppId);
22592
- if (binding.url && !explicitUrl) {
22593
- anvilUrl = normalizeAnvilUrl(binding.url);
22594
- logger_logger.verbose(external_chalk_default().cyan("Resolved URL from binding for app ID: ") + external_chalk_default().bold(anvilUrl));
22595
- }
22596
- if (binding.username && !explicitUsername) {
22597
- username = binding.username;
22598
- logger_logger.verbose(external_chalk_default().cyan("Resolved username from binding for app ID: ") + external_chalk_default().bold(username));
22599
- }
22600
- const remoteInfo = lookupRemoteInfoForAppId(explicitAppId, detectedFromAllRemotes);
22601
- if (remoteInfo.detectedUrl && !explicitUrl && !anvilUrl) {
22602
- anvilUrl = normalizeAnvilUrl(remoteInfo.detectedUrl);
22603
- logger_logger.verbose(external_chalk_default().cyan("Resolved URL from remote for app ID: ") + external_chalk_default().bold(anvilUrl));
22604
- }
22605
- if (remoteInfo.detectedUsername && !explicitUsername && !username) {
22606
- username = remoteInfo.detectedUsername;
22607
- logger_logger.verbose(external_chalk_default().cyan("Resolved username from remote for app ID: ") + external_chalk_default().bold(username));
22608
- }
22609
- } else {
22610
- logger_logger.verbose(external_chalk_default().cyan("No app ID provided, attempting auto-detection..."));
22611
- if (0 === filteredCandidates.length) {
22612
- logger_logger.verbose(external_chalk_default().gray("No app IDs found in git remotes."));
22613
- const resolvedFallbackUrl = await resolveUrlForFallback(explicitUrl);
22614
- if (null === resolvedFallbackUrl) {
22615
- logger_logger.warn("Operation cancelled.");
22616
- process.exit(0);
22617
- }
22618
- fallbackUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22619
- const lookupDecision = await confirmReverseLookupWithResolvedUser(fallbackUrl, username);
22620
- if (null === lookupDecision) {
22621
- logger_logger.warn("Operation cancelled.");
22622
- process.exit(0);
22623
- }
22624
- username = lookupDecision.username;
22625
- if (lookupDecision.shouldContinue) {
22626
- logger_logger.progress("detect", `Searching ${fallbackUrl} ${username ? `for ${username}` : ""} for matching app IDs...`);
22627
- const reverseLookupCandidates = await detectAppIdsByCommitLookup(repoPath, {
22628
- anvilUrl: fallbackUrl,
22629
- username,
22630
- includeRemotes: false
22631
- });
22632
- logger_logger.progressEnd("detect");
22633
- for (const c of reverseLookupCandidates)filteredCandidates.push({
22634
- ...c,
22635
- detectedUrl: fallbackUrl
22636
- });
22637
- }
22638
- }
22639
- if (filteredCandidates.length > 0) {
22640
- for (const c of filteredCandidates)logger_logger.verbose(external_chalk_default().gray(` Found: ${formatCandidateLabel(c)}`));
22641
- if (filteredCandidates.length > 1) logger_logger.verbose(external_chalk_default().yellow(`Found ${filteredCandidates.length} potential app IDs`));
22642
- }
22643
- if (useFirst && filteredCandidates.length > 0) {
22644
- const selected = filteredCandidates[0];
22645
- finalAppId = selected.appId;
22646
- if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22647
- if (selected.detectedUsername && !username) username = selected.detectedUsername;
22648
- logger_logger.success("Auto-selected first app ID: " + external_chalk_default().bold(finalAppId));
22649
- } else {
22650
- const selected = await selectAppId(filteredCandidates);
22651
- if (!selected) {
22652
- logger_logger.error("No app ID provided. Cannot continue without an app ID.");
22653
- process.exit(1);
22654
- }
22655
- finalAppId = selected.appId;
22656
- if (selected.detectedUrl) anvilUrl = normalizeAnvilUrl(selected.detectedUrl);
22657
- if (selected.detectedUsername && !username) username = selected.detectedUsername;
22658
- }
22659
- }
22660
- const binding = await getAppAuthBinding(repoPath, finalAppId);
22661
- if (binding.url && !explicitUrl) {
22662
- anvilUrl = normalizeAnvilUrl(binding.url);
22663
- logger_logger.verbose(external_chalk_default().cyan("Using app binding URL: ") + external_chalk_default().bold(anvilUrl));
22664
- }
22665
- if (binding.username && !explicitUsername) {
22666
- username = binding.username;
22667
- logger_logger.verbose(external_chalk_default().cyan("Using app binding username: ") + external_chalk_default().bold(username));
22668
- }
22669
- shouldPersistUsernameBinding = !binding.username && !explicitUsername;
22670
- if (explicitUrl) anvilUrl = normalizeAnvilUrl(explicitUrl);
22671
- else if (!anvilUrl) if (fallbackUrl) anvilUrl = fallbackUrl;
22672
- else {
22673
- const resolvedFallbackUrl = await resolveUrlForFallback();
22674
- if (null === resolvedFallbackUrl) {
23245
+ let account;
23246
+ try {
23247
+ account = await resolveAppAccount(repoPath, {
23248
+ appId: explicitAppId,
23249
+ url: explicitUrl,
23250
+ user: explicitUsername,
23251
+ useFirst
23252
+ });
23253
+ } catch (e) {
23254
+ if (e instanceof AppAccountCancelledError) {
22675
23255
  logger_logger.warn("Operation cancelled.");
22676
23256
  process.exit(0);
22677
23257
  }
22678
- anvilUrl = normalizeAnvilUrl(resolvedFallbackUrl);
22679
- }
22680
- anvilUrl = normalizeAnvilUrl(anvilUrl);
22681
- logger_logger.verbose(external_chalk_default().green("Using app ID: ") + external_chalk_default().bold(finalAppId));
22682
- logger_logger.verbose(external_chalk_default().cyan("Using Anvil URL: ") + external_chalk_default().bold(anvilUrl));
22683
- const resolvedUsername = await resolveUsernameForUrl(anvilUrl, username);
22684
- if (null === resolvedUsername) {
22685
- logger_logger.warn("Operation cancelled.");
22686
- process.exit(0);
22687
- }
22688
- username = resolvedUsername;
22689
- if (shouldPersistUsernameBinding && username && (await auth_getAccountsForUrl(anvilUrl)).length > 1) {
22690
- await setAppAuthBinding(repoPath, finalAppId, {
22691
- url: anvilUrl,
22692
- username
22693
- });
22694
- logger_logger.verbose(external_chalk_default().cyan("Saved app account binding for future non-interactive use."));
23258
+ if (e instanceof NoAppIdError) {
23259
+ logger_logger.error(e.message);
23260
+ process.exit(1);
23261
+ }
23262
+ throw e;
22695
23263
  }
22696
- if (username) logger_logger.verbose(external_chalk_default().cyan("Using account: ") + external_chalk_default().bold(username));
23264
+ const { appId: finalAppId, anvilUrl, username } = account;
22697
23265
  if (!await hasTokensForUrl(anvilUrl, username)) {
22698
23266
  if (username) logger_logger.error(`Not logged in to ${anvilUrl} as ${username}`);
22699
23267
  else logger_logger.error(`Not logged in to ${anvilUrl}`);
@@ -22750,7 +23318,7 @@ print(json.dumps({"issues": issues}))
22750
23318
  }
22751
23319
  }
22752
23320
  function registerWatchCommand(program) {
22753
- const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
23321
+ const watchCommand = addAppAccountOptions(program.command("watch [path]")).description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").action(async (path, options, command)=>{
22754
23322
  const globalOptions = command.optsWithGlobals();
22755
23323
  if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
22756
23324
  await handleWatchCommand({
@@ -22780,6 +23348,7 @@ print(json.dumps({"issues": issues}))
22780
23348
  logger_logger.success("Logged in as " + external_chalk_default().bold(result.email));
22781
23349
  } catch (e) {
22782
23350
  logger_logger.error("Error: " + e.message);
23351
+ logErrorCause(e, logger_logger);
22783
23352
  process.exit(1);
22784
23353
  }
22785
23354
  });
@@ -23355,165 +23924,1141 @@ print(json.dumps({"issues": issues}))
23355
23924
  function registerConfigureCommand(program, version) {
23356
23925
  program.command("configure").description("Guided setup for configuration and login").action(async ()=>{
23357
23926
  try {
23358
- await runConfigureFlow(version, configure_defaultDeps);
23359
- } catch (e) {
23360
- logger_logger.error("Error: " + errors_getErrorMessage(e));
23361
- process.exit(1);
23927
+ await runConfigureFlow(version, configure_defaultDeps);
23928
+ } catch (e) {
23929
+ logger_logger.error("Error: " + errors_getErrorMessage(e));
23930
+ logErrorCause(e, logger_logger);
23931
+ process.exit(1);
23932
+ }
23933
+ });
23934
+ }
23935
+ function registerDepsCommand(program) {
23936
+ const deps = program.command("deps").description("Manage app dependencies");
23937
+ addAppAccountOptions(deps.command("fetch")).description("Fetch dependencies for checkout HEAD into .anvil/deps/").option("--commit <commit>", "Fetch deps at a specific app commit instead of checkout HEAD").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
23938
+ try {
23939
+ const projectRoot = process.cwd();
23940
+ const { appId: mainAppId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
23941
+ logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
23942
+ const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
23943
+ username,
23944
+ commit: options.commit,
23945
+ includeDocs: !options.excludeDocs
23946
+ });
23947
+ if (await getDependencyCacheWarning(projectRoot)) logger_logger.warn("Dependency cache does not match local anvil.yaml. Fetch uses committed dependency declarations; sync dependency changes to Anvil before fetching again.");
23948
+ if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
23949
+ if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
23950
+ if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
23951
+ logger_logger.success(`Fetched ${response.dependencies.length} dependencies:`);
23952
+ for (const dep of response.dependencies)logger_logger.info(` - ${dep.package_name}`);
23953
+ } catch (e) {
23954
+ logger_logger.error(e.message);
23955
+ process.exit(1);
23956
+ }
23957
+ });
23958
+ deps.command("status").description("Show cached dependencies").action(async ()=>{
23959
+ try {
23960
+ const cached = await getCachedPackages(process.cwd());
23961
+ if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
23962
+ logger_logger.info("Cached dependencies:");
23963
+ for (const pkg of cached)logger_logger.info(` - ${pkg}`);
23964
+ } catch (e) {
23965
+ logger_logger.error(e.message);
23966
+ process.exit(1);
23967
+ }
23968
+ });
23969
+ deps.command("clear").description("Clear cached dependencies").action(async ()=>{
23970
+ try {
23971
+ await clearDepsCache(process.cwd());
23972
+ logger_logger.success("Cleared .anvil/deps/");
23973
+ } catch (e) {
23974
+ logger_logger.error(e.message);
23975
+ process.exit(1);
23976
+ }
23977
+ });
23978
+ }
23979
+ async function applyDatabaseSchema(appId, options = {}) {
23980
+ const projectRoot = options.projectRoot ?? process.cwd();
23981
+ auth_setRepoContext(projectRoot);
23982
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23983
+ const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
23984
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
23985
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
23986
+ method: "PUT",
23987
+ headers: {
23988
+ Authorization: `Bearer ${token}`,
23989
+ "Content-Type": "application/json"
23990
+ },
23991
+ body: JSON.stringify({
23992
+ schema: appYaml?.db_schema ?? {},
23993
+ table_id_hints: appYaml?.table_id_hints
23994
+ })
23995
+ });
23996
+ if (!response.ok) throw new Error(formatHttpError("Failed to apply database schema", response.status, await response.text()));
23997
+ return await response.json();
23998
+ }
23999
+ async function confirmSchemaApplication(force = false) {
24000
+ if (force) return true;
24001
+ assertCanPrompt("Applying database schema", "Pass --force to apply without prompting.");
24002
+ return logger_logger.confirm("Apply the app environment's schema to its database? This may delete tables, columns, or data.", true);
24003
+ }
24004
+ function registerDbCommand(program) {
24005
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24006
+ const db = program.command("db").description("Manage app databases");
24007
+ addAppAccountOptions(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)=>{
24008
+ try {
24009
+ const projectRoot = process.cwd();
24010
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24011
+ if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
24012
+ const response = await applyDatabaseSchema(appId, {
24013
+ anvilUrl,
24014
+ username,
24015
+ projectRoot
24016
+ });
24017
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24018
+ data: response
24019
+ });
24020
+ else logger_logger.success("Database schema applied.");
24021
+ } catch (error) {
24022
+ const message = errors_getErrorMessage(error);
24023
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24024
+ error: message
24025
+ });
24026
+ else logger_logger.error(message);
24027
+ process.exit(1);
24028
+ }
24029
+ });
24030
+ }
24031
+ async function ensureEnvironmentUrl(appId, envPid, options = {}) {
24032
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24033
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24034
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
24035
+ method: "POST",
24036
+ headers: {
24037
+ Authorization: `Bearer ${token}`
24038
+ }
24039
+ });
24040
+ if (!response.ok) throw new Error(formatHttpError("Failed to get environment URL", response.status, await response.text()));
24041
+ const { url, expires_at } = await response.json();
24042
+ return {
24043
+ url,
24044
+ expires_at
24045
+ };
24046
+ }
24047
+ async function getTemporaryUplinkKey(appId, envPid, options = {}) {
24048
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24049
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24050
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
24051
+ method: "POST",
24052
+ headers: {
24053
+ Authorization: `Bearer ${token}`
24054
+ }
24055
+ });
24056
+ if (!response.ok) throw new Error(formatHttpError("Failed to get temporary uplink key", response.status, await response.text()));
24057
+ const { key, url, expires_at } = await response.json();
24058
+ return {
24059
+ key,
24060
+ url,
24061
+ expires_at
24062
+ };
24063
+ }
24064
+ async function resolveEnvironmentContext(options = {}) {
24065
+ const projectRoot = process.cwd();
24066
+ const envPid = await readEnvironmentPid(projectRoot);
24067
+ if (!envPid) throw new Error("No app environment associated with the current directory.");
24068
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, options);
24069
+ return {
24070
+ envPid,
24071
+ anvilUrl,
24072
+ appId,
24073
+ ...username ? {
24074
+ username
24075
+ } : {}
24076
+ };
24077
+ }
24078
+ function registerEnvCommand(program) {
24079
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24080
+ const env = program.command("env").description("Manage app environments");
24081
+ addAppAccountOptions(env.command("url")).description("Get a temporary private app URL for the current environment").action(async (options)=>{
24082
+ try {
24083
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
24084
+ const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
24085
+ anvilUrl,
24086
+ username
24087
+ });
24088
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24089
+ data: {
24090
+ url,
24091
+ expires_at
24092
+ }
24093
+ });
24094
+ else logger_logger.info(`URL: ${url}\nExpires at: ${expires_at}`);
24095
+ } catch (error) {
24096
+ const message = errors_getErrorMessage(error);
24097
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24098
+ error: message
24099
+ });
24100
+ else logger_logger.error(message);
24101
+ process.exit(1);
24102
+ }
24103
+ });
24104
+ addAppAccountOptions(env.command("uplink")).description("Get a temporary uplink key for the current environment").action(async (options)=>{
24105
+ try {
24106
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
24107
+ const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
24108
+ anvilUrl,
24109
+ username
24110
+ });
24111
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24112
+ data: {
24113
+ key,
24114
+ url,
24115
+ expires_at
24116
+ }
24117
+ });
24118
+ else logger_logger.info(`Key: ${key}\nURL: ${url}\nExpires at: ${expires_at}`);
24119
+ } catch (error) {
24120
+ const message = errors_getErrorMessage(error);
24121
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24122
+ error: message
24123
+ });
24124
+ else logger_logger.error(message);
24125
+ process.exit(1);
24126
+ }
24127
+ });
24128
+ }
24129
+ const external_events_namespaceObject = require("events");
24130
+ function replError(error) {
24131
+ if ("string" == typeof error) return {
24132
+ message: error
24133
+ };
24134
+ if (error && "object" == typeof error) {
24135
+ const value = error;
24136
+ return {
24137
+ message: String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value)),
24138
+ ..."string" == typeof value.type ? {
24139
+ type: value.type
24140
+ } : {},
24141
+ ...Array.isArray(value.trace) ? {
24142
+ trace: value.trace.filter((frame)=>Array.isArray(frame) && "string" == typeof frame[0] && "number" == typeof frame[1])
24143
+ } : {}
24144
+ };
24145
+ }
24146
+ return {
24147
+ message: String(error)
24148
+ };
24149
+ }
24150
+ class ReplServerError extends Error {
24151
+ detail;
24152
+ constructor(detail){
24153
+ super(detail.message), this.detail = detail;
24154
+ }
24155
+ }
24156
+ async function runRepl(appId, envPid, options) {
24157
+ const interrupted = ()=>new Error("REPL interrupted.");
24158
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24159
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24160
+ const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
24161
+ url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
24162
+ if (options.signal?.aborted) throw interrupted();
24163
+ const ws = new (external_ws_default())(url, {
24164
+ headers: {
24165
+ Authorization: `Bearer ${token}`
24166
+ },
24167
+ handshakeTimeout: 30000
24168
+ });
24169
+ const controller = new AbortController();
24170
+ let repl;
24171
+ let nextId = 1;
24172
+ let output = "";
24173
+ let keepalive;
24174
+ let pongTimeout;
24175
+ let closeTimeout;
24176
+ const startupTimeout = setTimeout(()=>{
24177
+ controller.abort(new Error("Timed out waiting for the REPL to start."));
24178
+ }, 30000);
24179
+ const onAbort = ()=>controller.abort(interrupted());
24180
+ const markResponsive = ()=>{
24181
+ clearTimeout(pongTimeout);
24182
+ pongTimeout = void 0;
24183
+ };
24184
+ options.signal?.addEventListener("abort", onAbort, {
24185
+ once: true
24186
+ });
24187
+ ws.on("pong", markResponsive);
24188
+ ws.on("unexpected-response", (request, response)=>{
24189
+ response.resume();
24190
+ controller.abort(new Error(`REPL websocket handshake failed (${response.statusCode} ${response.statusMessage}).`));
24191
+ request.destroy();
24192
+ });
24193
+ ws.on("error", (error)=>controller.abort(new Error(`REPL websocket connection failed: ${error.message}`)));
24194
+ ws.on("close", (code, reason)=>{
24195
+ clearTimeout(closeTimeout);
24196
+ controller.abort(new Error(`REPL websocket disconnected unexpectedly (code ${code}${reason.length ? `: ${reason}` : ""}).`));
24197
+ });
24198
+ const messages = (0, external_events_namespaceObject.on)(ws, "message", {
24199
+ signal: controller.signal
24200
+ });
24201
+ function send(message) {
24202
+ const id = nextId++;
24203
+ ws.send(JSON.stringify({
24204
+ ...message,
24205
+ id
24206
+ }), (error)=>{
24207
+ if (error) controller.abort(new Error(`REPL websocket send failed: ${error.message}`));
24208
+ });
24209
+ return id;
24210
+ }
24211
+ async function readUntil(matches) {
24212
+ while(true){
24213
+ const { value, done } = await messages.next();
24214
+ options.signal?.throwIfAborted();
24215
+ if (done) throw new Error("REPL websocket message stream ended unexpectedly.");
24216
+ markResponsive();
24217
+ let message;
24218
+ try {
24219
+ message = JSON.parse(value[0].toString());
24220
+ if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
24221
+ } catch {
24222
+ throw new Error("Invalid REPL websocket message.");
24223
+ }
24224
+ if ("REPL_UPDATE" === message.event) {
24225
+ if (!repl || message.repl !== repl) continue;
24226
+ if ("string" == typeof message.output) {
24227
+ output += message.output;
24228
+ options.onOutput?.(message.output);
24229
+ options.signal?.throwIfAborted();
24230
+ }
24231
+ }
24232
+ if (message.error) throw new ReplServerError(replError(message.error));
24233
+ if ("REPL_UPDATE" === message.event && message.terminated) throw new Error("REPL terminated unexpectedly.");
24234
+ if (matches(message)) return message;
24235
+ }
24236
+ }
24237
+ try {
24238
+ await (0, external_events_namespaceObject.once)(ws, "open", {
24239
+ signal: controller.signal
24240
+ });
24241
+ const launchId = send({
24242
+ cmd: "LAUNCH_REPL"
24243
+ });
24244
+ const launched = await readUntil((message)=>message.id === launchId);
24245
+ if ("string" != typeof launched.repl || !launched.repl) throw new Error("Invalid REPL launch response: missing REPL ID.");
24246
+ repl = launched.repl;
24247
+ keepalive = setInterval(()=>{
24248
+ try {
24249
+ send({
24250
+ cmd: "REPL_KEEPALIVE",
24251
+ repl
24252
+ });
24253
+ pongTimeout = setTimeout(()=>controller.abort(new Error("REPL websocket heartbeat timed out.")), 10000);
24254
+ ws.ping();
24255
+ } catch (error) {
24256
+ controller.abort(error);
24257
+ }
24258
+ }, 20000);
24259
+ const isReady = (message)=>"REPL_UPDATE" === message.event && true === message.ready;
24260
+ await readUntil(isReady);
24261
+ clearTimeout(startupTimeout);
24262
+ send({
24263
+ cmd: "REPL_COMMAND",
24264
+ repl,
24265
+ command: options.code
24266
+ });
24267
+ await readUntil(isReady);
24268
+ return {
24269
+ output
24270
+ };
24271
+ } catch (error) {
24272
+ if (error instanceof ReplServerError) return {
24273
+ output,
24274
+ error: error.detail
24275
+ };
24276
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
24277
+ throw failure instanceof Error ? failure : new Error(String(failure));
24278
+ } finally{
24279
+ clearTimeout(startupTimeout);
24280
+ clearInterval(keepalive);
24281
+ clearTimeout(pongTimeout);
24282
+ options.signal?.removeEventListener("abort", onAbort);
24283
+ controller.abort();
24284
+ await messages.return?.();
24285
+ if (ws.readyState === external_ws_default().OPEN) {
24286
+ if (repl) ws.send(JSON.stringify({
24287
+ id: nextId++,
24288
+ cmd: "TERMINATE_REPL",
24289
+ repl
24290
+ }), ()=>{});
24291
+ ws.close();
24292
+ } else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
24293
+ if (ws.readyState !== external_ws_default().CLOSED) {
24294
+ closeTimeout = setTimeout(()=>ws.terminate(), 1000);
24295
+ closeTimeout.unref();
24296
+ }
24297
+ }
24298
+ }
24299
+ function writeJsonReplOutput(output) {
24300
+ process.stdout.write(JSON.stringify({
24301
+ type: "repl_output",
24302
+ output,
24303
+ timestamp: new Date().toISOString()
24304
+ }) + "\n");
24305
+ }
24306
+ function registerReplCommand(program) {
24307
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24308
+ addAppAccountOptions(program.command("repl <code>")).description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
24309
+ Examples:
24310
+ anvil repl '1 + 2'
24311
+ anvil --json repl '1 + 2'
24312
+ anvil repl - < script.py
24313
+ `).action(async (code, options)=>{
24314
+ const controller = new AbortController();
24315
+ const onInterrupt = ()=>controller.abort();
24316
+ try {
24317
+ const { appId, envPid, anvilUrl, username } = await resolveEnvironmentContext(appAccountOptions(options));
24318
+ if ("-" === code) {
24319
+ const chunks = [];
24320
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
24321
+ code = Buffer.concat(chunks).toString("utf8");
24322
+ }
24323
+ process.on("SIGINT", onInterrupt);
24324
+ const response = await runRepl(appId, envPid, {
24325
+ code,
24326
+ anvilUrl,
24327
+ username,
24328
+ signal: controller.signal,
24329
+ onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
24330
+ process.stdout.write(output);
24331
+ }
24332
+ });
24333
+ const error = response.error;
24334
+ const message = error ? `${error.type ? `${error.type}: ` : ""}${error.message}` : void 0;
24335
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(!error, {
24336
+ data: response,
24337
+ error: message
24338
+ });
24339
+ else if (error) {
24340
+ process.stderr.write(`${message}\n`);
24341
+ for (const [file, line] of error.trace ?? [])process.stderr.write(` at ${file}:${line}\n`);
24342
+ }
24343
+ if (error) process.exitCode = 1;
24344
+ } catch (error) {
24345
+ const message = errors_getErrorMessage(error);
24346
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24347
+ error: message
24348
+ });
24349
+ else logger_logger.error(message);
24350
+ process.exitCode = controller.signal.aborted ? 130 : 1;
24351
+ } finally{
24352
+ process.removeListener("SIGINT", onInterrupt);
23362
24353
  }
23363
24354
  });
23364
24355
  }
23365
- async function resolvePrimaryAppId(projectRoot, anvilUrl) {
23366
- const remoteCandidates = await detectAppIdsFromAllRemotes(projectRoot);
23367
- if (remoteCandidates.length > 0) return remoteCandidates[0].appId;
23368
- const lookupCandidates = await detectAppIdsByCommitLookup(projectRoot, {
23369
- anvilUrl,
23370
- includeRemotes: false
24356
+ const external_yaml_namespaceObject = require("yaml");
24357
+ async function encryptSecret(appId, secretName, options = {}) {
24358
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24359
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24360
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/encrypt`, {
24361
+ method: "POST",
24362
+ headers: {
24363
+ Authorization: `Bearer ${token}`,
24364
+ "Content-Type": "application/json"
24365
+ },
24366
+ body: JSON.stringify({
24367
+ name: secretName,
24368
+ ...void 0 === options.secret ? {} : {
24369
+ secret: options.secret
24370
+ }
24371
+ })
23371
24372
  });
23372
- return lookupCandidates[0]?.appId ?? null;
24373
+ if (!response.ok) throw new Error(formatHttpError("Failed to set secret", response.status, await response.text()));
24374
+ return await response.json();
23373
24375
  }
23374
- function registerDepsCommand(program) {
23375
- const deps = program.command("deps").description("Manage app dependencies");
23376
- 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)=>{
24376
+ async function generateSecretKey(appId, keyName, options = {}) {
24377
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24378
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24379
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/generate-key`, {
24380
+ method: "POST",
24381
+ headers: {
24382
+ Authorization: `Bearer ${token}`,
24383
+ "Content-Type": "application/json"
24384
+ },
24385
+ body: JSON.stringify({
24386
+ name: keyName
24387
+ })
24388
+ });
24389
+ if (!response.ok) throw new Error(formatHttpError("Failed to generate key", response.status, await response.text()));
24390
+ return await response.json();
24391
+ }
24392
+ function secret_anvilYamlPath(projectRoot) {
24393
+ return external_path_default().join(projectRoot, "anvil.yaml");
24394
+ }
24395
+ function checkSecretTypeInDoc(doc, secretName, type) {
24396
+ const existingType = doc.getIn([
24397
+ "secrets",
24398
+ secretName,
24399
+ "type"
24400
+ ]);
24401
+ if (void 0 !== existingType && existingType !== type) throw new Error(`'${secretName}' is a ${existingType} in anvil.yaml, not a ${type}`);
24402
+ }
24403
+ function secretExistsInDoc(doc, secretName, appId) {
24404
+ return doc.hasIn([
24405
+ "secrets",
24406
+ secretName,
24407
+ "value",
24408
+ appId
24409
+ ]);
24410
+ }
24411
+ async function findSecretInAnvilYaml(projectRoot, secretName, appId) {
24412
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(secret_anvilYamlPath(projectRoot), "utf8"));
24413
+ if (!doc.hasIn([
24414
+ "secrets",
24415
+ secretName
24416
+ ])) return;
24417
+ return {
24418
+ type: doc.getIn([
24419
+ "secrets",
24420
+ secretName,
24421
+ "type"
24422
+ ]),
24423
+ existsForApp: secretExistsInDoc(doc, secretName, appId)
24424
+ };
24425
+ }
24426
+ async function writeSecretToAnvilYaml(projectRoot, secretName, options) {
24427
+ const { appId, encrypted, overwrite, type } = options;
24428
+ const filePath = secret_anvilYamlPath(projectRoot);
24429
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(filePath, "utf8"));
24430
+ checkSecretTypeInDoc(doc, secretName, type);
24431
+ if (secretExistsInDoc(doc, secretName, appId)) {
24432
+ if (!overwrite) throw new Error(`'${secretName}' already exists for app ${appId} in anvil.yaml`);
24433
+ if (!doc.hasIn([
24434
+ "secrets",
24435
+ secretName,
24436
+ "value",
24437
+ appId,
24438
+ "all"
24439
+ ])) throw new Error(`'${secretName}' for app ${appId} in anvil.yaml cannot be overridden`);
24440
+ }
24441
+ if (doc.hasIn([
24442
+ "secrets",
24443
+ secretName
24444
+ ])) {
24445
+ if (!(0, external_yaml_namespaceObject.isMap)(doc.getIn([
24446
+ "secrets",
24447
+ secretName,
24448
+ "value"
24449
+ ]))) doc.setIn([
24450
+ "secrets",
24451
+ secretName,
24452
+ "value"
24453
+ ], doc.createNode({}));
24454
+ } else doc.setIn([
24455
+ "secrets",
24456
+ secretName
24457
+ ], doc.createNode({
24458
+ type,
24459
+ value: {}
24460
+ }));
24461
+ const value = new external_yaml_namespaceObject.Scalar(encrypted);
24462
+ value.type = external_yaml_namespaceObject.Scalar.QUOTE_DOUBLE;
24463
+ if ((0, external_yaml_namespaceObject.isMap)(doc.getIn([
24464
+ "secrets",
24465
+ secretName,
24466
+ "value",
24467
+ appId
24468
+ ]))) doc.setIn([
24469
+ "secrets",
24470
+ secretName,
24471
+ "value",
24472
+ appId,
24473
+ "all"
24474
+ ], value);
24475
+ else doc.setIn([
24476
+ "secrets",
24477
+ secretName,
24478
+ "value",
24479
+ appId
24480
+ ], doc.createNode({
24481
+ all: value
24482
+ }, {
24483
+ flow: true
24484
+ }));
24485
+ await external_fs_.promises.writeFile(filePath, doc.toString(), "utf8");
24486
+ }
24487
+ function registerSecretCommand(program) {
24488
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24489
+ const secret = program.command("secret").description("Manage app secrets");
24490
+ addAppAccountOptions(secret.command("set")).description("Set an app secret. The value is read from stdin, or prompted for if stdin is a terminal.").argument("<name>", "Name of the secret").option("--generate", "Have the server autogenerate the secret value instead of reading it").option("-f, --force", "Overwrite the secret if it is already set for this app").action(async (name, options)=>{
23377
24491
  try {
23378
24492
  const projectRoot = process.cwd();
23379
- const anvilUrl = resolveAnvilUrl();
23380
- const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23381
- if (!mainAppId) {
23382
- logger_logger.error("No Anvil app found in current directory");
23383
- logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23384
- process.exit(1);
23385
- }
23386
- logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
23387
- const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
23388
- commit: options.commit,
23389
- includeDocs: !options.excludeDocs
24493
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24494
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24495
+ appId,
24496
+ type: "secret",
24497
+ force: options.force
23390
24498
  });
23391
- if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
23392
- if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
23393
- if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
23394
- logger_logger.success(`Fetched ${response.dependencies.length} dependencies:`);
23395
- for (const dep of response.dependencies)logger_logger.info(` - ${dep.package_name}`);
23396
- } catch (e) {
23397
- logger_logger.error(e.message);
23398
- process.exit(1);
23399
- }
23400
- });
23401
- deps.command("status").description("Show cached dependencies").action(async ()=>{
23402
- try {
23403
- const cached = await getCachedPackages(process.cwd());
23404
- if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
23405
- logger_logger.info("Cached dependencies:");
23406
- for (const pkg of cached)logger_logger.info(` - ${pkg}`);
23407
- } catch (e) {
23408
- logger_logger.error(e.message);
24499
+ const value = options.generate ? void 0 : await readSecretValue(name);
24500
+ const response = await encryptSecret(appId, name, {
24501
+ anvilUrl,
24502
+ username,
24503
+ secret: value
24504
+ });
24505
+ await writeSecretToAnvilYaml(projectRoot, name, {
24506
+ appId,
24507
+ encrypted: response.encrypted,
24508
+ overwrite,
24509
+ type: "secret"
24510
+ });
24511
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24512
+ data: {
24513
+ encrypted: response.encrypted
24514
+ }
24515
+ });
24516
+ else logger_logger.info(`Secret '${name}' written to anvil.yaml`);
24517
+ } catch (error) {
24518
+ const message = errors_getErrorMessage(error);
24519
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24520
+ error: message
24521
+ });
24522
+ else logger_logger.error(message);
23409
24523
  process.exit(1);
23410
24524
  }
23411
24525
  });
23412
- deps.command("clear").description("Clear cached dependencies").action(async ()=>{
24526
+ addAppAccountOptions(secret.command("generate-key")).description("Generate an app encryption key. The key is always generated by the server, and its value is never shown.").argument("<name>", "Name of the key").option("-f, --force", "Replace the key if it is already set for this app").action(async (name, options)=>{
23413
24527
  try {
23414
- await clearDepsCache(process.cwd());
23415
- logger_logger.success("Cleared .anvil/deps/");
23416
- } catch (e) {
23417
- logger_logger.error(e.message);
24528
+ const projectRoot = process.cwd();
24529
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24530
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24531
+ appId,
24532
+ type: "key",
24533
+ force: options.force
24534
+ });
24535
+ const response = await generateSecretKey(appId, name, {
24536
+ anvilUrl,
24537
+ username
24538
+ });
24539
+ await writeSecretToAnvilYaml(projectRoot, name, {
24540
+ appId,
24541
+ encrypted: response.encrypted,
24542
+ overwrite,
24543
+ type: "key"
24544
+ });
24545
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24546
+ data: {
24547
+ encrypted: response.encrypted
24548
+ }
24549
+ });
24550
+ else logger_logger.info(`Key '${name}' written to anvil.yaml`);
24551
+ } catch (error) {
24552
+ const message = errors_getErrorMessage(error);
24553
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24554
+ error: message
24555
+ });
24556
+ else logger_logger.error(message);
23418
24557
  process.exit(1);
23419
24558
  }
23420
24559
  });
23421
24560
  }
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}`;
24561
+ async function confirmOverwriteIfExists(projectRoot, name, options) {
24562
+ const { appId, type, force = false } = options;
24563
+ const existing = await findSecretInAnvilYaml(projectRoot, name, appId);
24564
+ if (existing?.type && existing.type !== type) throw new Error(`'${name}' is a ${existing.type} in anvil.yaml, not a ${type}`);
24565
+ if (!existing?.existsForApp) return false;
24566
+ if (force) return true;
24567
+ const message = `'${name}' already exists for app ${appId} in anvil.yaml`;
24568
+ if (!process.stdin.isTTY) throw new Error(`${message}. Use --force to overwrite it.`);
24569
+ if (!await logger_logger.confirm(`${message}. Overwrite it?`, false)) throw new Error(`Not overwriting '${name}'`);
24570
+ return true;
23430
24571
  }
23431
- async function fetchTableMappings(appId, anvilUrl = resolveAnvilUrl()) {
23432
- const token = await auth_getValidAuthToken(anvilUrl);
23433
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db`, {
24572
+ async function readSecretValue(name) {
24573
+ if (process.stdin.isTTY) {
24574
+ const { secret } = await logger_logger.prompt([
24575
+ {
24576
+ type: "password",
24577
+ name: "secret",
24578
+ message: `Value for secret '${name}':`
24579
+ }
24580
+ ]);
24581
+ if (!secret) throw new Error("No secret value entered");
24582
+ return secret;
24583
+ }
24584
+ {
24585
+ const chunks = [];
24586
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
24587
+ const secret = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
24588
+ if (!secret) throw new Error("No secret value received on stdin");
24589
+ return secret;
24590
+ }
24591
+ }
24592
+ const REORDER_WAIT_MS = 200;
24593
+ const LATE_OUTPUT_QUIET_MS = 500;
24594
+ const LATE_OUTPUT_MAX_MS = 3000;
24595
+ const CANCEL_WAIT_MS = 5000;
24596
+ class ServerTestsError extends Error {
24597
+ }
24598
+ class InterruptedError extends Error {
24599
+ }
24600
+ function serverErrorMessage(error) {
24601
+ if (error && "object" == typeof error) {
24602
+ const value = error;
24603
+ return String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value));
24604
+ }
24605
+ return String(error);
24606
+ }
24607
+ class OrderedLog {
24608
+ write;
24609
+ nextSeq = 1;
24610
+ held = new Map();
24611
+ timer;
24612
+ constructor(write){
24613
+ this.write = write;
24614
+ }
24615
+ add(seq, text) {
24616
+ if ("number" != typeof seq || seq < this.nextSeq) return void this.write(text);
24617
+ this.held.set(seq, text);
24618
+ while(this.held.has(this.nextSeq)){
24619
+ const next = this.held.get(this.nextSeq);
24620
+ this.held.delete(this.nextSeq);
24621
+ this.nextSeq++;
24622
+ this.write(next);
24623
+ }
24624
+ if (0 === this.held.size) {
24625
+ clearTimeout(this.timer);
24626
+ this.timer = void 0;
24627
+ } else this.timer ??= setTimeout(()=>{
24628
+ this.timer = void 0;
24629
+ this.flush();
24630
+ }, REORDER_WAIT_MS);
24631
+ }
24632
+ flush() {
24633
+ clearTimeout(this.timer);
24634
+ this.timer = void 0;
24635
+ for (const seq of [
24636
+ ...this.held.keys()
24637
+ ].sort((a, b)=>a - b)){
24638
+ this.write(this.held.get(seq));
24639
+ this.nextSeq = seq + 1;
24640
+ }
24641
+ this.held.clear();
24642
+ }
24643
+ }
24644
+ async function runServerTests(appId, options) {
24645
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24646
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
24647
+ const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/ws`);
24648
+ url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
24649
+ if (options.signal?.aborted) throw new InterruptedError("Interrupted.");
24650
+ const ws = new (external_ws_default())(url, {
23434
24651
  headers: {
23435
24652
  Authorization: `Bearer ${token}`
23436
- }
24653
+ },
24654
+ handshakeTimeout: 30000
23437
24655
  });
23438
- if (!resp.ok) {
23439
- const errorBody = await resp.text();
23440
- throw new Error(tables_formatFetchError("Failed to fetch table mappings", resp.status, errorBody));
24656
+ const controller = new AbortController();
24657
+ const replies = new Map();
24658
+ let nextId = 1;
24659
+ let runId;
24660
+ let sessionId;
24661
+ let launchMessage = "";
24662
+ let cancelId;
24663
+ let keepalive;
24664
+ let pongTimeout;
24665
+ let closeTimeout;
24666
+ const unmatchedLogs = new Map();
24667
+ const runLog = new OrderedLog((text)=>{
24668
+ if (text) options.onOutput?.(text);
24669
+ });
24670
+ let onRunLogEvent;
24671
+ const fail = (error)=>controller.abort(error);
24672
+ const connectionLost = (detail)=>runId ? new Error(`Lost connection to Anvil. The test run (${runId}) may still be running.`) : new Error(`Lost connection to Anvil (${detail}).`);
24673
+ const markResponsive = ()=>{
24674
+ clearTimeout(pongTimeout);
24675
+ pongTimeout = void 0;
24676
+ };
24677
+ function addRunLog(message) {
24678
+ runLog.add(message.seq, "print" === message.type && "string" == typeof message.log_text ? message.log_text : void 0);
24679
+ }
24680
+ function setSession(id) {
24681
+ if (sessionId || "string" != typeof id || !id) return;
24682
+ sessionId = id;
24683
+ for (const message of unmatchedLogs.get(id) ?? [])addRunLog(message);
24684
+ unmatchedLogs.clear();
24685
+ }
24686
+ function handleMessage(raw) {
24687
+ markResponsive();
24688
+ let message;
24689
+ try {
24690
+ message = JSON.parse(raw.toString());
24691
+ if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
24692
+ } catch {
24693
+ fail(new Error("Invalid message from Anvil."));
24694
+ return;
24695
+ }
24696
+ if ("number" == typeof message.id) {
24697
+ if (message.error && message.id !== cancelId) return void fail(new ServerTestsError(serverErrorMessage(message.error)));
24698
+ const resolve = replies.get(message.id);
24699
+ replies.delete(message.id);
24700
+ resolve?.(message);
24701
+ return;
24702
+ }
24703
+ switch(message.event){
24704
+ case "LOG_EVENT":
24705
+ if ("string" != typeof message.session_id) return;
24706
+ if (sessionId) {
24707
+ if (message.session_id !== sessionId) return;
24708
+ addRunLog(message);
24709
+ onRunLogEvent?.();
24710
+ } else {
24711
+ const logs = unmatchedLogs.get(message.session_id) ?? [];
24712
+ logs.push(message);
24713
+ unmatchedLogs.set(message.session_id, logs);
24714
+ }
24715
+ return;
24716
+ case "TEST_EVENT":
24717
+ {
24718
+ if (!runId || message.run_id !== runId) return;
24719
+ setSession(message.session_id);
24720
+ const testEvent = message.test_event;
24721
+ if (testEvent?.type === "test_run_update" && "string" == typeof testEvent.launch_message) {
24722
+ const text = testEvent.launch_message;
24723
+ const added = text.startsWith(launchMessage) ? text.slice(launchMessage.length) : text;
24724
+ launchMessage = text;
24725
+ if (added) options.onLaunchMessage?.(added);
24726
+ }
24727
+ return;
24728
+ }
24729
+ }
24730
+ }
24731
+ function send(message) {
24732
+ const id = nextId++;
24733
+ ws.send(JSON.stringify({
24734
+ id,
24735
+ ...message
24736
+ }), (error)=>{
24737
+ if (error) fail(connectionLost(`send failed: ${error.message}`));
24738
+ });
24739
+ return id;
24740
+ }
24741
+ function reply(id) {
24742
+ const signal = controller.signal;
24743
+ return new Promise((resolve, reject)=>{
24744
+ if (signal.aborted) return void reject(signal.reason);
24745
+ const onAbort = ()=>{
24746
+ replies.delete(id);
24747
+ reject(signal.reason);
24748
+ };
24749
+ signal.addEventListener("abort", onAbort, {
24750
+ once: true
24751
+ });
24752
+ replies.set(id, (message)=>{
24753
+ signal.removeEventListener("abort", onAbort);
24754
+ resolve(message);
24755
+ });
24756
+ });
23441
24757
  }
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
24758
+ const request = (message)=>reply(send(message));
24759
+ async function cancel(waited) {
24760
+ let cancelTimeout;
24761
+ let confirmed = false;
24762
+ try {
24763
+ cancelId = send({
24764
+ cmd: "CANCEL_SERVER_TEST_RUN",
24765
+ run_id: runId
24766
+ });
24767
+ const answer = await Promise.race([
24768
+ reply(cancelId),
24769
+ waited,
24770
+ new Promise((_, reject)=>{
24771
+ cancelTimeout = setTimeout(()=>reject(new Error("Timed out")), CANCEL_WAIT_MS);
24772
+ })
24773
+ ]);
24774
+ confirmed = !answer.error;
24775
+ } catch {} finally{
24776
+ clearTimeout(cancelTimeout);
24777
+ }
24778
+ runLog.flush();
24779
+ throw new InterruptedError(confirmed ? `Cancelled the test run (${runId}).` : `Couldn't confirm that the test run (${runId}) was cancelled. It may still be running.`);
24780
+ }
24781
+ async function readLateOutput() {
24782
+ await new Promise((resolve)=>{
24783
+ let quietTimeout;
24784
+ const done = ()=>{
24785
+ clearTimeout(quietTimeout);
24786
+ clearTimeout(maxTimeout);
24787
+ onRunLogEvent = void 0;
24788
+ controller.signal.removeEventListener("abort", done);
24789
+ options.signal?.removeEventListener("abort", done);
24790
+ resolve();
24791
+ };
24792
+ const resetQuiet = ()=>{
24793
+ clearTimeout(quietTimeout);
24794
+ quietTimeout = setTimeout(done, LATE_OUTPUT_QUIET_MS);
24795
+ };
24796
+ const maxTimeout = setTimeout(done, LATE_OUTPUT_MAX_MS);
24797
+ onRunLogEvent = resetQuiet;
24798
+ resetQuiet();
24799
+ controller.signal.addEventListener("abort", done, {
24800
+ once: true
24801
+ });
24802
+ options.signal?.addEventListener("abort", done, {
24803
+ once: true
24804
+ });
24805
+ });
24806
+ runLog.flush();
24807
+ }
24808
+ const onAbort = ()=>fail(new InterruptedError("Interrupted."));
24809
+ ws.on("pong", markResponsive);
24810
+ ws.on("unexpected-response", (request, response)=>{
24811
+ response.resume();
24812
+ fail(new Error(`Couldn't connect to Anvil (${response.statusCode} ${response.statusMessage}).`));
24813
+ request.destroy();
23448
24814
  });
23449
- return {
23450
- tables: allTables
23451
- };
23452
- }
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
24815
+ ws.on("error", (error)=>fail(connectionLost(error.message)));
24816
+ ws.on("close", (code, reason)=>{
24817
+ clearTimeout(closeTimeout);
24818
+ fail(connectionLost(`code ${code}${reason.length ? `: ${reason}` : ""}`));
23457
24819
  });
23458
- await promises_default().writeFile(filePath, JSON.stringify(mappings, null, 2));
24820
+ ws.on("message", handleMessage);
24821
+ try {
24822
+ options.signal?.addEventListener("abort", onAbort, {
24823
+ once: true
24824
+ });
24825
+ await new Promise((resolve, reject)=>{
24826
+ ws.once("open", ()=>resolve());
24827
+ controller.signal.addEventListener("abort", ()=>reject(controller.signal.reason), {
24828
+ once: true
24829
+ });
24830
+ });
24831
+ keepalive = setInterval(()=>{
24832
+ pongTimeout ??= setTimeout(()=>fail(connectionLost("heartbeat timed out")), 10000);
24833
+ ws.ping();
24834
+ }, 20000);
24835
+ await request({
24836
+ cmd: "WATCH_APP",
24837
+ app: appId,
24838
+ only_branch: options.branch
24839
+ });
24840
+ const launched = await request({
24841
+ cmd: "RUN_SERVER_TESTS",
24842
+ commit_id: options.commitId,
24843
+ ...options.selection ? {
24844
+ selection: options.selection
24845
+ } : {},
24846
+ ...options.collectOnly ? {
24847
+ collect_only: true
24848
+ } : {}
24849
+ });
24850
+ if ("string" != typeof launched.run_id || !launched.run_id) throw new Error("Invalid response from Anvil: missing test run ID.");
24851
+ runId = launched.run_id;
24852
+ options.onRunCreated?.(runId);
24853
+ options.signal?.removeEventListener("abort", onAbort);
24854
+ const interrupted = new Promise((resolve)=>{
24855
+ if (options.signal?.aborted) resolve();
24856
+ options.signal?.addEventListener("abort", ()=>resolve(), {
24857
+ once: true
24858
+ });
24859
+ });
24860
+ const watchId = send({
24861
+ cmd: "WATCH_APP",
24862
+ app: appId,
24863
+ only_branch: options.branch,
24864
+ test_run_ids: [
24865
+ runId
24866
+ ]
24867
+ });
24868
+ const waitId = send({
24869
+ cmd: "WAIT_FOR_SERVER_TEST_RUN",
24870
+ run_id: runId
24871
+ });
24872
+ reply(watchId).catch(()=>{});
24873
+ const waited = reply(waitId);
24874
+ const done = await Promise.race([
24875
+ waited,
24876
+ interrupted.then(()=>void 0)
24877
+ ]);
24878
+ if (!done) return await cancel(waited);
24879
+ setSession(done.session_id);
24880
+ if (!sessionId) unmatchedLogs.clear();
24881
+ await readLateOutput();
24882
+ return {
24883
+ runId,
24884
+ ...sessionId ? {
24885
+ sessionId
24886
+ } : {},
24887
+ state: done.state ?? {}
24888
+ };
24889
+ } catch (error) {
24890
+ if (error instanceof InterruptedError) throw error;
24891
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
24892
+ throw failure instanceof Error ? failure : new Error(String(failure));
24893
+ } finally{
24894
+ clearInterval(keepalive);
24895
+ clearTimeout(pongTimeout);
24896
+ runLog.flush();
24897
+ options.signal?.removeEventListener("abort", onAbort);
24898
+ controller.abort();
24899
+ if (ws.readyState === external_ws_default().OPEN) ws.close();
24900
+ else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
24901
+ if (ws.readyState !== external_ws_default().CLOSED) {
24902
+ closeTimeout = setTimeout(()=>ws.terminate(), 1000);
24903
+ closeTimeout.unref();
24904
+ }
24905
+ }
24906
+ }
24907
+ const PYTEST_EXIT_CODES = {
24908
+ OK: 0,
24909
+ TESTS_FAILED: 1,
24910
+ INTERRUPTED: 2,
24911
+ INTERNAL_ERROR: 3,
24912
+ USAGE_ERROR: 4,
24913
+ NO_TESTS_COLLECTED: 5
24914
+ };
24915
+ class UsageError extends Error {
24916
+ }
24917
+ function toServerTestTarget(target, options) {
24918
+ const separator = target.indexOf("::");
24919
+ const targetPath = -1 === separator ? target : target.slice(0, separator);
24920
+ const suffix = -1 === separator ? "" : target.slice(separator);
24921
+ const absolutePath = external_path_default().resolve(options.cwd, targetPath);
24922
+ const serverTestsPath = external_path_default().relative(external_path_default().join(options.appRoot, "tests", "server_tests"), absolutePath);
24923
+ if (external_path_default().isAbsolute(serverTestsPath) || ".." === serverTestsPath || serverTestsPath.startsWith(`..${external_path_default().sep}`)) throw new UsageError(`${target} isn't in tests/server_tests.`);
24924
+ return external_path_default().relative(external_path_default().join(options.appRoot, "tests"), absolutePath).split(external_path_default().sep).join("/") + suffix;
24925
+ }
24926
+ async function checkCommitOnAnvil(git, options) {
24927
+ const notOnAnvil = ()=>new Error(`Commit ${options.commitId.slice(0, 7)} isn't on Anvil. Push ${options.branch} first.`);
24928
+ try {
24929
+ await git.fetch(options.fetchUrl, `refs/heads/${options.branch}`);
24930
+ } catch (error) {
24931
+ const message = errors_getErrorMessage(error);
24932
+ if (/couldn't find remote ref/i.test(message)) throw notOnAnvil();
24933
+ throw new Error(message.replace(/\/\/git:[^@\s]*@/g, "//git:***@"));
24934
+ }
24935
+ if (!await git.isAncestor(options.commitId, "FETCH_HEAD")) throw notOnAnvil();
23459
24936
  }
23460
- async function readTableMappings(projectRoot) {
23461
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
24937
+ async function getBranch(git) {
23462
24938
  try {
23463
- const content = await promises_default().readFile(filePath, "utf-8");
23464
- return JSON.parse(content);
24939
+ return await git.getCurrentBranch();
23465
24940
  } catch {
23466
- return null;
24941
+ throw new Error("HEAD isn't on a branch. Check out the branch you've pushed to Anvil.");
23467
24942
  }
23468
24943
  }
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
23473
- });
23474
- }
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 ()=>{
23478
- 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);
23495
- process.exit(1);
23496
- }
23497
- });
23498
- tables.command("status").description("Show cached table mappings").action(async ()=>{
24944
+ async function runTestCommand(targets, options) {
24945
+ const jsonMode = getGlobalOutputConfig().jsonMode;
24946
+ const controller = new AbortController();
24947
+ let runId;
24948
+ let interrupted = false;
24949
+ let outputEndsLine = true;
24950
+ const writeOutput = (text)=>{
24951
+ process.stdout.write(text);
24952
+ outputEndsLine = text.endsWith("\n");
24953
+ };
24954
+ const writeJsonResult = (...args)=>{
24955
+ if (!outputEndsLine) writeOutput("\n");
24956
+ logJsonResult(...args);
24957
+ };
24958
+ const onInterrupt = ()=>{
24959
+ if (interrupted) process.exit(130);
24960
+ interrupted = true;
24961
+ if (runId) process.stderr.write("\nCancelling test run...\n");
24962
+ controller.abort();
24963
+ };
24964
+ try {
24965
+ const projectRoot = process.cwd();
24966
+ const git = new GitService(projectRoot);
24967
+ let appRoot;
23499
24968
  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);
23506
- process.exit(1);
24969
+ appRoot = external_path_default().resolve(projectRoot, (await git.getGit().revparse([
24970
+ "--show-cdup"
24971
+ ])).trim());
24972
+ } catch {
24973
+ throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
23507
24974
  }
23508
- });
23509
- tables.command("clear").description("Clear cached table mappings").action(async ()=>{
23510
- 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);
24975
+ const testTargets = targets.map((target)=>toServerTestTarget(target, {
24976
+ appRoot,
24977
+ cwd: projectRoot
24978
+ }));
24979
+ const selection = {
24980
+ ...testTargets.length > 0 ? {
24981
+ test_targets: testTargets
24982
+ } : {},
24983
+ ...void 0 !== options.k ? {
24984
+ keyword_expression: options.k
24985
+ } : {}
24986
+ };
24987
+ const { appId, anvilUrl, username } = await resolveAppAccount(projectRoot, appAccountOptions(options));
24988
+ const branch = await getBranch(git);
24989
+ const commitId = await git.getCommitId();
24990
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
24991
+ await checkCommitOnAnvil(git, {
24992
+ fetchUrl: getGitFetchUrl(appId, authToken, anvilUrl),
24993
+ branch,
24994
+ commitId
24995
+ });
24996
+ if (await git.hasUncommittedChanges()) process.stderr.write(`You have uncommitted changes, which won't be included in the test run.\n`);
24997
+ process.on("SIGINT", onInterrupt);
24998
+ const { runId: completedRunId, state } = await runServerTests(appId, {
24999
+ commitId,
25000
+ branch,
25001
+ ...Object.keys(selection).length > 0 ? {
25002
+ selection
25003
+ } : {},
25004
+ collectOnly: options.collectOnly,
25005
+ anvilUrl,
25006
+ username,
25007
+ signal: controller.signal,
25008
+ onOutput: writeOutput,
25009
+ onLaunchMessage: (text)=>{
25010
+ process.stderr.write(text);
25011
+ },
25012
+ onRunCreated: (id)=>{
25013
+ runId = id;
25014
+ }
25015
+ });
25016
+ const exitCode = state.exit_code ? PYTEST_EXIT_CODES[state.exit_code] ?? 1 : 1;
25017
+ if (state.error?.message) process.stderr.write(`${state.error.message}\n`);
25018
+ else if (!state.exit_code) process.stderr.write("The test run ended without a result.\n");
25019
+ if (jsonMode) {
25020
+ let snapshot;
25021
+ try {
25022
+ snapshot = await getTestRunSnapshot(appId, completedRunId, {
25023
+ anvilUrl,
25024
+ username
25025
+ });
25026
+ } catch (error) {
25027
+ writeJsonResult(false, {
25028
+ error: errors_getErrorMessage(error)
25029
+ });
25030
+ process.exitCode = 1;
25031
+ return;
25032
+ }
25033
+ const success = "OK" === state.exit_code || !!options.collectOnly && "NO_TESTS_COLLECTED" === state.exit_code;
25034
+ writeJsonResult(success, {
25035
+ data: snapshot
25036
+ });
23516
25037
  }
25038
+ process.exitCode = exitCode;
25039
+ } catch (error) {
25040
+ const message = errors_getErrorMessage(error);
25041
+ if (jsonMode) writeJsonResult(false, {
25042
+ error: message
25043
+ });
25044
+ else process.stderr.write(`${message}\n`);
25045
+ process.exitCode = controller.signal.aborted ? 130 : error instanceof UsageError ? PYTEST_EXIT_CODES.USAGE_ERROR : 1;
25046
+ } finally{
25047
+ process.removeListener("SIGINT", onInterrupt);
25048
+ }
25049
+ }
25050
+ function registerTestCommand(program) {
25051
+ addAppAccountOptions(program.command("test")).description("Run the app's tests on Anvil, against the commit you have checked out").argument("[targets...]", "Test paths or pytest node IDs, relative to your current directory").option("-k <expression>", "Only run tests that match the pytest keyword expression").option("--collect-only", "List the tests without running them").addHelpText("after", `
25052
+ The commit you have checked out must already be pushed to Anvil.
25053
+ The exit code is the same as pytest: 0 if every test passed, 1 if any failed.
25054
+
25055
+ Examples:
25056
+ anvil test
25057
+ anvil test tests/server_tests/test_orders.py::test_refund
25058
+ anvil test -k "login and not slow"
25059
+ anvil --json test
25060
+ `).action(async (targets, options)=>{
25061
+ await runTestCommand(targets, options);
23517
25062
  });
23518
25063
  }
23519
25064
  const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
@@ -23927,6 +25472,7 @@ print(json.dumps({"issues": issues}))
23927
25472
  });
23928
25473
  const watchCommand = registerWatchCommand(program);
23929
25474
  registerCheckoutCommand(program);
25475
+ registerCreateCommand(program);
23930
25476
  registerGitAuthCommand(program);
23931
25477
  registerGitCredentialCommand(program);
23932
25478
  registerLoginCommand(program);
@@ -23936,7 +25482,11 @@ print(json.dumps({"issues": issues}))
23936
25482
  registerVersionCommand(program, VERSION);
23937
25483
  registerConfigureCommand(program, VERSION);
23938
25484
  registerDepsCommand(program);
23939
- registerTablesCommand(program);
25485
+ registerDbCommand(program);
25486
+ registerEnvCommand(program);
25487
+ registerReplCommand(program);
25488
+ registerSecretCommand(program);
25489
+ registerTestCommand(program);
23940
25490
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
23941
25491
  await handleUpdateCommand();
23942
25492
  });