@anvil-works/anvil-cli 0.8.0-canary.2 → 0.8.0-canary.21

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 (67) hide show
  1. package/README.md +3 -1
  2. package/dist/SavePathRouter.d.ts.map +1 -1
  3. package/dist/WatchSession.d.ts +2 -0
  4. package/dist/WatchSession.d.ts.map +1 -1
  5. package/dist/api.d.ts +7 -2
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/cli.js +2255 -1086
  8. package/dist/commands/checkout-picker.d.ts +1 -2
  9. package/dist/commands/checkout-picker.d.ts.map +1 -1
  10. package/dist/commands/configure.d.ts.map +1 -1
  11. package/dist/commands/db.d.ts +3 -0
  12. package/dist/commands/db.d.ts.map +1 -0
  13. package/dist/commands/deps.d.ts.map +1 -1
  14. package/dist/commands/env.d.ts +8 -0
  15. package/dist/commands/env.d.ts.map +1 -0
  16. package/dist/commands/gitAuth.d.ts +5 -1
  17. package/dist/commands/gitAuth.d.ts.map +1 -1
  18. package/dist/commands/index.d.ts +4 -1
  19. package/dist/commands/index.d.ts.map +1 -1
  20. package/dist/commands/login.d.ts.map +1 -1
  21. package/dist/commands/modelToken.d.ts +33 -0
  22. package/dist/commands/modelToken.d.ts.map +1 -0
  23. package/dist/commands/repl.d.ts +3 -0
  24. package/dist/commands/repl.d.ts.map +1 -0
  25. package/dist/commands/secret.d.ts +3 -0
  26. package/dist/commands/secret.d.ts.map +1 -0
  27. package/dist/commands/watch.d.ts.map +1 -1
  28. package/dist/componentTypeSpec.d.ts +1 -1
  29. package/dist/componentTypeSpec.d.ts.map +1 -1
  30. package/dist/errors.d.ts +11 -1
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/formTemplateValidation.d.ts.map +1 -1
  33. package/dist/index.js +1555 -1030
  34. package/dist/program.d.ts +5 -14
  35. package/dist/program.d.ts.map +1 -1
  36. package/dist/services/anvil-api.d.ts.map +1 -1
  37. package/dist/services/auth.d.ts +42 -3
  38. package/dist/services/auth.d.ts.map +1 -1
  39. package/dist/services/db.d.ts +7 -0
  40. package/dist/services/db.d.ts.map +1 -0
  41. package/dist/services/deps.d.ts +9 -4
  42. package/dist/services/deps.d.ts.map +1 -1
  43. package/dist/services/environment.d.ts +16 -0
  44. package/dist/services/environment.d.ts.map +1 -0
  45. package/dist/services/git-auth.d.ts +38 -1
  46. package/dist/services/git-auth.d.ts.map +1 -1
  47. package/dist/services/git.d.ts +1 -0
  48. package/dist/services/git.d.ts.map +1 -1
  49. package/dist/services/http.d.ts +2 -0
  50. package/dist/services/http.d.ts.map +1 -0
  51. package/dist/services/repl.d.ts +18 -0
  52. package/dist/services/repl.d.ts.map +1 -0
  53. package/dist/services/secret.d.ts +39 -0
  54. package/dist/services/secret.d.ts.map +1 -0
  55. package/dist/validateFormTemplateHtml.d.ts +6 -2
  56. package/dist/validateFormTemplateHtml.d.ts.map +1 -1
  57. package/dist/validatePython.d.ts +29 -3
  58. package/dist/validatePython.d.ts.map +1 -1
  59. package/dist/validators.d.ts +5 -1
  60. package/dist/validators.d.ts.map +1 -1
  61. package/dist/watch/SaveProcessor.d.ts +2 -0
  62. package/dist/watch/SaveProcessor.d.ts.map +1 -1
  63. package/package.json +3 -2
  64. package/dist/commands/tables.d.ts +0 -7
  65. package/dist/commands/tables.d.ts.map +0 -1
  66. package/dist/services/tables.d.ts +0 -27
  67. package/dist/services/tables.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -13384,9 +13384,10 @@ var __webpack_exports__ = {};
13384
13384
  type: "auth_invalid",
13385
13385
  message
13386
13386
  }),
13387
- refreshFailed: (message)=>({
13387
+ refreshFailed: (message, status)=>({
13388
13388
  type: "token_refresh_failed",
13389
- message
13389
+ message,
13390
+ status
13390
13391
  })
13391
13392
  };
13392
13393
  const createAppError = {
@@ -13600,8 +13601,23 @@ var __webpack_exports__ = {};
13600
13601
  return "An unknown error occurred";
13601
13602
  }
13602
13603
  }
13604
+ function getErrorCauseMessage(error) {
13605
+ if ("object" != typeof error || null === error || !("cause" in error)) return null;
13606
+ const cause = error.cause;
13607
+ if (null == cause) return null;
13608
+ const message = errors_getErrorMessage(cause);
13609
+ const code = "object" == typeof cause && null !== cause && "code" in cause && "string" == typeof cause.code ? cause.code : null;
13610
+ if (code && !message.includes(code)) return `${message} (${code})`;
13611
+ return message;
13612
+ }
13613
+ function logErrorCause(error, ui) {
13614
+ const causeMessage = getErrorCauseMessage(error);
13615
+ if (causeMessage) ui.verbose(`Cause: ${causeMessage}`);
13616
+ }
13603
13617
  const external_module_namespaceObject = require("module");
13604
13618
  const requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
13619
+ const GIT_CREDENTIAL_HELPER_FILENAME = "anvil-credential-helper.cjs";
13620
+ const GIT_CREDENTIAL_HELPER_LOCATOR = `$(git rev-parse --git-common-dir)/${GIT_CREDENTIAL_HELPER_FILENAME}`;
13605
13621
  function getUrlConfigKey(appId) {
13606
13622
  return `anvil.auth.${appId}.url`;
13607
13623
  }
@@ -13618,12 +13634,14 @@ var __webpack_exports__ = {};
13618
13634
  const platform = options?.platform ?? process.platform;
13619
13635
  const execPath = options?.execPath ?? process.execPath;
13620
13636
  const helperPath = options.helperPath;
13637
+ const dynamicHelperPath = `"${GIT_CREDENTIAL_HELPER_LOCATOR}"`;
13621
13638
  if ("win32" === platform) {
13622
13639
  const resolvedExecPath = external_path_default().win32.resolve(execPath);
13623
- const resolvedHelperPath = external_path_default().win32.resolve(helperPath);
13624
- return `!${shellQuoteDouble(resolvedExecPath)} ${shellQuoteDouble(resolvedHelperPath)}`;
13640
+ const helperArgument = helperPath ? shellQuoteDouble(external_path_default().win32.resolve(helperPath)) : dynamicHelperPath;
13641
+ return `!${shellQuoteDouble(resolvedExecPath)} ${helperArgument}`;
13625
13642
  }
13626
- return `!${shellQuote(external_path_default().resolve(execPath))} ${shellQuote(external_path_default().resolve(helperPath))}`;
13643
+ const helperArgument = helperPath ? shellQuote(external_path_default().resolve(helperPath)) : dynamicHelperPath;
13644
+ return `!${shellQuote(external_path_default().resolve(execPath))} ${helperArgument}`;
13627
13645
  }
13628
13646
  async function getLocalConfigValue(repoPath, key) {
13629
13647
  try {
@@ -13672,21 +13690,14 @@ var __webpack_exports__ = {};
13672
13690
  return `#!/usr/bin/env node
13673
13691
  "use strict";
13674
13692
 
13675
- const fs = require("fs");
13676
- const path = require("path");
13693
+ const { execFileSync } = require("child_process");
13677
13694
  const { executeGitCredentialOperation } = require(${JSON.stringify(modulePath)});
13678
13695
 
13679
13696
  function resolveRepoPath() {
13680
- // Git's credential-helper stdin only includes the remote URL path
13681
- // (/git/APP.git), not the local checkout path. Repo-local auth lives under
13682
- // this checkout's git dir, so derive the repo path from the generated helper
13683
- // location instead of the credential request.
13684
- const gitDir = __dirname;
13685
- const linkedWorktreeGitdir = path.join(gitDir, "gitdir");
13686
- if (fs.existsSync(linkedWorktreeGitdir)) {
13687
- return path.dirname(path.resolve(gitDir, fs.readFileSync(linkedWorktreeGitdir, "utf8").trim()));
13688
- }
13689
- return path.dirname(gitDir);
13697
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
13698
+ cwd: process.cwd(),
13699
+ encoding: "utf8",
13700
+ }).trim();
13690
13701
  }
13691
13702
 
13692
13703
  Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
@@ -13721,16 +13732,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13721
13732
  if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
13722
13733
  return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
13723
13734
  }
13724
- async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
13725
- const rawGitDir = (await git.raw([
13735
+ async function getRepositoryGitPath(repoPath, revParseArg, git = esm_default(repoPath)) {
13736
+ const rawGitPath = (await git.raw([
13726
13737
  "rev-parse",
13727
- "--git-dir"
13738
+ revParseArg
13728
13739
  ])).trim();
13729
- return external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(repoPath, rawGitDir);
13740
+ return external_path_default().isAbsolute(rawGitPath) ? rawGitPath : external_path_default().resolve(repoPath, rawGitPath);
13741
+ }
13742
+ async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
13743
+ return getRepositoryGitPath(repoPath, "--git-dir", git);
13744
+ }
13745
+ async function getRepositoryCommonGitDir(repoPath, git = esm_default(repoPath)) {
13746
+ return getRepositoryGitPath(repoPath, "--git-common-dir", git);
13730
13747
  }
13731
13748
  async function writeCredentialHelperScript(options) {
13732
- const gitDir = await getRepositoryGitDir(options.repoPath, options.git);
13733
- const helperPath = external_path_default().join(gitDir, "anvil-credential-helper.cjs");
13749
+ const gitCommonDir = await getRepositoryCommonGitDir(options.repoPath, options.git);
13750
+ const helperPath = external_path_default().join(gitCommonDir, GIT_CREDENTIAL_HELPER_FILENAME);
13734
13751
  await external_fs_.promises.mkdir(external_path_default().dirname(helperPath), {
13735
13752
  recursive: true
13736
13753
  });
@@ -13745,7 +13762,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13745
13762
  const url = new URL(normalized);
13746
13763
  const scope = `${url.protocol}//${url.host}`;
13747
13764
  const git = esm_default(repoPath);
13748
- const helperPath = await writeCredentialHelperScript({
13765
+ await writeCredentialHelperScript({
13749
13766
  repoPath,
13750
13767
  git,
13751
13768
  modulePath: options?.modulePath
@@ -13772,7 +13789,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13772
13789
  `credential.${scope}.helper`,
13773
13790
  getGitCredentialHelperCommand({
13774
13791
  execPath: options?.execPath,
13775
- helperPath,
13776
13792
  platform: options?.platform
13777
13793
  })
13778
13794
  ]);
@@ -13818,6 +13834,76 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13818
13834
  function getAuthFilePath(gitDir) {
13819
13835
  return external_path_default().join(gitDir, "anvil-auth.json");
13820
13836
  }
13837
+ const AUTH_FILE_LOCK_DEFAULTS = {
13838
+ staleMs: 30000,
13839
+ timeoutMs: 60000,
13840
+ retryDelayMs: 100,
13841
+ heartbeatMs: 5000
13842
+ };
13843
+ function git_auth_delay(ms) {
13844
+ return new Promise((resolve)=>setTimeout(resolve, ms));
13845
+ }
13846
+ async function acquireAuthFileLock(lockPath, options) {
13847
+ const deadline = Date.now() + options.timeoutMs;
13848
+ while(true){
13849
+ try {
13850
+ const handle = await external_fs_.promises.open(lockPath, "wx", 384);
13851
+ try {
13852
+ await handle.writeFile(JSON.stringify({
13853
+ pid: process.pid,
13854
+ acquiredAt: new Date().toISOString()
13855
+ }));
13856
+ } finally{
13857
+ await handle.close();
13858
+ }
13859
+ return;
13860
+ } catch (e) {
13861
+ if ("EEXIST" !== e.code) throw e;
13862
+ }
13863
+ try {
13864
+ const lockStat = await external_fs_.promises.stat(lockPath);
13865
+ if (Date.now() - lockStat.mtimeMs > options.staleMs) {
13866
+ await external_fs_.promises.rm(lockPath, {
13867
+ force: true
13868
+ });
13869
+ continue;
13870
+ }
13871
+ } catch {
13872
+ continue;
13873
+ }
13874
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for the Anvil auth file lock at ${lockPath}`);
13875
+ await git_auth_delay(options.retryDelayMs);
13876
+ }
13877
+ }
13878
+ async function withRepoAuthFileLock(repoPath, fn, options) {
13879
+ const gitDir = await getRepositoryGitDir(repoPath);
13880
+ const lockPath = `${getAuthFilePath(gitDir)}.lock`;
13881
+ const lockOptions = {
13882
+ ...AUTH_FILE_LOCK_DEFAULTS,
13883
+ ...options
13884
+ };
13885
+ await external_fs_.promises.mkdir(external_path_default().dirname(lockPath), {
13886
+ recursive: true
13887
+ });
13888
+ await acquireAuthFileLock(lockPath, lockOptions);
13889
+ const heartbeat = startAuthFileLockHeartbeat(lockPath, lockOptions.heartbeatMs);
13890
+ try {
13891
+ return await fn();
13892
+ } finally{
13893
+ clearInterval(heartbeat);
13894
+ await external_fs_.promises.rm(lockPath, {
13895
+ force: true
13896
+ });
13897
+ }
13898
+ }
13899
+ function startAuthFileLockHeartbeat(lockPath, heartbeatMs) {
13900
+ const heartbeat = setInterval(()=>{
13901
+ const now = new Date();
13902
+ external_fs_.promises.utimes(lockPath, now, now).catch(()=>{});
13903
+ }, heartbeatMs);
13904
+ heartbeat.unref();
13905
+ return heartbeat;
13906
+ }
13821
13907
  function decodeKey(b64key) {
13822
13908
  const key = Buffer.from(b64key, "base64");
13823
13909
  if (32 !== key.length) throw new Error("Encryption key must be a base64-encoded 32-byte key");
@@ -13862,10 +13948,19 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13862
13948
  recursive: true
13863
13949
  });
13864
13950
  const authJson = JSON.stringify(auth, null, 2);
13865
- await external_fs_.promises.writeFile(authFilePath, encrypt(authJson, encryptionKey), {
13866
- mode: 384
13867
- });
13868
- await external_fs_.promises.chmod(authFilePath, 448);
13951
+ const tempFilePath = `${authFilePath}.tmp-${external_node_crypto_default().randomBytes(6).toString("hex")}`;
13952
+ try {
13953
+ await external_fs_.promises.writeFile(tempFilePath, encrypt(authJson, encryptionKey), {
13954
+ mode: 384
13955
+ });
13956
+ await external_fs_.promises.chmod(tempFilePath, 448);
13957
+ await external_fs_.promises.rename(tempFilePath, authFilePath);
13958
+ } catch (e) {
13959
+ await external_fs_.promises.rm(tempFilePath, {
13960
+ force: true
13961
+ });
13962
+ throw e;
13963
+ }
13869
13964
  await git.raw([
13870
13965
  "config",
13871
13966
  "--local",
@@ -13874,8 +13969,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13874
13969
  ]);
13875
13970
  return authFilePath;
13876
13971
  }
13877
- const readAuthFromRepo = async (repoPath)=>{
13972
+ const git_auth_readAuthFromRepo = async (repoPath)=>{
13878
13973
  const git = esm_default(repoPath);
13974
+ if (!await git.checkIsRepo()) return;
13879
13975
  const gitDir = await getRepositoryGitDir(repoPath, git);
13880
13976
  const authFilePath = getAuthFilePath(gitDir);
13881
13977
  if (!external_fs_default().existsSync(authFilePath)) return;
@@ -13887,14 +13983,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13887
13983
  return resolveAnvilUrl();
13888
13984
  }
13889
13985
  const ANVIL_SYNC_CLIENT_ID = "anvil-sync";
13986
+ const REFRESH_REQUEST_TIMEOUT_MS = 12000;
13890
13987
  let inMemoryAuth = null;
13891
13988
  function authMatches(auth, anvilUrl, username) {
13892
13989
  return auth.anvilUrl === normalizeAnvilUrl(anvilUrl) && (!username || auth.username === username);
13893
13990
  }
13894
13991
  let repoContext = null;
13895
- function setRepoContext(repoPath) {
13992
+ function auth_setRepoContext(repoPath) {
13896
13993
  repoContext = repoPath;
13897
13994
  }
13995
+ async function resolveAuthAnvilUrl() {
13996
+ if (inMemoryAuth) return inMemoryAuth.anvilUrl;
13997
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13998
+ return repoAuth?.anvilUrl ?? resolveAnvilUrl();
13999
+ }
13898
14000
  const inFlightRefreshes = new Map();
13899
14001
  async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13900
14002
  try {
@@ -13915,7 +14017,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13915
14017
  throw errors_createAuthError.invalid(`Network error: ${e.message}`);
13916
14018
  }
13917
14019
  }
13918
- async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID) {
14020
+ async function refreshAccessToken(refreshToken, anvilUrl = getDefaultAnvilUrl(), clientId = ANVIL_SYNC_CLIENT_ID, options) {
13919
14021
  try {
13920
14022
  const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
13921
14023
  method: "POST",
@@ -13926,11 +14028,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13926
14028
  grant_type: "refresh_token",
13927
14029
  refresh_token: refreshToken,
13928
14030
  client_id: clientId
13929
- })
14031
+ }),
14032
+ signal: AbortSignal.timeout(options?.timeoutMs ?? REFRESH_REQUEST_TIMEOUT_MS)
13930
14033
  });
13931
14034
  if (!tokenResponse.ok) {
13932
14035
  const errorText = await tokenResponse.text();
13933
- throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`);
14036
+ throw errors_createAuthError.refreshFailed(`Failed to refresh token: ${tokenResponse.status} ${errorText}`, tokenResponse.status);
13934
14037
  }
13935
14038
  const tokenData = await tokenResponse.json();
13936
14039
  return tokenData;
@@ -13942,22 +14045,21 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13942
14045
  async function hasTokensForUrl(anvilUrl, username) {
13943
14046
  if (inMemoryAuth) return authMatches(inMemoryAuth, anvilUrl, username);
13944
14047
  {
13945
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14048
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13946
14049
  if (repoAuth && authMatches(repoAuth, anvilUrl, username)) return true;
13947
14050
  return hasTokens(anvilUrl, username);
13948
14051
  }
13949
14052
  }
13950
- async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
14053
+ async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username, options) {
13951
14054
  const normalized = normalizeAnvilUrl(anvilUrl);
13952
14055
  const source = await resolveTokenSource(normalized, username);
13953
14056
  const tokens = getSourceTokens(source);
13954
14057
  if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
13955
- const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
13956
- if (tokens.refreshToken && isExpired) {
14058
+ if (tokens.refreshToken && isAccessTokenExpired(tokens)) {
13957
14059
  const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13958
14060
  const existingRefresh = inFlightRefreshes.get(refreshKey);
13959
14061
  if (existingRefresh) return existingRefresh;
13960
- const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
14062
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken, options).finally(()=>{
13961
14063
  inFlightRefreshes.delete(refreshKey);
13962
14064
  });
13963
14065
  inFlightRefreshes.set(refreshKey, refreshPromise);
@@ -13971,7 +14073,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13971
14073
  type: "memory",
13972
14074
  auth: inMemoryAuth
13973
14075
  };
13974
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14076
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
13975
14077
  if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13976
14078
  type: "repo",
13977
14079
  auth: repoAuth,
@@ -13987,6 +14089,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13987
14089
  function getSourceTokens(source) {
13988
14090
  return "store" === source.type ? source.tokens : source.auth.tokens;
13989
14091
  }
14092
+ function isAccessTokenExpired(tokens) {
14093
+ return null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
14094
+ }
13990
14095
  function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13991
14096
  if (username) return username;
13992
14097
  const urlTokens = getUrlTokens(normalizedUrl);
@@ -14005,11 +14110,43 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14005
14110
  return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
14006
14111
  }
14007
14112
  }
14008
- async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
14113
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken, options) {
14114
+ if ("repo" === source.type) return refreshRepoAuthTokens(source.repoPath, {
14115
+ anvilUrl: normalizedUrl,
14116
+ username,
14117
+ auth: source.auth,
14118
+ refreshToken,
14119
+ lock: options?.lock,
14120
+ requestTimeoutMs: options?.requestTimeoutMs
14121
+ });
14122
+ return performTokenRefresh(normalizedUrl, username, source, refreshToken, options?.requestTimeoutMs);
14123
+ }
14124
+ async function refreshRepoAuthTokens(repoPath, options) {
14125
+ const normalizedUrl = normalizeAnvilUrl(options.anvilUrl);
14126
+ return withRepoAuthFileLock(repoPath, async ()=>{
14127
+ let auth = options.auth;
14128
+ let refreshToken = options.refreshToken;
14129
+ const current = await git_auth_readAuthFromRepo(repoPath);
14130
+ if (current && authMatches(current, normalizedUrl, options.username)) {
14131
+ if (current.tokens.authToken && !isAccessTokenExpired(current.tokens)) return current.tokens.authToken;
14132
+ auth = current;
14133
+ refreshToken = current.tokens.refreshToken ?? refreshToken;
14134
+ }
14135
+ if (!auth || !refreshToken) throw errors_createAuthError.required("No repo auth tokens available to refresh.");
14136
+ return performTokenRefresh(normalizedUrl, options.username, {
14137
+ type: "repo",
14138
+ auth,
14139
+ repoPath
14140
+ }, refreshToken, options.requestTimeoutMs);
14141
+ }, options.lock);
14142
+ }
14143
+ async function performTokenRefresh(normalizedUrl, username, source, refreshToken, requestTimeoutMs) {
14009
14144
  try {
14010
14145
  const tokens = getSourceTokens(source);
14011
14146
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
14012
- const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
14147
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId, {
14148
+ timeoutMs: requestTimeoutMs
14149
+ });
14013
14150
  const newTokens = {
14014
14151
  authToken: tokenData.access_token,
14015
14152
  refreshToken: tokenData.refresh_token,
@@ -14031,12 +14168,28 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14031
14168
  }
14032
14169
  return tokenData.access_token;
14033
14170
  } catch (e) {
14171
+ if ("repo" === source.type && isRefreshTokenRejection(e)) await clearRepoAuthTokens(source.repoPath, source.auth).catch(()=>{});
14034
14172
  await clearTokensForAccount(normalizedUrl, username, refreshToken);
14035
14173
  throw e;
14036
14174
  }
14037
14175
  }
14176
+ function isRefreshTokenRejection(e) {
14177
+ const error = e;
14178
+ return error?.type === "token_refresh_failed" && (400 === error.status || 401 === error.status);
14179
+ }
14180
+ async function clearRepoAuthTokens(repoPath, auth) {
14181
+ const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
14182
+ if (!encryptionKey) return;
14183
+ auth.tokens = {
14184
+ authToken: null,
14185
+ refreshToken: null,
14186
+ authTokenExpiresAt: null,
14187
+ clientId: auth.tokens.clientId
14188
+ };
14189
+ await writeAuthToFile(repoPath, auth, encryptionKey);
14190
+ }
14038
14191
  async function clearTokensForAccount(url, username, refreshToken) {
14039
- if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
14192
+ if (inMemoryAuth || repoContext && await git_auth_readAuthFromRepo(repoContext)) return;
14040
14193
  {
14041
14194
  const normalized = normalizeAnvilUrl(url);
14042
14195
  if (username) deleteAccountTokens(normalized, username);
@@ -14066,7 +14219,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14066
14219
  loggedOut: false,
14067
14220
  message: "Cannot log out when using in-memory auth tokens"
14068
14221
  };
14069
- if (repoContext && await readAuthFromRepo(repoContext)) return {
14222
+ if (repoContext && await git_auth_readAuthFromRepo(repoContext)) return {
14070
14223
  loggedOut: false,
14071
14224
  message: "Cannot log out when using auth tokens stored in the local git config."
14072
14225
  };
@@ -14097,7 +14250,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14097
14250
  inMemoryAuth.username
14098
14251
  ] : [];
14099
14252
  {
14100
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14253
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14101
14254
  if (repoAuth && authMatches(repoAuth, url)) return [
14102
14255
  repoAuth.username
14103
14256
  ];
@@ -14109,587 +14262,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14109
14262
  inMemoryAuth.anvilUrl
14110
14263
  ];
14111
14264
  {
14112
- const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
14265
+ const repoAuth = repoContext ? await git_auth_readAuthFromRepo(repoContext) : void 0;
14113
14266
  if (repoAuth) return [
14114
14267
  repoAuth.anvilUrl
14115
14268
  ];
14116
14269
  return getAvailableUrls();
14117
14270
  }
14118
14271
  }
14119
- function anvil_api_getDefaultAnvilUrl() {
14120
- return resolveAnvilUrl();
14272
+ const promises_namespaceObject = require("timers/promises");
14273
+ const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
14274
+ const FETCH_RETRY_DELAY_MS = 200;
14275
+ function isWantNotValidError(error) {
14276
+ return WANT_NOT_VALID_RE.test(error.message || "");
14121
14277
  }
14122
- async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14123
- const authToken = await auth_getValidAuthToken(anvilUrl, username);
14124
- try {
14125
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
14126
- method: "POST",
14127
- headers: {
14128
- Authorization: `Bearer ${authToken}`,
14129
- "Content-Type": "application/json"
14130
- },
14131
- body: JSON.stringify({
14132
- app_id: appId
14133
- })
14134
- });
14135
- const data = await resp.json();
14136
- if (resp.ok) return {
14137
- valid: data.valid ?? false,
14138
- app_name: data.app_name
14139
- };
14140
- if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14141
- if (403 === resp.status) throw createAppError.accessDenied(appId);
14142
- if (404 === resp.status) throw createAppError.notFound(appId);
14143
- logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
14144
- return {
14145
- valid: false,
14146
- error: data.error || `Server error: ${resp.status}`
14147
- };
14148
- } catch (error) {
14149
- if (error.type) throw error;
14150
- throw errors_createNetworkError.network(error.message);
14278
+ class GitService extends Emitter {
14279
+ git;
14280
+ repoPath;
14281
+ static INDEX_LOCK_RETRY_DELAYS_MS = [
14282
+ 25,
14283
+ 50,
14284
+ 100,
14285
+ 200
14286
+ ];
14287
+ constructor(repoPath){
14288
+ super();
14289
+ this.repoPath = external_path_default().resolve(repoPath);
14290
+ this.git = esm_default(this.repoPath);
14151
14291
  }
14152
- }
14153
- async function listAppsForCheckout(options = {}) {
14154
- const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
14155
- const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
14156
- const params = new URLSearchParams();
14157
- if ("number" == typeof options.limit) params.set("limit", String(options.limit));
14158
- if (options.cursor) params.set("cursor", options.cursor);
14159
- if (options.q && options.q.trim()) params.set("q", options.q.trim());
14160
- const query = params.toString();
14161
- const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
14162
- try {
14163
- const resp = await fetch(url, {
14164
- method: "GET",
14165
- headers: {
14166
- Authorization: `Bearer ${authToken}`
14167
- },
14168
- signal: options.signal
14169
- });
14170
- if (!resp.ok) {
14171
- if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14172
- throw errors_createNetworkError.server(resp.status, resp.statusText);
14173
- }
14174
- const data = await resp.json();
14175
- return {
14176
- apps: Array.isArray(data.apps) ? data.apps : [],
14177
- next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
14178
- };
14179
- } catch (error) {
14180
- if (error.type) throw error;
14181
- throw errors_createNetworkError.network(error.message);
14292
+ getGit() {
14293
+ return this.git;
14182
14294
  }
14183
- }
14184
- function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
14185
- const url = new URL(gitUrl);
14186
- const encodedToken = encodeURIComponent(authToken);
14187
- return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
14188
- }
14189
- function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14190
- const url = new URL(anvilUrl);
14191
- const encodedToken = encodeURIComponent(authToken);
14192
- return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
14193
- }
14194
- function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14195
- return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
14196
- }
14197
- async function getLatestVersion() {
14198
- try {
14199
- const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
14200
- if (!response.ok) return null;
14201
- const data = await response.json();
14202
- return data.version;
14203
- } catch (e) {
14204
- return null;
14295
+ getRepoPath() {
14296
+ return this.repoPath;
14205
14297
  }
14206
- }
14207
- function filterCandidates(candidates, explicitUrl, explicitUsername) {
14208
- let filtered = candidates;
14209
- if (explicitUrl) {
14210
- const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
14211
- filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
14298
+ async getGitDir() {
14299
+ try {
14300
+ const gitDir = (await this.git.revparse([
14301
+ "--git-dir"
14302
+ ])).trim();
14303
+ if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
14304
+ return gitDir;
14305
+ } catch (e) {
14306
+ throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14307
+ }
14212
14308
  }
14213
- if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
14214
- return filtered;
14215
- }
14216
- function formatCandidateLabel(candidate) {
14217
- const parts = [
14218
- candidate.appId
14219
- ];
14220
- if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
14221
- else parts.push(`(${candidate.detectedUrl})`);
14222
- parts.push(`- ${candidate.description}`);
14223
- return parts.join(" ");
14224
- }
14225
- function lookupRemoteInfoForAppId(appId, detectedRemotes) {
14226
- const matches = detectedRemotes.filter((c)=>c.appId === appId);
14227
- if (0 === matches.length) return {};
14228
- const withUsername = matches.find((c)=>c.detectedUsername);
14229
- if (withUsername) return {
14230
- detectedUrl: withUsername.detectedUrl,
14231
- detectedUsername: withUsername.detectedUsername
14232
- };
14233
- return {
14234
- detectedUrl: matches[0].detectedUrl,
14235
- detectedUsername: matches[0].detectedUsername
14236
- };
14237
- }
14238
- async function detectAppIdsFromAllRemotes(repoPath) {
14239
- const git = esm_default(repoPath);
14240
- const out = [];
14241
- try {
14242
- const remotes = await git.getRemotes(true);
14243
- for (const remote of remotes){
14244
- const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
14245
- if (httpMatch) {
14246
- const [, host, detectedAppId] = httpMatch;
14247
- out.push({
14248
- appId: detectedAppId,
14249
- source: "remote",
14250
- description: `Git remote '${remote.name}'`,
14251
- detectedUrl: normalizeAnvilUrl(host)
14252
- });
14253
- continue;
14254
- }
14255
- const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
14256
- if (sshMatch) {
14257
- const [, usernamePart, host, , detectedAppId] = sshMatch;
14258
- const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
14259
- const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
14260
- out.push({
14261
- appId: detectedAppId,
14262
- source: "remote",
14263
- description: `Git remote '${remote.name}' (SSH)`,
14264
- detectedUrl: normalizeAnvilUrl(host),
14265
- detectedUsername: finalUsername
14266
- });
14267
- }
14268
- }
14269
- } catch (_e) {}
14270
- return out;
14271
- }
14272
- async function detectAppIdsByCommitLookup(repoPath, options) {
14273
- const anvilUrl = options.anvilUrl || resolveAnvilUrl();
14274
- const username = options.username;
14275
- const git = esm_default(repoPath);
14276
- const out = [];
14277
- try {
14278
- const authToken = await auth_getValidAuthToken(anvilUrl, username);
14279
- const branchRef = await git.revparse([
14280
- "--abbrev-ref",
14281
- "HEAD"
14282
- ]);
14283
- const currentBranch = "HEAD" === branchRef ? "master" : branchRef;
14284
- const commitId = (await git.revparse([
14285
- "HEAD"
14286
- ]))?.trim();
14287
- if (!commitId) return out;
14288
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
14289
- method: "POST",
14290
- headers: {
14291
- Authorization: `Bearer ${authToken}`,
14292
- "Content-Type": "application/json"
14293
- },
14294
- body: JSON.stringify({
14295
- commit_id: commitId,
14296
- branch_name: currentBranch
14297
- })
14298
- });
14299
- logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${currentBranch} returned ${resp.status}`);
14300
- if (!resp.ok) {
14301
- const errorBody = await resp.text();
14302
- logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
14303
- }
14304
- if (resp.ok) {
14305
- const data = await resp.json();
14306
- logger_logger.debug(`data ${JSON.stringify(data)}`);
14307
- const apps = data?.apps ?? [];
14308
- for (const app of apps)if (app?.app_id) out.push({
14309
- appId: app.app_id,
14310
- source: "remote",
14311
- description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
14312
- });
14313
- }
14314
- } catch (_e) {}
14315
- return out;
14316
- }
14317
- const promises_namespaceObject = require("fs/promises");
14318
- var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
14319
- function normalizeLineEndings(content) {
14320
- return content.replace(/\r\n/g, "\n");
14321
- }
14322
- function pythonifyName(name) {
14323
- return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14324
- }
14325
- function extractPythonName(relativePath, skipParts = 1) {
14326
- const parts = relativePath.slice(0, -3).split("/");
14327
- const isPackage = relativePath.endsWith("__init__.py");
14328
- const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14329
- return relevantParts.map(pythonifyName).join(".");
14330
- }
14331
- function deepEqual(a, b) {
14332
- if (a === b) return true;
14333
- if (null == a || null == b) return false;
14334
- if (typeof a !== typeof b) return false;
14335
- if ("object" != typeof a) return a === b;
14336
- if (Array.isArray(a) !== Array.isArray(b)) return false;
14337
- if (Array.isArray(a)) {
14338
- if (a.length !== b.length) return false;
14339
- return a.every((item, index)=>deepEqual(item, b[index]));
14340
- }
14341
- const keysA = Object.keys(a);
14342
- const keysB = Object.keys(b);
14343
- if (keysA.length !== keysB.length) return false;
14344
- return keysA.every((key)=>deepEqual(a[key], b[key]));
14345
- }
14346
- function parseHtmlWithFrontmatter(htmlContent) {
14347
- const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14348
- const match = htmlContent.match(frontmatterPattern);
14349
- if (match) {
14350
- const yamlStr = match[1];
14351
- const html = match[2];
14352
- const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14353
- return {
14354
- frontmatter,
14355
- html
14356
- };
14357
- }
14358
- return {
14359
- frontmatter: {},
14360
- html: htmlContent
14361
- };
14362
- }
14363
- const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
14364
- async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
14365
- const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
14366
- try {
14367
- const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
14368
- if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
14369
- const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
14370
- await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14371
- return true;
14372
- } catch (error) {
14373
- if ("ENOENT" !== error.code) throw error;
14374
- await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14375
- return true;
14376
- }
14377
- }
14378
- const ANVIL_DEPS_DIR = ".anvil/deps";
14379
- const ANVIL_DEPS_MANIFEST = "manifest.json";
14380
- const DEPS_MANIFEST_VERSION = 1;
14381
- function formatFetchError(prefix, status, errorBody) {
14382
- if (!errorBody) return `${prefix}: ${status}`;
14383
- try {
14384
- const parsed = JSON.parse(errorBody);
14385
- if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14386
- } catch {}
14387
- return `${prefix}: ${status} ${errorBody}`;
14388
- }
14389
- async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14390
- const { commit, includeDocs = true, knownDependencies = [] } = options;
14391
- const token = await auth_getValidAuthToken(anvilUrl);
14392
- const body = {
14393
- include_docs: includeDocs,
14394
- known_dependencies: knownDependencies
14395
- };
14396
- if (commit) body.commit = commit;
14397
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
14398
- method: "POST",
14399
- headers: {
14400
- Authorization: `Bearer ${token}`,
14401
- "Content-Type": "application/json"
14402
- },
14403
- body: JSON.stringify(body)
14404
- });
14405
- if (!resp.ok) {
14406
- const errorBody = await resp.text();
14407
- throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
14408
- }
14409
- return await resp.json();
14410
- }
14411
- async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14412
- const includeDocs = options.includeDocs ?? true;
14413
- const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
14414
- const response = await fetchAllDependencies(appId, anvilUrl, {
14415
- commit: options.commit,
14416
- includeDocs,
14417
- knownDependencies
14418
- });
14419
- if (!response.resolved_dependencies.length) {
14420
- await clearDepsCache(projectRoot);
14421
- return {
14422
- response,
14423
- addedGitignoreEntry: false
14424
- };
14425
- }
14426
- const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
14427
- await writeAllDepsToCache(projectRoot, response, {
14428
- includeDocs
14429
- });
14430
- return {
14431
- response,
14432
- addedGitignoreEntry
14433
- };
14434
- }
14435
- function depsDir(projectRoot) {
14436
- return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
14437
- }
14438
- function depsManifestPath(projectRoot) {
14439
- return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
14440
- }
14441
- function isResolvedDependencyManifestEntry(value) {
14442
- if (!value || "object" != typeof value) return false;
14443
- const entry = value;
14444
- 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);
14445
- }
14446
- function isCachedDependencyManifestEntry(value) {
14447
- if (!isResolvedDependencyManifestEntry(value)) return false;
14448
- const entry = value;
14449
- return "string" == typeof entry.package_name && entry.package_name.length > 0;
14450
- }
14451
- function isDepsCacheManifest(value) {
14452
- if (!value || "object" != typeof value) return false;
14453
- const manifest = value;
14454
- return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
14455
- }
14456
- async function readDepsCacheManifest(projectRoot) {
14457
- try {
14458
- const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
14459
- const parsed = JSON.parse(raw);
14460
- return isDepsCacheManifest(parsed) ? parsed : void 0;
14461
- } catch {
14462
- return;
14463
- }
14464
- }
14465
- async function getKnownDependenciesForCache(projectRoot, includeDocs) {
14466
- const manifest = await readDepsCacheManifest(projectRoot);
14467
- if (!manifest || manifest.includeDocs !== includeDocs) return [];
14468
- return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
14469
- app_id,
14470
- commit_id
14471
- }));
14472
- }
14473
- function getDependencyWatchSubscriptionsFromManifest(manifest) {
14474
- if (!manifest) return [];
14475
- const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
14476
- dep.app_id,
14477
- dep
14478
- ]));
14479
- return manifest.resolved_dependencies.flatMap((dep)=>{
14480
- if (!dep.branch) return [];
14481
- return [
14482
- {
14483
- appId: dep.app_id,
14484
- branch: dep.branch,
14485
- packageName: cachedByAppId.get(dep.app_id)?.package_name
14486
- }
14487
- ];
14488
- });
14489
- }
14490
- function normalizeAnvilYamlDependenciesFromContent(content) {
14491
- const parsed = external_js_yaml_default().load(content);
14492
- if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
14493
- return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
14494
- }
14495
- function normalizeAnvilYamlDependenciesValue(value) {
14496
- if (!Array.isArray(value)) return [];
14497
- return normalizeForComparison(value);
14498
- }
14499
- async function readAnvilYamlDependencies(projectRoot) {
14500
- try {
14501
- const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
14502
- return normalizeAnvilYamlDependenciesFromContent(content);
14503
- } catch {
14504
- return [];
14505
- }
14506
- }
14507
- function anvilYamlDependenciesEqual(left, right) {
14508
- return deepEqual(left, right);
14509
- }
14510
- function normalizeForComparison(value) {
14511
- if (Array.isArray(value)) return value.map(normalizeForComparison);
14512
- if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
14513
- key,
14514
- normalizeForComparison(entryValue)
14515
- ]));
14516
- return value;
14517
- }
14518
- function cachedManifestEntry(dep, packageName) {
14519
- return {
14520
- app_id: dep.app_id,
14521
- commit_id: dep.commit_id,
14522
- ...dep.branch ? {
14523
- branch: dep.branch
14524
- } : {},
14525
- package_name: packageName
14526
- };
14527
- }
14528
- function safeDependencyPath(filePath, kind) {
14529
- const normalizedPath = external_path_default().normalize(filePath);
14530
- if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
14531
- return normalizedPath;
14532
- }
14533
- async function writeDepToCache(projectRoot, dep) {
14534
- const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
14535
- await promises_default().rm(depDir, {
14536
- recursive: true,
14537
- force: true
14538
- });
14539
- await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
14540
- recursive: true
14541
- });
14542
- await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
14543
- recursive: true
14544
- });
14545
- await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
14546
- package_name: dep.package_name
14547
- }));
14548
- for (const mod of dep.modules ?? [])if (mod.is_package) {
14549
- const modDir = external_path_default().join(depDir, "client_code", mod.name);
14550
- await promises_default().mkdir(modDir, {
14551
- recursive: true
14552
- });
14553
- await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14554
- } else await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${mod.name}.py`), mod.code);
14555
- for (const form of dep.forms ?? []){
14556
- const { code, class_name, is_package, ...formTemplate } = form;
14557
- if (is_package) {
14558
- const formDir = external_path_default().join(depDir, "client_code", class_name);
14559
- await promises_default().mkdir(formDir, {
14560
- recursive: true
14561
- });
14562
- await promises_default().writeFile(external_path_default().join(formDir, "__init__.py"), code);
14563
- await promises_default().writeFile(external_path_default().join(formDir, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
14564
- } else {
14565
- await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.py`), code);
14566
- await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.yaml`), external_js_yaml_default().dump(formTemplate));
14567
- }
14568
- }
14569
- for (const mod of dep.server_modules ?? [])if (mod.is_package) {
14570
- const modDir = external_path_default().join(depDir, "server_code", mod.name);
14571
- await promises_default().mkdir(modDir, {
14572
- recursive: true
14573
- });
14574
- await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14575
- } else await promises_default().writeFile(external_path_default().join(depDir, "server_code", `${mod.name}.py`), mod.code);
14576
- for (const asset of dep.assets ?? []){
14577
- const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
14578
- await promises_default().mkdir(external_path_default().dirname(targetPath), {
14579
- recursive: true
14580
- });
14581
- await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
14582
- }
14583
- for (const doc of dep.docs ?? []){
14584
- const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
14585
- await promises_default().mkdir(external_path_default().dirname(targetPath), {
14586
- recursive: true
14587
- });
14588
- await promises_default().writeFile(targetPath, doc.content);
14589
- }
14590
- }
14591
- async function writeAllDepsToCache(projectRoot, response, options = {}) {
14592
- const includeDocs = options.includeDocs ?? true;
14593
- const manifest = await readDepsCacheManifest(projectRoot);
14594
- const canReuseExistingCache = manifest?.includeDocs === includeDocs;
14595
- if (!canReuseExistingCache) await clearDepsCache(projectRoot);
14596
- const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
14597
- const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
14598
- 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), {
14599
- recursive: true,
14600
- force: true
14601
- });
14602
- for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
14603
- const returnedByAppId = new Map(response.dependencies.map((dep)=>[
14604
- dep.app_id,
14605
- dep
14606
- ]));
14607
- const previousByAppId = new Map(previousCached.map((dep)=>[
14608
- dep.app_id,
14609
- dep
14610
- ]));
14611
- const cachedDependencies = response.resolved_dependencies.map((dep)=>{
14612
- const returned = returnedByAppId.get(dep.app_id);
14613
- if (returned) return cachedManifestEntry(returned, returned.package_name);
14614
- const previous = previousByAppId.get(dep.app_id);
14615
- if (!previous) return;
14616
- return cachedManifestEntry(dep, previous.package_name);
14617
- }).filter((dep)=>void 0 !== dep);
14618
- const newManifest = {
14619
- version: DEPS_MANIFEST_VERSION,
14620
- includeDocs,
14621
- resolved_dependencies: response.resolved_dependencies,
14622
- cached_dependencies: cachedDependencies
14623
- };
14624
- await promises_default().mkdir(depsDir(projectRoot), {
14625
- recursive: true
14626
- });
14627
- await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
14628
- }
14629
- async function clearDepsCache(projectRoot) {
14630
- await promises_default().rm(depsDir(projectRoot), {
14631
- recursive: true,
14632
- force: true
14633
- });
14634
- }
14635
- async function getCachedPackages(projectRoot) {
14636
- try {
14637
- const entries = await promises_default().readdir(depsDir(projectRoot), {
14638
- withFileTypes: true
14639
- });
14640
- return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
14641
- } catch {
14642
- return [];
14643
- }
14644
- }
14645
- const external_timers_promises_namespaceObject = require("timers/promises");
14646
- const WANT_NOT_VALID_RE = /\bwant\s+[0-9a-f]{7,40}\s+not valid\b/i;
14647
- const FETCH_RETRY_DELAY_MS = 200;
14648
- function isWantNotValidError(error) {
14649
- return WANT_NOT_VALID_RE.test(error.message || "");
14650
- }
14651
- class GitService extends Emitter {
14652
- git;
14653
- repoPath;
14654
- static INDEX_LOCK_RETRY_DELAYS_MS = [
14655
- 25,
14656
- 50,
14657
- 100,
14658
- 200
14659
- ];
14660
- constructor(repoPath){
14661
- super();
14662
- this.repoPath = external_path_default().resolve(repoPath);
14663
- this.git = esm_default(this.repoPath);
14664
- }
14665
- getGit() {
14666
- return this.git;
14667
- }
14668
- getRepoPath() {
14669
- return this.repoPath;
14670
- }
14671
- async getGitDir() {
14672
- try {
14673
- const gitDir = (await this.git.revparse([
14674
- "--git-dir"
14675
- ])).trim();
14676
- if (!external_path_default().isAbsolute(gitDir)) return external_path_default().resolve(this.repoPath, gitDir);
14677
- return gitDir;
14678
- } catch (e) {
14679
- throw errors_createGitError.commandFailed("rev-parse --git-dir", e.message);
14680
- }
14681
- }
14682
- async getCurrentBranch() {
14683
- try {
14684
- const branchRef = await this.git.revparse([
14685
- "--abbrev-ref",
14686
- "HEAD"
14687
- ]);
14688
- if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14689
- return branchRef;
14690
- } catch (e) {
14691
- if ("detached_head" === e.type) throw e;
14692
- throw createSyncError.detachedHead();
14309
+ async getCurrentBranch() {
14310
+ try {
14311
+ const branchRef = await this.git.revparse([
14312
+ "--abbrev-ref",
14313
+ "HEAD"
14314
+ ]);
14315
+ if ("HEAD" === branchRef) throw createSyncError.detachedHead();
14316
+ return branchRef;
14317
+ } catch (e) {
14318
+ if ("detached_head" === e.type) throw e;
14319
+ throw createSyncError.detachedHead();
14693
14320
  }
14694
14321
  }
14695
14322
  async getCommitId() {
@@ -14755,341 +14382,951 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14755
14382
  }
14756
14383
  return discarded;
14757
14384
  }
14758
- async discardStagedModeOnlyChanges(paths) {
14759
- const discarded = [];
14760
- for (const relativePath of paths)try {
14761
- const headMode = await this.getHeadFileMode(relativePath);
14762
- if ("100644" !== headMode && "100755" !== headMode) continue;
14763
- const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14764
- if (!indexContentUnchanged) continue;
14385
+ async discardStagedModeOnlyChanges(paths) {
14386
+ const discarded = [];
14387
+ for (const relativePath of paths)try {
14388
+ const headMode = await this.getHeadFileMode(relativePath);
14389
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14390
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14391
+ if (!indexContentUnchanged) continue;
14392
+ await this.git.raw([
14393
+ "reset",
14394
+ "-q",
14395
+ "HEAD",
14396
+ "--",
14397
+ relativePath
14398
+ ]);
14399
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14400
+ discarded.push(relativePath);
14401
+ } catch (e) {
14402
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14403
+ }
14404
+ return discarded;
14405
+ }
14406
+ async getHeadFileMode(relativePath) {
14407
+ const output = await this.git.raw([
14408
+ "ls-tree",
14409
+ "HEAD",
14410
+ "--",
14411
+ relativePath
14412
+ ]);
14413
+ const match = output.match(/^(\d{6})\s/);
14414
+ return match?.[1] ?? null;
14415
+ }
14416
+ async hasSameContentAsHead(relativePath) {
14417
+ const headHash = (await this.git.revparse([
14418
+ `HEAD:${relativePath}`
14419
+ ])).trim();
14420
+ const worktreeHash = (await this.git.raw([
14421
+ "hash-object",
14422
+ "--",
14423
+ relativePath
14424
+ ])).trim();
14425
+ return headHash === worktreeHash;
14426
+ }
14427
+ async hasSameIndexContentAsHead(relativePath) {
14428
+ const headHash = (await this.git.revparse([
14429
+ `HEAD:${relativePath}`
14430
+ ])).trim();
14431
+ const indexHash = (await this.git.revparse([
14432
+ `:${relativePath}`
14433
+ ])).trim();
14434
+ return headHash === indexHash;
14435
+ }
14436
+ async restoreExecutableBit(relativePath, executable) {
14437
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14438
+ const stat = await external_fs_.promises.stat(filePath);
14439
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14440
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14441
+ }
14442
+ async hasUncommittedChanges() {
14443
+ const status = await this.getStatus();
14444
+ return !status.isClean;
14445
+ }
14446
+ async fetch(url, refSpec) {
14447
+ let attempt = 0;
14448
+ while(true)try {
14449
+ await this.git.fetch([
14450
+ url,
14451
+ refSpec
14452
+ ]);
14453
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14454
+ attempts: attempt,
14455
+ outcome: "succeeded"
14456
+ });
14457
+ return;
14458
+ } catch (e) {
14459
+ const err = e;
14460
+ if (!isWantNotValidError(err)) {
14461
+ if (attempt > 0) this.emit("fetch-retry-resolved", {
14462
+ attempts: attempt,
14463
+ outcome: "failed"
14464
+ });
14465
+ throw errors_createGitError.fetchFailed(err.message);
14466
+ }
14467
+ attempt++;
14468
+ this.emit("fetch-retry", {
14469
+ attempt,
14470
+ delayMs: FETCH_RETRY_DELAY_MS,
14471
+ error: err.message
14472
+ });
14473
+ logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
14474
+ await (0, promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
14475
+ }
14476
+ }
14477
+ async reset(ref, mode = "mixed") {
14478
+ await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14479
+ `--${mode}`,
14480
+ ref
14481
+ ]));
14482
+ }
14483
+ async checkout(paths) {
14484
+ await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14485
+ }
14486
+ async runGitCommandWithIndexLockRetry(command, operation) {
14487
+ let lastError;
14488
+ for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
14489
+ await operation();
14490
+ return;
14491
+ } catch (e) {
14492
+ lastError = e;
14493
+ if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
14494
+ await (0, promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
14495
+ }
14496
+ throw errors_createGitError.commandFailed(command, lastError.message);
14497
+ }
14498
+ isIndexLockError(error) {
14499
+ const message = error.message ?? "";
14500
+ return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
14501
+ }
14502
+ async stash(message) {
14503
+ try {
14504
+ const args = [
14505
+ "stash",
14506
+ "push",
14507
+ "--include-untracked"
14508
+ ];
14509
+ if (message) args.push("-m", message);
14510
+ const result = await this.git.raw(args);
14511
+ return !result.includes("No local changes to save");
14512
+ } catch (e) {
14513
+ throw errors_createGitError.commandFailed("stash", e.message);
14514
+ }
14515
+ }
14516
+ async stashPop() {
14517
+ try {
14518
+ await this.git.raw([
14519
+ "stash",
14520
+ "pop"
14521
+ ]);
14522
+ } catch (e) {
14523
+ throw errors_createGitError.commandFailed("stash pop", e.message);
14524
+ }
14525
+ }
14526
+ async clean(files) {
14527
+ try {
14528
+ await this.git.clean(CleanOptions.FORCE, files);
14529
+ } catch (e) {
14530
+ throw errors_createGitError.commandFailed("clean", e.message);
14531
+ }
14532
+ }
14533
+ async diffNames(fromCommit, toCommit) {
14534
+ try {
14535
+ const diffResult = await this.git.diff([
14536
+ fromCommit,
14537
+ toCommit,
14538
+ "--name-only"
14539
+ ]);
14540
+ const files = diffResult.split("\n").filter((f)=>f.trim());
14541
+ return files;
14542
+ } catch (e) {
14543
+ throw errors_createGitError.commandFailed("diff", e.message);
14544
+ }
14545
+ }
14546
+ async getAheadBehind(localRef, remoteRef) {
14547
+ try {
14548
+ const result = await this.git.raw([
14549
+ "rev-list",
14550
+ "--left-right",
14551
+ "--count",
14552
+ `${remoteRef}...${localRef}`
14553
+ ]);
14554
+ const [behind, ahead] = result.trim().split("\t").map(Number);
14555
+ return {
14556
+ ahead,
14557
+ behind
14558
+ };
14559
+ } catch (e) {
14560
+ throw errors_createGitError.commandFailed("rev-list", e.message);
14561
+ }
14562
+ }
14563
+ async show(refPath) {
14564
+ try {
14565
+ const content = await this.git.show([
14566
+ refPath
14567
+ ]);
14568
+ return content;
14569
+ } catch (e) {
14570
+ throw errors_createGitError.commandFailed("show", e.message);
14571
+ }
14572
+ }
14573
+ async push(url, refSpec, force = false) {
14574
+ try {
14575
+ const args = force ? [
14576
+ "push",
14577
+ "--force",
14578
+ url,
14579
+ refSpec
14580
+ ] : [
14581
+ "push",
14582
+ url,
14583
+ refSpec
14584
+ ];
14585
+ await this.git.raw(args);
14586
+ } catch (e) {
14587
+ throw errors_createGitError.commandFailed("push", e.message);
14588
+ }
14589
+ }
14590
+ async rebase(onto) {
14591
+ try {
14592
+ await this.git.raw([
14593
+ "rebase",
14594
+ onto
14595
+ ]);
14596
+ } catch (e) {
14597
+ throw errors_createGitError.commandFailed("rebase", e.message);
14598
+ }
14599
+ }
14600
+ async rebaseAbort() {
14601
+ try {
14602
+ await this.git.raw([
14603
+ "rebase",
14604
+ "--abort"
14605
+ ]);
14606
+ } catch (e) {
14607
+ throw errors_createGitError.commandFailed("rebase --abort", e.message);
14608
+ }
14609
+ }
14610
+ async isAncestor(ancestorRef, descendantRef) {
14611
+ try {
14612
+ const mergeBase = (await this.git.raw([
14613
+ "merge-base",
14614
+ ancestorRef,
14615
+ descendantRef
14616
+ ])).trim();
14617
+ const ancestorCommit = (await this.git.revparse([
14618
+ ancestorRef
14619
+ ])).trim();
14620
+ return mergeBase === ancestorCommit;
14621
+ } catch {
14622
+ return false;
14623
+ }
14624
+ }
14625
+ async mergeFastForward(ref) {
14626
+ await this.runGitCommandWithIndexLockRetry("merge --ff-only", ()=>this.git.raw([
14627
+ "merge",
14628
+ "--ff-only",
14629
+ ref
14630
+ ]));
14631
+ }
14632
+ async deleteRef(ref) {
14633
+ try {
14765
14634
  await this.git.raw([
14766
- "reset",
14767
- "-q",
14768
- "HEAD",
14769
- "--",
14770
- relativePath
14635
+ "update-ref",
14636
+ "-d",
14637
+ ref
14771
14638
  ]);
14772
- await this.restoreExecutableBit(relativePath, "100755" === headMode);
14773
- discarded.push(relativePath);
14639
+ } catch (e) {}
14640
+ }
14641
+ async getRemotes() {
14642
+ try {
14643
+ const remotes = await this.git.getRemotes(true);
14644
+ return remotes.map((r)=>({
14645
+ name: r.name,
14646
+ fetchUrl: r.refs.fetch
14647
+ }));
14774
14648
  } catch (e) {
14775
- throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14649
+ throw errors_createGitError.commandFailed("remote", e.message);
14776
14650
  }
14777
- return discarded;
14778
- }
14779
- async getHeadFileMode(relativePath) {
14780
- const output = await this.git.raw([
14781
- "ls-tree",
14782
- "HEAD",
14783
- "--",
14784
- relativePath
14785
- ]);
14786
- const match = output.match(/^(\d{6})\s/);
14787
- return match?.[1] ?? null;
14788
- }
14789
- async hasSameContentAsHead(relativePath) {
14790
- const headHash = (await this.git.revparse([
14791
- `HEAD:${relativePath}`
14792
- ])).trim();
14793
- const worktreeHash = (await this.git.raw([
14794
- "hash-object",
14795
- "--",
14796
- relativePath
14797
- ])).trim();
14798
- return headHash === worktreeHash;
14799
- }
14800
- async hasSameIndexContentAsHead(relativePath) {
14801
- const headHash = (await this.git.revparse([
14802
- `HEAD:${relativePath}`
14803
- ])).trim();
14804
- const indexHash = (await this.git.revparse([
14805
- `:${relativePath}`
14806
- ])).trim();
14807
- return headHash === indexHash;
14808
14651
  }
14809
- async restoreExecutableBit(relativePath, executable) {
14810
- const filePath = external_path_default().join(this.repoPath, relativePath);
14811
- const stat = await external_fs_.promises.stat(filePath);
14812
- const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14813
- if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14652
+ async isGitInitialized() {
14653
+ const gitPath = external_path_default().join(this.repoPath, ".git");
14654
+ return external_fs_.existsSync(gitPath);
14814
14655
  }
14815
- async hasUncommittedChanges() {
14816
- const status = await this.getStatus();
14817
- return !status.isClean;
14656
+ async removeEmptyDirectories(removedFiles) {
14657
+ const directoriesToCheck = new Set();
14658
+ for (const file of removedFiles){
14659
+ let dir = external_path_default().dirname(file);
14660
+ while(dir && "." !== dir){
14661
+ directoriesToCheck.add(dir);
14662
+ const parentDir = external_path_default().dirname(dir);
14663
+ if (parentDir === dir) break;
14664
+ dir = parentDir;
14665
+ }
14666
+ }
14667
+ const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
14668
+ for (const dir of sortedDirs)try {
14669
+ const dirPath = external_path_default().join(this.repoPath, dir);
14670
+ if (!external_fs_.existsSync(dirPath)) continue;
14671
+ const entries = await external_fs_.promises.readdir(dirPath);
14672
+ if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
14673
+ } catch (error) {}
14818
14674
  }
14819
- async fetch(url, refSpec) {
14820
- let attempt = 0;
14821
- while(true)try {
14822
- await this.git.fetch([
14823
- url,
14824
- refSpec
14825
- ]);
14826
- if (attempt > 0) this.emit("fetch-retry-resolved", {
14827
- attempts: attempt,
14828
- outcome: "succeeded"
14675
+ }
14676
+ async function getStagedFileChanges(git, gitService) {
14677
+ try {
14678
+ const status = await git.status();
14679
+ const renames = status.renamed || [];
14680
+ const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
14681
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14682
+ const stagedRenames = [];
14683
+ for (const f of stagedFiles)if ("R" === f.index) {
14684
+ const rename = renames.find((r)=>r.to === f.path);
14685
+ if (rename) stagedRenames.push({
14686
+ path: rename.to,
14687
+ type: "rename",
14688
+ from: rename.from
14829
14689
  });
14830
- return;
14831
- } catch (e) {
14832
- const err = e;
14833
- if (!isWantNotValidError(err)) {
14834
- if (attempt > 0) this.emit("fetch-retry-resolved", {
14835
- attempts: attempt,
14836
- outcome: "failed"
14837
- });
14838
- throw errors_createGitError.fetchFailed(err.message);
14690
+ }
14691
+ const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14692
+ const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14693
+ const changes = [];
14694
+ for (const f of stagedFiles){
14695
+ if ("R" !== f.index) {
14696
+ if (!stagedModeOnlyPaths.has(f.path)) {
14697
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14698
+ path: f.path,
14699
+ type: "unlink"
14700
+ });
14701
+ else if ("A" === f.index || "?" === f.index) changes.push({
14702
+ path: f.path,
14703
+ type: "add"
14704
+ });
14705
+ else changes.push({
14706
+ path: f.path,
14707
+ type: "change"
14708
+ });
14709
+ }
14839
14710
  }
14840
- attempt++;
14841
- this.emit("fetch-retry", {
14842
- attempt,
14843
- delayMs: FETCH_RETRY_DELAY_MS,
14844
- error: err.message
14845
- });
14846
- logger_logger.verbose(`git fetch race (attempt ${attempt}, sleeping ${FETCH_RETRY_DELAY_MS}ms): ${err.message}`);
14847
- await (0, external_timers_promises_namespaceObject.setTimeout)(FETCH_RETRY_DELAY_MS);
14848
14711
  }
14712
+ changes.push(...stagedRenames);
14713
+ return changes;
14714
+ } catch (e) {
14715
+ throw errors_createGitError.commandFailed("status", e.message);
14849
14716
  }
14850
- async reset(ref, mode = "mixed") {
14851
- await this.runGitCommandWithIndexLockRetry("reset", ()=>this.git.reset([
14852
- `--${mode}`,
14853
- ref
14854
- ]));
14855
- }
14856
- async checkout(paths) {
14857
- await this.runGitCommandWithIndexLockRetry("checkout", ()=>this.git.checkout(paths));
14717
+ }
14718
+ async function getCurrentOrFallbackBranchName(git) {
14719
+ const branchRef = (await git.revparse([
14720
+ "--abbrev-ref",
14721
+ "HEAD"
14722
+ ])).trim();
14723
+ if ("HEAD" !== branchRef) return branchRef;
14724
+ const [localBranches, remoteBranches] = await Promise.all([
14725
+ git.raw([
14726
+ "branch",
14727
+ "--format=%(refname:short)",
14728
+ "--contains",
14729
+ "HEAD"
14730
+ ]),
14731
+ git.raw([
14732
+ "branch",
14733
+ "-r",
14734
+ "--format=%(refname:short)",
14735
+ "--contains",
14736
+ "HEAD"
14737
+ ])
14738
+ ]);
14739
+ const branchNames = [
14740
+ ...parseBranchList(localBranches),
14741
+ ...parseBranchList(remoteBranches).filter((branchName)=>!branchName.endsWith("/HEAD")).map((branchName)=>branchName.replace(/^[^/]+\//, ""))
14742
+ ];
14743
+ const branchSet = new Set(branchNames);
14744
+ if (branchSet.has("master")) return "master";
14745
+ if (branchSet.has("main")) return "main";
14746
+ return "master";
14747
+ }
14748
+ function parseBranchList(output) {
14749
+ return output.split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
14750
+ }
14751
+ function anvil_api_getDefaultAnvilUrl() {
14752
+ return resolveAnvilUrl();
14753
+ }
14754
+ async function validateAppId(appId, anvilUrl = anvil_api_getDefaultAnvilUrl(), username) {
14755
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14756
+ try {
14757
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/validate-app-id`, {
14758
+ method: "POST",
14759
+ headers: {
14760
+ Authorization: `Bearer ${authToken}`,
14761
+ "Content-Type": "application/json"
14762
+ },
14763
+ body: JSON.stringify({
14764
+ app_id: appId
14765
+ })
14766
+ });
14767
+ const data = await resp.json();
14768
+ if (resp.ok) return {
14769
+ valid: data.valid ?? false,
14770
+ app_name: data.app_name
14771
+ };
14772
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14773
+ if (403 === resp.status) throw createAppError.accessDenied(appId);
14774
+ if (404 === resp.status) throw createAppError.notFound(appId);
14775
+ logger_logger.debug(`validate-app-id failed: ${resp.status} ${resp.statusText}`);
14776
+ return {
14777
+ valid: false,
14778
+ error: data.error || `Server error: ${resp.status}`
14779
+ };
14780
+ } catch (error) {
14781
+ if (error.type) throw error;
14782
+ throw errors_createNetworkError.network(error.message);
14858
14783
  }
14859
- async runGitCommandWithIndexLockRetry(command, operation) {
14860
- let lastError;
14861
- for(let attempt = 0; attempt <= GitService.INDEX_LOCK_RETRY_DELAYS_MS.length; attempt += 1)try {
14862
- await operation();
14863
- return;
14864
- } catch (e) {
14865
- lastError = e;
14866
- if (!this.isIndexLockError(e) || attempt === GitService.INDEX_LOCK_RETRY_DELAYS_MS.length) throw errors_createGitError.commandFailed(command, e.message);
14867
- await (0, external_timers_promises_namespaceObject.setTimeout)(GitService.INDEX_LOCK_RETRY_DELAYS_MS[attempt]);
14784
+ }
14785
+ async function listAppsForCheckout(options = {}) {
14786
+ const anvilUrl = options.anvilUrl ?? anvil_api_getDefaultAnvilUrl();
14787
+ const authToken = await auth_getValidAuthToken(anvilUrl, options.username);
14788
+ const params = new URLSearchParams();
14789
+ if ("number" == typeof options.limit) params.set("limit", String(options.limit));
14790
+ if (options.cursor) params.set("cursor", options.cursor);
14791
+ if (options.q && options.q.trim()) params.set("q", options.q.trim());
14792
+ const query = params.toString();
14793
+ const url = `${anvilUrl}/ide/api/_/apps${query ? `?${query}` : ""}`;
14794
+ try {
14795
+ const resp = await fetch(url, {
14796
+ method: "GET",
14797
+ headers: {
14798
+ Authorization: `Bearer ${authToken}`
14799
+ },
14800
+ signal: options.signal
14801
+ });
14802
+ if (!resp.ok) {
14803
+ if (401 === resp.status) throw errors_createAuthError.invalid("Authentication failed");
14804
+ throw errors_createNetworkError.server(resp.status, resp.statusText);
14868
14805
  }
14869
- throw errors_createGitError.commandFailed(command, lastError.message);
14806
+ const data = await resp.json();
14807
+ return {
14808
+ apps: Array.isArray(data.apps) ? data.apps : [],
14809
+ next_cursor: "string" == typeof data.next_cursor ? data.next_cursor : null
14810
+ };
14811
+ } catch (error) {
14812
+ if (error.type) throw error;
14813
+ throw errors_createNetworkError.network(error.message);
14814
+ }
14815
+ }
14816
+ function getGitFetchUrl(appId, authToken, gitUrl = anvil_api_getDefaultAnvilUrl()) {
14817
+ const url = new URL(gitUrl);
14818
+ const encodedToken = encodeURIComponent(authToken);
14819
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git?no_freeze=true&q=`;
14820
+ }
14821
+ function getGitPushUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14822
+ const url = new URL(anvilUrl);
14823
+ const encodedToken = encodeURIComponent(authToken);
14824
+ return `${url.protocol}//git:${encodedToken}@${url.hostname}${url.port ? ":" + url.port : ""}/git/${appId}.git`;
14825
+ }
14826
+ function getWebSocketUrl(appId, authToken, anvilUrl = anvil_api_getDefaultAnvilUrl()) {
14827
+ return anvilUrl.replace(/^http/, "ws") + `/ide/api/_/apps/${appId}/ws?access_token=${authToken}`;
14828
+ }
14829
+ async function getLatestVersion() {
14830
+ try {
14831
+ const response = await fetch("https://registry.npmjs.org/@anvil-works/anvil-cli/latest");
14832
+ if (!response.ok) return null;
14833
+ const data = await response.json();
14834
+ return data.version;
14835
+ } catch (e) {
14836
+ return null;
14870
14837
  }
14871
- isIndexLockError(error) {
14872
- const message = error.message ?? "";
14873
- return message.includes("Unable to create") && message.includes("index.lock") && message.includes("File exists");
14838
+ }
14839
+ function filterCandidates(candidates, explicitUrl, explicitUsername) {
14840
+ let filtered = candidates;
14841
+ if (explicitUrl) {
14842
+ const normalizedExplicit = normalizeAnvilUrl(explicitUrl);
14843
+ filtered = filtered.filter((c)=>c.detectedUrl && normalizeAnvilUrl(c.detectedUrl) === normalizedExplicit);
14874
14844
  }
14875
- async stash(message) {
14876
- try {
14877
- const args = [
14878
- "stash",
14879
- "push",
14880
- "--include-untracked"
14881
- ];
14882
- if (message) args.push("-m", message);
14883
- const result = await this.git.raw(args);
14884
- return !result.includes("No local changes to save");
14885
- } catch (e) {
14886
- throw errors_createGitError.commandFailed("stash", e.message);
14845
+ if (explicitUsername) filtered = filtered.filter((c)=>!c.detectedUsername || c.detectedUsername === explicitUsername);
14846
+ return filtered;
14847
+ }
14848
+ function formatCandidateLabel(candidate) {
14849
+ const parts = [
14850
+ candidate.appId
14851
+ ];
14852
+ if (candidate.detectedUrl) if (candidate.detectedUsername) parts.push(`(${candidate.detectedUsername} on ${candidate.detectedUrl})`);
14853
+ else parts.push(`(${candidate.detectedUrl})`);
14854
+ parts.push(`- ${candidate.description}`);
14855
+ return parts.join(" ");
14856
+ }
14857
+ function lookupRemoteInfoForAppId(appId, detectedRemotes) {
14858
+ const matches = detectedRemotes.filter((c)=>c.appId === appId);
14859
+ if (0 === matches.length) return {};
14860
+ const withUsername = matches.find((c)=>c.detectedUsername);
14861
+ if (withUsername) return {
14862
+ detectedUrl: withUsername.detectedUrl,
14863
+ detectedUsername: withUsername.detectedUsername
14864
+ };
14865
+ return {
14866
+ detectedUrl: matches[0].detectedUrl,
14867
+ detectedUsername: matches[0].detectedUsername
14868
+ };
14869
+ }
14870
+ async function detectAppIdsFromAllRemotes(repoPath) {
14871
+ const git = esm_default(repoPath);
14872
+ const out = [];
14873
+ try {
14874
+ const remotes = await git.getRemotes(true);
14875
+ for (const remote of remotes){
14876
+ const httpMatch = remote.refs.fetch?.match(/(?:http|https):\/\/(?:[^@]+@)?([^:\/]+)(?::\d+)?\/git\/([A-Z0-9]+)\.git/);
14877
+ if (httpMatch) {
14878
+ const [, host, detectedAppId] = httpMatch;
14879
+ out.push({
14880
+ appId: detectedAppId,
14881
+ source: "remote",
14882
+ description: `Git remote '${remote.name}'`,
14883
+ detectedUrl: normalizeAnvilUrl(host)
14884
+ });
14885
+ continue;
14886
+ }
14887
+ const sshMatch = remote.refs.fetch?.match(/ssh:\/\/([^@]+)@([^:]+):(\d+)\/(?:git\/)?([A-Z0-9]+)\.git/);
14888
+ if (sshMatch) {
14889
+ const [, usernamePart, host, , detectedAppId] = sshMatch;
14890
+ const detectedUsername = usernamePart.includes("%") ? decodeURIComponent(usernamePart) : usernamePart;
14891
+ const finalUsername = "git" !== detectedUsername ? detectedUsername : void 0;
14892
+ out.push({
14893
+ appId: detectedAppId,
14894
+ source: "remote",
14895
+ description: `Git remote '${remote.name}' (SSH)`,
14896
+ detectedUrl: normalizeAnvilUrl(host),
14897
+ detectedUsername: finalUsername
14898
+ });
14899
+ }
14887
14900
  }
14888
- }
14889
- async stashPop() {
14890
- try {
14891
- await this.git.raw([
14892
- "stash",
14893
- "pop"
14894
- ]);
14895
- } catch (e) {
14896
- throw errors_createGitError.commandFailed("stash pop", e.message);
14901
+ } catch (_e) {}
14902
+ return out;
14903
+ }
14904
+ async function detectAppIdsByCommitLookup(repoPath, options) {
14905
+ const anvilUrl = options.anvilUrl || resolveAnvilUrl();
14906
+ const username = options.username;
14907
+ const git = esm_default(repoPath);
14908
+ const out = [];
14909
+ try {
14910
+ const authToken = await auth_getValidAuthToken(anvilUrl, username);
14911
+ const commitId = (await git.revparse([
14912
+ "HEAD"
14913
+ ]))?.trim();
14914
+ if (!commitId) return out;
14915
+ const branchName = await getCurrentOrFallbackBranchName(git);
14916
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/lookup-by-commit`, {
14917
+ method: "POST",
14918
+ headers: {
14919
+ Authorization: `Bearer ${authToken}`,
14920
+ "Content-Type": "application/json"
14921
+ },
14922
+ body: JSON.stringify({
14923
+ commit_id: commitId,
14924
+ branch_name: branchName
14925
+ })
14926
+ });
14927
+ logger_logger.debug(`Reverse lookup by commit ${commitId} on branch ${branchName} returned ${resp.status}`);
14928
+ if (!resp.ok) {
14929
+ const errorBody = await resp.text();
14930
+ logger_logger.debug(`lookup-by-commit error: ${errorBody}`);
14897
14931
  }
14898
- }
14899
- async clean(files) {
14900
- try {
14901
- await this.git.clean(CleanOptions.FORCE, files);
14902
- } catch (e) {
14903
- throw errors_createGitError.commandFailed("clean", e.message);
14932
+ if (resp.ok) {
14933
+ const data = await resp.json();
14934
+ logger_logger.debug(`data ${JSON.stringify(data)}`);
14935
+ const apps = data?.apps ?? [];
14936
+ for (const app of apps)if (app?.app_id) out.push({
14937
+ appId: app.app_id,
14938
+ source: "remote",
14939
+ description: `Reverse lookup by commit${app.branch ? ` on branch '${app.branch}'` : ""}`
14940
+ });
14904
14941
  }
14942
+ } catch (_e) {}
14943
+ return out;
14944
+ }
14945
+ const external_fs_promises_namespaceObject = require("fs/promises");
14946
+ var external_fs_promises_default = /*#__PURE__*/ __webpack_require__.n(external_fs_promises_namespaceObject);
14947
+ function normalizeLineEndings(content) {
14948
+ return content.replace(/\r\n/g, "\n");
14949
+ }
14950
+ function pythonifyName(name) {
14951
+ return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14952
+ }
14953
+ function extractPythonName(relativePath, skipParts = 1) {
14954
+ const parts = relativePath.slice(0, -3).split("/");
14955
+ const isPackage = relativePath.endsWith("__init__.py");
14956
+ const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14957
+ return relevantParts.map(pythonifyName).join(".");
14958
+ }
14959
+ function deepEqual(a, b) {
14960
+ if (a === b) return true;
14961
+ if (null == a || null == b) return false;
14962
+ if (typeof a !== typeof b) return false;
14963
+ if ("object" != typeof a) return a === b;
14964
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
14965
+ if (Array.isArray(a)) {
14966
+ if (a.length !== b.length) return false;
14967
+ return a.every((item, index)=>deepEqual(item, b[index]));
14905
14968
  }
14906
- async diffNames(fromCommit, toCommit) {
14907
- try {
14908
- const diffResult = await this.git.diff([
14909
- fromCommit,
14910
- toCommit,
14911
- "--name-only"
14912
- ]);
14913
- const files = diffResult.split("\n").filter((f)=>f.trim());
14914
- return files;
14915
- } catch (e) {
14916
- throw errors_createGitError.commandFailed("diff", e.message);
14917
- }
14969
+ const keysA = Object.keys(a);
14970
+ const keysB = Object.keys(b);
14971
+ if (keysA.length !== keysB.length) return false;
14972
+ return keysA.every((key)=>deepEqual(a[key], b[key]));
14973
+ }
14974
+ function parseHtmlWithFrontmatter(htmlContent) {
14975
+ const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14976
+ const match = htmlContent.match(frontmatterPattern);
14977
+ if (match) {
14978
+ const yamlStr = match[1];
14979
+ const html = match[2];
14980
+ const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14981
+ return {
14982
+ frontmatter,
14983
+ html
14984
+ };
14918
14985
  }
14919
- async getAheadBehind(localRef, remoteRef) {
14920
- try {
14921
- const result = await this.git.raw([
14922
- "rev-list",
14923
- "--left-right",
14924
- "--count",
14925
- `${remoteRef}...${localRef}`
14926
- ]);
14927
- const [behind, ahead] = result.trim().split("\t").map(Number);
14928
- return {
14929
- ahead,
14930
- behind
14931
- };
14932
- } catch (e) {
14933
- throw errors_createGitError.commandFailed("rev-list", e.message);
14934
- }
14986
+ return {
14987
+ frontmatter: {},
14988
+ html: htmlContent
14989
+ };
14990
+ }
14991
+ function formatHttpError(prefix, status, body) {
14992
+ if (!body) return `${prefix}: ${status}`;
14993
+ try {
14994
+ const parsed = JSON.parse(body);
14995
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14996
+ } catch {}
14997
+ return `${prefix}: ${status} ${body}`;
14998
+ }
14999
+ const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
15000
+ async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
15001
+ const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
15002
+ try {
15003
+ const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
15004
+ if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
15005
+ const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
15006
+ await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15007
+ return true;
15008
+ } catch (error) {
15009
+ if ("ENOENT" !== error.code) throw error;
15010
+ await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
15011
+ return true;
14935
15012
  }
14936
- async show(refPath) {
14937
- try {
14938
- const content = await this.git.show([
14939
- refPath
14940
- ]);
14941
- return content;
14942
- } catch (e) {
14943
- throw errors_createGitError.commandFailed("show", e.message);
14944
- }
15013
+ }
15014
+ const ANVIL_DEPS_DIR = ".anvil/deps";
15015
+ const ANVIL_DEPS_MANIFEST = "manifest.json";
15016
+ const DEPS_MANIFEST_VERSION = 3;
15017
+ const DEPENDENCY_CACHE_WARNING = "Dependency cache is missing or stale. Run `anvil deps fetch` before validation.";
15018
+ async function fetchAllDependencies(appId, anvilUrl, options = {}) {
15019
+ const { commit, includeDocs = true, knownDependencies = [] } = options;
15020
+ anvilUrl ??= await resolveAuthAnvilUrl();
15021
+ const token = await auth_getValidAuthToken(anvilUrl, options.username);
15022
+ const body = {
15023
+ include_docs: includeDocs,
15024
+ known_dependencies: knownDependencies
15025
+ };
15026
+ if (commit) body.commit = commit;
15027
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
15028
+ method: "POST",
15029
+ headers: {
15030
+ Authorization: `Bearer ${token}`,
15031
+ "Content-Type": "application/json"
15032
+ },
15033
+ body: JSON.stringify(body)
15034
+ });
15035
+ if (!resp.ok) {
15036
+ const errorBody = await resp.text();
15037
+ throw new Error(formatHttpError("Failed to fetch dependencies", resp.status, errorBody));
14945
15038
  }
14946
- async push(url, refSpec, force = false) {
14947
- try {
14948
- const args = force ? [
14949
- "push",
14950
- "--force",
14951
- url,
14952
- refSpec
14953
- ] : [
14954
- "push",
14955
- url,
14956
- refSpec
14957
- ];
14958
- await this.git.raw(args);
14959
- } catch (e) {
14960
- throw errors_createGitError.commandFailed("push", e.message);
14961
- }
15039
+ return await resp.json();
15040
+ }
15041
+ async function refreshDependencyCache(projectRoot, appId, anvilUrl, options = {}) {
15042
+ auth_setRepoContext(projectRoot);
15043
+ anvilUrl ??= await resolveAuthAnvilUrl();
15044
+ const includeDocs = options.includeDocs ?? true;
15045
+ const git = new GitService(projectRoot);
15046
+ const commit = options.commit ?? await git.getCommitId();
15047
+ const sourceYaml = await git.show(`${commit}:anvil.yaml`).catch((error)=>{
15048
+ if (options.commit) return null;
15049
+ throw error;
15050
+ });
15051
+ const sourceDependencies = null === sourceYaml ? null : normalizeAnvilYamlDependenciesFromContent(sourceYaml);
15052
+ const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
15053
+ const response = await fetchAllDependencies(appId, anvilUrl, {
15054
+ username: options.username,
15055
+ commit,
15056
+ includeDocs,
15057
+ knownDependencies
15058
+ });
15059
+ if (!response.resolved_dependencies.length) {
15060
+ await clearDepsCache(projectRoot);
15061
+ return {
15062
+ response,
15063
+ addedGitignoreEntry: false
15064
+ };
14962
15065
  }
14963
- async rebase(onto) {
14964
- try {
14965
- await this.git.raw([
14966
- "rebase",
14967
- onto
14968
- ]);
14969
- } catch (e) {
14970
- throw errors_createGitError.commandFailed("rebase", e.message);
14971
- }
15066
+ const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
15067
+ await writeAllDepsToCache(projectRoot, response, {
15068
+ includeDocs,
15069
+ sourceDependencies
15070
+ });
15071
+ return {
15072
+ response,
15073
+ addedGitignoreEntry
15074
+ };
15075
+ }
15076
+ function depsDir(projectRoot) {
15077
+ return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
15078
+ }
15079
+ function depsManifestPath(projectRoot) {
15080
+ return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
15081
+ }
15082
+ function isResolvedDependencyManifestEntry(value) {
15083
+ if (!value || "object" != typeof value) return false;
15084
+ const entry = value;
15085
+ 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);
15086
+ }
15087
+ function isCachedDependencyManifestEntry(value) {
15088
+ if (!isResolvedDependencyManifestEntry(value)) return false;
15089
+ const entry = value;
15090
+ return "string" == typeof entry.package_name && entry.package_name.length > 0;
15091
+ }
15092
+ function isDepsCacheManifest(value) {
15093
+ if (!value || "object" != typeof value) return false;
15094
+ const manifest = value;
15095
+ 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);
15096
+ }
15097
+ async function readDepsCacheManifest(projectRoot) {
15098
+ try {
15099
+ const raw = await external_fs_promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
15100
+ const parsed = JSON.parse(raw);
15101
+ return isDepsCacheManifest(parsed) ? parsed : void 0;
15102
+ } catch {
15103
+ return;
14972
15104
  }
14973
- async rebaseAbort() {
14974
- try {
14975
- await this.git.raw([
14976
- "rebase",
14977
- "--abort"
14978
- ]);
14979
- } catch (e) {
14980
- throw errors_createGitError.commandFailed("rebase --abort", e.message);
14981
- }
15105
+ }
15106
+ async function getKnownDependenciesForCache(projectRoot, includeDocs) {
15107
+ const manifest = await readDepsCacheManifest(projectRoot);
15108
+ if (!manifest || manifest.includeDocs !== includeDocs) return [];
15109
+ return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
15110
+ app_id,
15111
+ commit_id
15112
+ }));
15113
+ }
15114
+ function getDependencyWatchSubscriptionsFromManifest(manifest) {
15115
+ if (!manifest) return [];
15116
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
15117
+ dep.app_id,
15118
+ dep
15119
+ ]));
15120
+ return manifest.resolved_dependencies.flatMap((dep)=>{
15121
+ if (!dep.branch) return [];
15122
+ return [
15123
+ {
15124
+ appId: dep.app_id,
15125
+ branch: dep.branch,
15126
+ packageName: cachedByAppId.get(dep.app_id)?.package_name
15127
+ }
15128
+ ];
15129
+ });
15130
+ }
15131
+ function normalizeAnvilYamlDependenciesFromContent(content) {
15132
+ const parsed = external_js_yaml_default().load(content);
15133
+ if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
15134
+ return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
15135
+ }
15136
+ function normalizeAnvilYamlDependenciesValue(value) {
15137
+ if (!Array.isArray(value)) return [];
15138
+ return value.map(normalizeForComparison);
15139
+ }
15140
+ async function readAnvilYamlDependencies(projectRoot) {
15141
+ try {
15142
+ const content = await external_fs_promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
15143
+ return normalizeAnvilYamlDependenciesFromContent(content);
15144
+ } catch {
15145
+ return [];
14982
15146
  }
14983
- async isAncestor(ancestorRef, descendantRef) {
15147
+ }
15148
+ async function getDependencyCacheWarning(projectRoot) {
15149
+ const sourceDependencies = await readAnvilYamlDependencies(projectRoot);
15150
+ if (!Array.isArray(sourceDependencies) || 0 === sourceDependencies.length) return;
15151
+ const manifest = await readDepsCacheManifest(projectRoot);
15152
+ if (!manifest || !anvilYamlDependenciesEqual(sourceDependencies, manifest.source_dependencies)) return DEPENDENCY_CACHE_WARNING;
15153
+ if (0 === manifest.resolved_dependencies.length) return DEPENDENCY_CACHE_WARNING;
15154
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dependency)=>[
15155
+ dependency.app_id,
15156
+ dependency
15157
+ ]));
15158
+ for (const dependency of manifest.resolved_dependencies){
15159
+ const cached = cachedByAppId.get(dependency.app_id);
15160
+ if (!cached || cached.commit_id !== dependency.commit_id) return DEPENDENCY_CACHE_WARNING;
14984
15161
  try {
14985
- const mergeBase = (await this.git.raw([
14986
- "merge-base",
14987
- ancestorRef,
14988
- descendantRef
14989
- ])).trim();
14990
- const ancestorCommit = (await this.git.revparse([
14991
- ancestorRef
14992
- ])).trim();
14993
- return mergeBase === ancestorCommit;
15162
+ await external_fs_promises_default().access(external_path_default().join(depsDir(projectRoot), cached.package_name, "anvil.yaml"));
14994
15163
  } catch {
14995
- return false;
15164
+ return DEPENDENCY_CACHE_WARNING;
14996
15165
  }
14997
15166
  }
14998
- async mergeFastForward(ref) {
14999
- try {
15000
- await this.git.raw([
15001
- "merge",
15002
- "--ff-only",
15003
- ref
15004
- ]);
15005
- } catch (e) {
15006
- throw errors_createGitError.commandFailed("merge --ff-only", e.message);
15167
+ }
15168
+ function anvilYamlDependenciesEqual(left, right) {
15169
+ return deepEqual(left, right);
15170
+ }
15171
+ function normalizeForComparison(value) {
15172
+ if (Array.isArray(value)) return value.map(normalizeForComparison);
15173
+ if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
15174
+ key,
15175
+ normalizeForComparison(entryValue)
15176
+ ]));
15177
+ return value;
15178
+ }
15179
+ function cachedManifestEntry(dep, packageName) {
15180
+ return {
15181
+ app_id: dep.app_id,
15182
+ commit_id: dep.commit_id,
15183
+ ...dep.branch ? {
15184
+ branch: dep.branch
15185
+ } : {},
15186
+ package_name: packageName
15187
+ };
15188
+ }
15189
+ function safeDependencyPath(filePath, kind) {
15190
+ const normalizedPath = external_path_default().normalize(filePath);
15191
+ if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
15192
+ return normalizedPath;
15193
+ }
15194
+ function dependencyPythonPath(name, kind) {
15195
+ const parts = name.split(".");
15196
+ if (0 === parts.length || parts.some((part)=>!part || "." === part || ".." === part || part.includes("/") || part.includes("\\"))) throw new Error(`Invalid dependency ${kind} name: ${name}`);
15197
+ return external_path_default().join(...parts);
15198
+ }
15199
+ async function writeDepToCache(projectRoot, dep) {
15200
+ const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
15201
+ await external_fs_promises_default().rm(depDir, {
15202
+ recursive: true,
15203
+ force: true
15204
+ });
15205
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
15206
+ recursive: true
15207
+ });
15208
+ await external_fs_promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
15209
+ recursive: true
15210
+ });
15211
+ await external_fs_promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
15212
+ package_name: dep.package_name
15213
+ }));
15214
+ for (const mod of dep.modules ?? []){
15215
+ const modulePath = external_path_default().join(depDir, "client_code", dependencyPythonPath(mod.name, "module"));
15216
+ if (mod.is_package) {
15217
+ await external_fs_promises_default().mkdir(modulePath, {
15218
+ recursive: true
15219
+ });
15220
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15221
+ } else {
15222
+ const moduleFile = `${modulePath}.py`;
15223
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15224
+ recursive: true
15225
+ });
15226
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15007
15227
  }
15008
15228
  }
15009
- async deleteRef(ref) {
15010
- try {
15011
- await this.git.raw([
15012
- "update-ref",
15013
- "-d",
15014
- ref
15015
- ]);
15016
- } catch (e) {}
15229
+ for (const form of dep.forms ?? []){
15230
+ const { code, class_name, is_package, ...formTemplate } = form;
15231
+ const formPath = external_path_default().join(depDir, "client_code", dependencyPythonPath(class_name, "form"));
15232
+ if (is_package) {
15233
+ await external_fs_promises_default().mkdir(formPath, {
15234
+ recursive: true
15235
+ });
15236
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "__init__.py"), code);
15237
+ await external_fs_promises_default().writeFile(external_path_default().join(formPath, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
15238
+ } else {
15239
+ const formCodeFile = `${formPath}.py`;
15240
+ await external_fs_promises_default().mkdir(external_path_default().dirname(formCodeFile), {
15241
+ recursive: true
15242
+ });
15243
+ await external_fs_promises_default().writeFile(formCodeFile, code);
15244
+ await external_fs_promises_default().writeFile(`${formPath}.yaml`, external_js_yaml_default().dump(formTemplate));
15245
+ }
15017
15246
  }
15018
- async getRemotes() {
15019
- try {
15020
- const remotes = await this.git.getRemotes(true);
15021
- return remotes.map((r)=>({
15022
- name: r.name,
15023
- fetchUrl: r.refs.fetch
15024
- }));
15025
- } catch (e) {
15026
- throw errors_createGitError.commandFailed("remote", e.message);
15247
+ for (const mod of dep.server_modules ?? []){
15248
+ const modulePath = external_path_default().join(depDir, "server_code", dependencyPythonPath(mod.name, "server module"));
15249
+ if (mod.is_package) {
15250
+ await external_fs_promises_default().mkdir(modulePath, {
15251
+ recursive: true
15252
+ });
15253
+ await external_fs_promises_default().writeFile(external_path_default().join(modulePath, "__init__.py"), mod.code);
15254
+ } else {
15255
+ const moduleFile = `${modulePath}.py`;
15256
+ await external_fs_promises_default().mkdir(external_path_default().dirname(moduleFile), {
15257
+ recursive: true
15258
+ });
15259
+ await external_fs_promises_default().writeFile(moduleFile, mod.code);
15027
15260
  }
15028
15261
  }
15029
- async isGitInitialized() {
15030
- const gitPath = external_path_default().join(this.repoPath, ".git");
15031
- return external_fs_.existsSync(gitPath);
15262
+ for (const asset of dep.assets ?? []){
15263
+ const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
15264
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15265
+ recursive: true
15266
+ });
15267
+ await external_fs_promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
15032
15268
  }
15033
- async removeEmptyDirectories(removedFiles) {
15034
- const directoriesToCheck = new Set();
15035
- for (const file of removedFiles){
15036
- let dir = external_path_default().dirname(file);
15037
- while(dir && "." !== dir){
15038
- directoriesToCheck.add(dir);
15039
- const parentDir = external_path_default().dirname(dir);
15040
- if (parentDir === dir) break;
15041
- dir = parentDir;
15042
- }
15043
- }
15044
- const sortedDirs = Array.from(directoriesToCheck).sort((a, b)=>b.split("/").length - a.split("/").length);
15045
- for (const dir of sortedDirs)try {
15046
- const dirPath = external_path_default().join(this.repoPath, dir);
15047
- if (!external_fs_.existsSync(dirPath)) continue;
15048
- const entries = await external_fs_.promises.readdir(dirPath);
15049
- if (0 === entries.length) await external_fs_.promises.rmdir(dirPath);
15050
- } catch (error) {}
15269
+ for (const doc of dep.docs ?? []){
15270
+ const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
15271
+ await external_fs_promises_default().mkdir(external_path_default().dirname(targetPath), {
15272
+ recursive: true
15273
+ });
15274
+ await external_fs_promises_default().writeFile(targetPath, doc.content);
15051
15275
  }
15052
15276
  }
15053
- async function getStagedFileChanges(git, gitService) {
15277
+ async function writeAllDepsToCache(projectRoot, response, options = {}) {
15278
+ const includeDocs = options.includeDocs ?? true;
15279
+ const manifest = await readDepsCacheManifest(projectRoot);
15280
+ const canReuseExistingCache = manifest?.includeDocs === includeDocs;
15281
+ if (!canReuseExistingCache) await clearDepsCache(projectRoot);
15282
+ const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
15283
+ const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
15284
+ 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), {
15285
+ recursive: true,
15286
+ force: true
15287
+ });
15288
+ for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
15289
+ const returnedByAppId = new Map(response.dependencies.map((dep)=>[
15290
+ dep.app_id,
15291
+ dep
15292
+ ]));
15293
+ const previousByAppId = new Map(previousCached.map((dep)=>[
15294
+ dep.app_id,
15295
+ dep
15296
+ ]));
15297
+ const cachedDependencies = response.resolved_dependencies.map((dep)=>{
15298
+ const returned = returnedByAppId.get(dep.app_id);
15299
+ if (returned) return cachedManifestEntry(returned, returned.package_name);
15300
+ const previous = previousByAppId.get(dep.app_id);
15301
+ if (!previous) return;
15302
+ return cachedManifestEntry(dep, previous.package_name);
15303
+ }).filter((dep)=>void 0 !== dep);
15304
+ const newManifest = {
15305
+ version: DEPS_MANIFEST_VERSION,
15306
+ includeDocs,
15307
+ source_dependencies: void 0 === options.sourceDependencies ? await readAnvilYamlDependencies(projectRoot) : options.sourceDependencies,
15308
+ resolved_dependencies: response.resolved_dependencies,
15309
+ cached_dependencies: cachedDependencies
15310
+ };
15311
+ await external_fs_promises_default().mkdir(depsDir(projectRoot), {
15312
+ recursive: true
15313
+ });
15314
+ await external_fs_promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
15315
+ }
15316
+ async function clearDepsCache(projectRoot) {
15317
+ await external_fs_promises_default().rm(depsDir(projectRoot), {
15318
+ recursive: true,
15319
+ force: true
15320
+ });
15321
+ }
15322
+ async function getCachedPackages(projectRoot) {
15054
15323
  try {
15055
- const status = await git.status();
15056
- const renames = status.renamed || [];
15057
- const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
15058
- const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
15059
- const stagedRenames = [];
15060
- for (const f of stagedFiles)if ("R" === f.index) {
15061
- const rename = renames.find((r)=>r.to === f.path);
15062
- if (rename) stagedRenames.push({
15063
- path: rename.to,
15064
- type: "rename",
15065
- from: rename.from
15066
- });
15067
- }
15068
- const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
15069
- const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
15070
- const changes = [];
15071
- for (const f of stagedFiles){
15072
- if ("R" !== f.index) {
15073
- if (!stagedModeOnlyPaths.has(f.path)) {
15074
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
15075
- path: f.path,
15076
- type: "unlink"
15077
- });
15078
- else if ("A" === f.index || "?" === f.index) changes.push({
15079
- path: f.path,
15080
- type: "add"
15081
- });
15082
- else changes.push({
15083
- path: f.path,
15084
- type: "change"
15085
- });
15086
- }
15087
- }
15088
- }
15089
- changes.push(...stagedRenames);
15090
- return changes;
15091
- } catch (e) {
15092
- throw errors_createGitError.commandFailed("status", e.message);
15324
+ const entries = await external_fs_promises_default().readdir(depsDir(projectRoot), {
15325
+ withFileTypes: true
15326
+ });
15327
+ return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
15328
+ } catch {
15329
+ return [];
15093
15330
  }
15094
15331
  }
15095
15332
  const external_chokidar_namespaceObject = require("chokidar");
@@ -15863,7 +16100,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15863
16100
  if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15864
16101
  }
15865
16102
  const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15866
- const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
16103
+ const LEGACY_CUSTOM_COMPONENT_SPEC_RE = /^form:(?:([^:]+):)?(.+)$/;
16104
+ const PACKAGE_QUALIFIED_FORM_NAME_RE = /^([^.]+)\.(.+)$/;
15867
16105
  const componentTypeSpec_PYTHON_KEYWORDS = new Set([
15868
16106
  "False",
15869
16107
  "None",
@@ -15903,32 +16141,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15903
16141
  "match",
15904
16142
  "case"
15905
16143
  ]);
15906
- const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
16144
+ const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form, form:APP_ID:Package.Form)";
15907
16145
  const COMPONENT_TYPE_REFERENCE_MESSAGE = "must be a known Anvil component type (see componentIcons.ts), a form: reference, or a package-qualified form reference (Package.Form)";
15908
16146
  function isValidIdentifierSegment(value) {
15909
16147
  return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
15910
16148
  }
15911
16149
  function isValidLegacyFormTypeReference(typeName) {
15912
- if (!typeName.startsWith("form:")) return false;
15913
- const rest = typeName.slice(5);
15914
- if (0 === rest.length) return false;
15915
- let classPath = rest;
15916
- if (rest.startsWith("dep_")) {
15917
- const splitIndex = rest.indexOf(":");
15918
- if (splitIndex <= 0) return false;
15919
- const depId = rest.slice(0, splitIndex);
15920
- if (!DEP_ID_RE.test(depId)) return false;
15921
- classPath = rest.slice(splitIndex + 1);
15922
- }
15923
- if (0 === classPath.length) return false;
15924
- return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
16150
+ return LEGACY_CUSTOM_COMPONENT_SPEC_RE.test(typeName);
15925
16151
  }
15926
16152
  function isValidPackageQualifiedComponentType(typeName) {
15927
16153
  if (typeName.startsWith("anvil.")) return false;
15928
- if (!typeName.includes(".")) return false;
15929
- const parts = typeName.split(".");
15930
- if (parts.length < 2) return false;
15931
- return parts.every((part)=>isValidIdentifierSegment(part));
16154
+ return PACKAGE_QUALIFIED_FORM_NAME_RE.test(typeName);
15932
16155
  }
15933
16156
  function componentTypeSpec_pushIssue(issues, path, message) {
15934
16157
  issues.push({
@@ -16011,8 +16234,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16011
16234
  message
16012
16235
  });
16013
16236
  }
16237
+ function validateNonEmptyString(value, path, issues) {
16238
+ if ("string" != typeof value || 0 === value.length) {
16239
+ formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
16240
+ return false;
16241
+ }
16242
+ return true;
16243
+ }
16014
16244
  function validateIdentifier(value, path, issues) {
16015
- if ("string" != typeof value || 0 === value.length) return void formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
16245
+ if (!validateNonEmptyString(value, path, issues)) return;
16016
16246
  if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
16017
16247
  if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
16018
16248
  }
@@ -16037,7 +16267,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16037
16267
  const bindingPath = `${path}[${index}]`;
16038
16268
  if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
16039
16269
  if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
16040
- if ("string" != typeof binding.code || 0 === binding.code.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
16270
+ if ("string" != typeof binding.code) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a string");
16041
16271
  if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
16042
16272
  });
16043
16273
  }
@@ -16095,7 +16325,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16095
16325
  validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
16096
16326
  if (isPlainObject(value.target)) {
16097
16327
  if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
16098
- if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
16328
+ if ("slot" === value.target.type) validateNonEmptyString(value.target.name, `${path}.target.name`, issues);
16329
+ else if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
16099
16330
  } else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
16100
16331
  if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
16101
16332
  }
@@ -16152,7 +16383,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16152
16383
  } else {
16153
16384
  const componentNames = options.componentNames ?? new Set();
16154
16385
  for (const [slotName, slotDef] of Object.entries(value.slots)){
16155
- validateIdentifier(slotName, path(`slots.${slotName}.name`), issues);
16386
+ validateNonEmptyString(slotName, path(`slots.${slotName}.name`), issues);
16156
16387
  validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
16157
16388
  }
16158
16389
  }
@@ -16160,6 +16391,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16160
16391
  }
16161
16392
  if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
16162
16393
  if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
16394
+ if (void 0 !== value.help_text && "string" != typeof value.help_text) formTemplateValidation_pushIssue(issues, path("help_text"), "must be a string");
16163
16395
  if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
16164
16396
  if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16165
16397
  } else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
@@ -16234,7 +16466,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16234
16466
  "toolbox_item",
16235
16467
  "layout_metadata",
16236
16468
  "item_type",
16237
- "slots"
16469
+ "slots",
16470
+ "help_text"
16238
16471
  ]);
16239
16472
  const FRONTMATTER_DISALLOWED_KEYS = new Set([
16240
16473
  "container",
@@ -16353,7 +16586,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16353
16586
  function validateParsedHtmlTemplate(parsed) {
16354
16587
  return validateFormTemplateData(parsed);
16355
16588
  }
16356
- function validateFormTemplateHtml(htmlContent, parseHtml = form_template_parser_namespaceObject.parseSerializedHtml) {
16589
+ function validateFormTemplateHtml(htmlContent, parseHtml = form_template_parser_namespaceObject.parseSerializedHtml, options = {}) {
16357
16590
  const { frontmatter, body, bodyStartOffset } = splitHtmlFormTemplateFrontmatter(htmlContent);
16358
16591
  if (null !== frontmatter) {
16359
16592
  const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
@@ -16366,7 +16599,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16366
16599
  let parsed;
16367
16600
  try {
16368
16601
  parsed = parseHtml(body, {
16369
- warnings: true
16602
+ warnings: true,
16603
+ ...options.formSpecPackageContext ? {
16604
+ formSpecPackageContext: options.formSpecPackageContext
16605
+ } : {}
16370
16606
  });
16371
16607
  } catch (error) {
16372
16608
  return {
@@ -16397,6 +16633,58 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16397
16633
  ok: true
16398
16634
  };
16399
16635
  }
16636
+ const validatePython_requireFromHere = (0, external_module_namespaceObject.createRequire)(__filename);
16637
+ const DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB = 768;
16638
+ const DEFAULT_PYRIGHT_TIMEOUT_MS = 300000;
16639
+ function parsePositiveIntegerEnv(env, name, defaultValue) {
16640
+ const rawValue = env[name];
16641
+ if (void 0 === rawValue || "" === rawValue) return {
16642
+ value: defaultValue
16643
+ };
16644
+ if (!/^\d+$/.test(rawValue) || Number(rawValue) <= 0 || !Number.isSafeInteger(Number(rawValue))) return {
16645
+ error: `${name} must be a positive integer`
16646
+ };
16647
+ return {
16648
+ value: Number(rawValue)
16649
+ };
16650
+ }
16651
+ function pyrightChildNodeOptions(nodeOptions, maxOldSpaceMb) {
16652
+ const maxOldSpaceFlag = "--max[-_]old[-_]space[-_]size";
16653
+ const withoutEqualsForm = (nodeOptions ?? "").replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}=\\S+`, "g"), "$1");
16654
+ const withoutSeparateForm = withoutEqualsForm.replace(new RegExp(`(^|\\s)${maxOldSpaceFlag}\\s+\\S+`, "g"), "$1");
16655
+ return [
16656
+ withoutSeparateForm.trim(),
16657
+ `--max-old-space-size=${maxOldSpaceMb}`
16658
+ ].filter(Boolean).join(" ");
16659
+ }
16660
+ function semanticProcessFailure(message) {
16661
+ return {
16662
+ ok: false,
16663
+ message: "Python semantic validation could not be run",
16664
+ issues: [
16665
+ {
16666
+ path: "root",
16667
+ message,
16668
+ severity: "error"
16669
+ }
16670
+ ]
16671
+ };
16672
+ }
16673
+ function isV8HeapOutOfMemory(stderr) {
16674
+ return /(?:JavaScript heap out of memory|Reached heap limit|Ineffective mark-compacts near heap limit)/i.test(stderr);
16675
+ }
16676
+ function resolveBundledAnvilPyright() {
16677
+ try {
16678
+ return {
16679
+ command: process.execPath,
16680
+ args: [
16681
+ validatePython_requireFromHere.resolve("@anvil-works/anvil-pyright")
16682
+ ]
16683
+ };
16684
+ } catch {
16685
+ return null;
16686
+ }
16687
+ }
16400
16688
  const PYTHON_VALIDATOR_SCRIPT = String.raw`
16401
16689
  import ast
16402
16690
  import json
@@ -16502,6 +16790,36 @@ print(json.dumps({"issues": issues}))
16502
16790
  }
16503
16791
  return null;
16504
16792
  }
16793
+ function findAnvilPyright(options = {}) {
16794
+ const env = options.env ?? process.env;
16795
+ const probe = options.probe ?? external_child_process_namespaceObject.spawnSync;
16796
+ const bundled = resolveBundledAnvilPyright();
16797
+ const candidates = [
16798
+ ..."string" == typeof env.ANVIL_PYRIGHT && env.ANVIL_PYRIGHT.length > 0 ? [
16799
+ {
16800
+ command: env.ANVIL_PYRIGHT,
16801
+ args: []
16802
+ }
16803
+ ] : [],
16804
+ ...bundled ? [
16805
+ bundled
16806
+ ] : [],
16807
+ {
16808
+ command: "anvil-pyright",
16809
+ args: []
16810
+ }
16811
+ ];
16812
+ for (const candidate of candidates){
16813
+ const result = probe(candidate.command, [
16814
+ ...candidate.args,
16815
+ "--version"
16816
+ ], {
16817
+ encoding: "utf8"
16818
+ });
16819
+ if (!result.error && 0 === result.status) return candidate;
16820
+ }
16821
+ return null;
16822
+ }
16505
16823
  function isClientFormPython(filePath) {
16506
16824
  if ("__init__.py" === external_path_default().basename(filePath)) {
16507
16825
  const formDir = external_path_default().dirname(filePath);
@@ -16580,6 +16898,126 @@ print(json.dumps({"issues": issues}))
16580
16898
  ok: true
16581
16899
  };
16582
16900
  }
16901
+ function pyrightIssuePath(diagnostic) {
16902
+ const line = diagnostic.range?.start?.line;
16903
+ const character = diagnostic.range?.start?.character;
16904
+ if ("number" != typeof line) return "root";
16905
+ if ("number" != typeof character) return `line ${line + 1}`;
16906
+ return `line ${line + 1}, column ${character + 1}`;
16907
+ }
16908
+ function pyrightIssueMessage(diagnostic) {
16909
+ if ("string" == typeof diagnostic.message && diagnostic.message.length > 0) return diagnostic.message;
16910
+ return "Python semantic validation failed";
16911
+ }
16912
+ function pyrightDiagnosticIssue(diagnostic, severity) {
16913
+ const line = diagnostic.range?.start?.line;
16914
+ const column = diagnostic.range?.start?.character;
16915
+ const endLine = diagnostic.range?.end?.line;
16916
+ const endColumn = diagnostic.range?.end?.character;
16917
+ return {
16918
+ ...diagnostic.file ? {
16919
+ file: external_path_default().resolve(diagnostic.file)
16920
+ } : {},
16921
+ path: pyrightIssuePath(diagnostic),
16922
+ message: pyrightIssueMessage(diagnostic),
16923
+ ...diagnostic.rule ? {
16924
+ code: diagnostic.rule,
16925
+ rule: diagnostic.rule
16926
+ } : {},
16927
+ severity,
16928
+ ..."number" == typeof line ? {
16929
+ line: line + 1
16930
+ } : {},
16931
+ ..."number" == typeof column ? {
16932
+ column: column + 1
16933
+ } : {},
16934
+ ..."number" == typeof endLine ? {
16935
+ endLine: endLine + 1
16936
+ } : {},
16937
+ ..."number" == typeof endColumn ? {
16938
+ endColumn: endColumn + 1
16939
+ } : {}
16940
+ };
16941
+ }
16942
+ function validatePythonWithPyright(appRoot, options = {}) {
16943
+ const env = options.env ?? process.env;
16944
+ const maxOldSpace = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_MAX_OLD_SPACE_MB", DEFAULT_PYRIGHT_MAX_OLD_SPACE_MB);
16945
+ if ("error" in maxOldSpace) return semanticProcessFailure(maxOldSpace.error);
16946
+ const timeout = parsePositiveIntegerEnv(env, "ANVIL_PYRIGHT_TIMEOUT_MS", DEFAULT_PYRIGHT_TIMEOUT_MS);
16947
+ if ("error" in timeout) return semanticProcessFailure(timeout.error);
16948
+ const pyright = void 0 === options.pyright ? findAnvilPyright({
16949
+ env
16950
+ }) : options.pyright;
16951
+ if (!pyright) return {
16952
+ ok: false,
16953
+ message: "Python semantic validation could not be run",
16954
+ issues: [
16955
+ {
16956
+ path: "root",
16957
+ message: "Python semantic validation requires the bundled @anvil-works/anvil-pyright package, anvil-pyright on PATH, or ANVIL_PYRIGHT to be set.",
16958
+ severity: "error"
16959
+ }
16960
+ ]
16961
+ };
16962
+ const result = (0, external_child_process_namespaceObject.spawnSync)(pyright.command, [
16963
+ ...pyright.args,
16964
+ "--outputjson",
16965
+ "--level",
16966
+ "warning",
16967
+ "--project",
16968
+ appRoot,
16969
+ ...options.files ?? []
16970
+ ], {
16971
+ encoding: "utf8",
16972
+ env: {
16973
+ ...env,
16974
+ NODE_OPTIONS: pyrightChildNodeOptions(env.NODE_OPTIONS, maxOldSpace.value)
16975
+ },
16976
+ maxBuffer: 10485760,
16977
+ timeout: timeout.value
16978
+ });
16979
+ if (result.error) {
16980
+ 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.`);
16981
+ return semanticProcessFailure(result.error.message);
16982
+ }
16983
+ 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.`);
16984
+ if (0 !== result.status && 1 !== result.status) return semanticProcessFailure(result.stderr.trim() || `anvil-pyright exited with status ${result.status}`);
16985
+ let parsed;
16986
+ try {
16987
+ parsed = JSON.parse(result.stdout.trim());
16988
+ } catch (error) {
16989
+ const detail = result.stderr.trim() || result.stdout.trim();
16990
+ return {
16991
+ ok: false,
16992
+ message: "Python semantic validation could not be read",
16993
+ issues: [
16994
+ {
16995
+ path: "root",
16996
+ message: detail || (error instanceof Error ? error.message : String(error)),
16997
+ severity: "error"
16998
+ }
16999
+ ]
17000
+ };
17001
+ }
17002
+ const issues = (parsed.generalDiagnostics ?? []).filter((diagnostic)=>"error" === diagnostic.severity).map((diagnostic)=>pyrightDiagnosticIssue(diagnostic, "error"));
17003
+ const warnings = (parsed.generalDiagnostics ?? []).filter((diagnostic)=>"warning" === diagnostic.severity).map((diagnostic)=>pyrightDiagnosticIssue(diagnostic, "warning"));
17004
+ if (issues.length > 0) return {
17005
+ ok: false,
17006
+ message: "Python semantic validation failed",
17007
+ issues,
17008
+ ...warnings.length > 0 ? {
17009
+ warnings
17010
+ } : {}
17011
+ };
17012
+ return warnings.length > 0 ? {
17013
+ ok: true,
17014
+ issues: [],
17015
+ warnings
17016
+ } : {
17017
+ ok: true,
17018
+ issues: []
17019
+ };
17020
+ }
16583
17021
  const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16584
17022
  const DB_ACCESS_LEVELS = new Set([
16585
17023
  "none",
@@ -16860,7 +17298,7 @@ print(json.dumps({"issues": issues}))
16860
17298
  }
16861
17299
  function validateScheduledTasks(value, path) {
16862
17300
  const issues = [];
16863
- if (void 0 === value) return issues;
17301
+ if (null == value) return issues;
16864
17302
  if (!Array.isArray(value)) {
16865
17303
  validators_pushIssue(issues, path, "must be an array");
16866
17304
  return issues;
@@ -17008,15 +17446,25 @@ print(json.dumps({"issues": issues}))
17008
17446
  const existingPath = resolveExistingPath(filePath);
17009
17447
  if (!existingPath) return null;
17010
17448
  let currentDir = external_fs_.statSync(existingPath).isDirectory() ? existingPath : external_path_default().dirname(existingPath);
17011
- let appRoot = null;
17449
+ const candidates = [];
17012
17450
  while(true){
17013
- if (external_fs_.existsSync(external_path_default().join(currentDir, "anvil.yaml"))) appRoot = currentDir;
17451
+ if (external_fs_.existsSync(external_path_default().join(currentDir, "anvil.yaml"))) candidates.push(currentDir);
17014
17452
  const parentDir = external_path_default().dirname(currentDir);
17015
- if (parentDir === currentDir) return appRoot;
17453
+ if (parentDir === currentDir) break;
17016
17454
  currentDir = parentDir;
17017
17455
  }
17456
+ return candidates.find((candidate)=>!candidates.some((outerCandidate)=>{
17457
+ if (candidate === outerCandidate) return false;
17458
+ return [
17459
+ "client_code",
17460
+ "server_code"
17461
+ ].some((codeDir)=>{
17462
+ const relativePath = external_path_default().relative(external_path_default().join(outerCandidate, codeDir), candidate);
17463
+ return !external_path_default().isAbsolute(relativePath) && ".." !== relativePath && !relativePath.startsWith(`..${external_path_default().sep}`);
17464
+ });
17465
+ })) ?? null;
17018
17466
  }
17019
- function validatePath(filePath, fileContent) {
17467
+ function validatePath(filePath, fileContent, options = {}) {
17020
17468
  const target = inferValidationTarget(filePath);
17021
17469
  const existingPath = resolveExistingPath(filePath);
17022
17470
  const appRoot = findAnvilAppRoot(filePath);
@@ -17089,7 +17537,9 @@ print(json.dumps({"issues": issues}))
17089
17537
  ]
17090
17538
  };
17091
17539
  }
17092
- const result = validateFormTemplateHtml(fileContent);
17540
+ const result = validateFormTemplateHtml(fileContent, void 0, {
17541
+ formSpecPackageContext: options.formSpecPackageContext
17542
+ });
17093
17543
  return result.ok ? validPathResult(target, result.warnings) : {
17094
17544
  ...result,
17095
17545
  target
@@ -17906,7 +18356,7 @@ print(json.dumps({"issues": issues}))
17906
18356
  ]
17907
18357
  });
17908
18358
  }
17909
- async function buildFormTemplateData(relativePath, templateInfo, context) {
18359
+ async function buildFormTemplateData(relativePath, templateInfo, context, missingCodeBehavior = "error") {
17910
18360
  const { repoPath, stagedOnly = false } = context;
17911
18361
  const { kind } = templateInfo;
17912
18362
  const loadResult = await loadFormTemplateData(relativePath, templateInfo, context);
@@ -17920,15 +18370,28 @@ print(json.dumps({"issues": issues}))
17920
18370
  };
17921
18371
  const codeRelativePath = "package" === kind ? relativePath.replace(/form_template\.(yaml|html)$/i, "__init__.py") : relativePath.replace(/\.(yaml|html)$/i, ".py");
17922
18372
  const className = extractPythonName(codeRelativePath);
17923
- const codeContent = await readFileContent(repoPath, codeRelativePath, stagedOnly);
17924
- if (!codeContent) return {
17925
- formData: {},
17926
- className: "",
17927
- ignoreResult: {
17928
- type: "ignore",
17929
- reason: "package" === kind ? "Form __init__.py file not found - cannot save template without code" : "Form .py file not found - cannot save template without code"
18373
+ let codeContent;
18374
+ try {
18375
+ codeContent = await readFileContent(repoPath, codeRelativePath, stagedOnly);
18376
+ } catch (error) {
18377
+ if (stagedOnly || !isFileNotFoundError(error) || "error" === missingCodeBehavior) throw error;
18378
+ const missingCodeReason = `Form code file ${codeRelativePath} is not present yet`;
18379
+ const knownForm = context.editorYaml.hasId("forms", className);
18380
+ if ("defer" === missingCodeBehavior || knownForm) {
18381
+ const action = knownForm ? `not saving existing form ${className}` : `deferring form ${className}`;
18382
+ logger_logger.warn(external_chalk_default().yellow(`Form template ${relativePath} has no code file ${codeRelativePath} yet; ${action}.`));
18383
+ return {
18384
+ formData: {},
18385
+ className: "",
18386
+ ignoreResult: {
18387
+ type: "ignore",
18388
+ reason: missingCodeReason
18389
+ }
18390
+ };
17930
18391
  }
17931
- };
18392
+ codeContent = "";
18393
+ logger_logger.warn(external_chalk_default().yellow(`Form template ${relativePath} has no code file ${codeRelativePath} yet; creating ${className} with empty code.`));
18394
+ }
17932
18395
  const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
17933
18396
  className,
17934
18397
  code: codeContent,
@@ -17940,6 +18403,9 @@ print(json.dumps({"issues": issues}))
17940
18403
  ignoreResult: null
17941
18404
  };
17942
18405
  }
18406
+ function isFileNotFoundError(error) {
18407
+ return "object" == typeof error && null !== error && "ENOENT" === error.code;
18408
+ }
17943
18409
  async function handleFormTemplateChange(change, context) {
17944
18410
  const { editorYaml } = context;
17945
18411
  const { relativePath, changeType } = change;
@@ -17950,10 +18416,11 @@ print(json.dumps({"issues": issues}))
17950
18416
  };
17951
18417
  const kind = templateInfo.kind;
17952
18418
  const format = templateInfo.format;
18419
+ const missingCodeBehavior = "add" === changeType ? "create-new-form-if-unknown" : "defer";
17953
18420
  const result = await buildFormTemplateData(relativePath, {
17954
18421
  kind,
17955
18422
  format
17956
- }, context);
18423
+ }, context, missingCodeBehavior);
17957
18424
  if (result.ignoreResult) return result.ignoreResult;
17958
18425
  const { formData, className } = result;
17959
18426
  return handleIdBasedOperation({
@@ -18239,10 +18706,13 @@ print(json.dumps({"issues": issues}))
18239
18706
  code: error.type,
18240
18707
  recoverability: "requires_user_action",
18241
18708
  requiresUserAction: true,
18242
- suggestedAction: "Inspect the repository state and resolve the Git error before retrying.",
18709
+ suggestedAction: isIndexLockFailure(error) ? "Confirm no Git process is running for this repository, then remove the stale .git/index.lock file and retry." : "Inspect the repository state and resolve the Git error before retrying.",
18243
18710
  gitCommand: "git_command_failed" === error.type ? error.command : void 0
18244
18711
  };
18245
18712
  }
18713
+ function isIndexLockFailure(error) {
18714
+ return "git_command_failed" === error.type && error.message.includes("Unable to create") && error.message.includes("index.lock") && error.message.includes("File exists");
18715
+ }
18246
18716
  function networkDiagnostic(error) {
18247
18717
  return {
18248
18718
  code: error.type,
@@ -18327,6 +18797,15 @@ print(json.dumps({"issues": issues}))
18327
18797
  this.hasPendingChanges = true;
18328
18798
  await this.processSaveBatchWithEvents();
18329
18799
  }
18800
+ async flush() {
18801
+ const requireReady = ()=>{
18802
+ if (this.config.isPaused() || this.config.isCleanedUp() || !this.config.isSettlePeriodOver()) throw new Error("Cannot flush saves while watching is paused, closed, or changing branch");
18803
+ };
18804
+ requireReady();
18805
+ await this.forceSave();
18806
+ requireReady();
18807
+ if (this.hasPendingChanges) throw new Error("Saves remain pending");
18808
+ }
18330
18809
  cleanup() {
18331
18810
  if (this.hasPendingChanges) {
18332
18811
  logger_logger.progressEnd("sync");
@@ -18423,10 +18902,7 @@ print(json.dumps({"issues": issues}))
18423
18902
  status = await this.config.gitService.getStatus();
18424
18903
  } catch (e) {
18425
18904
  logger_logger.error(external_chalk_default().red(`Error reading git status: ${errors_getErrorMessage(e)}`));
18426
- return {
18427
- skipped: true,
18428
- reason: "Git status error"
18429
- };
18905
+ throw e;
18430
18906
  }
18431
18907
  let allChanges;
18432
18908
  if (this.config.stagedOnly) {
@@ -19272,6 +19748,7 @@ print(json.dumps({"issues": issues}))
19272
19748
  try {
19273
19749
  const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
19274
19750
  const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
19751
+ username: this.username,
19275
19752
  includeDocs: true
19276
19753
  });
19277
19754
  if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
@@ -19583,6 +20060,12 @@ print(json.dumps({"issues": issues}))
19583
20060
  if (!this.saveProcessor) return;
19584
20061
  return this.saveProcessor.forceSave();
19585
20062
  }
20063
+ async flushSaves() {
20064
+ if (!this.saveProcessor || this.isSwitchingForEnvironment) throw new Error("App synchronization is not ready");
20065
+ const branch = this.currentBranch;
20066
+ await this.saveProcessor.flush();
20067
+ if (this.isSwitchingForEnvironment || this.currentBranch !== branch || await this.gitService.getCurrentBranch() !== branch) throw new Error("App branch changed while flushing saves");
20068
+ }
19586
20069
  async getSuccessMessage() {
19587
20070
  try {
19588
20071
  const info = await this.gitService.getCommitInfo();
@@ -21117,29 +21600,30 @@ print(json.dumps({"issues": issues}))
21117
21600
  return;
21118
21601
  }
21119
21602
  }
21120
- function inspectCredentialHelperCommand(raw) {
21603
+ function inspectCredentialHelperCommand(raw, options = {}) {
21121
21604
  const details = {
21122
21605
  raw
21123
21606
  };
21124
21607
  try {
21125
21608
  const parsed = parseCredentialHelperCommand(raw);
21126
21609
  details.execPath = parsed.execPath;
21127
- details.helperPath = parsed.helperPath;
21610
+ details.helperPath = parsed.helperPath === GIT_CREDENTIAL_HELPER_LOCATOR && options.gitCommonDir ? external_path_default().join(options.gitCommonDir, GIT_CREDENTIAL_HELPER_FILENAME) : parsed.helperPath;
21128
21611
  } catch (e) {
21129
21612
  details.parseError = e.message;
21130
21613
  return details;
21131
21614
  }
21132
21615
  if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
21133
- if (details.helperPath) {
21134
- details.helperExists = external_fs_default().existsSync(details.helperPath);
21616
+ const helperPath = details.helperPath;
21617
+ if (helperPath && helperPath !== GIT_CREDENTIAL_HELPER_LOCATOR) {
21618
+ details.helperExists = external_fs_default().existsSync(helperPath);
21135
21619
  if (details.helperExists) {
21136
21620
  try {
21137
- const stat = external_fs_default().statSync(details.helperPath);
21621
+ const stat = external_fs_default().statSync(helperPath);
21138
21622
  details.helperExecutable = (73 & stat.mode) !== 0;
21139
21623
  } catch {
21140
21624
  details.helperExecutable = false;
21141
21625
  }
21142
- details.modulePath = readHelperModulePath(details.helperPath);
21626
+ details.modulePath = readHelperModulePath(helperPath);
21143
21627
  if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
21144
21628
  }
21145
21629
  }
@@ -21238,7 +21722,7 @@ print(json.dumps({"issues": issues}))
21238
21722
  child.stdin.end(input);
21239
21723
  });
21240
21724
  }
21241
- async function buildAppReport(repoPath, remote, deps) {
21725
+ async function buildAppReport(repoPath, gitCommonDir, remote, deps) {
21242
21726
  const appId = remote.appId;
21243
21727
  const anvilUrl = remote.anvilUrl;
21244
21728
  const url = new URL(anvilUrl);
@@ -21246,7 +21730,9 @@ print(json.dumps({"issues": issues}))
21246
21730
  const helperConfigKey = `credential.${credentialScope}.helper`;
21247
21731
  const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
21248
21732
  const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
21249
- const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
21733
+ const helperDetails = nonEmptyHelperValues.map((value)=>inspectCredentialHelperCommand(value, {
21734
+ gitCommonDir
21735
+ }));
21250
21736
  const findings = [];
21251
21737
  if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
21252
21738
  else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
@@ -21325,12 +21811,18 @@ print(json.dumps({"issues": issues}))
21325
21811
  "--git-dir"
21326
21812
  ])).trim();
21327
21813
  const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
21328
- setRepoContext(repoPath);
21814
+ const rawGitCommonDir = (await git.raw([
21815
+ "rev-parse",
21816
+ "--git-common-dir"
21817
+ ])).trim();
21818
+ const resolvedGitCommonDir = external_path_default().isAbsolute(rawGitCommonDir) ? rawGitCommonDir : external_path_default().resolve(inputPath, rawGitCommonDir);
21819
+ const gitCommonDir = external_fs_default().realpathSync(resolvedGitCommonDir);
21820
+ auth_setRepoContext(repoPath);
21329
21821
  const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
21330
21822
  const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
21331
21823
  const findings = [];
21332
21824
  if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
21333
- const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
21825
+ const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, gitCommonDir, remote, deps)));
21334
21826
  const status = getHighestStatus([
21335
21827
  ...findings.map((item)=>item.status),
21336
21828
  ...apps.flatMap((app)=>app.findings.map((item)=>item.status))
@@ -21338,6 +21830,7 @@ print(json.dumps({"issues": issues}))
21338
21830
  return {
21339
21831
  repoPath,
21340
21832
  gitDir,
21833
+ gitCommonDir,
21341
21834
  remotes,
21342
21835
  apps,
21343
21836
  findings,
@@ -21354,6 +21847,7 @@ print(json.dumps({"issues": issues}))
21354
21847
  lines.push(external_chalk_default().bold("Git auth doctor"));
21355
21848
  lines.push(`Repository: ${report.repoPath}`);
21356
21849
  lines.push(`Git dir: ${report.gitDir}`);
21850
+ lines.push(`Git common dir: ${report.gitCommonDir}`);
21357
21851
  lines.push(`Overall: ${formatStatus(report.status)}`);
21358
21852
  lines.push("");
21359
21853
  lines.push(external_chalk_default().bold("Detected remotes:"));
@@ -21449,7 +21943,7 @@ print(json.dumps({"issues": issues}))
21449
21943
  const repoPath = options.repoPath ?? deps.cwd();
21450
21944
  if ("get" !== operation) return null;
21451
21945
  if (!request.protocol || !request.host) return null;
21452
- setRepoContext(repoPath);
21946
+ auth_setRepoContext(repoPath);
21453
21947
  const appId = parseAppIdFromGitPath(request.path);
21454
21948
  if (!appId) return null;
21455
21949
  const requestUrl = normalizeAnvilUrl(`${request.protocol}://${request.host}`);
@@ -21500,7 +21994,6 @@ print(json.dumps({"issues": issues}))
21500
21994
  }
21501
21995
  });
21502
21996
  }
21503
- const DEFAULT_ANVIL_URL = resolveAnvilUrl();
21504
21997
  async function syncToLatest(repoPath, appId, options) {
21505
21998
  const { anvilUrl, gitUrl = anvilUrl, authToken, currentBranch, username } = options;
21506
21999
  try {
@@ -21529,9 +22022,10 @@ print(json.dumps({"issues": issues}))
21529
22022
  }
21530
22023
  }
21531
22024
  async function watch(repoPath, appId, options = {}) {
21532
- const { anvilUrl = DEFAULT_ANVIL_URL, gitUrl = anvilUrl, stagedOnly = false, username } = options;
21533
22025
  repoPath = external_path_default().resolve(repoPath);
21534
- setRepoContext(repoPath);
22026
+ auth_setRepoContext(repoPath);
22027
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
22028
+ const { gitUrl = anvilUrl, stagedOnly = false, username } = options;
21535
22029
  const storedEnvPid = options.environment?.envPid ? void 0 : await readEnvironmentPid(repoPath);
21536
22030
  const environment = options.environment ?? (storedEnvPid ? {
21537
22031
  envPid: storedEnvPid
@@ -21959,7 +22453,7 @@ print(json.dumps({"issues": issues}))
21959
22453
  });
21960
22454
  logger_logger.progressEnd("verify");
21961
22455
  const currentCategory = getSyncStateCategory(freshSession.syncStatus);
21962
- const currentBranch = freshSession.getBranchName() || "master";
22456
+ const currentBranch = freshSession.getBranchName() || await getFallbackBranchName(options.repoPath);
21963
22457
  if (currentCategory !== previousCategory || currentBranch !== previousBranch) {
21964
22458
  logger_logger.warn("Repository status has changed. Re-evaluating...");
21965
22459
  return freshSession;
@@ -21992,7 +22486,7 @@ print(json.dumps({"issues": issues}))
21992
22486
  }
21993
22487
  async function checkSyncStatusAndStart(session, options, deps = defaultSyncStartDeps) {
21994
22488
  const syncStatus = session.syncStatus;
21995
- const branchName = session.getBranchName() || "master";
22489
+ const branchName = session.getBranchName() || await getFallbackBranchName(options.repoPath);
21996
22490
  const hasUncommitted = session.hasUncommittedChanges;
21997
22491
  const stateCategory = getSyncStateCategory(syncStatus);
21998
22492
  if (syncStatus?.branchMissing) {
@@ -22312,6 +22806,9 @@ print(json.dumps({"issues": issues}))
22312
22806
  const suggestedAction = error.suggestedAction;
22313
22807
  if ("string" == typeof suggestedAction && suggestedAction.length > 0) logger_logger.warn(suggestedAction);
22314
22808
  }
22809
+ async function getFallbackBranchName(repoPath) {
22810
+ return getCurrentOrFallbackBranchName(esm_default(repoPath));
22811
+ }
22315
22812
  async function handleWatchCommand(options) {
22316
22813
  const invoked = process.argv[2];
22317
22814
  if ("sync" === invoked) {
@@ -22319,7 +22816,7 @@ print(json.dumps({"issues": issues}))
22319
22816
  process.exit(1);
22320
22817
  }
22321
22818
  const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
22322
- setRepoContext(repoPath);
22819
+ auth_setRepoContext(repoPath);
22323
22820
  try {
22324
22821
  const validationResult = await validateAnvilApp(repoPath);
22325
22822
  if (validationResult.appName) logger_logger.info(external_chalk_default().green("Anvil app: ") + external_chalk_default().bold(validationResult.appName));
@@ -22526,6 +23023,7 @@ print(json.dumps({"issues": issues}))
22526
23023
  logger_logger.success("Logged in as " + external_chalk_default().bold(result.email));
22527
23024
  } catch (e) {
22528
23025
  logger_logger.error("Error: " + e.message);
23026
+ logErrorCause(e, logger_logger);
22529
23027
  process.exit(1);
22530
23028
  }
22531
23029
  });
@@ -23104,6 +23602,7 @@ print(json.dumps({"issues": issues}))
23104
23602
  await runConfigureFlow(version, configure_defaultDeps);
23105
23603
  } catch (e) {
23106
23604
  logger_logger.error("Error: " + errors_getErrorMessage(e));
23605
+ logErrorCause(e, logger_logger);
23107
23606
  process.exit(1);
23108
23607
  }
23109
23608
  });
@@ -23119,10 +23618,11 @@ print(json.dumps({"issues": issues}))
23119
23618
  }
23120
23619
  function registerDepsCommand(program) {
23121
23620
  const deps = program.command("deps").description("Manage app dependencies");
23122
- 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)=>{
23621
+ 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)=>{
23123
23622
  try {
23124
23623
  const projectRoot = process.cwd();
23125
- const anvilUrl = resolveAnvilUrl();
23624
+ auth_setRepoContext(projectRoot);
23625
+ const anvilUrl = await resolveAuthAnvilUrl();
23126
23626
  const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23127
23627
  if (!mainAppId) {
23128
23628
  logger_logger.error("No Anvil app found in current directory");
@@ -23134,6 +23634,7 @@ print(json.dumps({"issues": issues}))
23134
23634
  commit: options.commit,
23135
23635
  includeDocs: !options.excludeDocs
23136
23636
  });
23637
+ 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.");
23137
23638
  if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
23138
23639
  if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
23139
23640
  if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
@@ -23144,124 +23645,643 @@ print(json.dumps({"issues": issues}))
23144
23645
  process.exit(1);
23145
23646
  }
23146
23647
  });
23147
- deps.command("status").description("Show cached dependencies").action(async ()=>{
23648
+ deps.command("status").description("Show cached dependencies").action(async ()=>{
23649
+ try {
23650
+ const cached = await getCachedPackages(process.cwd());
23651
+ if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
23652
+ logger_logger.info("Cached dependencies:");
23653
+ for (const pkg of cached)logger_logger.info(` - ${pkg}`);
23654
+ } catch (e) {
23655
+ logger_logger.error(e.message);
23656
+ process.exit(1);
23657
+ }
23658
+ });
23659
+ deps.command("clear").description("Clear cached dependencies").action(async ()=>{
23660
+ try {
23661
+ await clearDepsCache(process.cwd());
23662
+ logger_logger.success("Cleared .anvil/deps/");
23663
+ } catch (e) {
23664
+ logger_logger.error(e.message);
23665
+ process.exit(1);
23666
+ }
23667
+ });
23668
+ }
23669
+ async function applyDatabaseSchema(appId, options = {}) {
23670
+ const projectRoot = options.projectRoot ?? process.cwd();
23671
+ auth_setRepoContext(projectRoot);
23672
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23673
+ const appYaml = external_js_yaml_default().load(await external_fs_.promises.readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf8"));
23674
+ const token = await auth_getValidAuthToken(anvilUrl);
23675
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db/infer-from-env/apply-schema`, {
23676
+ method: "PUT",
23677
+ headers: {
23678
+ Authorization: `Bearer ${token}`,
23679
+ "Content-Type": "application/json"
23680
+ },
23681
+ body: JSON.stringify({
23682
+ schema: appYaml?.db_schema ?? {},
23683
+ table_id_hints: appYaml?.table_id_hints
23684
+ })
23685
+ });
23686
+ if (!response.ok) throw new Error(formatHttpError("Failed to apply database schema", response.status, await response.text()));
23687
+ return await response.json();
23688
+ }
23689
+ async function confirmSchemaApplication(force = false) {
23690
+ if (force) return true;
23691
+ assertCanPrompt("Applying database schema", "Pass --force to apply without prompting.");
23692
+ return logger_logger.confirm("Apply the app environment's schema to its database? This may delete tables, columns, or data.", true);
23693
+ }
23694
+ function registerDbCommand(program) {
23695
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23696
+ const db = program.command("db").description("Manage app databases");
23697
+ 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)=>{
23698
+ try {
23699
+ const projectRoot = process.cwd();
23700
+ auth_setRepoContext(projectRoot);
23701
+ const anvilUrl = await resolveAuthAnvilUrl();
23702
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23703
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
23704
+ if (!await confirmSchemaApplication(options.force)) return void logger_logger.info("Schema application cancelled.");
23705
+ const response = await applyDatabaseSchema(appId, {
23706
+ anvilUrl,
23707
+ projectRoot
23708
+ });
23709
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23710
+ data: response
23711
+ });
23712
+ else logger_logger.success("Database schema applied.");
23713
+ } catch (error) {
23714
+ const message = errors_getErrorMessage(error);
23715
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23716
+ error: message
23717
+ });
23718
+ else logger_logger.error(message);
23719
+ process.exit(1);
23720
+ }
23721
+ });
23722
+ }
23723
+ async function ensureEnvironmentUrl(appId, envPid, options = {}) {
23724
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23725
+ const token = await auth_getValidAuthToken(anvilUrl);
23726
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-url`, {
23727
+ method: "POST",
23728
+ headers: {
23729
+ Authorization: `Bearer ${token}`
23730
+ }
23731
+ });
23732
+ if (!response.ok) throw new Error(formatHttpError("Failed to get environment URL", response.status, await response.text()));
23733
+ const { url, expires_at } = await response.json();
23734
+ return {
23735
+ url,
23736
+ expires_at
23737
+ };
23738
+ }
23739
+ async function getTemporaryUplinkKey(appId, envPid, options = {}) {
23740
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23741
+ const token = await auth_getValidAuthToken(anvilUrl);
23742
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/temporary-uplink-key`, {
23743
+ method: "POST",
23744
+ headers: {
23745
+ Authorization: `Bearer ${token}`
23746
+ }
23747
+ });
23748
+ if (!response.ok) throw new Error(formatHttpError("Failed to get temporary uplink key", response.status, await response.text()));
23749
+ const { key, url, expires_at } = await response.json();
23750
+ return {
23751
+ key,
23752
+ url,
23753
+ expires_at
23754
+ };
23755
+ }
23756
+ async function resolveEnvironmentContext() {
23757
+ const projectRoot = process.cwd();
23758
+ const envPid = await readEnvironmentPid(projectRoot);
23759
+ if (!envPid) throw new Error("No app environment associated with the current directory.");
23760
+ auth_setRepoContext(projectRoot);
23761
+ const anvilUrl = await resolveAuthAnvilUrl();
23762
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23763
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
23764
+ return {
23765
+ envPid,
23766
+ anvilUrl,
23767
+ appId
23768
+ };
23769
+ }
23770
+ function registerEnvCommand(program) {
23771
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23772
+ const env = program.command("env").description("Manage app environments");
23773
+ env.command("url").description("Get a temporary private app URL for the current environment").action(async ()=>{
23148
23774
  try {
23149
- const cached = await getCachedPackages(process.cwd());
23150
- if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
23151
- logger_logger.info("Cached dependencies:");
23152
- for (const pkg of cached)logger_logger.info(` - ${pkg}`);
23153
- } catch (e) {
23154
- logger_logger.error(e.message);
23775
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
23776
+ const { url, expires_at } = await ensureEnvironmentUrl(appId, envPid, {
23777
+ anvilUrl
23778
+ });
23779
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23780
+ data: {
23781
+ url,
23782
+ expires_at
23783
+ }
23784
+ });
23785
+ else logger_logger.info(`URL: ${url}\nExpires at: ${expires_at}`);
23786
+ } catch (error) {
23787
+ const message = errors_getErrorMessage(error);
23788
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23789
+ error: message
23790
+ });
23791
+ else logger_logger.error(message);
23155
23792
  process.exit(1);
23156
23793
  }
23157
23794
  });
23158
- deps.command("clear").description("Clear cached dependencies").action(async ()=>{
23795
+ env.command("uplink").description("Get a temporary uplink key for the current environment").action(async ()=>{
23159
23796
  try {
23160
- await clearDepsCache(process.cwd());
23161
- logger_logger.success("Cleared .anvil/deps/");
23162
- } catch (e) {
23163
- logger_logger.error(e.message);
23797
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
23798
+ const { key, url, expires_at } = await getTemporaryUplinkKey(appId, envPid, {
23799
+ anvilUrl
23800
+ });
23801
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23802
+ data: {
23803
+ key,
23804
+ url,
23805
+ expires_at
23806
+ }
23807
+ });
23808
+ else logger_logger.info(`Key: ${key}\nURL: ${url}\nExpires at: ${expires_at}`);
23809
+ } catch (error) {
23810
+ const message = errors_getErrorMessage(error);
23811
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23812
+ error: message
23813
+ });
23814
+ else logger_logger.error(message);
23164
23815
  process.exit(1);
23165
23816
  }
23166
23817
  });
23167
23818
  }
23168
- const TABLE_MAPPINGS_PATH = ".anvil/table-mappings.json";
23169
- function tables_formatFetchError(prefix, status, errorBody) {
23170
- if (!errorBody) return `${prefix}: ${status}`;
23171
- try {
23172
- const parsed = JSON.parse(errorBody);
23173
- if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
23174
- } catch {}
23175
- return `${prefix}: ${status} ${errorBody}`;
23819
+ const external_events_namespaceObject = require("events");
23820
+ function replError(error) {
23821
+ if ("string" == typeof error) return {
23822
+ message: error
23823
+ };
23824
+ if (error && "object" == typeof error) {
23825
+ const value = error;
23826
+ return {
23827
+ message: String(value.message ?? value["anvil/server-error"] ?? JSON.stringify(value)),
23828
+ ..."string" == typeof value.type ? {
23829
+ type: value.type
23830
+ } : {},
23831
+ ...Array.isArray(value.trace) ? {
23832
+ trace: value.trace.filter((frame)=>Array.isArray(frame) && "string" == typeof frame[0] && "number" == typeof frame[1])
23833
+ } : {}
23834
+ };
23835
+ }
23836
+ return {
23837
+ message: String(error)
23838
+ };
23176
23839
  }
23177
- async function fetchTableMappings(appId, anvilUrl = resolveAnvilUrl()) {
23840
+ class ReplServerError extends Error {
23841
+ detail;
23842
+ constructor(detail){
23843
+ super(detail.message), this.detail = detail;
23844
+ }
23845
+ }
23846
+ async function runRepl(appId, envPid, options) {
23847
+ const interrupted = ()=>new Error("REPL interrupted.");
23848
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
23178
23849
  const token = await auth_getValidAuthToken(anvilUrl);
23179
- const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db`, {
23850
+ const url = new URL(`${anvilUrl.replace(/\/$/, "")}/ide/api/_/apps/${encodeURIComponent(appId)}/environments/${encodeURIComponent(envPid)}/ws`);
23851
+ url.protocol = "https:" === url.protocol ? "wss:" : "ws:";
23852
+ if (options.signal?.aborted) throw interrupted();
23853
+ const ws = new (external_ws_default())(url, {
23180
23854
  headers: {
23181
23855
  Authorization: `Bearer ${token}`
23182
- }
23856
+ },
23857
+ handshakeTimeout: 30000
23183
23858
  });
23184
- if (!resp.ok) {
23185
- const errorBody = await resp.text();
23186
- throw new Error(tables_formatFetchError("Failed to fetch table mappings", resp.status, errorBody));
23859
+ const controller = new AbortController();
23860
+ let repl;
23861
+ let nextId = 1;
23862
+ let output = "";
23863
+ let keepalive;
23864
+ let pongTimeout;
23865
+ let closeTimeout;
23866
+ const startupTimeout = setTimeout(()=>{
23867
+ controller.abort(new Error("Timed out waiting for the REPL to start."));
23868
+ }, 30000);
23869
+ const onAbort = ()=>controller.abort(interrupted());
23870
+ const markResponsive = ()=>{
23871
+ clearTimeout(pongTimeout);
23872
+ pongTimeout = void 0;
23873
+ };
23874
+ options.signal?.addEventListener("abort", onAbort, {
23875
+ once: true
23876
+ });
23877
+ ws.on("pong", markResponsive);
23878
+ ws.on("unexpected-response", (request, response)=>{
23879
+ response.resume();
23880
+ controller.abort(new Error(`REPL websocket handshake failed (${response.statusCode} ${response.statusMessage}).`));
23881
+ request.destroy();
23882
+ });
23883
+ ws.on("error", (error)=>controller.abort(new Error(`REPL websocket connection failed: ${error.message}`)));
23884
+ ws.on("close", (code, reason)=>{
23885
+ clearTimeout(closeTimeout);
23886
+ controller.abort(new Error(`REPL websocket disconnected unexpectedly (code ${code}${reason.length ? `: ${reason}` : ""}).`));
23887
+ });
23888
+ const messages = (0, external_events_namespaceObject.on)(ws, "message", {
23889
+ signal: controller.signal
23890
+ });
23891
+ function send(message) {
23892
+ const id = nextId++;
23893
+ ws.send(JSON.stringify({
23894
+ ...message,
23895
+ id
23896
+ }), (error)=>{
23897
+ if (error) controller.abort(new Error(`REPL websocket send failed: ${error.message}`));
23898
+ });
23899
+ return id;
23900
+ }
23901
+ async function readUntil(matches) {
23902
+ while(true){
23903
+ const { value, done } = await messages.next();
23904
+ options.signal?.throwIfAborted();
23905
+ if (done) throw new Error("REPL websocket message stream ended unexpectedly.");
23906
+ markResponsive();
23907
+ let message;
23908
+ try {
23909
+ message = JSON.parse(value[0].toString());
23910
+ if (!message || "object" != typeof message || Array.isArray(message)) throw new Error("Expected an object");
23911
+ } catch {
23912
+ throw new Error("Invalid REPL websocket message.");
23913
+ }
23914
+ if ("REPL_UPDATE" === message.event) {
23915
+ if (!repl || message.repl !== repl) continue;
23916
+ if ("string" == typeof message.output) {
23917
+ output += message.output;
23918
+ options.onOutput?.(message.output);
23919
+ options.signal?.throwIfAborted();
23920
+ }
23921
+ }
23922
+ if (message.error) throw new ReplServerError(replError(message.error));
23923
+ if ("REPL_UPDATE" === message.event && message.terminated) throw new Error("REPL terminated unexpectedly.");
23924
+ if (matches(message)) return message;
23925
+ }
23187
23926
  }
23188
- const databases = await resp.json();
23189
- const allTables = [];
23190
- for (const db of databases)for (const table of db.tables || [])allTables.push({
23191
- table_id: table.id,
23192
- python_name: table.python_name,
23193
- name: table.name
23927
+ try {
23928
+ await (0, external_events_namespaceObject.once)(ws, "open", {
23929
+ signal: controller.signal
23930
+ });
23931
+ const launchId = send({
23932
+ cmd: "LAUNCH_REPL"
23933
+ });
23934
+ const launched = await readUntil((message)=>message.id === launchId);
23935
+ if ("string" != typeof launched.repl || !launched.repl) throw new Error("Invalid REPL launch response: missing REPL ID.");
23936
+ repl = launched.repl;
23937
+ keepalive = setInterval(()=>{
23938
+ try {
23939
+ send({
23940
+ cmd: "REPL_KEEPALIVE",
23941
+ repl
23942
+ });
23943
+ pongTimeout = setTimeout(()=>controller.abort(new Error("REPL websocket heartbeat timed out.")), 10000);
23944
+ ws.ping();
23945
+ } catch (error) {
23946
+ controller.abort(error);
23947
+ }
23948
+ }, 20000);
23949
+ const isReady = (message)=>"REPL_UPDATE" === message.event && true === message.ready;
23950
+ await readUntil(isReady);
23951
+ clearTimeout(startupTimeout);
23952
+ send({
23953
+ cmd: "REPL_COMMAND",
23954
+ repl,
23955
+ command: options.code
23956
+ });
23957
+ await readUntil(isReady);
23958
+ return {
23959
+ output
23960
+ };
23961
+ } catch (error) {
23962
+ if (error instanceof ReplServerError) return {
23963
+ output,
23964
+ error: error.detail
23965
+ };
23966
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
23967
+ throw failure instanceof Error ? failure : new Error(String(failure));
23968
+ } finally{
23969
+ clearTimeout(startupTimeout);
23970
+ clearInterval(keepalive);
23971
+ clearTimeout(pongTimeout);
23972
+ options.signal?.removeEventListener("abort", onAbort);
23973
+ controller.abort();
23974
+ await messages.return?.();
23975
+ if (ws.readyState === external_ws_default().OPEN) {
23976
+ if (repl) ws.send(JSON.stringify({
23977
+ id: nextId++,
23978
+ cmd: "TERMINATE_REPL",
23979
+ repl
23980
+ }), ()=>{});
23981
+ ws.close();
23982
+ } else if (ws.readyState === external_ws_default().CONNECTING) ws.terminate();
23983
+ if (ws.readyState !== external_ws_default().CLOSED) {
23984
+ closeTimeout = setTimeout(()=>ws.terminate(), 1000);
23985
+ closeTimeout.unref();
23986
+ }
23987
+ }
23988
+ }
23989
+ function writeJsonReplOutput(output) {
23990
+ process.stdout.write(JSON.stringify({
23991
+ type: "repl_output",
23992
+ output,
23993
+ timestamp: new Date().toISOString()
23994
+ }) + "\n");
23995
+ }
23996
+ function registerReplCommand(program) {
23997
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
23998
+ program.command("repl <code>").description("Run Python in a fresh server REPL for the current environment; use - to read stdin").addHelpText("after", `
23999
+ Examples:
24000
+ anvil repl '1 + 2'
24001
+ anvil --json repl '1 + 2'
24002
+ anvil repl - < script.py
24003
+ `).action(async (code)=>{
24004
+ const controller = new AbortController();
24005
+ const onInterrupt = ()=>controller.abort();
24006
+ try {
24007
+ const { appId, envPid, anvilUrl } = await resolveEnvironmentContext();
24008
+ if ("-" === code) {
24009
+ const chunks = [];
24010
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
24011
+ code = Buffer.concat(chunks).toString("utf8");
24012
+ }
24013
+ process.on("SIGINT", onInterrupt);
24014
+ const response = await runRepl(appId, envPid, {
24015
+ code,
24016
+ anvilUrl,
24017
+ signal: controller.signal,
24018
+ onOutput: getGlobalOutputConfig().jsonMode ? writeJsonReplOutput : (output)=>{
24019
+ process.stdout.write(output);
24020
+ }
24021
+ });
24022
+ const error = response.error;
24023
+ const message = error ? `${error.type ? `${error.type}: ` : ""}${error.message}` : void 0;
24024
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(!error, {
24025
+ data: response,
24026
+ error: message
24027
+ });
24028
+ else if (error) {
24029
+ process.stderr.write(`${message}\n`);
24030
+ for (const [file, line] of error.trace ?? [])process.stderr.write(` at ${file}:${line}\n`);
24031
+ }
24032
+ if (error) process.exitCode = 1;
24033
+ } catch (error) {
24034
+ const message = errors_getErrorMessage(error);
24035
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24036
+ error: message
24037
+ });
24038
+ else logger_logger.error(message);
24039
+ process.exitCode = controller.signal.aborted ? 130 : 1;
24040
+ } finally{
24041
+ process.removeListener("SIGINT", onInterrupt);
24042
+ }
23194
24043
  });
23195
- return {
23196
- tables: allTables
23197
- };
23198
24044
  }
23199
- async function writeTableMappings(projectRoot, mappings) {
23200
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23201
- await promises_default().mkdir(external_path_default().dirname(filePath), {
23202
- recursive: true
24045
+ const external_yaml_namespaceObject = require("yaml");
24046
+ async function encryptSecret(appId, secretName, options = {}) {
24047
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24048
+ const token = await auth_getValidAuthToken(anvilUrl);
24049
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/encrypt`, {
24050
+ method: "POST",
24051
+ headers: {
24052
+ Authorization: `Bearer ${token}`,
24053
+ "Content-Type": "application/json"
24054
+ },
24055
+ body: JSON.stringify({
24056
+ name: secretName,
24057
+ ...void 0 === options.secret ? {} : {
24058
+ secret: options.secret
24059
+ }
24060
+ })
23203
24061
  });
23204
- await promises_default().writeFile(filePath, JSON.stringify(mappings, null, 2));
23205
- }
23206
- async function readTableMappings(projectRoot) {
23207
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23208
- try {
23209
- const content = await promises_default().readFile(filePath, "utf-8");
23210
- return JSON.parse(content);
23211
- } catch {
23212
- return null;
23213
- }
24062
+ if (!response.ok) throw new Error(formatHttpError("Failed to set secret", response.status, await response.text()));
24063
+ return await response.json();
23214
24064
  }
23215
- async function clearTableMappings(projectRoot) {
23216
- const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23217
- await promises_default().rm(filePath, {
23218
- force: true
24065
+ async function generateSecretKey(appId, keyName, options = {}) {
24066
+ const anvilUrl = options.anvilUrl ?? await resolveAuthAnvilUrl();
24067
+ const token = await auth_getValidAuthToken(anvilUrl);
24068
+ const response = await fetch(`${anvilUrl}/ide/api/_/apps/${encodeURIComponent(appId)}/secrets/generate-key`, {
24069
+ method: "POST",
24070
+ headers: {
24071
+ Authorization: `Bearer ${token}`,
24072
+ "Content-Type": "application/json"
24073
+ },
24074
+ body: JSON.stringify({
24075
+ name: keyName
24076
+ })
23219
24077
  });
24078
+ if (!response.ok) throw new Error(formatHttpError("Failed to generate key", response.status, await response.text()));
24079
+ return await response.json();
24080
+ }
24081
+ function secret_anvilYamlPath(projectRoot) {
24082
+ return external_path_default().join(projectRoot, "anvil.yaml");
24083
+ }
24084
+ function checkSecretTypeInDoc(doc, secretName, type) {
24085
+ const existingType = doc.getIn([
24086
+ "secrets",
24087
+ secretName,
24088
+ "type"
24089
+ ]);
24090
+ if (void 0 !== existingType && existingType !== type) throw new Error(`'${secretName}' is a ${existingType} in anvil.yaml, not a ${type}`);
24091
+ }
24092
+ function secretExistsInDoc(doc, secretName, appId) {
24093
+ return doc.hasIn([
24094
+ "secrets",
24095
+ secretName,
24096
+ "value",
24097
+ appId
24098
+ ]);
24099
+ }
24100
+ async function findSecretInAnvilYaml(projectRoot, secretName, appId) {
24101
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(secret_anvilYamlPath(projectRoot), "utf8"));
24102
+ if (!doc.hasIn([
24103
+ "secrets",
24104
+ secretName
24105
+ ])) return;
24106
+ return {
24107
+ type: doc.getIn([
24108
+ "secrets",
24109
+ secretName,
24110
+ "type"
24111
+ ]),
24112
+ existsForApp: secretExistsInDoc(doc, secretName, appId)
24113
+ };
24114
+ }
24115
+ async function writeSecretToAnvilYaml(projectRoot, secretName, options) {
24116
+ const { appId, encrypted, overwrite, type } = options;
24117
+ const filePath = secret_anvilYamlPath(projectRoot);
24118
+ const doc = (0, external_yaml_namespaceObject.parseDocument)(await external_fs_.promises.readFile(filePath, "utf8"));
24119
+ checkSecretTypeInDoc(doc, secretName, type);
24120
+ if (secretExistsInDoc(doc, secretName, appId)) {
24121
+ if (!overwrite) throw new Error(`'${secretName}' already exists for app ${appId} in anvil.yaml`);
24122
+ if (!doc.hasIn([
24123
+ "secrets",
24124
+ secretName,
24125
+ "value",
24126
+ appId,
24127
+ "all"
24128
+ ])) throw new Error(`'${secretName}' for app ${appId} in anvil.yaml cannot be overridden`);
24129
+ }
24130
+ if (doc.hasIn([
24131
+ "secrets",
24132
+ secretName
24133
+ ])) {
24134
+ if (!(0, external_yaml_namespaceObject.isMap)(doc.getIn([
24135
+ "secrets",
24136
+ secretName,
24137
+ "value"
24138
+ ]))) doc.setIn([
24139
+ "secrets",
24140
+ secretName,
24141
+ "value"
24142
+ ], doc.createNode({}));
24143
+ } else doc.setIn([
24144
+ "secrets",
24145
+ secretName
24146
+ ], doc.createNode({
24147
+ type,
24148
+ value: {}
24149
+ }));
24150
+ const value = new external_yaml_namespaceObject.Scalar(encrypted);
24151
+ value.type = external_yaml_namespaceObject.Scalar.QUOTE_DOUBLE;
24152
+ if ((0, external_yaml_namespaceObject.isMap)(doc.getIn([
24153
+ "secrets",
24154
+ secretName,
24155
+ "value",
24156
+ appId
24157
+ ]))) doc.setIn([
24158
+ "secrets",
24159
+ secretName,
24160
+ "value",
24161
+ appId,
24162
+ "all"
24163
+ ], value);
24164
+ else doc.setIn([
24165
+ "secrets",
24166
+ secretName,
24167
+ "value",
24168
+ appId
24169
+ ], doc.createNode({
24170
+ all: value
24171
+ }, {
24172
+ flow: true
24173
+ }));
24174
+ await external_fs_.promises.writeFile(filePath, doc.toString(), "utf8");
23220
24175
  }
23221
- function registerTablesCommand(program) {
23222
- const tables = program.command("tables").description("Manage table mappings");
23223
- tables.command("fetch").description("Fetch table mappings into .anvil/table-mappings.json").action(async ()=>{
24176
+ function registerSecretCommand(program) {
24177
+ if ("1" !== process.env.ANVIL_AGENT_HOST) return;
24178
+ const secret = program.command("secret").description("Manage app secrets");
24179
+ 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)=>{
23224
24180
  try {
23225
24181
  const projectRoot = process.cwd();
23226
- const anvilUrl = resolveAnvilUrl();
23227
- const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23228
- if (!mainAppId) {
23229
- logger_logger.error("No Anvil app found in current directory");
23230
- logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23231
- process.exit(1);
23232
- }
23233
- logger_logger.info("Fetching table mappings...");
23234
- const response = await fetchTableMappings(mainAppId, anvilUrl);
23235
- if (!response.tables.length) return void logger_logger.info("No tables found");
23236
- if (await ensureProjectGitignoreIgnoresAnvilDir(projectRoot)) logger_logger.info("Added /.anvil/ to .gitignore");
23237
- await writeTableMappings(projectRoot, response);
23238
- logger_logger.success(`Fetched ${response.tables.length} table mappings`);
23239
- } catch (e) {
23240
- logger_logger.error(e.message);
23241
- process.exit(1);
23242
- }
23243
- });
23244
- tables.command("status").description("Show cached table mappings").action(async ()=>{
23245
- try {
23246
- const mappings = await readTableMappings(process.cwd());
23247
- if (!mappings || !mappings.tables.length) return void logger_logger.info("No cached table mappings (run 'anvil tables fetch')");
23248
- logger_logger.info("Cached table mappings:");
23249
- for (const t of mappings.tables)logger_logger.info(` ${t.python_name} (${t.name}) - ID: ${t.table_id}`);
23250
- } catch (e) {
23251
- logger_logger.error(e.message);
24182
+ auth_setRepoContext(projectRoot);
24183
+ const anvilUrl = await resolveAuthAnvilUrl();
24184
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
24185
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
24186
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24187
+ appId,
24188
+ type: "secret",
24189
+ force: options.force
24190
+ });
24191
+ const value = options.generate ? void 0 : await readSecretValue(name);
24192
+ const response = await encryptSecret(appId, name, {
24193
+ anvilUrl,
24194
+ secret: value
24195
+ });
24196
+ await writeSecretToAnvilYaml(projectRoot, name, {
24197
+ appId,
24198
+ encrypted: response.encrypted,
24199
+ overwrite,
24200
+ type: "secret"
24201
+ });
24202
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24203
+ data: {
24204
+ encrypted: response.encrypted
24205
+ }
24206
+ });
24207
+ else logger_logger.info(`Secret '${name}' written to anvil.yaml`);
24208
+ } catch (error) {
24209
+ const message = errors_getErrorMessage(error);
24210
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24211
+ error: message
24212
+ });
24213
+ else logger_logger.error(message);
23252
24214
  process.exit(1);
23253
24215
  }
23254
24216
  });
23255
- tables.command("clear").description("Clear cached table mappings").action(async ()=>{
24217
+ 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)=>{
23256
24218
  try {
23257
- await clearTableMappings(process.cwd());
23258
- logger_logger.success("Cleared .anvil/table-mappings.json");
23259
- } catch (e) {
23260
- logger_logger.error(e.message);
24219
+ const projectRoot = process.cwd();
24220
+ auth_setRepoContext(projectRoot);
24221
+ const anvilUrl = await resolveAuthAnvilUrl();
24222
+ const appId = await resolvePrimaryAppId(projectRoot, anvilUrl);
24223
+ if (!appId) throw new Error("No Anvil app found in current directory. Make sure you're in a directory with an Anvil app git remote.");
24224
+ const overwrite = await confirmOverwriteIfExists(projectRoot, name, {
24225
+ appId,
24226
+ type: "key",
24227
+ force: options.force
24228
+ });
24229
+ const response = await generateSecretKey(appId, name, {
24230
+ anvilUrl
24231
+ });
24232
+ await writeSecretToAnvilYaml(projectRoot, name, {
24233
+ appId,
24234
+ encrypted: response.encrypted,
24235
+ overwrite,
24236
+ type: "key"
24237
+ });
24238
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
24239
+ data: {
24240
+ encrypted: response.encrypted
24241
+ }
24242
+ });
24243
+ else logger_logger.info(`Key '${name}' written to anvil.yaml`);
24244
+ } catch (error) {
24245
+ const message = errors_getErrorMessage(error);
24246
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
24247
+ error: message
24248
+ });
24249
+ else logger_logger.error(message);
23261
24250
  process.exit(1);
23262
24251
  }
23263
24252
  });
23264
24253
  }
24254
+ async function confirmOverwriteIfExists(projectRoot, name, options) {
24255
+ const { appId, type, force = false } = options;
24256
+ const existing = await findSecretInAnvilYaml(projectRoot, name, appId);
24257
+ if (existing?.type && existing.type !== type) throw new Error(`'${name}' is a ${existing.type} in anvil.yaml, not a ${type}`);
24258
+ if (!existing?.existsForApp) return false;
24259
+ if (force) return true;
24260
+ const message = `'${name}' already exists for app ${appId} in anvil.yaml`;
24261
+ if (!process.stdin.isTTY) throw new Error(`${message}. Use --force to overwrite it.`);
24262
+ if (!await logger_logger.confirm(`${message}. Overwrite it?`, false)) throw new Error(`Not overwriting '${name}'`);
24263
+ return true;
24264
+ }
24265
+ async function readSecretValue(name) {
24266
+ if (process.stdin.isTTY) {
24267
+ const { secret } = await logger_logger.prompt([
24268
+ {
24269
+ type: "password",
24270
+ name: "secret",
24271
+ message: `Value for secret '${name}':`
24272
+ }
24273
+ ]);
24274
+ if (!secret) throw new Error("No secret value entered");
24275
+ return secret;
24276
+ }
24277
+ {
24278
+ const chunks = [];
24279
+ for await (const chunk of process.stdin)chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
24280
+ const secret = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
24281
+ if (!secret) throw new Error("No secret value received on stdin");
24282
+ return secret;
24283
+ }
24284
+ }
23265
24285
  const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
23266
24286
  const VERSION = program_packageJson.version;
23267
24287
  setLogger(new CLILogger({
@@ -23302,7 +24322,7 @@ print(json.dumps({"issues": issues}))
23302
24322
  walk(dir);
23303
24323
  return files.sort();
23304
24324
  }
23305
- function validateSingleFile(file) {
24325
+ async function validateSingleFileBasic(file) {
23306
24326
  let fileContent;
23307
24327
  try {
23308
24328
  fileContent = (0, external_fs_.readFileSync)(file, "utf8");
@@ -23320,7 +24340,10 @@ print(json.dumps({"issues": issues}))
23320
24340
  ]
23321
24341
  };
23322
24342
  }
23323
- const result = validatePath(file, fileContent);
24343
+ const formSpecPackageContext = "form_template.html" === inferValidationTarget(file) ? await buildFormSpecPackageContext(file) : void 0;
24344
+ const result = validatePath(file, fileContent, {
24345
+ formSpecPackageContext
24346
+ });
23324
24347
  if (result.ok) return {
23325
24348
  file,
23326
24349
  type: result.target,
@@ -23340,6 +24363,74 @@ print(json.dumps({"issues": issues}))
23340
24363
  } : {}
23341
24364
  };
23342
24365
  }
24366
+ function diagnosticsForFile(file, diagnostics, options = {}) {
24367
+ const absoluteFile = external_path_default().resolve(file);
24368
+ const globalDiagnostics = false === options.includeGlobalDiagnostics ? [] : diagnostics.filter((diagnostic)=>!diagnostic.file);
24369
+ const fileDiagnostics = diagnostics.filter((diagnostic)=>diagnostic.file && external_path_default().resolve(diagnostic.file) === absoluteFile);
24370
+ return [
24371
+ ...fileDiagnostics,
24372
+ ...globalDiagnostics
24373
+ ];
24374
+ }
24375
+ function pythonFileResult(file, result, options = {}) {
24376
+ const issues = result.ok ? [] : diagnosticsForFile(file, result.issues, options);
24377
+ const warnings = diagnosticsForFile(file, result.warnings ?? [], options);
24378
+ if (result.ok || 0 === issues.length) return {
24379
+ file,
24380
+ type: "python",
24381
+ valid: true,
24382
+ ...warnings.length > 0 ? {
24383
+ warnings
24384
+ } : {}
24385
+ };
24386
+ return {
24387
+ file,
24388
+ type: "python",
24389
+ valid: false,
24390
+ error: result.message,
24391
+ issues,
24392
+ ...warnings.length > 0 ? {
24393
+ warnings
24394
+ } : {}
24395
+ };
24396
+ }
24397
+ function groupProjectDiagnostics(diagnostics) {
24398
+ const grouped = new Map();
24399
+ for (const diagnostic of diagnostics){
24400
+ const appDiagnostics = grouped.get(diagnostic.appRoot) ?? [];
24401
+ appDiagnostics.push(diagnostic);
24402
+ grouped.set(diagnostic.appRoot, appDiagnostics);
24403
+ }
24404
+ return grouped;
24405
+ }
24406
+ async function dependencyCacheDiagnostic(appRoot) {
24407
+ const message = await getDependencyCacheWarning(appRoot);
24408
+ if (!message) return;
24409
+ return {
24410
+ path: "root",
24411
+ message,
24412
+ code: "reportAnvilDependencyCache",
24413
+ rule: "reportAnvilDependencyCache",
24414
+ severity: "warning"
24415
+ };
24416
+ }
24417
+ async function validateSingleFile(file) {
24418
+ const appRoot = findAnvilAppRoot(file);
24419
+ const result = "python" === inferValidationTarget(file) && appRoot ? pythonFileResult(file, validatePythonWithPyright(appRoot, {
24420
+ files: [
24421
+ external_path_default().resolve(file)
24422
+ ]
24423
+ })) : await validateSingleFileBasic(file);
24424
+ if (!appRoot) return result;
24425
+ const cacheDiagnostic = await dependencyCacheDiagnostic(appRoot);
24426
+ return cacheDiagnostic ? {
24427
+ ...result,
24428
+ warnings: [
24429
+ ...result.warnings ?? [],
24430
+ cacheDiagnostic
24431
+ ]
24432
+ } : result;
24433
+ }
23343
24434
  function logValidationFailure(fileResult) {
23344
24435
  logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
23345
24436
  for (const issue of fileResult.issues ?? []){
@@ -23355,8 +24446,8 @@ print(json.dumps({"issues": issues}))
23355
24446
  function logValidationWarnings(fileResult) {
23356
24447
  for (const warning of fileResult.warnings ?? [])logger_logger.warn(formatValidationWarning(fileResult.file, warning));
23357
24448
  }
23358
- function handleValidateFile(file) {
23359
- const fileResult = validateSingleFile(file);
24449
+ async function handleValidateFile(file) {
24450
+ const fileResult = await validateSingleFile(file);
23360
24451
  if (fileResult.valid) {
23361
24452
  if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23362
24453
  data: {
@@ -23391,7 +24482,7 @@ print(json.dumps({"issues": issues}))
23391
24482
  }
23392
24483
  process.exitCode = 1;
23393
24484
  }
23394
- function handleValidateDirectory(dir) {
24485
+ async function handleValidateDirectory(dir) {
23395
24486
  const files = collectValidationFiles(dir);
23396
24487
  if (0 === files.length) {
23397
24488
  const error = `No supported files found under ${dir}`;
@@ -23407,25 +24498,100 @@ print(json.dumps({"issues": issues}))
23407
24498
  process.exitCode = 1;
23408
24499
  return;
23409
24500
  }
23410
- const fileResults = files.map(validateSingleFile);
24501
+ const pythonFiles = files.filter((file)=>"python" === inferValidationTarget(file));
24502
+ const nonPythonResults = new Map(await Promise.all(files.filter((file)=>"python" !== inferValidationTarget(file)).map(async (file)=>[
24503
+ file,
24504
+ await validateSingleFileBasic(file)
24505
+ ])));
24506
+ const pythonResults = new Map();
24507
+ let globalPythonIssues = [];
24508
+ let globalPythonWarnings = [];
24509
+ if (pythonFiles.length > 0) {
24510
+ const pyright = findAnvilPyright();
24511
+ const filesByAppRoot = new Map();
24512
+ const filesWithoutAppRoot = [];
24513
+ for (const file of pythonFiles){
24514
+ const appRoot = findAnvilAppRoot(file);
24515
+ if (!appRoot) {
24516
+ filesWithoutAppRoot.push(file);
24517
+ continue;
24518
+ }
24519
+ const appFiles = filesByAppRoot.get(appRoot) ?? [];
24520
+ appFiles.push(file);
24521
+ filesByAppRoot.set(appRoot, appFiles);
24522
+ }
24523
+ for (const [appRoot, appFiles] of filesByAppRoot){
24524
+ const relativeAppRoot = external_path_default().relative(external_path_default().resolve(dir), external_path_default().resolve(appRoot));
24525
+ const includesWholeApp = "" === relativeAppRoot || !external_path_default().isAbsolute(relativeAppRoot) && ".." !== relativeAppRoot && !relativeAppRoot.startsWith(`..${external_path_default().sep}`);
24526
+ const result = validatePythonWithPyright(appRoot, {
24527
+ ...includesWholeApp ? {} : {
24528
+ files: appFiles.map((file)=>external_path_default().resolve(file))
24529
+ },
24530
+ pyright
24531
+ });
24532
+ if (!result.ok) globalPythonIssues.push(...result.issues.filter((diagnostic)=>!diagnostic.file).map((diagnostic)=>({
24533
+ ...diagnostic,
24534
+ appRoot
24535
+ })));
24536
+ globalPythonWarnings.push(...(result.warnings ?? []).filter((diagnostic)=>!diagnostic.file).map((diagnostic)=>({
24537
+ ...diagnostic,
24538
+ appRoot
24539
+ })));
24540
+ for (const file of appFiles)pythonResults.set(file, pythonFileResult(file, result, {
24541
+ includeGlobalDiagnostics: false
24542
+ }));
24543
+ }
24544
+ for (const file of filesWithoutAppRoot)pythonResults.set(file, await validateSingleFileBasic(file));
24545
+ }
24546
+ const appRoots = new Set(files.map((file)=>findAnvilAppRoot(file)).filter((appRoot)=>null !== appRoot));
24547
+ for (const appRoot of appRoots){
24548
+ const cacheDiagnostic = await dependencyCacheDiagnostic(appRoot);
24549
+ if (cacheDiagnostic) globalPythonWarnings.push({
24550
+ ...cacheDiagnostic,
24551
+ appRoot
24552
+ });
24553
+ }
24554
+ const fileResults = files.map((file)=>pythonResults.get(file) ?? nonPythonResults.get(file));
23411
24555
  const failedResults = fileResults.filter((result)=>!result.valid);
23412
- if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
23413
- error: failedResults.length > 0 ? "Validation failed" : void 0,
24556
+ const valid = 0 === failedResults.length && 0 === globalPythonIssues.length;
24557
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(valid, {
24558
+ error: valid ? void 0 : "Validation failed",
23414
24559
  data: {
23415
24560
  file: dir,
23416
- valid: 0 === failedResults.length,
24561
+ valid,
23417
24562
  count: fileResults.length,
23418
- files: fileResults
24563
+ files: fileResults,
24564
+ ...globalPythonIssues.length > 0 ? {
24565
+ issues: globalPythonIssues
24566
+ } : {},
24567
+ ...globalPythonWarnings.length > 0 ? {
24568
+ warnings: globalPythonWarnings
24569
+ } : {}
23419
24570
  }
23420
24571
  });
23421
- else for (const fileResult of fileResults)if (fileResult.valid) {
23422
- logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23423
- logValidationWarnings(fileResult);
23424
- } else {
23425
- logValidationFailure(fileResult);
23426
- logValidationWarnings(fileResult);
24572
+ else {
24573
+ for (const [appRoot, issues] of groupProjectDiagnostics(globalPythonIssues))logValidationFailure({
24574
+ file: appRoot,
24575
+ type: "python",
24576
+ valid: false,
24577
+ error: "Python semantic validation failed",
24578
+ issues
24579
+ });
24580
+ for (const [appRoot, warnings] of groupProjectDiagnostics(globalPythonWarnings))logValidationWarnings({
24581
+ file: appRoot,
24582
+ type: "python",
24583
+ valid: true,
24584
+ warnings
24585
+ });
24586
+ for (const fileResult of fileResults)if (fileResult.valid) {
24587
+ logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
24588
+ logValidationWarnings(fileResult);
24589
+ } else {
24590
+ logValidationFailure(fileResult);
24591
+ logValidationWarnings(fileResult);
24592
+ }
23427
24593
  }
23428
- if (failedResults.length > 0) process.exitCode = 1;
24594
+ if (!valid) process.exitCode = 1;
23429
24595
  }
23430
24596
  function getUpdateInstructions(platform) {
23431
24597
  if ("win32" === platform) return [
@@ -23536,11 +24702,14 @@ print(json.dumps({"issues": issues}))
23536
24702
  registerVersionCommand(program, VERSION);
23537
24703
  registerConfigureCommand(program, VERSION);
23538
24704
  registerDepsCommand(program);
23539
- registerTablesCommand(program);
24705
+ registerDbCommand(program);
24706
+ registerEnvCommand(program);
24707
+ registerReplCommand(program);
24708
+ registerSecretCommand(program);
23540
24709
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
23541
24710
  await handleUpdateCommand();
23542
24711
  });
23543
- program.command("validate").description("Validate Anvil app files by path or directory (anvil.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, or server_code/**/*.py)").argument("<path>", "Path to file or directory").action((file)=>{
24712
+ program.command("validate").description("Validate Anvil app files by path or directory (anvil.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, or server_code/**/*.py)").argument("<path>", "Path to file or directory").action(async (file)=>{
23544
24713
  let pathStat;
23545
24714
  try {
23546
24715
  pathStat = (0, external_fs_.statSync)(file);
@@ -23549,8 +24718,8 @@ print(json.dumps({"issues": issues}))
23549
24718
  process.exitCode = 1;
23550
24719
  return;
23551
24720
  }
23552
- if (pathStat.isDirectory()) return void handleValidateDirectory(file);
23553
- handleValidateFile(file);
24721
+ if (pathStat.isDirectory()) return void await handleValidateDirectory(file);
24722
+ await handleValidateFile(file);
23554
24723
  });
23555
24724
  if (watchCommand) {
23556
24725
  const watchOptions = watchCommand.options.map((opt)=>{