@anvil-works/anvil-cli 0.7.0-canary.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +39 -3
  2. package/dist/SavePathRouter.d.ts +2 -5
  3. package/dist/SavePathRouter.d.ts.map +1 -1
  4. package/dist/WatchSession.d.ts +21 -0
  5. package/dist/WatchSession.d.ts.map +1 -1
  6. package/dist/api.d.ts +4 -0
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/cli.js +2569 -497
  9. package/dist/commands/appDetection.d.ts +2 -0
  10. package/dist/commands/appDetection.d.ts.map +1 -0
  11. package/dist/commands/convertTemplate.d.ts +25 -0
  12. package/dist/commands/convertTemplate.d.ts.map +1 -0
  13. package/dist/commands/deps.d.ts +7 -0
  14. package/dist/commands/deps.d.ts.map +1 -0
  15. package/dist/commands/gitAuth.d.ts +89 -0
  16. package/dist/commands/gitAuth.d.ts.map +1 -0
  17. package/dist/commands/gitCredential.d.ts.map +1 -1
  18. package/dist/commands/index.d.ts +4 -0
  19. package/dist/commands/index.d.ts.map +1 -1
  20. package/dist/commands/tables.d.ts +7 -0
  21. package/dist/commands/tables.d.ts.map +1 -0
  22. package/dist/commands/watch.d.ts +4 -0
  23. package/dist/commands/watch.d.ts.map +1 -1
  24. package/dist/componentBuiltinTypes.d.ts.map +1 -1
  25. package/dist/componentName.d.ts +5 -0
  26. package/dist/componentName.d.ts.map +1 -0
  27. package/dist/componentTypeSpec.d.ts +8 -0
  28. package/dist/componentTypeSpec.d.ts.map +1 -0
  29. package/dist/formSpecContext.d.ts +4 -0
  30. package/dist/formSpecContext.d.ts.map +1 -0
  31. package/dist/formTemplateValidation.d.ts +13 -0
  32. package/dist/formTemplateValidation.d.ts.map +1 -0
  33. package/dist/index.js +2050 -427
  34. package/dist/program.d.ts.map +1 -1
  35. package/dist/services/auth.d.ts.map +1 -1
  36. package/dist/services/deps.d.ts +106 -0
  37. package/dist/services/deps.d.ts.map +1 -0
  38. package/dist/services/git-auth.d.ts.map +1 -1
  39. package/dist/services/git.d.ts +25 -1
  40. package/dist/services/git.d.ts.map +1 -1
  41. package/dist/services/gitignore.d.ts +2 -0
  42. package/dist/services/gitignore.d.ts.map +1 -0
  43. package/dist/services/tables.d.ts +27 -0
  44. package/dist/services/tables.d.ts.map +1 -0
  45. package/dist/validateFormTemplateHtml.d.ts +10 -0
  46. package/dist/validateFormTemplateHtml.d.ts.map +1 -0
  47. package/dist/validatePython.d.ts +22 -0
  48. package/dist/validatePython.d.ts.map +1 -0
  49. package/dist/validators.d.ts +8 -8
  50. package/dist/validators.d.ts.map +1 -1
  51. package/dist/watch/ConflictResolver.d.ts +8 -1
  52. package/dist/watch/ConflictResolver.d.ts.map +1 -1
  53. package/dist/watch/SaveProcessor.d.ts +10 -1
  54. package/dist/watch/SaveProcessor.d.ts.map +1 -1
  55. package/dist/watch/SyncManager.d.ts +7 -2
  56. package/dist/watch/SyncManager.d.ts.map +1 -1
  57. package/dist/watch/WebSocketClient.d.ts +21 -0
  58. package/dist/watch/WebSocketClient.d.ts.map +1 -1
  59. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -7371,6 +7371,7 @@ var __webpack_exports__ = {};
7371
7371
  setConfig: ()=>setConfig,
7372
7372
  getAccountsForUrl: ()=>auth_getAccountsForUrl,
7373
7373
  AppIdWithContext: ()=>anvil_api_namespaceObject.AppIdWithContext,
7374
+ inspectCredentialHelperCommand: ()=>inspectCredentialHelperCommand,
7374
7375
  executeCheckout: ()=>executeCheckout,
7375
7376
  validateCheckoutDestination: ()=>validateCheckoutDestination,
7376
7377
  DeviceAuthorizationResponse: ()=>oauth_login_namespaceObject.DeviceAuthorizationResponse,
@@ -7386,7 +7387,9 @@ var __webpack_exports__ = {};
7386
7387
  verifyAndStoreTokens: ()=>auth_verifyAndStoreTokens,
7387
7388
  formatValidationPath: ()=>formatValidationPath,
7388
7389
  resolveAnvilUrl: ()=>resolveAnvilUrl,
7390
+ buildGitAuthDoctorReport: ()=>buildGitAuthDoctorReport,
7389
7391
  parseCheckoutInput: ()=>parseCheckoutInput,
7392
+ formatGitAuthDoctorReport: ()=>formatGitAuthDoctorReport,
7390
7393
  resetConfig: ()=>resetConfig,
7391
7394
  FormTemplateValidationResult: ()=>validators_namespaceObject.FormTemplateValidationResult,
7392
7395
  createWatchDiagnostic: ()=>createWatchDiagnostic,
@@ -7400,6 +7403,7 @@ var __webpack_exports__ = {};
7400
7403
  CheckoutAppListItem: ()=>anvil_api_namespaceObject.CheckoutAppListItem,
7401
7404
  getConfig: ()=>config_getConfig,
7402
7405
  CheckoutExecutionDeps: ()=>checkout_namespaceObject.CheckoutExecutionDeps,
7406
+ validateFormTemplateHtml: ()=>validateFormTemplateHtml,
7403
7407
  ValidateBranchSyncStatusOptions: ()=>validation_namespaceObject.ValidateBranchSyncStatusOptions,
7404
7408
  BranchSyncStatus: ()=>validation_namespaceObject.BranchSyncStatus,
7405
7409
  getLatestVersion: ()=>getLatestVersion,
@@ -7449,6 +7453,7 @@ var __webpack_exports__ = {};
7449
7453
  executeGitCredentialOperation: ()=>executeGitCredentialOperation,
7450
7454
  CheckoutResult: ()=>checkout_namespaceObject.CheckoutResult,
7451
7455
  ParsedCheckoutInput: ()=>checkout_namespaceObject.ParsedCheckoutInput,
7456
+ parseCredentialHelperCommand: ()=>parseCredentialHelperCommand,
7452
7457
  clearInMemoryTokens: ()=>clearInMemoryTokens,
7453
7458
  getDefaultDestinationDirectory: ()=>getDefaultDestinationDirectory,
7454
7459
  lookupByCommit: ()=>lookupByCommit,
@@ -13693,7 +13698,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13693
13698
  }
13694
13699
  function getCredentialHelperModulePath(options) {
13695
13700
  const currentModulePath = options?.currentModulePath ?? __filename;
13696
- if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
13701
+ if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
13697
13702
  return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
13698
13703
  }
13699
13704
  async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
@@ -13880,6 +13885,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13880
13885
  function auth_setRepoContext(repoPath) {
13881
13886
  repoContext = repoPath;
13882
13887
  }
13888
+ const inFlightRefreshes = new Map();
13883
13889
  async function auth_verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13884
13890
  try {
13885
13891
  const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
@@ -13933,53 +13939,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13933
13939
  }
13934
13940
  async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
13935
13941
  const normalized = config_normalizeAnvilUrl(anvilUrl);
13936
- let tokens;
13937
- let repoAuth;
13938
- if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
13939
- else {
13940
- repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13941
- tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
13942
- }
13942
+ const source = await resolveTokenSource(normalized, username);
13943
+ const tokens = getSourceTokens(source);
13943
13944
  if (!tokens.authToken && !tokens.refreshToken) throw createAuthError.required("Not logged in. Please log in first.");
13944
13945
  const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
13945
- if (tokens.refreshToken && isExpired) try {
13946
+ if (tokens.refreshToken && isExpired) {
13947
+ const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13948
+ const existingRefresh = inFlightRefreshes.get(refreshKey);
13949
+ if (existingRefresh) return existingRefresh;
13950
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
13951
+ inFlightRefreshes.delete(refreshKey);
13952
+ });
13953
+ inFlightRefreshes.set(refreshKey, refreshPromise);
13954
+ return refreshPromise;
13955
+ }
13956
+ if (tokens.authToken) return tokens.authToken;
13957
+ throw createAuthError.required("No valid token available");
13958
+ }
13959
+ async function resolveTokenSource(normalizedUrl, username) {
13960
+ if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
13961
+ type: "memory",
13962
+ auth: inMemoryAuth
13963
+ };
13964
+ const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13965
+ if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13966
+ type: "repo",
13967
+ auth: repoAuth,
13968
+ repoPath: repoContext
13969
+ };
13970
+ const tokens = getTokensAutoSelect(normalizedUrl, username);
13971
+ return {
13972
+ type: "store",
13973
+ tokens,
13974
+ accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
13975
+ };
13976
+ }
13977
+ function getSourceTokens(source) {
13978
+ return "store" === source.type ? source.tokens : source.auth.tokens;
13979
+ }
13980
+ function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13981
+ if (username) return username;
13982
+ const urlTokens = getUrlTokens(normalizedUrl);
13983
+ if (!urlTokens) return;
13984
+ const accounts = Object.keys(urlTokens);
13985
+ if (1 === accounts.length) return accounts[0];
13986
+ return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
13987
+ }
13988
+ function getRefreshKey(normalizedUrl, username, source, refreshToken) {
13989
+ switch(source.type){
13990
+ case "memory":
13991
+ return `memory:${normalizedUrl}:${source.auth.username}`;
13992
+ case "repo":
13993
+ return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
13994
+ case "store":
13995
+ return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
13996
+ }
13997
+ }
13998
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
13999
+ try {
14000
+ const tokens = getSourceTokens(source);
13946
14001
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
13947
- const tokenData = await refreshAccessToken(tokens.refreshToken, normalized, clientId);
13948
- const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
14002
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
13949
14003
  const newTokens = {
13950
14004
  authToken: tokenData.access_token,
13951
14005
  refreshToken: tokenData.refresh_token,
13952
- authTokenExpiresAt: newExpiresAt,
14006
+ authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
13953
14007
  clientId
13954
14008
  };
13955
- if (inMemoryAuth) inMemoryAuth.tokens = newTokens;
13956
- else if (repoAuth) {
14009
+ if ("memory" === source.type) source.auth.tokens = newTokens;
14010
+ else if ("repo" === source.type) {
13957
14011
  const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
13958
14012
  if (!encryptionKey) throw createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
13959
- repoAuth.tokens = newTokens;
13960
- await writeAuthToFile(repoContext, repoAuth, encryptionKey);
14013
+ source.auth.tokens = newTokens;
14014
+ await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
13961
14015
  } else {
13962
- let accountUsername = username;
13963
- if (!accountUsername) {
13964
- const urlTokens = getUrlTokens(normalized);
13965
- if (urlTokens) {
13966
- const accounts = Object.keys(urlTokens);
13967
- accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
13968
- }
13969
- }
13970
- if (!accountUsername) {
13971
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14016
+ if (!source.accountUsername) {
14017
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13972
14018
  throw createAuthError.required("Cannot determine which account to refresh. Please log in again.");
13973
14019
  }
13974
- setAccountTokens(normalized, accountUsername, newTokens);
14020
+ setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
13975
14021
  }
13976
14022
  return tokenData.access_token;
13977
14023
  } catch (e) {
13978
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14024
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13979
14025
  throw e;
13980
14026
  }
13981
- if (tokens.authToken) return tokens.authToken;
13982
- throw createAuthError.required("No valid token available");
13983
14027
  }
13984
14028
  async function clearTokensForAccount(url, username, refreshToken) {
13985
14029
  if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
@@ -14309,6 +14353,324 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14309
14353
  } catch (_e) {}
14310
14354
  return out;
14311
14355
  }
14356
+ const promises_namespaceObject = require("fs/promises");
14357
+ var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
14358
+ function normalizeLineEndings(content) {
14359
+ return content.replace(/\r\n/g, "\n");
14360
+ }
14361
+ function pythonifyName(name) {
14362
+ return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14363
+ }
14364
+ function extractPythonName(relativePath, skipParts = 1) {
14365
+ const parts = relativePath.slice(0, -3).split("/");
14366
+ const isPackage = relativePath.endsWith("__init__.py");
14367
+ const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14368
+ return relevantParts.map(pythonifyName).join(".");
14369
+ }
14370
+ function deepEqual(a, b) {
14371
+ if (a === b) return true;
14372
+ if (null == a || null == b) return false;
14373
+ if (typeof a !== typeof b) return false;
14374
+ if ("object" != typeof a) return a === b;
14375
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
14376
+ if (Array.isArray(a)) {
14377
+ if (a.length !== b.length) return false;
14378
+ return a.every((item, index)=>deepEqual(item, b[index]));
14379
+ }
14380
+ const keysA = Object.keys(a);
14381
+ const keysB = Object.keys(b);
14382
+ if (keysA.length !== keysB.length) return false;
14383
+ return keysA.every((key)=>deepEqual(a[key], b[key]));
14384
+ }
14385
+ function parseHtmlWithFrontmatter(htmlContent) {
14386
+ const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14387
+ const match = htmlContent.match(frontmatterPattern);
14388
+ if (match) {
14389
+ const yamlStr = match[1];
14390
+ const html = match[2];
14391
+ const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14392
+ return {
14393
+ frontmatter,
14394
+ html
14395
+ };
14396
+ }
14397
+ return {
14398
+ frontmatter: {},
14399
+ html: htmlContent
14400
+ };
14401
+ }
14402
+ const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
14403
+ async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
14404
+ const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
14405
+ try {
14406
+ const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
14407
+ if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
14408
+ const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
14409
+ await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14410
+ return true;
14411
+ } catch (error) {
14412
+ if ("ENOENT" !== error.code) throw error;
14413
+ await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14414
+ return true;
14415
+ }
14416
+ }
14417
+ const ANVIL_DEPS_DIR = ".anvil/deps";
14418
+ const ANVIL_DEPS_MANIFEST = "manifest.json";
14419
+ const DEPS_MANIFEST_VERSION = 1;
14420
+ function formatFetchError(prefix, status, errorBody) {
14421
+ if (!errorBody) return `${prefix}: ${status}`;
14422
+ try {
14423
+ const parsed = JSON.parse(errorBody);
14424
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14425
+ } catch {}
14426
+ return `${prefix}: ${status} ${errorBody}`;
14427
+ }
14428
+ async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14429
+ const { commit, includeDocs = true, knownDependencies = [] } = options;
14430
+ const token = await auth_getValidAuthToken(anvilUrl);
14431
+ const body = {
14432
+ include_docs: includeDocs,
14433
+ known_dependencies: knownDependencies
14434
+ };
14435
+ if (commit) body.commit = commit;
14436
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
14437
+ method: "POST",
14438
+ headers: {
14439
+ Authorization: `Bearer ${token}`,
14440
+ "Content-Type": "application/json"
14441
+ },
14442
+ body: JSON.stringify(body)
14443
+ });
14444
+ if (!resp.ok) {
14445
+ const errorBody = await resp.text();
14446
+ throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
14447
+ }
14448
+ return await resp.json();
14449
+ }
14450
+ async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14451
+ const includeDocs = options.includeDocs ?? true;
14452
+ const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
14453
+ const response = await fetchAllDependencies(appId, anvilUrl, {
14454
+ commit: options.commit,
14455
+ includeDocs,
14456
+ knownDependencies
14457
+ });
14458
+ if (!response.resolved_dependencies.length) {
14459
+ await clearDepsCache(projectRoot);
14460
+ return {
14461
+ response,
14462
+ addedGitignoreEntry: false
14463
+ };
14464
+ }
14465
+ const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
14466
+ await writeAllDepsToCache(projectRoot, response, {
14467
+ includeDocs
14468
+ });
14469
+ return {
14470
+ response,
14471
+ addedGitignoreEntry
14472
+ };
14473
+ }
14474
+ function depsDir(projectRoot) {
14475
+ return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
14476
+ }
14477
+ function depsManifestPath(projectRoot) {
14478
+ return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
14479
+ }
14480
+ function isResolvedDependencyManifestEntry(value) {
14481
+ if (!value || "object" != typeof value) return false;
14482
+ const entry = value;
14483
+ 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);
14484
+ }
14485
+ function isCachedDependencyManifestEntry(value) {
14486
+ if (!isResolvedDependencyManifestEntry(value)) return false;
14487
+ const entry = value;
14488
+ return "string" == typeof entry.package_name && entry.package_name.length > 0;
14489
+ }
14490
+ function isDepsCacheManifest(value) {
14491
+ if (!value || "object" != typeof value) return false;
14492
+ const manifest = value;
14493
+ 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);
14494
+ }
14495
+ async function readDepsCacheManifest(projectRoot) {
14496
+ try {
14497
+ const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
14498
+ const parsed = JSON.parse(raw);
14499
+ return isDepsCacheManifest(parsed) ? parsed : void 0;
14500
+ } catch {
14501
+ return;
14502
+ }
14503
+ }
14504
+ async function getKnownDependenciesForCache(projectRoot, includeDocs) {
14505
+ const manifest = await readDepsCacheManifest(projectRoot);
14506
+ if (!manifest || manifest.includeDocs !== includeDocs) return [];
14507
+ return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
14508
+ app_id,
14509
+ commit_id
14510
+ }));
14511
+ }
14512
+ function getDependencyWatchSubscriptionsFromManifest(manifest) {
14513
+ if (!manifest) return [];
14514
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
14515
+ dep.app_id,
14516
+ dep
14517
+ ]));
14518
+ return manifest.resolved_dependencies.flatMap((dep)=>{
14519
+ if (!dep.branch) return [];
14520
+ return [
14521
+ {
14522
+ appId: dep.app_id,
14523
+ branch: dep.branch,
14524
+ packageName: cachedByAppId.get(dep.app_id)?.package_name
14525
+ }
14526
+ ];
14527
+ });
14528
+ }
14529
+ function normalizeAnvilYamlDependenciesFromContent(content) {
14530
+ const parsed = external_js_yaml_default().load(content);
14531
+ if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
14532
+ return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
14533
+ }
14534
+ function normalizeAnvilYamlDependenciesValue(value) {
14535
+ if (!Array.isArray(value)) return [];
14536
+ return normalizeForComparison(value);
14537
+ }
14538
+ async function readAnvilYamlDependencies(projectRoot) {
14539
+ try {
14540
+ const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
14541
+ return normalizeAnvilYamlDependenciesFromContent(content);
14542
+ } catch {
14543
+ return [];
14544
+ }
14545
+ }
14546
+ function anvilYamlDependenciesEqual(left, right) {
14547
+ return deepEqual(left, right);
14548
+ }
14549
+ function normalizeForComparison(value) {
14550
+ if (Array.isArray(value)) return value.map(normalizeForComparison);
14551
+ if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
14552
+ key,
14553
+ normalizeForComparison(entryValue)
14554
+ ]));
14555
+ return value;
14556
+ }
14557
+ function cachedManifestEntry(dep, packageName) {
14558
+ return {
14559
+ app_id: dep.app_id,
14560
+ commit_id: dep.commit_id,
14561
+ ...dep.branch ? {
14562
+ branch: dep.branch
14563
+ } : {},
14564
+ package_name: packageName
14565
+ };
14566
+ }
14567
+ function safeDependencyPath(filePath, kind) {
14568
+ const normalizedPath = external_path_default().normalize(filePath);
14569
+ if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
14570
+ return normalizedPath;
14571
+ }
14572
+ async function writeDepToCache(projectRoot, dep) {
14573
+ const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
14574
+ await promises_default().rm(depDir, {
14575
+ recursive: true,
14576
+ force: true
14577
+ });
14578
+ await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
14579
+ recursive: true
14580
+ });
14581
+ await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
14582
+ recursive: true
14583
+ });
14584
+ await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
14585
+ package_name: dep.package_name
14586
+ }));
14587
+ for (const mod of dep.modules ?? [])if (mod.is_package) {
14588
+ const modDir = external_path_default().join(depDir, "client_code", mod.name);
14589
+ await promises_default().mkdir(modDir, {
14590
+ recursive: true
14591
+ });
14592
+ await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14593
+ } else await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${mod.name}.py`), mod.code);
14594
+ for (const form of dep.forms ?? []){
14595
+ const { code, class_name, is_package, ...formTemplate } = form;
14596
+ if (is_package) {
14597
+ const formDir = external_path_default().join(depDir, "client_code", class_name);
14598
+ await promises_default().mkdir(formDir, {
14599
+ recursive: true
14600
+ });
14601
+ await promises_default().writeFile(external_path_default().join(formDir, "__init__.py"), code);
14602
+ await promises_default().writeFile(external_path_default().join(formDir, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
14603
+ } else {
14604
+ await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.py`), code);
14605
+ await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.yaml`), external_js_yaml_default().dump(formTemplate));
14606
+ }
14607
+ }
14608
+ for (const mod of dep.server_modules ?? [])if (mod.is_package) {
14609
+ const modDir = external_path_default().join(depDir, "server_code", mod.name);
14610
+ await promises_default().mkdir(modDir, {
14611
+ recursive: true
14612
+ });
14613
+ await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14614
+ } else await promises_default().writeFile(external_path_default().join(depDir, "server_code", `${mod.name}.py`), mod.code);
14615
+ for (const asset of dep.assets ?? []){
14616
+ const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
14617
+ await promises_default().mkdir(external_path_default().dirname(targetPath), {
14618
+ recursive: true
14619
+ });
14620
+ await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
14621
+ }
14622
+ for (const doc of dep.docs ?? []){
14623
+ const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
14624
+ await promises_default().mkdir(external_path_default().dirname(targetPath), {
14625
+ recursive: true
14626
+ });
14627
+ await promises_default().writeFile(targetPath, doc.content);
14628
+ }
14629
+ }
14630
+ async function writeAllDepsToCache(projectRoot, response, options = {}) {
14631
+ const includeDocs = options.includeDocs ?? true;
14632
+ const manifest = await readDepsCacheManifest(projectRoot);
14633
+ const canReuseExistingCache = manifest?.includeDocs === includeDocs;
14634
+ if (!canReuseExistingCache) await clearDepsCache(projectRoot);
14635
+ const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
14636
+ const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
14637
+ 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), {
14638
+ recursive: true,
14639
+ force: true
14640
+ });
14641
+ for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
14642
+ const returnedByAppId = new Map(response.dependencies.map((dep)=>[
14643
+ dep.app_id,
14644
+ dep
14645
+ ]));
14646
+ const previousByAppId = new Map(previousCached.map((dep)=>[
14647
+ dep.app_id,
14648
+ dep
14649
+ ]));
14650
+ const cachedDependencies = response.resolved_dependencies.map((dep)=>{
14651
+ const returned = returnedByAppId.get(dep.app_id);
14652
+ if (returned) return cachedManifestEntry(returned, returned.package_name);
14653
+ const previous = previousByAppId.get(dep.app_id);
14654
+ if (!previous) return;
14655
+ return cachedManifestEntry(dep, previous.package_name);
14656
+ }).filter((dep)=>void 0 !== dep);
14657
+ const newManifest = {
14658
+ version: DEPS_MANIFEST_VERSION,
14659
+ includeDocs,
14660
+ resolved_dependencies: response.resolved_dependencies,
14661
+ cached_dependencies: cachedDependencies
14662
+ };
14663
+ await promises_default().mkdir(depsDir(projectRoot), {
14664
+ recursive: true
14665
+ });
14666
+ await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
14667
+ }
14668
+ async function clearDepsCache(projectRoot) {
14669
+ await promises_default().rm(depsDir(projectRoot), {
14670
+ recursive: true,
14671
+ force: true
14672
+ });
14673
+ }
14312
14674
  class git_GitService {
14313
14675
  git;
14314
14676
  repoPath;
@@ -14395,6 +14757,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14395
14757
  throw errors_createGitError.commandFailed("status", e.message);
14396
14758
  }
14397
14759
  }
14760
+ async discardModeOnlyChanges(paths) {
14761
+ const discarded = [];
14762
+ for (const relativePath of paths)try {
14763
+ const headMode = await this.getHeadFileMode(relativePath);
14764
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14765
+ const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14766
+ if (!contentUnchanged) continue;
14767
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14768
+ discarded.push(relativePath);
14769
+ } catch (e) {
14770
+ throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14771
+ }
14772
+ return discarded;
14773
+ }
14774
+ async discardStagedModeOnlyChanges(paths) {
14775
+ const discarded = [];
14776
+ for (const relativePath of paths)try {
14777
+ const headMode = await this.getHeadFileMode(relativePath);
14778
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14779
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14780
+ if (!indexContentUnchanged) continue;
14781
+ await this.git.raw([
14782
+ "reset",
14783
+ "-q",
14784
+ "HEAD",
14785
+ "--",
14786
+ relativePath
14787
+ ]);
14788
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14789
+ discarded.push(relativePath);
14790
+ } catch (e) {
14791
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14792
+ }
14793
+ return discarded;
14794
+ }
14795
+ async getHeadFileMode(relativePath) {
14796
+ const output = await this.git.raw([
14797
+ "ls-tree",
14798
+ "HEAD",
14799
+ "--",
14800
+ relativePath
14801
+ ]);
14802
+ const match = output.match(/^(\d{6})\s/);
14803
+ return match?.[1] ?? null;
14804
+ }
14805
+ async hasSameContentAsHead(relativePath) {
14806
+ const headHash = (await this.git.revparse([
14807
+ `HEAD:${relativePath}`
14808
+ ])).trim();
14809
+ const worktreeHash = (await this.git.raw([
14810
+ "hash-object",
14811
+ "--",
14812
+ relativePath
14813
+ ])).trim();
14814
+ return headHash === worktreeHash;
14815
+ }
14816
+ async hasSameIndexContentAsHead(relativePath) {
14817
+ const headHash = (await this.git.revparse([
14818
+ `HEAD:${relativePath}`
14819
+ ])).trim();
14820
+ const indexHash = (await this.git.revparse([
14821
+ `:${relativePath}`
14822
+ ])).trim();
14823
+ return headHash === indexHash;
14824
+ }
14825
+ async restoreExecutableBit(relativePath, executable) {
14826
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14827
+ const stat = await external_fs_.promises.stat(filePath);
14828
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14829
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14830
+ }
14398
14831
  async hasUncommittedChanges() {
14399
14832
  const status = await this.getStatus();
14400
14833
  return !status.isClean;
@@ -14534,6 +14967,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14534
14967
  throw errors_createGitError.commandFailed("rebase --abort", e.message);
14535
14968
  }
14536
14969
  }
14970
+ async isAncestor(ancestorRef, descendantRef) {
14971
+ try {
14972
+ const mergeBase = (await this.git.raw([
14973
+ "merge-base",
14974
+ ancestorRef,
14975
+ descendantRef
14976
+ ])).trim();
14977
+ const ancestorCommit = (await this.git.revparse([
14978
+ ancestorRef
14979
+ ])).trim();
14980
+ return mergeBase === ancestorCommit;
14981
+ } catch {
14982
+ return false;
14983
+ }
14984
+ }
14985
+ async mergeFastForward(ref) {
14986
+ try {
14987
+ await this.git.raw([
14988
+ "merge",
14989
+ "--ff-only",
14990
+ ref
14991
+ ]);
14992
+ } catch (e) {
14993
+ throw errors_createGitError.commandFailed("merge --ff-only", e.message);
14994
+ }
14995
+ }
14537
14996
  async deleteRef(ref) {
14538
14997
  try {
14539
14998
  await this.git.raw([
@@ -14578,11 +15037,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14578
15037
  } catch (error) {}
14579
15038
  }
14580
15039
  }
14581
- async function getStagedFileChanges(git) {
15040
+ async function getStagedFileChanges(git, gitService) {
14582
15041
  try {
14583
15042
  const status = await git.status();
14584
15043
  const renames = status.renamed || [];
14585
15044
  const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
15045
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14586
15046
  const stagedRenames = [];
14587
15047
  for (const f of stagedFiles)if ("R" === f.index) {
14588
15048
  const rename = renames.find((r)=>r.to === f.path);
@@ -14595,19 +15055,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14595
15055
  const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14596
15056
  const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14597
15057
  const changes = [];
14598
- for (const f of stagedFiles)if ("R" !== f.index) {
14599
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14600
- path: f.path,
14601
- type: "unlink"
14602
- });
14603
- else if ("A" === f.index || "?" === f.index) changes.push({
14604
- path: f.path,
14605
- type: "add"
14606
- });
14607
- else changes.push({
14608
- path: f.path,
14609
- type: "change"
14610
- });
15058
+ for (const f of stagedFiles){
15059
+ if ("R" !== f.index) {
15060
+ if (!stagedModeOnlyPaths.has(f.path)) {
15061
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
15062
+ path: f.path,
15063
+ type: "unlink"
15064
+ });
15065
+ else if ("A" === f.index || "?" === f.index) changes.push({
15066
+ path: f.path,
15067
+ type: "add"
15068
+ });
15069
+ else changes.push({
15070
+ path: f.path,
15071
+ type: "change"
15072
+ });
15073
+ }
15074
+ }
14611
15075
  }
14612
15076
  changes.push(...stagedRenames);
14613
15077
  return changes;
@@ -14615,50 +15079,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14615
15079
  throw errors_createGitError.commandFailed("status", e.message);
14616
15080
  }
14617
15081
  }
14618
- function normalizeLineEndings(content) {
14619
- return content.replace(/\r\n/g, "\n");
14620
- }
14621
- function pythonifyName(name) {
14622
- return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14623
- }
14624
- function extractPythonName(relativePath, skipParts = 1) {
14625
- const parts = relativePath.slice(0, -3).split("/");
14626
- const isPackage = relativePath.endsWith("__init__.py");
14627
- const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14628
- return relevantParts.map(pythonifyName).join(".");
14629
- }
14630
- function deepEqual(a, b) {
14631
- if (a === b) return true;
14632
- if (null == a || null == b) return false;
14633
- if (typeof a !== typeof b) return false;
14634
- if ("object" != typeof a) return a === b;
14635
- if (Array.isArray(a) !== Array.isArray(b)) return false;
14636
- if (Array.isArray(a)) {
14637
- if (a.length !== b.length) return false;
14638
- return a.every((item, index)=>deepEqual(item, b[index]));
14639
- }
14640
- const keysA = Object.keys(a);
14641
- const keysB = Object.keys(b);
14642
- if (keysA.length !== keysB.length) return false;
14643
- return keysA.every((key)=>deepEqual(a[key], b[key]));
14644
- }
14645
- function parseHtmlWithFrontmatter(htmlContent) {
14646
- const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14647
- const match = htmlContent.match(frontmatterPattern);
14648
- if (match) {
14649
- const yamlStr = match[1];
14650
- const html = match[2];
14651
- const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14652
- return {
14653
- frontmatter,
14654
- html
14655
- };
14656
- }
14657
- return {
14658
- frontmatter: {},
14659
- html: htmlContent
14660
- };
14661
- }
14662
15082
  const external_chokidar_namespaceObject = require("chokidar");
14663
15083
  var external_chokidar_default = /*#__PURE__*/ __webpack_require__.n(external_chokidar_namespaceObject);
14664
15084
  class FileWatcher extends Emitter {
@@ -14808,6 +15228,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14808
15228
  editSession;
14809
15229
  username;
14810
15230
  environment;
15231
+ dependencyWatchEnabled;
15232
+ dependencySubscriptions;
15233
+ activeDependencySubscriptions = new Map();
15234
+ watchedPrimaryBranch = null;
15235
+ nextSubscriptionId = 1;
15236
+ pendingDependencySubscriptionById = new Map();
14811
15237
  reconnectAttempts = 0;
14812
15238
  connectAttempts = 0;
14813
15239
  reconnectTimer = null;
@@ -14830,6 +15256,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14830
15256
  this.editSession = options.editSession;
14831
15257
  this.username = options.username;
14832
15258
  this.environment = options.environment;
15259
+ this.dependencyWatchEnabled = options.dependencyWatch ?? true;
15260
+ this.dependencySubscriptions = dedupeDependencySubscriptions(options.dependencySubscriptions ?? []);
14833
15261
  this.sessionId = Math.random().toString(36).substring(2, 10);
14834
15262
  this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
14835
15263
  }
@@ -14855,6 +15283,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14855
15283
  const subscribeMsg = this.buildSubscribeMessage(this.currentBranch);
14856
15284
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing: ${JSON.stringify(subscribeMsg)}`);
14857
15285
  this.ws?.send(JSON.stringify(subscribeMsg));
15286
+ this.watchedPrimaryBranch = this.currentBranch;
15287
+ this.activeDependencySubscriptions.clear();
15288
+ this.subscribeToDependencies();
14858
15289
  this.emit("connected", void 0);
14859
15290
  });
14860
15291
  this.ws.on("unexpected-response", (request, response)=>{
@@ -14877,6 +15308,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14877
15308
  try {
14878
15309
  const msg = JSON.parse(data.toString());
14879
15310
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
15311
+ this.handleDependencySubscriptionReply(msg);
14880
15312
  this.handleMessage(msg);
14881
15313
  } catch (error) {
14882
15314
  logger_logger.error(external_chalk_default().red(`Failed to parse WebSocket message: ${error.message}`));
@@ -14927,7 +15359,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14927
15359
  if (!ref_update) return void logger_logger.debug("Ignoring REPO_UPDATE with no ref_update");
14928
15360
  const { ref } = ref_update;
14929
15361
  const commitHash = "string" == typeof commit ? commit : commit?.v;
15362
+ const appId = this.getMessageAppId(msg);
14930
15363
  this.emit("repo-update", {
15364
+ appId,
14931
15365
  ref,
14932
15366
  commitHash: commitHash || "unknown",
14933
15367
  saveSeq: ref_update?.["save-seq"]
@@ -14942,11 +15376,26 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14942
15376
  }
14943
15377
  updateBranch(branch) {
14944
15378
  this.currentBranch = branch;
14945
- if (this.ws?.readyState === external_ws_default().OPEN) this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
15379
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15380
+ this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
15381
+ this.watchedPrimaryBranch = branch;
15382
+ this.subscribeToDependencies();
15383
+ }
14946
15384
  }
14947
15385
  updateEnvironment(environment) {
14948
15386
  this.environment = environment;
14949
15387
  }
15388
+ updateDependencySubscriptions(subscriptions) {
15389
+ const previousSubscriptions = this.dependencySubscriptions;
15390
+ this.dependencySubscriptions = dedupeDependencySubscriptions(subscriptions);
15391
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15392
+ this.unsubscribeStaleDependencies(previousSubscriptions, this.dependencySubscriptions);
15393
+ this.subscribeToDependencies();
15394
+ }
15395
+ }
15396
+ getDependencySubscriptions() {
15397
+ return this.dependencySubscriptions;
15398
+ }
14950
15399
  getEditSession() {
14951
15400
  return this.editSession;
14952
15401
  }
@@ -14957,7 +15406,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14957
15406
  clearTimeout(this.reconnectTimer);
14958
15407
  this.reconnectTimer = null;
14959
15408
  }
15409
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15410
+ this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(this.appId)));
15411
+ this.unwatchDependencySubscriptions([
15412
+ ...this.activeDependencySubscriptions.values()
15413
+ ]);
15414
+ }
15415
+ this.teardownSocket();
15416
+ }
15417
+ reconnect() {
15418
+ if (this.isClosing) return;
14960
15419
  this.teardownSocket();
15420
+ this.connect().catch((error)=>{
15421
+ this.emit("error", {
15422
+ error: error instanceof Error ? error : new Error(String(error))
15423
+ });
15424
+ });
14961
15425
  }
14962
15426
  isConnected() {
14963
15427
  return this.ws?.readyState === external_ws_default().OPEN;
@@ -14990,6 +15454,72 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14990
15454
  } : {}
14991
15455
  };
14992
15456
  }
15457
+ buildUnsubscribeMessage(appId) {
15458
+ return {
15459
+ cmd: "UNWATCH_APP",
15460
+ app: appId
15461
+ };
15462
+ }
15463
+ subscribeToDependencies() {
15464
+ if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
15465
+ for (const subscription of this.dependencySubscriptions){
15466
+ const key = this.dependencySubscriptionKey(subscription);
15467
+ if (this.activeDependencySubscriptions.has(key)) continue;
15468
+ const id = this.nextSubscriptionId++;
15469
+ this.pendingDependencySubscriptionById.set(id, subscription);
15470
+ this.activeDependencySubscriptions.set(key, subscription);
15471
+ const subscribeMsg = {
15472
+ id,
15473
+ cmd: "WATCH_APP",
15474
+ app: subscription.appId,
15475
+ only_branch: subscription.branch
15476
+ };
15477
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing dependency: ${JSON.stringify(subscribeMsg)}`);
15478
+ logger_logger.verbose(external_chalk_default().gray(`Watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch})`));
15479
+ this.ws.send(JSON.stringify(subscribeMsg));
15480
+ }
15481
+ }
15482
+ handleDependencySubscriptionReply(msg) {
15483
+ if ("number" != typeof msg.id) return;
15484
+ const subscription = this.pendingDependencySubscriptionById.get(msg.id);
15485
+ if (!subscription) return;
15486
+ this.pendingDependencySubscriptionById.delete(msg.id);
15487
+ if (!msg.error) return;
15488
+ this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
15489
+ logger_logger.warn(external_chalk_default().yellow(`Could not watch dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch}): ${this.formatDependencySubscriptionError(msg.error)}`));
15490
+ }
15491
+ formatDependencySubscriptionError(error) {
15492
+ if ("string" == typeof error) return error;
15493
+ if (error && "object" == typeof error && "string" == typeof error.message) return error.message;
15494
+ return JSON.stringify(error);
15495
+ }
15496
+ getMessageAppId(msg) {
15497
+ if ("string" == typeof msg.app) return msg.app;
15498
+ if ("string" == typeof msg.app_id) return msg.app_id;
15499
+ if ("string" == typeof msg.ref_update?.app) return msg.ref_update.app;
15500
+ if ("string" == typeof msg.ref_update?.app_id) return msg.ref_update.app_id;
15501
+ return this.appId;
15502
+ }
15503
+ unsubscribeStaleDependencies(previousSubscriptions, nextSubscriptions) {
15504
+ const nextAppIds = new Set(nextSubscriptions.map((subscription)=>subscription.appId));
15505
+ const staleSubscriptions = previousSubscriptions.filter((subscription)=>!nextAppIds.has(subscription.appId));
15506
+ this.unwatchDependencySubscriptions(staleSubscriptions);
15507
+ const nextKeys = new Set(nextSubscriptions.map((subscription)=>this.dependencySubscriptionKey(subscription)));
15508
+ for (const subscription of previousSubscriptions){
15509
+ const key = this.dependencySubscriptionKey(subscription);
15510
+ if (!nextKeys.has(key)) this.activeDependencySubscriptions.delete(key);
15511
+ }
15512
+ }
15513
+ unwatchDependencySubscriptions(subscriptions) {
15514
+ if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
15515
+ const unwatchedAppIds = new Set();
15516
+ for (const subscription of subscriptions)if (!unwatchedAppIds.has(subscription.appId)) {
15517
+ unwatchedAppIds.add(subscription.appId);
15518
+ logger_logger.verbose(external_chalk_default().gray(`Stopped watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId})`));
15519
+ this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(subscription.appId)));
15520
+ }
15521
+ for (const subscription of subscriptions)this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
15522
+ }
14993
15523
  getConnectionContext() {
14994
15524
  return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
14995
15525
  }
@@ -15061,6 +15591,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15061
15591
  }
15062
15592
  teardownSocket() {
15063
15593
  this.stopHeartbeat();
15594
+ this.pendingDependencySubscriptionById.clear();
15595
+ this.activeDependencySubscriptions.clear();
15596
+ this.watchedPrimaryBranch = null;
15064
15597
  const socket = this.ws;
15065
15598
  this.ws = null;
15066
15599
  if (!socket) return;
@@ -15076,6 +15609,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15076
15609
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
15077
15610
  }
15078
15611
  }
15612
+ dependencySubscriptionKey(subscription) {
15613
+ return `${subscription.appId}\0${subscription.branch}`;
15614
+ }
15079
15615
  }
15080
15616
  function normalizeEnvironmentBinding(msg) {
15081
15617
  return {
@@ -15084,6 +15620,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15084
15620
  tableMappingId: msg.tableMappingId ?? msg.table_mapping_id ?? null
15085
15621
  };
15086
15622
  }
15623
+ function dedupeDependencySubscriptions(subscriptions) {
15624
+ const seen = new Set();
15625
+ const deduped = [];
15626
+ for (const subscription of subscriptions){
15627
+ const key = `${subscription.appId}\0${subscription.branch}`;
15628
+ if (!seen.has(key)) {
15629
+ seen.add(key);
15630
+ deduped.push(subscription);
15631
+ }
15632
+ }
15633
+ return deduped;
15634
+ }
15087
15635
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
15088
15636
  try {
15089
15637
  const files = await gitService.diffNames(oldCommitId, newCommitId);
@@ -15134,7 +15682,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15134
15682
  ...status.modified,
15135
15683
  ...status.notAdded,
15136
15684
  ...status.created,
15137
- ...status.deleted
15685
+ ...status.deleted,
15686
+ ...(status.renamed ?? []).flatMap((r)=>[
15687
+ r.from,
15688
+ r.to
15689
+ ])
15138
15690
  ]);
15139
15691
  }
15140
15692
  function extractChangesFromStatus(status) {
@@ -15176,12 +15728,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15176
15728
  result.localOnlyChanges.forEach((c)=>logger_logger.verbose(external_chalk_default().gray(` ${c.path}`)));
15177
15729
  }
15178
15730
  }
15179
- async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
15731
+ async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles, options = {}) {
15180
15732
  const filesToRemove = [];
15181
- for (const file of unstagedFiles)if (remotelyChangedFiles.has(file)) try {
15182
- await gitService.show(`HEAD:${file}`);
15183
- } catch (e) {
15184
- filesToRemove.push(file);
15733
+ const localChangesToPreserve = options.localChangesToPreserve ?? new Set();
15734
+ for (const file of unstagedFiles)if (!localChangesToPreserve.has(file)) {
15735
+ try {
15736
+ await gitService.show(`HEAD:${file}`);
15737
+ continue;
15738
+ } catch (e) {}
15739
+ if (options.oldCommitId) {
15740
+ try {
15741
+ await gitService.show(`${options.oldCommitId}:${file}`);
15742
+ filesToRemove.push(file);
15743
+ } catch (e) {}
15744
+ continue;
15745
+ }
15746
+ if (remotelyChangedFiles.has(file)) filesToRemove.push(file);
15185
15747
  }
15186
15748
  if (filesToRemove.length > 0) try {
15187
15749
  await gitService.clean(filesToRemove);
@@ -15193,6 +15755,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15193
15755
  return filesToRemove;
15194
15756
  }
15195
15757
  const external_crypto_namespaceObject = require("crypto");
15758
+ const form_template_parser_namespaceObject = require("@anvil-works/form-template-parser");
15196
15759
  const BUILTIN_COMPONENT_TYPES = new Set([
15197
15760
  "Label",
15198
15761
  "RichText",
@@ -15223,59 +15786,156 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15223
15786
  "CustomComponent",
15224
15787
  "LayoutSlot",
15225
15788
  "FlowPanel",
15226
- "HtmlTemplate"
15227
- ]);
15228
- const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
15229
- "/runtime/services/tables.yml",
15230
- "/runtime/services/google.yml",
15231
- "/runtime/services/uplink.yml",
15232
- "/runtime/services/stripe.yml",
15233
- "/runtime/services/segment.yml",
15234
- "/runtime/services/facebook.yml",
15235
- "/runtime/services/anvil/users.yml",
15236
- "/runtime/services/anvil/secrets.yml",
15237
- "/runtime/services/anvil/saml.yml",
15238
- "/runtime/services/anvil/microsoft.yml",
15239
- "/runtime/services/anvil/email.yml",
15240
- "/runtime/services/anvil/files.yml"
15789
+ "HtmlTemplate",
15790
+ "HtmlComponent"
15241
15791
  ]);
15242
15792
  const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15243
- const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
15244
- const DB_ACCESS_LEVELS = new Set([
15245
- "none",
15246
- "search",
15247
- "full"
15248
- ]);
15249
- const DB_BASIC_COLUMN_TYPES = new Set([
15250
- "string",
15251
- "number",
15252
- "bool",
15253
- "date",
15254
- "datetime",
15255
- "media",
15256
- "simpleObject"
15257
- ]);
15258
- const DB_LINK_COLUMN_TYPES = new Set([
15259
- "link_single",
15260
- "link_multiple"
15261
- ]);
15262
- const DB_TABLE_INDEX_TYPES = new Set([
15263
- "b_tree",
15264
- "trigram",
15265
- "full_text"
15266
- ]);
15267
- const SCHEDULE_EVERY_VALUES = new Set([
15268
- "minute",
15269
- "hour",
15270
- "day",
15271
- "week",
15272
- "month"
15793
+ const PYTHON_KEYWORDS = new Set([
15794
+ "False",
15795
+ "None",
15796
+ "True",
15797
+ "and",
15798
+ "as",
15799
+ "assert",
15800
+ "async",
15801
+ "await",
15802
+ "break",
15803
+ "class",
15804
+ "continue",
15805
+ "def",
15806
+ "del",
15807
+ "elif",
15808
+ "else",
15809
+ "except",
15810
+ "finally",
15811
+ "for",
15812
+ "from",
15813
+ "global",
15814
+ "if",
15815
+ "import",
15816
+ "in",
15817
+ "is",
15818
+ "lambda",
15819
+ "nonlocal",
15820
+ "not",
15821
+ "or",
15822
+ "pass",
15823
+ "raise",
15824
+ "return",
15825
+ "try",
15826
+ "while",
15827
+ "with",
15828
+ "yield",
15829
+ "match",
15830
+ "case"
15273
15831
  ]);
15274
- const CLIENT_VERSIONS = new Set([
15275
- "2",
15276
- "3"
15832
+ function isValidPythonIdentifier(value) {
15833
+ return IDENTIFIER_RE.test(value) && !PYTHON_KEYWORDS.has(value);
15834
+ }
15835
+ function isParserGeneratedComponentName(name) {
15836
+ if (!name.startsWith("$")) return false;
15837
+ const suffix = name.slice(1);
15838
+ return suffix.length > 0 && isValidPythonIdentifier(suffix);
15839
+ }
15840
+ function pushIssue(issues, path, message) {
15841
+ issues.push({
15842
+ path,
15843
+ message
15844
+ });
15845
+ }
15846
+ function validateComponentName(value, path, issues) {
15847
+ if (void 0 === value || "string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15848
+ if (isParserGeneratedComponentName(value)) return;
15849
+ if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
15850
+ if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15851
+ }
15852
+ const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15853
+ const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
15854
+ const componentTypeSpec_PYTHON_KEYWORDS = new Set([
15855
+ "False",
15856
+ "None",
15857
+ "True",
15858
+ "and",
15859
+ "as",
15860
+ "assert",
15861
+ "async",
15862
+ "await",
15863
+ "break",
15864
+ "class",
15865
+ "continue",
15866
+ "def",
15867
+ "del",
15868
+ "elif",
15869
+ "else",
15870
+ "except",
15871
+ "finally",
15872
+ "for",
15873
+ "from",
15874
+ "global",
15875
+ "if",
15876
+ "import",
15877
+ "in",
15878
+ "is",
15879
+ "lambda",
15880
+ "nonlocal",
15881
+ "not",
15882
+ "or",
15883
+ "pass",
15884
+ "raise",
15885
+ "return",
15886
+ "try",
15887
+ "while",
15888
+ "with",
15889
+ "yield",
15890
+ "match",
15891
+ "case"
15277
15892
  ]);
15278
- const PYTHON_KEYWORDS = new Set([
15893
+ const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
15894
+ 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)";
15895
+ function isValidIdentifierSegment(value) {
15896
+ return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
15897
+ }
15898
+ function isValidLegacyFormTypeReference(typeName) {
15899
+ if (!typeName.startsWith("form:")) return false;
15900
+ const rest = typeName.slice(5);
15901
+ if (0 === rest.length) return false;
15902
+ let classPath = rest;
15903
+ if (rest.startsWith("dep_")) {
15904
+ const splitIndex = rest.indexOf(":");
15905
+ if (splitIndex <= 0) return false;
15906
+ const depId = rest.slice(0, splitIndex);
15907
+ if (!DEP_ID_RE.test(depId)) return false;
15908
+ classPath = rest.slice(splitIndex + 1);
15909
+ }
15910
+ if (0 === classPath.length) return false;
15911
+ return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
15912
+ }
15913
+ function isValidPackageQualifiedComponentType(typeName) {
15914
+ if (typeName.startsWith("anvil.")) return false;
15915
+ if (!typeName.includes(".")) return false;
15916
+ const parts = typeName.split(".");
15917
+ if (parts.length < 2) return false;
15918
+ return parts.every((part)=>isValidIdentifierSegment(part));
15919
+ }
15920
+ function componentTypeSpec_pushIssue(issues, path, message) {
15921
+ issues.push({
15922
+ path,
15923
+ message
15924
+ });
15925
+ }
15926
+ function validateComponentTypeSpec(value, path, issues, allowedSingleSegmentTypes) {
15927
+ if ("string" != typeof value || 0 === value.length) return void componentTypeSpec_pushIssue(issues, path, "must be a non-empty string");
15928
+ const typeName = value;
15929
+ if (typeName.startsWith("form:")) {
15930
+ if (!isValidLegacyFormTypeReference(typeName)) componentTypeSpec_pushIssue(issues, path, LEGACY_FORM_TYPE_REFERENCE_MESSAGE);
15931
+ return;
15932
+ }
15933
+ if (isValidPackageQualifiedComponentType(typeName)) return;
15934
+ if (!isValidIdentifierSegment(typeName)) return void componentTypeSpec_pushIssue(issues, path, "must be a valid identifier");
15935
+ if (!allowedSingleSegmentTypes.has(typeName)) componentTypeSpec_pushIssue(issues, path, COMPONENT_TYPE_REFERENCE_MESSAGE);
15936
+ }
15937
+ const formTemplateValidation_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15938
+ const formTemplateValidation_PYTHON_KEYWORDS = new Set([
15279
15939
  "False",
15280
15940
  "None",
15281
15941
  "True",
@@ -15329,58 +15989,29 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15329
15989
  "padding",
15330
15990
  "spacing"
15331
15991
  ]);
15332
- function isValidFormReference(typeName) {
15333
- if (!typeName.startsWith("form:")) return false;
15334
- const rest = typeName.slice(5);
15335
- if (0 === rest.length) return false;
15336
- let classPath = rest;
15337
- if (rest.startsWith("dep_")) {
15338
- const splitIndex = rest.indexOf(":");
15339
- if (splitIndex <= 0) return false;
15340
- const depId = rest.slice(0, splitIndex);
15341
- if (!DEP_ID_RE.test(depId)) return false;
15342
- classPath = rest.slice(splitIndex + 1);
15343
- }
15344
- if (0 === classPath.length) return false;
15345
- return classPath.split(".").every((part)=>IDENTIFIER_RE.test(part));
15346
- }
15347
- function isBuiltinComponentType(typeName) {
15348
- return BUILTIN_COMPONENT_TYPES.has(typeName);
15349
- }
15350
15992
  function isPlainObject(value) {
15351
15993
  return "object" == typeof value && null !== value && !Array.isArray(value);
15352
15994
  }
15353
- function pushIssue(issues, path, message) {
15995
+ function formTemplateValidation_pushIssue(issues, path, message) {
15354
15996
  issues.push({
15355
15997
  path,
15356
15998
  message
15357
15999
  });
15358
16000
  }
15359
- function formatValidationPath(path) {
15360
- return path || "root";
15361
- }
15362
16001
  function validateIdentifier(value, path, issues) {
15363
- if ("string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15364
- if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
15365
- if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
16002
+ if ("string" != typeof value || 0 === value.length) return void formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
16003
+ if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
16004
+ if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15366
16005
  }
15367
16006
  function validateComponentType(value, path, issues) {
15368
- if ("string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15369
- const typeName = value;
15370
- if (typeName.startsWith("form:")) {
15371
- if (!isValidFormReference(typeName)) pushIssue(issues, path, "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)");
15372
- return;
15373
- }
15374
- if (!IDENTIFIER_RE.test(typeName)) return void pushIssue(issues, path, "must be a valid identifier");
15375
- if (PYTHON_KEYWORDS.has(typeName)) return void pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15376
- if (!isBuiltinComponentType(typeName)) pushIssue(issues, path, "must be a known Anvil component type (see componentIcons.ts) or a form: reference");
16007
+ validateComponentTypeSpec(value, path, issues, BUILTIN_COMPONENT_TYPES);
15377
16008
  }
15378
16009
  function validatePropertiesObject(value, path, issues) {
15379
- if (void 0 !== value && !isPlainObject(value)) pushIssue(issues, path, "must be an object");
16010
+ if (void 0 !== value && !isPlainObject(value)) formTemplateValidation_pushIssue(issues, path, "must be an object");
15380
16011
  }
15381
16012
  function validateEventBindingsObject(value, path, issues) {
15382
16013
  if (void 0 === value) return;
15383
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16014
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15384
16015
  for (const [eventName, handlerName] of Object.entries(value)){
15385
16016
  validateIdentifier(eventName, `${path}.${eventName}.name`, issues);
15386
16017
  validateIdentifier(handlerName, `${path}.${eventName}`, issues);
@@ -15388,13 +16019,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15388
16019
  }
15389
16020
  function validateDataBindings(value, path, issues) {
15390
16021
  if (void 0 === value) return;
15391
- if (!Array.isArray(value)) return void pushIssue(issues, path, "must be an array");
16022
+ if (!Array.isArray(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an array");
15392
16023
  value.forEach((binding, index)=>{
15393
16024
  const bindingPath = `${path}[${index}]`;
15394
- if (!isPlainObject(binding)) return void pushIssue(issues, bindingPath, "must be an object");
15395
- if ("string" != typeof binding.property || 0 === binding.property.length) pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
15396
- if ("string" != typeof binding.code || 0 === binding.code.length) pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
15397
- if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
16025
+ if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
16026
+ if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
16027
+ if ("string" != typeof binding.code || 0 === binding.code.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
16028
+ if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
15398
16029
  });
15399
16030
  }
15400
16031
  function validateComponentArray(components, pathPrefix, issues, seenNames) {
@@ -15402,146 +16033,563 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15402
16033
  const childPath = `${pathPrefix}[${index}]`;
15403
16034
  validateComponentNode(component, childPath, issues, seenNames);
15404
16035
  if (isPlainObject(component) && "string" == typeof component.name && component.name.length > 0) {
15405
- if (seenNames.has(component.name)) pushIssue(issues, `${childPath}.name`, "must be unique across form components");
16036
+ if (seenNames.has(component.name)) formTemplateValidation_pushIssue(issues, `${childPath}.name`, "must be unique across form components");
15406
16037
  seenNames.add(component.name);
15407
16038
  }
15408
16039
  });
15409
16040
  }
15410
16041
  function validateComponentNode(value, path, issues, seenNames) {
15411
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16042
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15412
16043
  validateComponentType(value.type, `${path}.type`, issues);
15413
- if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) pushIssue(issues, `${path}.name`, "must be a non-empty string");
15414
- else validateIdentifier(value.name, `${path}.name`, issues);
16044
+ validateComponentName(value.name, `${path}.name`, issues);
15415
16045
  validatePropertiesObject(value.properties, `${path}.properties`, issues);
15416
16046
  validatePropertiesObject(value.layout_properties, `${path}.layout_properties`, issues);
15417
16047
  validateEventBindingsObject(value.event_bindings, `${path}.event_bindings`, issues);
15418
16048
  validateDataBindings(value.data_bindings, `${path}.data_bindings`, issues);
15419
16049
  if (void 0 !== value.components) if (Array.isArray(value.components)) validateComponentArray(value.components, `${path}.components`, issues, seenNames);
15420
- else pushIssue(issues, `${path}.components`, "must be an array");
16050
+ else formTemplateValidation_pushIssue(issues, `${path}.components`, "must be an array");
15421
16051
  }
15422
16052
  function validatePropertyDefinition(value, path, issues) {
15423
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16053
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15424
16054
  validateIdentifier(value.name, `${path}.name`, issues);
15425
- if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) pushIssue(issues, `${path}.type`, "must be a supported property type");
15426
- if (void 0 !== value.description && "string" != typeof value.description) pushIssue(issues, `${path}.description`, "must be a string");
15427
- if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
15428
- if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
16055
+ if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) formTemplateValidation_pushIssue(issues, `${path}.type`, "must be a supported property type");
16056
+ if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
16057
+ if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) formTemplateValidation_pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
16058
+ if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) formTemplateValidation_pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
15429
16059
  if (void 0 !== value.binding_writeback_events) if (Array.isArray(value.binding_writeback_events)) value.binding_writeback_events.forEach((eventName, index)=>{
15430
- if ("string" != typeof eventName || 0 === eventName.length) pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
16060
+ if ("string" != typeof eventName || 0 === eventName.length) formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
15431
16061
  });
15432
- else pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
16062
+ else formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
15433
16063
  if ("enum" === value.type) {
15434
- if (!Array.isArray(value.options) || value.options.some((option)=>"string" != typeof option)) pushIssue(issues, `${path}.options`, "must be an array of strings for enum properties");
16064
+ if (!Array.isArray(value.options) || value.options.some((option)=>"string" != typeof option)) formTemplateValidation_pushIssue(issues, `${path}.options`, "must be an array of strings for enum properties");
15435
16065
  }
15436
16066
  }
15437
16067
  function validateEventDefinition(value, path, issues) {
15438
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16068
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15439
16069
  validateIdentifier(value.name, `${path}.name`, issues);
15440
- if (void 0 !== value.description && "string" != typeof value.description) pushIssue(issues, `${path}.description`, "must be a string");
15441
- if (void 0 !== value.default_event && "boolean" != typeof value.default_event) pushIssue(issues, `${path}.default_event`, "must be a boolean");
16070
+ if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
16071
+ if (void 0 !== value.default_event && "boolean" != typeof value.default_event) formTemplateValidation_pushIssue(issues, `${path}.default_event`, "must be a boolean");
15442
16072
  if (void 0 !== value.parameters) if (Array.isArray(value.parameters)) value.parameters.forEach((parameter, index)=>{
15443
- if (!isPlainObject(parameter)) return void pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
16073
+ if (!isPlainObject(parameter)) return void formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
15444
16074
  validateIdentifier(parameter.name, `${path}.parameters[${index}].name`, issues);
15445
- if (void 0 !== parameter.description && "string" != typeof parameter.description) pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
16075
+ if (void 0 !== parameter.description && "string" != typeof parameter.description) formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
15446
16076
  });
15447
- else pushIssue(issues, `${path}.parameters`, "must be an array");
16077
+ else formTemplateValidation_pushIssue(issues, `${path}.parameters`, "must be an array");
15448
16078
  }
15449
16079
  function validateSlotDefinition(value, path, issues, seenNames) {
15450
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15451
- if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) pushIssue(issues, `${path}.index`, "must be a non-negative integer");
16080
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
16081
+ if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) formTemplateValidation_pushIssue(issues, `${path}.index`, "must be a non-negative integer");
15452
16082
  validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
15453
16083
  if (isPlainObject(value.target)) {
15454
- if ("container" !== value.target.type && "slot" !== value.target.type) pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
15455
- validateIdentifier(value.target.name, `${path}.target.name`, issues);
15456
- } else pushIssue(issues, `${path}.target`, "must be an object");
16084
+ if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
16085
+ if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
16086
+ } else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
15457
16087
  if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
15458
16088
  }
16089
+ function prefixedPath(pathPrefix, path) {
16090
+ return pathPrefix.length > 0 ? `${pathPrefix}.${path}` : path;
16091
+ }
16092
+ function validateFormTemplateMetadata(value, options = {}) {
16093
+ const issues = [];
16094
+ if (!isPlainObject(value)) {
16095
+ formTemplateValidation_pushIssue(issues, options.pathPrefix ?? "", "must be a YAML object");
16096
+ return issues;
16097
+ }
16098
+ const pathPrefix = options.pathPrefix ?? "";
16099
+ const path = (suffix)=>prefixedPath(pathPrefix, suffix);
16100
+ if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component"), "must be a boolean");
16101
+ if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "must be a boolean");
16102
+ if (true === value.custom_component_container && true !== value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "requires custom_component: true");
16103
+ const properties = value.properties;
16104
+ if (void 0 !== properties) if (Array.isArray(properties)) {
16105
+ const seenNames = new Set();
16106
+ let defaultBindingPropCount = 0;
16107
+ properties.forEach((property, index)=>{
16108
+ validatePropertyDefinition(property, path(`properties[${index}]`), issues);
16109
+ if (!isPlainObject(property) || "string" != typeof property.name) return;
16110
+ if (seenNames.has(property.name)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].name`), "must be unique");
16111
+ seenNames.add(property.name);
16112
+ if (property.default_binding_prop) defaultBindingPropCount++;
16113
+ });
16114
+ if (defaultBindingPropCount > 1) formTemplateValidation_pushIssue(issues, path("properties"), "can only define one default_binding_prop");
16115
+ } else formTemplateValidation_pushIssue(issues, path("properties"), "must be an array");
16116
+ const events = value.events;
16117
+ const eventNames = new Set();
16118
+ if (void 0 !== events) if (Array.isArray(events)) {
16119
+ let defaultEventCount = 0;
16120
+ events.forEach((event, index)=>{
16121
+ validateEventDefinition(event, path(`events[${index}]`), issues);
16122
+ if (!isPlainObject(event) || "string" != typeof event.name) return;
16123
+ if (eventNames.has(event.name)) formTemplateValidation_pushIssue(issues, path(`events[${index}].name`), "must be unique");
16124
+ eventNames.add(event.name);
16125
+ if (event.default_event) defaultEventCount++;
16126
+ });
16127
+ if (defaultEventCount > 1) formTemplateValidation_pushIssue(issues, path("events"), "can only define one default_event");
16128
+ } else formTemplateValidation_pushIssue(issues, path("events"), "must be an array");
16129
+ if (Array.isArray(properties)) properties.forEach((property, index)=>{
16130
+ if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
16131
+ property.binding_writeback_events.forEach((eventName, eventIndex)=>{
16132
+ if ("string" == typeof eventName && !eventNames.has(eventName)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].binding_writeback_events[${eventIndex}]`), "must reference a defined event");
16133
+ });
16134
+ });
16135
+ if (void 0 !== value.slots) {
16136
+ const slotsPath = path("slots");
16137
+ if (isPlainObject(value.slots)) if ("empty-marker" === options.slotsPolicy) {
16138
+ if (Object.keys(value.slots).length > 0) formTemplateValidation_pushIssue(issues, slotsPath, "must be empty when defined in frontmatter");
16139
+ } else {
16140
+ const componentNames = options.componentNames ?? new Set();
16141
+ for (const [slotName, slotDef] of Object.entries(value.slots)){
16142
+ validateIdentifier(slotName, path(`slots.${slotName}.name`), issues);
16143
+ validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
16144
+ }
16145
+ }
16146
+ else formTemplateValidation_pushIssue(issues, slotsPath, "must be an object");
16147
+ }
16148
+ if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
16149
+ if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
16150
+ if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
16151
+ if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16152
+ } else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
16153
+ return issues;
16154
+ }
15459
16155
  function validateFormTemplateData(value) {
15460
16156
  const issues = [];
15461
16157
  const componentNames = new Set();
15462
16158
  if (!isPlainObject(value)) {
15463
- pushIssue(issues, "", "must be a YAML object");
16159
+ formTemplateValidation_pushIssue(issues, "", "must be a YAML object");
15464
16160
  return issues;
15465
16161
  }
15466
16162
  const hasContainer = void 0 !== value.container;
15467
16163
  const hasLayout = void 0 !== value.layout;
15468
16164
  const hasComponents = void 0 !== value.components;
15469
16165
  const hasComponentsBySlot = void 0 !== value.components_by_slot;
15470
- if (hasContainer && hasLayout) pushIssue(issues, "", "cannot define both container and layout");
15471
- if (!hasContainer && !hasLayout) pushIssue(issues, "", "must define either container or layout");
15472
- if (void 0 !== value.is_package && "boolean" != typeof value.is_package) pushIssue(issues, "is_package", "must be a boolean");
16166
+ if (hasContainer && hasLayout) formTemplateValidation_pushIssue(issues, "", "cannot define both container and layout");
16167
+ if (!hasContainer && !hasLayout) formTemplateValidation_pushIssue(issues, "", "must define either container or layout");
16168
+ if (void 0 !== value.is_package && "boolean" != typeof value.is_package) formTemplateValidation_pushIssue(issues, "is_package", "must be a boolean");
15473
16169
  if (hasContainer) if (isPlainObject(value.container)) {
15474
16170
  validateComponentType(value.container.type, "container.type", issues);
15475
16171
  validatePropertiesObject(value.container.properties, "container.properties", issues);
15476
16172
  validatePropertiesObject(value.container.layout_properties, "container.layout_properties", issues);
15477
16173
  validateEventBindingsObject(value.container.event_bindings, "container.event_bindings", issues);
15478
16174
  validateDataBindings(value.container.data_bindings, "container.data_bindings", issues);
15479
- } else pushIssue(issues, "container", "must be an object");
16175
+ } else formTemplateValidation_pushIssue(issues, "container", "must be an object");
15480
16176
  if (hasLayout) if (isPlainObject(value.layout)) {
15481
16177
  validateComponentType(value.layout.type, "layout.type", issues);
15482
16178
  validatePropertiesObject(value.layout.properties, "layout.properties", issues);
15483
16179
  validateEventBindingsObject(value.layout.event_bindings, "layout.event_bindings", issues);
15484
16180
  validateEventBindingsObject(value.layout.form_event_bindings, "layout.form_event_bindings", issues);
15485
16181
  validateDataBindings(value.layout.data_bindings, "layout.data_bindings", issues);
15486
- } else pushIssue(issues, "layout", "must be an object");
15487
- if (hasComponents && hasLayout) pushIssue(issues, "components", "can only be used with container-based forms");
15488
- else if (hasComponents && !hasContainer) pushIssue(issues, "components", "can only be used with container-based forms");
16182
+ } else formTemplateValidation_pushIssue(issues, "layout", "must be an object");
16183
+ if (hasComponents && hasLayout) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
16184
+ else if (hasComponents && !hasContainer) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
15489
16185
  else if (hasComponents) if (Array.isArray(value.components)) validateComponentArray(value.components, "components", issues, componentNames);
15490
- else pushIssue(issues, "components", "must be an array");
15491
- if (hasComponentsBySlot && hasContainer) pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
15492
- else if (hasComponentsBySlot && !hasLayout) pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
16186
+ else formTemplateValidation_pushIssue(issues, "components", "must be an array");
16187
+ if (hasComponentsBySlot && hasContainer) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
16188
+ else if (hasComponentsBySlot && !hasLayout) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
15493
16189
  else if (hasComponentsBySlot) if (isPlainObject(value.components_by_slot)) for (const [slotName, slotComponents] of Object.entries(value.components_by_slot)){
15494
16190
  if (!Array.isArray(slotComponents)) {
15495
- pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
16191
+ formTemplateValidation_pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
15496
16192
  continue;
15497
16193
  }
15498
16194
  validateComponentArray(slotComponents, `components_by_slot.${slotName}`, issues, componentNames);
15499
16195
  }
15500
- else pushIssue(issues, "components_by_slot", "must be an object");
15501
- if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) pushIssue(issues, "custom_component", "must be a boolean");
15502
- if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) pushIssue(issues, "custom_component_container", "must be a boolean");
15503
- if (true === value.custom_component_container && true !== value.custom_component) pushIssue(issues, "custom_component_container", "requires custom_component: true");
15504
- const properties = value.properties;
15505
- if (void 0 !== properties) if (Array.isArray(properties)) {
15506
- const seenNames = new Set();
15507
- let defaultBindingPropCount = 0;
15508
- properties.forEach((property, index)=>{
15509
- validatePropertyDefinition(property, `properties[${index}]`, issues);
15510
- if (!isPlainObject(property) || "string" != typeof property.name) return;
15511
- if (seenNames.has(property.name)) pushIssue(issues, `properties[${index}].name`, "must be unique");
15512
- seenNames.add(property.name);
15513
- if (property.default_binding_prop) defaultBindingPropCount++;
15514
- });
15515
- if (defaultBindingPropCount > 1) pushIssue(issues, "properties", "can only define one default_binding_prop");
15516
- } else pushIssue(issues, "properties", "must be an array");
15517
- const events = value.events;
15518
- const eventNames = new Set();
15519
- if (void 0 !== events) if (Array.isArray(events)) {
15520
- let defaultEventCount = 0;
15521
- events.forEach((event, index)=>{
15522
- validateEventDefinition(event, `events[${index}]`, issues);
15523
- if (!isPlainObject(event) || "string" != typeof event.name) return;
15524
- if (eventNames.has(event.name)) pushIssue(issues, `events[${index}].name`, "must be unique");
15525
- eventNames.add(event.name);
15526
- if (event.default_event) defaultEventCount++;
15527
- });
15528
- if (defaultEventCount > 1) pushIssue(issues, "events", "can only define one default_event");
15529
- } else pushIssue(issues, "events", "must be an array");
15530
- if (Array.isArray(properties)) properties.forEach((property, index)=>{
15531
- if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
15532
- property.binding_writeback_events.forEach((eventName, eventIndex)=>{
15533
- if ("string" == typeof eventName && !eventNames.has(eventName)) pushIssue(issues, `properties[${index}].binding_writeback_events[${eventIndex}]`, "must reference a defined event");
16196
+ else formTemplateValidation_pushIssue(issues, "components_by_slot", "must be an object");
16197
+ issues.push(...validateFormTemplateMetadata(value, {
16198
+ componentNames
16199
+ }));
16200
+ return issues;
16201
+ }
16202
+ const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
16203
+ "/runtime/services/tables.yml",
16204
+ "/runtime/services/google.yml",
16205
+ "/runtime/services/uplink.yml",
16206
+ "/runtime/services/stripe.yml",
16207
+ "/runtime/services/segment.yml",
16208
+ "/runtime/services/facebook.yml",
16209
+ "/runtime/services/anvil/users.yml",
16210
+ "/runtime/services/anvil/secrets.yml",
16211
+ "/runtime/services/anvil/saml.yml",
16212
+ "/runtime/services/anvil/microsoft.yml",
16213
+ "/runtime/services/anvil/email.yml",
16214
+ "/runtime/services/anvil/files.yml"
16215
+ ]);
16216
+ const FRONTMATTER_ALLOWED_KEYS = new Set([
16217
+ "custom_component",
16218
+ "custom_component_container",
16219
+ "properties",
16220
+ "events",
16221
+ "toolbox_item",
16222
+ "layout_metadata",
16223
+ "item_type",
16224
+ "slots"
16225
+ ]);
16226
+ const FRONTMATTER_DISALLOWED_KEYS = new Set([
16227
+ "container",
16228
+ "components",
16229
+ "layout",
16230
+ "components_by_slot",
16231
+ "code",
16232
+ "class_name",
16233
+ "is_package",
16234
+ "save_as_html",
16235
+ "serialized_html"
16236
+ ]);
16237
+ function validateFormTemplateHtml_isPlainObject(value) {
16238
+ return "object" == typeof value && null !== value && !Array.isArray(value);
16239
+ }
16240
+ function validateFormTemplateHtml_pushIssue(issues, issuePath, message) {
16241
+ issues.push({
16242
+ path: issuePath,
16243
+ message
16244
+ });
16245
+ }
16246
+ function splitHtmlFormTemplateFrontmatter(htmlContent) {
16247
+ const match = htmlContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
16248
+ if (!match) return {
16249
+ frontmatter: null,
16250
+ body: htmlContent
16251
+ };
16252
+ return {
16253
+ frontmatter: match[1],
16254
+ body: htmlContent.slice(match[0].length)
16255
+ };
16256
+ }
16257
+ function validateHtmlFrontmatter(frontmatterYaml) {
16258
+ const issues = [];
16259
+ if (0 === frontmatterYaml.trim().length) {
16260
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must not be empty; omit frontmatter entirely when unused");
16261
+ return issues;
16262
+ }
16263
+ let data;
16264
+ try {
16265
+ data = external_js_yaml_namespaceObject.load(frontmatterYaml);
16266
+ } catch (error) {
16267
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", error instanceof Error ? error.message : String(error));
16268
+ return issues;
16269
+ }
16270
+ if (!validateFormTemplateHtml_isPlainObject(data)) {
16271
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must be a YAML object");
16272
+ return issues;
16273
+ }
16274
+ for (const key of Object.keys(data))if (FRONTMATTER_DISALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "belongs in the HTML body, not frontmatter");
16275
+ else if (!FRONTMATTER_ALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "is not an allowed frontmatter field");
16276
+ issues.push(...validateFormTemplateMetadata(data, {
16277
+ pathPrefix: "frontmatter",
16278
+ slotsPolicy: "empty-marker"
16279
+ }));
16280
+ return issues;
16281
+ }
16282
+ function validateParsedHtmlTemplate(parsed) {
16283
+ return validateFormTemplateData(parsed);
16284
+ }
16285
+ function validateFormTemplateHtml(htmlContent) {
16286
+ const { frontmatter, body } = splitHtmlFormTemplateFrontmatter(htmlContent);
16287
+ if (null !== frontmatter) {
16288
+ const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
16289
+ if (frontmatterIssues.length > 0) return {
16290
+ ok: false,
16291
+ message: "form_template.html is invalid for Anvil",
16292
+ issues: frontmatterIssues
16293
+ };
16294
+ }
16295
+ let parsed;
16296
+ try {
16297
+ parsed = (0, form_template_parser_namespaceObject.parseSerializedHtml)(body);
16298
+ } catch (error) {
16299
+ return {
16300
+ ok: false,
16301
+ message: "form_template.html could not be parsed",
16302
+ issues: [
16303
+ {
16304
+ path: "",
16305
+ message: error instanceof Error ? error.message : String(error)
16306
+ }
16307
+ ]
16308
+ };
16309
+ }
16310
+ const issues = validateParsedHtmlTemplate(parsed);
16311
+ if (issues.length > 0) return {
16312
+ ok: false,
16313
+ message: "form_template.html is invalid for Anvil",
16314
+ issues
16315
+ };
16316
+ return {
16317
+ ok: true
16318
+ };
16319
+ }
16320
+ const PYTHON_VALIDATOR_SCRIPT = String.raw`
16321
+ import ast
16322
+ import json
16323
+ import sys
16324
+
16325
+
16326
+ def issue_path(error):
16327
+ if getattr(error, "lineno", None) is None:
16328
+ return "root"
16329
+ if getattr(error, "offset", None) is None:
16330
+ return f"line {error.lineno}"
16331
+ return f"line {error.lineno}, column {error.offset}"
16332
+
16333
+
16334
+ def base_name(node):
16335
+ if isinstance(node, ast.Name):
16336
+ return node.id
16337
+ if isinstance(node, ast.Attribute):
16338
+ return node.attr
16339
+ return None
16340
+
16341
+
16342
+ def imports_template_from_anvil_designer(node, template_name):
16343
+ if node.level < 1:
16344
+ return False
16345
+ if node.module not in ("_anvil_designer", None):
16346
+ return False
16347
+ return any(alias.name == template_name for alias in node.names)
16348
+
16349
+
16350
+ file_path = sys.argv[1]
16351
+ class_name = sys.argv[2]
16352
+ template_name = f"{class_name}Template" if class_name else ""
16353
+ source = sys.stdin.read()
16354
+ issues = []
16355
+
16356
+ try:
16357
+ compile(source, file_path, "exec")
16358
+ except SyntaxError as error:
16359
+ issues.append({"path": issue_path(error), "message": error.msg})
16360
+ print(json.dumps({"issues": issues}))
16361
+ sys.exit(0)
16362
+
16363
+ if class_name:
16364
+ tree = ast.parse(source, filename=file_path)
16365
+ has_template_import = False
16366
+ has_form_class = False
16367
+
16368
+ for node in ast.walk(tree):
16369
+ if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
16370
+ has_template_import = True
16371
+ elif isinstance(node, ast.ClassDef) and node.name == class_name:
16372
+ if any(base_name(base) == template_name for base in node.bases):
16373
+ has_form_class = True
16374
+
16375
+ if not has_template_import:
16376
+ issues.append({
16377
+ "path": "root",
16378
+ "message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
16379
+ })
16380
+ if not has_form_class:
16381
+ issues.append({
16382
+ "path": "root",
16383
+ "message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
16384
+ })
16385
+
16386
+ print(json.dumps({"issues": issues}))
16387
+ `;
16388
+ function findPythonInterpreter(env = process.env, probe = external_child_process_namespaceObject.spawnSync) {
16389
+ const candidates = [
16390
+ ..."string" == typeof env.ANVIL_PYTHON && env.ANVIL_PYTHON.length > 0 ? [
16391
+ {
16392
+ command: env.ANVIL_PYTHON,
16393
+ args: []
16394
+ }
16395
+ ] : [],
16396
+ {
16397
+ command: "uv",
16398
+ args: [
16399
+ "run",
16400
+ "--isolated",
16401
+ "--no-python-downloads",
16402
+ "python"
16403
+ ]
16404
+ },
16405
+ {
16406
+ command: "python3",
16407
+ args: []
16408
+ },
16409
+ {
16410
+ command: "python",
16411
+ args: []
16412
+ }
16413
+ ];
16414
+ for (const candidate of candidates){
16415
+ const result = probe(candidate.command, [
16416
+ ...candidate.args,
16417
+ "--version"
16418
+ ], {
16419
+ encoding: "utf8"
15534
16420
  });
16421
+ if (!result.error && 0 === result.status) return candidate;
16422
+ }
16423
+ return null;
16424
+ }
16425
+ function isClientFormPython(filePath) {
16426
+ if ("__init__.py" === external_path_default().basename(filePath)) {
16427
+ const formDir = external_path_default().dirname(filePath);
16428
+ return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
16429
+ }
16430
+ const parsed = external_path_default().parse(filePath);
16431
+ return external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.html`)) || external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.yaml`));
16432
+ }
16433
+ function expectedFormClassName(filePath) {
16434
+ if (!isClientFormPython(filePath)) return null;
16435
+ if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
16436
+ return external_path_default().basename(filePath, ".py");
16437
+ }
16438
+ function isUnderDirectory(filePath, directoryPath) {
16439
+ const relativePath = external_path_default().relative(directoryPath, filePath);
16440
+ return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
16441
+ }
16442
+ function validatePython(filePath, fileContent, appRoot) {
16443
+ const interpreter = findPythonInterpreter();
16444
+ if (!interpreter) return {
16445
+ ok: false,
16446
+ message: "Python file could not be validated",
16447
+ issues: [
16448
+ {
16449
+ path: "root",
16450
+ message: "Python validation requires uv, python3, or python on PATH, or ANVIL_PYTHON to be set"
16451
+ }
16452
+ ]
16453
+ };
16454
+ const absolutePath = external_path_default().resolve(filePath);
16455
+ const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
16456
+ const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
16457
+ const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter.command, [
16458
+ ...interpreter.args,
16459
+ "-c",
16460
+ PYTHON_VALIDATOR_SCRIPT,
16461
+ filePath,
16462
+ formClassName ?? ""
16463
+ ], {
16464
+ encoding: "utf8",
16465
+ input: fileContent,
16466
+ maxBuffer: 1048576
16467
+ });
16468
+ if (result.error || 0 !== result.status) return {
16469
+ ok: false,
16470
+ message: "Python file could not be validated",
16471
+ issues: [
16472
+ {
16473
+ path: "root",
16474
+ message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
16475
+ }
16476
+ ]
16477
+ };
16478
+ let parsed;
16479
+ try {
16480
+ parsed = JSON.parse(result.stdout);
16481
+ } catch (error) {
16482
+ return {
16483
+ ok: false,
16484
+ message: "Python file could not be validated",
16485
+ issues: [
16486
+ {
16487
+ path: "root",
16488
+ message: error instanceof Error ? error.message : String(error)
16489
+ }
16490
+ ]
16491
+ };
16492
+ }
16493
+ const issues = parsed.issues ?? [];
16494
+ if (issues.length > 0) return {
16495
+ ok: false,
16496
+ message: "Python file is invalid for Anvil",
16497
+ issues
16498
+ };
16499
+ return {
16500
+ ok: true
16501
+ };
16502
+ }
16503
+ const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16504
+ const DB_ACCESS_LEVELS = new Set([
16505
+ "none",
16506
+ "search",
16507
+ "full"
16508
+ ]);
16509
+ const DB_BASIC_COLUMN_TYPES = new Set([
16510
+ "string",
16511
+ "number",
16512
+ "bool",
16513
+ "date",
16514
+ "datetime",
16515
+ "media",
16516
+ "simpleObject"
16517
+ ]);
16518
+ const DB_LINK_COLUMN_TYPES = new Set([
16519
+ "link_single",
16520
+ "link_multiple"
16521
+ ]);
16522
+ const DB_TABLE_INDEX_TYPES = new Set([
16523
+ "b_tree",
16524
+ "trigram",
16525
+ "full_text"
16526
+ ]);
16527
+ const SCHEDULE_EVERY_VALUES = new Set([
16528
+ "minute",
16529
+ "hour",
16530
+ "day",
16531
+ "week",
16532
+ "month"
16533
+ ]);
16534
+ const CLIENT_VERSIONS = new Set([
16535
+ "2",
16536
+ "3"
16537
+ ]);
16538
+ const validators_PYTHON_KEYWORDS = new Set([
16539
+ "False",
16540
+ "None",
16541
+ "True",
16542
+ "and",
16543
+ "as",
16544
+ "assert",
16545
+ "async",
16546
+ "await",
16547
+ "break",
16548
+ "class",
16549
+ "continue",
16550
+ "def",
16551
+ "del",
16552
+ "elif",
16553
+ "else",
16554
+ "except",
16555
+ "finally",
16556
+ "for",
16557
+ "from",
16558
+ "global",
16559
+ "if",
16560
+ "import",
16561
+ "in",
16562
+ "is",
16563
+ "lambda",
16564
+ "nonlocal",
16565
+ "not",
16566
+ "or",
16567
+ "pass",
16568
+ "raise",
16569
+ "return",
16570
+ "try",
16571
+ "while",
16572
+ "with",
16573
+ "yield",
16574
+ "match",
16575
+ "case"
16576
+ ]);
16577
+ function validators_isPlainObject(value) {
16578
+ return "object" == typeof value && null !== value && !Array.isArray(value);
16579
+ }
16580
+ function validators_pushIssue(issues, path, message) {
16581
+ issues.push({
16582
+ path,
16583
+ message
15535
16584
  });
15536
- if (void 0 !== value.slots) if (isPlainObject(value.slots)) for (const [slotName, slotDef] of Object.entries(value.slots)){
15537
- validateIdentifier(slotName, `slots.${slotName}.name`, issues);
15538
- validateSlotDefinition(slotDef, `slots.${slotName}`, issues, componentNames);
15539
- }
15540
- else pushIssue(issues, "slots", "must be an object");
15541
- if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
15542
- if ("number" != typeof value.item_type.table_id) pushIssue(issues, "item_type.table_id", "must be a number");
15543
- } else pushIssue(issues, "item_type", "must be an object");
15544
- return issues;
16585
+ }
16586
+ function formatValidationPath(path) {
16587
+ return path || "root";
16588
+ }
16589
+ function validators_validateIdentifier(value, path, issues) {
16590
+ if ("string" != typeof value || 0 === value.length) return void validators_pushIssue(issues, path, "must be a non-empty string");
16591
+ if (!validators_IDENTIFIER_RE.test(value)) return void validators_pushIssue(issues, path, "must be a valid identifier");
16592
+ if (validators_PYTHON_KEYWORDS.has(value)) validators_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15545
16593
  }
15546
16594
  function validateFormTemplate(yamlContent) {
15547
16595
  try {
@@ -15568,7 +16616,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15568
16616
  };
15569
16617
  }
15570
16618
  }
15571
- const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>isPlainObject(input), "must be an object");
16619
+ const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>validators_isPlainObject(input), "must be an object");
15572
16620
  const AnvilServiceEntrySchema = external_valibot_namespaceObject.looseObject({
15573
16621
  source: external_valibot_namespaceObject.pipe(external_valibot_namespaceObject.string(), external_valibot_namespaceObject.nonEmpty()),
15574
16622
  client_config: external_valibot_namespaceObject.optional(serviceConfigObject),
@@ -15596,14 +16644,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15596
16644
  function validateRuntimeOptions(value, path) {
15597
16645
  const issues = [];
15598
16646
  if (void 0 === value) return issues;
15599
- if (!isPlainObject(value)) return issues;
16647
+ if (!validators_isPlainObject(value)) return issues;
15600
16648
  if (void 0 !== value.client_version) {
15601
- if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
16649
+ if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) validators_pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
15602
16650
  }
15603
16651
  if (void 0 !== value.server_spec && null !== value.server_spec) {
15604
- if (!isPlainObject(value.server_spec)) return issues;
15605
- if ("requirements" in value.server_spec) pushIssue(issues, `${path}.server_spec.requirements`, "is not a valid anvil.yaml field; requirements come from server_code/requirements.txt");
15606
- if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) pushIssue(issues, `${path}.server_spec.base`, "must be a string");
16652
+ if (!validators_isPlainObject(value.server_spec)) return issues;
16653
+ if ("requirements" in value.server_spec) validators_pushIssue(issues, `${path}.server_spec.requirements`, "is not a valid anvil.yaml field; requirements come from server_code/requirements.txt");
16654
+ if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) validators_pushIssue(issues, `${path}.server_spec.base`, "must be a string");
15607
16655
  }
15608
16656
  return issues;
15609
16657
  }
@@ -15611,27 +16659,27 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15611
16659
  const issues = [];
15612
16660
  if (void 0 === value) return issues;
15613
16661
  if (null === value) {
15614
- pushIssue(issues, path, "must be an object, an empty array, or omitted");
16662
+ validators_pushIssue(issues, path, "must be an object, an empty array, or omitted");
15615
16663
  return issues;
15616
16664
  }
15617
16665
  if (Array.isArray(value)) {
15618
16666
  if (0 === value.length) return issues;
15619
- pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
16667
+ validators_pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
15620
16668
  return issues;
15621
16669
  }
15622
- if (!isPlainObject(value)) {
15623
- pushIssue(issues, path, "must be an object mapping table names to schemas");
16670
+ if (!validators_isPlainObject(value)) {
16671
+ validators_pushIssue(issues, path, "must be an object mapping table names to schemas");
15624
16672
  return issues;
15625
16673
  }
15626
16674
  for (const tableName of Object.keys(value)){
15627
16675
  const tablePath = `${path}.${tableName}`;
15628
- if (!IDENTIFIER_RE.test(tableName)) {
15629
- pushIssue(issues, tablePath, "must be a valid Python identifier");
16676
+ if (!validators_IDENTIFIER_RE.test(tableName)) {
16677
+ validators_pushIssue(issues, tablePath, "must be a valid Python identifier");
15630
16678
  continue;
15631
16679
  }
15632
16680
  const table = value[tableName];
15633
- if (!isPlainObject(table)) {
15634
- pushIssue(issues, tablePath, "must be an object");
16681
+ if (!validators_isPlainObject(table)) {
16682
+ validators_pushIssue(issues, tablePath, "must be an object");
15635
16683
  continue;
15636
16684
  }
15637
16685
  for (const req of [
@@ -15639,61 +16687,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15639
16687
  "server",
15640
16688
  "title",
15641
16689
  "columns"
15642
- ])if (void 0 === table[req]) pushIssue(issues, `${tablePath}.${req}`, "is required");
16690
+ ])if (void 0 === table[req]) validators_pushIssue(issues, `${tablePath}.${req}`, "is required");
15643
16691
  if (void 0 !== table.client) {
15644
- if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
16692
+ if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) validators_pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
15645
16693
  }
15646
16694
  if (void 0 !== table.server) {
15647
- if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
16695
+ if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) validators_pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
15648
16696
  }
15649
16697
  if (void 0 !== table.title) {
15650
- if ("string" != typeof table.title || 0 === table.title.length) pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
16698
+ if ("string" != typeof table.title || 0 === table.title.length) validators_pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
15651
16699
  }
15652
16700
  if (void 0 !== table.columns) if (Array.isArray(table.columns)) table.columns.forEach((column, index)=>{
15653
16701
  validateDbSchemaColumn(column, `${tablePath}.columns[${index}]`, issues);
15654
16702
  });
15655
- else pushIssue(issues, `${tablePath}.columns`, "must be an array");
16703
+ else validators_pushIssue(issues, `${tablePath}.columns`, "must be an array");
15656
16704
  if (void 0 !== table.indexes) if (Array.isArray(table.indexes)) table.indexes.forEach((indexEntry, index)=>{
15657
16705
  validateDbTableIndex(indexEntry, `${tablePath}.indexes[${index}]`, issues);
15658
16706
  });
15659
- else pushIssue(issues, `${tablePath}.indexes`, "must be an array");
16707
+ else validators_pushIssue(issues, `${tablePath}.indexes`, "must be an array");
15660
16708
  }
15661
16709
  return issues;
15662
16710
  }
15663
16711
  function validateDbTableIndex(value, path, issues) {
15664
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15665
- if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) pushIssue(issues, `${path}.columns`, "must be an array of strings");
15666
- if (void 0 === value.type) pushIssue(issues, `${path}.type`, "is required");
15667
- else if ("string" != typeof value.type || !DB_TABLE_INDEX_TYPES.has(value.type)) pushIssue(issues, `${path}.type`, 'must be "b_tree", "trigram", or "full_text"');
16712
+ if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
16713
+ if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) validators_pushIssue(issues, `${path}.columns`, "must be an array of strings");
16714
+ if (void 0 === value.type) validators_pushIssue(issues, `${path}.type`, "is required");
16715
+ else if ("string" != typeof value.type || !DB_TABLE_INDEX_TYPES.has(value.type)) validators_pushIssue(issues, `${path}.type`, 'must be "b_tree", "trigram", or "full_text"');
15668
16716
  }
15669
16717
  function validateDbSchemaColumn(value, path, issues) {
15670
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15671
- if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) pushIssue(issues, `${path}.name`, "must be a non-empty string");
15672
- else if (!IDENTIFIER_RE.test(value.name)) pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
15673
- if (void 0 === value.type) return void pushIssue(issues, `${path}.type`, "is required");
15674
- if ("string" != typeof value.type) return void pushIssue(issues, `${path}.type`, "must be a string");
16718
+ if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
16719
+ if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) validators_pushIssue(issues, `${path}.name`, "must be a non-empty string");
16720
+ else if (!validators_IDENTIFIER_RE.test(value.name)) validators_pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
16721
+ if (void 0 === value.type) return void validators_pushIssue(issues, `${path}.type`, "is required");
16722
+ if ("string" != typeof value.type) return void validators_pushIssue(issues, `${path}.type`, "must be a string");
15675
16723
  const colType = value.type;
15676
16724
  if (DB_LINK_COLUMN_TYPES.has(colType)) {
15677
- if (void 0 === value.target || "string" != typeof value.target || 0 === value.target.length) pushIssue(issues, `${path}.target`, "must be a non-empty string (target table python name)");
15678
- else if (!IDENTIFIER_RE.test(value.target)) pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
15679
- } else if (!DB_BASIC_COLUMN_TYPES.has(colType)) pushIssue(issues, `${path}.type`, `must be a basic column type (${[
16725
+ if (void 0 === value.target || "string" != typeof value.target || 0 === value.target.length) validators_pushIssue(issues, `${path}.target`, "must be a non-empty string (target table python name)");
16726
+ else if (!validators_IDENTIFIER_RE.test(value.target)) validators_pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
16727
+ } else if (!DB_BASIC_COLUMN_TYPES.has(colType)) validators_pushIssue(issues, `${path}.type`, `must be a basic column type (${[
15680
16728
  ...DB_BASIC_COLUMN_TYPES
15681
16729
  ].join(", ")}) or ${[
15682
16730
  ...DB_LINK_COLUMN_TYPES
15683
16731
  ].join(", ")}`);
15684
- if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
15685
- if (void 0 !== value.admin_ui && null !== value.admin_ui) if (isPlainObject(value.admin_ui)) {
16732
+ if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) validators_pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
16733
+ if (void 0 !== value.admin_ui && null !== value.admin_ui) if (validators_isPlainObject(value.admin_ui)) {
15686
16734
  const ui = value.admin_ui;
15687
- if (void 0 !== ui.width && "number" != typeof ui.width) pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
15688
- if (void 0 !== ui.order && "number" != typeof ui.order) pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
15689
- } else pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
16735
+ if (void 0 !== ui.width && "number" != typeof ui.width) validators_pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
16736
+ if (void 0 !== ui.order && "number" != typeof ui.order) validators_pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
16737
+ } else validators_pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
15690
16738
  if (void 0 !== value.indexes) {
15691
- if (!Array.isArray(value.indexes)) return void pushIssue(issues, `${path}.indexes`, "must be an array");
16739
+ if (!Array.isArray(value.indexes)) return void validators_pushIssue(issues, `${path}.indexes`, "must be an array");
15692
16740
  value.indexes.forEach((entry, i)=>{
15693
16741
  const idxPath = `${path}.indexes[${i}]`;
15694
- if (!isPlainObject(entry)) return void pushIssue(issues, idxPath, "must be an object");
15695
- if (void 0 === entry.type) pushIssue(issues, `${idxPath}.type`, "is required");
15696
- else if ("string" != typeof entry.type || !DB_TABLE_INDEX_TYPES.has(entry.type)) pushIssue(issues, `${idxPath}.type`, 'must be "b_tree", "trigram", or "full_text"');
16742
+ if (!validators_isPlainObject(entry)) return void validators_pushIssue(issues, idxPath, "must be an object");
16743
+ if (void 0 === entry.type) validators_pushIssue(issues, `${idxPath}.type`, "is required");
16744
+ else if ("string" != typeof entry.type || !DB_TABLE_INDEX_TYPES.has(entry.type)) validators_pushIssue(issues, `${idxPath}.type`, 'must be "b_tree", "trigram", or "full_text"');
15697
16745
  });
15698
16746
  }
15699
16747
  }
@@ -15702,8 +16750,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15702
16750
  if (void 0 === services) return issues;
15703
16751
  if (!Array.isArray(services)) return issues;
15704
16752
  services.forEach((service, index)=>{
15705
- if (!isPlainObject(service) || "string" != typeof service.source) return;
15706
- if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
16753
+ if (!validators_isPlainObject(service) || "string" != typeof service.source) return;
16754
+ if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) validators_pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
15707
16755
  });
15708
16756
  return issues;
15709
16757
  }
@@ -15711,9 +16759,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15711
16759
  const issues = [];
15712
16760
  if (void 0 === services || !Array.isArray(services)) return issues;
15713
16761
  services.forEach((service, index)=>{
15714
- if (!isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
15715
- if (!isPlainObject(service.client_config)) return;
15716
- if (true === service.client_config.allow_remember_me && void 0 === service.client_config.remember_me_days) pushIssue(issues, `${path}[${index}].client_config.remember_me_days`, "must be set if allow_remember_me is true");
16762
+ if (!validators_isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
16763
+ if (!validators_isPlainObject(service.client_config)) return;
16764
+ if (true === service.client_config.allow_remember_me && void 0 === service.client_config.remember_me_days) validators_pushIssue(issues, `${path}[${index}].client_config.remember_me_days`, "must be set if allow_remember_me is true");
15717
16765
  });
15718
16766
  return issues;
15719
16767
  }
@@ -15721,25 +16769,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15721
16769
  const issues = [];
15722
16770
  if (void 0 === value) return issues;
15723
16771
  if (!Array.isArray(value)) {
15724
- pushIssue(issues, path, "must be an array");
16772
+ validators_pushIssue(issues, path, "must be an array");
15725
16773
  return issues;
15726
16774
  }
15727
16775
  value.forEach((task, index)=>{
15728
16776
  const taskPath = `${path}[${index}]`;
15729
- if (!isPlainObject(task)) return void pushIssue(issues, taskPath, "must be an object");
15730
- if ("string" != typeof task.job_id || 0 === task.job_id.length) pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
15731
- validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
15732
- if (!isPlainObject(task.time_spec)) return void pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
16777
+ if (!validators_isPlainObject(task)) return void validators_pushIssue(issues, taskPath, "must be an object");
16778
+ if ("string" != typeof task.job_id || 0 === task.job_id.length) validators_pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
16779
+ validators_validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
16780
+ if (!validators_isPlainObject(task.time_spec)) return void validators_pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
15733
16781
  const timeSpec = task.time_spec;
15734
- if ("string" != typeof timeSpec.every || !SCHEDULE_EVERY_VALUES.has(timeSpec.every)) pushIssue(issues, `${taskPath}.time_spec.every`, 'must be one of "minute", "hour", "day", "week", or "month"');
16782
+ if ("string" != typeof timeSpec.every || !SCHEDULE_EVERY_VALUES.has(timeSpec.every)) validators_pushIssue(issues, `${taskPath}.time_spec.every`, 'must be one of "minute", "hour", "day", "week", or "month"');
15735
16783
  const interval = timeSpec.n;
15736
- if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
15737
- if (void 0 !== timeSpec.at) if (isPlainObject(timeSpec.at)) {
16784
+ if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) validators_pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
16785
+ if (void 0 !== timeSpec.at) if (validators_isPlainObject(timeSpec.at)) {
15738
16786
  const at = timeSpec.at;
15739
- if (void 0 !== at.minute && !Number.isInteger(at.minute)) pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
15740
- if (void 0 !== at.hour && !Number.isInteger(at.hour)) pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
15741
- if (void 0 !== at.day && !Number.isInteger(at.day)) pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
15742
- } else pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
16787
+ if (void 0 !== at.minute && !Number.isInteger(at.minute)) validators_pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
16788
+ if (void 0 !== at.hour && !Number.isInteger(at.hour)) validators_pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
16789
+ if (void 0 !== at.day && !Number.isInteger(at.day)) validators_pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
16790
+ } else validators_pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
15743
16791
  });
15744
16792
  return issues;
15745
16793
  }
@@ -15751,13 +16799,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15751
16799
  dependencies: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(external_valibot_namespaceObject.any())),
15752
16800
  runtime_options: RuntimeOptionsSchema,
15753
16801
  services: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(AnvilServiceEntrySchema)),
15754
- startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.looseObject({
16802
+ startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.looseObject({
15755
16803
  type: external_valibot_namespaceObject.picklist([
15756
16804
  "module",
15757
16805
  "form"
15758
16806
  ]),
15759
16807
  module: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.string())
15760
- })),
16808
+ }))),
15761
16809
  startup_form: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.string())),
15762
16810
  db_schema: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
15763
16811
  scheduled_tasks: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
@@ -15848,6 +16896,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15848
16896
  const startIndex = normalizedPath.lastIndexOf(clientCodePrefix);
15849
16897
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
15850
16898
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16899
+ if (relativeToClientCode.endsWith(".html")) return "form_template.html";
16900
+ if (relativeToClientCode.endsWith(".py")) return "python";
16901
+ }
16902
+ const serverCodePrefix = "server_code/";
16903
+ if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
16904
+ const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
16905
+ const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
16906
+ if (relativeToServerCode.endsWith(".py")) return "python";
15851
16907
  }
15852
16908
  return null;
15853
16909
  }
@@ -15867,7 +16923,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15867
16923
  currentDir = parentDir;
15868
16924
  }
15869
16925
  }
15870
- function validatePath(filePath, yamlContent) {
16926
+ function validatePath(filePath, fileContent) {
15871
16927
  const target = inferValidationTarget(filePath);
15872
16928
  const existingPath = resolveExistingPath(filePath);
15873
16929
  const appRoot = findAnvilAppRoot(filePath);
@@ -15894,7 +16950,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15894
16950
  }
15895
16951
  ]
15896
16952
  };
15897
- const result = validateAnvilYaml(yamlContent);
16953
+ const result = validateAnvilYaml(fileContent);
15898
16954
  return result.ok ? {
15899
16955
  ok: true,
15900
16956
  target
@@ -15920,7 +16976,62 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15920
16976
  ]
15921
16977
  };
15922
16978
  }
15923
- const result = validateFormTemplate(yamlContent);
16979
+ const result = validateFormTemplate(fileContent);
16980
+ return result.ok ? {
16981
+ ok: true,
16982
+ target
16983
+ } : {
16984
+ ...result,
16985
+ target
16986
+ };
16987
+ }
16988
+ if ("form_template.html" === target) {
16989
+ if (existingPath && appRoot) {
16990
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
16991
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
16992
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
16993
+ if (!isInsideClientCode || !existingPath.endsWith(".html")) return {
16994
+ ok: false,
16995
+ target,
16996
+ message: "File is not a client_code HTML form",
16997
+ issues: [
16998
+ {
16999
+ path: "",
17000
+ message: `expected a .html file under ${clientCodeRoot}`
17001
+ }
17002
+ ]
17003
+ };
17004
+ }
17005
+ const result = validateFormTemplateHtml(fileContent);
17006
+ return result.ok ? {
17007
+ ok: true,
17008
+ target
17009
+ } : {
17010
+ ...result,
17011
+ target
17012
+ };
17013
+ }
17014
+ if ("python" === target) {
17015
+ if (existingPath && appRoot) {
17016
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
17017
+ const serverCodeRoot = external_path_default().join(appRoot, "server_code");
17018
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
17019
+ const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
17020
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
17021
+ const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
17022
+ if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
17023
+ ok: false,
17024
+ target,
17025
+ message: "File is not an Anvil app Python file",
17026
+ issues: [
17027
+ {
17028
+ path: "",
17029
+ message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
17030
+ }
17031
+ ]
17032
+ };
17033
+ }
17034
+ const result = validatePython(filePath, fileContent, appRoot);
15924
17035
  return result.ok ? {
15925
17036
  ok: true,
15926
17037
  target
@@ -15936,7 +17047,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15936
17047
  issues: [
15937
17048
  {
15938
17049
  path: "",
15939
- message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, and client_code/**/*.yaml"
17050
+ message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
15940
17051
  }
15941
17052
  ]
15942
17053
  };
@@ -15975,9 +17086,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15975
17086
  };
15976
17087
  return null;
15977
17088
  }
17089
+ function formatValidationFailure(fileName, validationResult) {
17090
+ const issueSummary = validationResult.issues.map((issue)=>` - ${formatValidationPath(issue.path)}: ${issue.message}`).join("\n");
17091
+ return issueSummary ? `${fileName} validation failed: ${validationResult.message}\n${issueSummary}` : `${fileName} validation failed: ${validationResult.message}`;
17092
+ }
15978
17093
  async function loadFormTemplateData(relativePath, templateInfo, context) {
15979
17094
  const { repoPath, stagedOnly = false } = context;
15980
- const { kind, format } = templateInfo;
17095
+ const { format } = templateInfo;
15981
17096
  try {
15982
17097
  const content = await readFileContent(repoPath, relativePath, stagedOnly);
15983
17098
  if ("yaml" === format) {
@@ -15990,26 +17105,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15990
17105
  reason: issueSummary ? `${validationResult.message}. ${issueSummary}` : validationResult.message
15991
17106
  };
15992
17107
  }
15993
- const parsed = external_js_yaml_default().load(content);
15994
- const template = {
15995
- ...parsed,
15996
- is_package: "package" === kind
15997
- };
15998
- return {
15999
- ok: true,
16000
- data: {
16001
- template
16002
- }
16003
- };
16004
17108
  }
16005
- const { frontmatter, html } = parseHtmlWithFrontmatter(content);
16006
- const template = frontmatter ?? {};
16007
17109
  return {
16008
17110
  ok: true,
16009
- data: {
16010
- template,
16011
- html
16012
- }
17111
+ source: (0, form_template_parser_namespaceObject.parseFormTemplateSource)(content, {
17112
+ format
17113
+ })
16013
17114
  };
16014
17115
  } catch (error) {
16015
17116
  return {
@@ -16716,20 +17817,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16716
17817
  ]
16717
17818
  });
16718
17819
  }
16719
- function buildFormDataFromTemplate(template, html, options) {
16720
- const { class_name: _templateClassName, code: _templateCode, is_package: _templateIsPackage, ...templateRest } = template && "object" == typeof template ? template : {};
16721
- const formData = {
16722
- ...templateRest,
16723
- class_name: options.className,
16724
- code: options.code,
16725
- is_package: options.isPackage
16726
- };
16727
- if (void 0 !== html) {
16728
- formData.save_as_html = true;
16729
- formData.serialized_html = html;
16730
- }
16731
- return formData;
16732
- }
16733
17820
  async function buildFormTemplateData(relativePath, templateInfo, context) {
16734
17821
  const { repoPath, stagedOnly = false } = context;
16735
17822
  const { kind } = templateInfo;
@@ -16753,10 +17840,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16753
17840
  reason: "package" === kind ? "Form __init__.py file not found - cannot save template without code" : "Form .py file not found - cannot save template without code"
16754
17841
  }
16755
17842
  };
16756
- const templateData = loadResult.data.template ?? {};
16757
- const html = loadResult.data.html;
16758
- const templateObject = templateData && "object" == typeof templateData && !Array.isArray(templateData) ? templateData : {};
16759
- const formData = buildFormDataFromTemplate(templateObject, html, {
17843
+ const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
16760
17844
  className,
16761
17845
  code: codeContent,
16762
17846
  isPackage: "package" === kind
@@ -16941,7 +18025,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16941
18025
  }
16942
18026
  };
16943
18027
  const validationResult = validateAnvilYaml(yamlContent);
16944
- if (!validationResult.ok) throw new Error("anvil.yaml validation failed - see errors above");
18028
+ if (!validationResult.ok) throw new Error(formatValidationFailure("anvil.yaml", validationResult));
16945
18029
  const changes = [];
16946
18030
  for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
16947
18031
  key,
@@ -17048,6 +18132,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17048
18132
  }
17049
18133
  }
17050
18134
  function authDiagnostic(error) {
18135
+ if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
18136
+ code: error.type,
18137
+ recoverability: "requires_user_action",
18138
+ requiresUserAction: true,
18139
+ suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
18140
+ };
17051
18141
  return {
17052
18142
  code: error.type,
17053
18143
  recoverability: "requires_user_action",
@@ -17110,6 +18200,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17110
18200
  class SaveProcessor extends Emitter {
17111
18201
  hasPendingChanges = false;
17112
18202
  debounceTimer = null;
18203
+ saveLoopPromise = null;
17113
18204
  saveSequence = 0;
17114
18205
  SAVE_DEBOUNCE_MS = 1000;
17115
18206
  MAX_RETRIES = 3;
@@ -17133,6 +18224,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17133
18224
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
17134
18225
  if (isFirstChange) logger_logger.progress("sync", "Change detected");
17135
18226
  this.debounceTimer = setTimeout(()=>{
18227
+ this.debounceTimer = null;
17136
18228
  this.processSaveBatchWithEvents().catch((error)=>{
17137
18229
  logger_logger.error(`Save failed: ${errors_getErrorMessage(error)}`);
17138
18230
  });
@@ -17157,6 +18249,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17157
18249
  }
17158
18250
  }
17159
18251
  async processSaveBatchWithEvents() {
18252
+ if (this.saveLoopPromise) return this.saveLoopPromise;
18253
+ this.saveLoopPromise = this.drainSaveQueue();
18254
+ try {
18255
+ await this.saveLoopPromise;
18256
+ } finally{
18257
+ this.saveLoopPromise = null;
18258
+ }
18259
+ }
18260
+ async drainSaveQueue() {
18261
+ while(this.hasPendingChanges && !this.config.isCleanedUp() && !this.config.isPaused())await this.processOneSaveBatchWithEvents();
18262
+ }
18263
+ async processOneSaveBatchWithEvents() {
17160
18264
  if (this.config.isCleanedUp()) return;
17161
18265
  logger_logger.progressUpdate("sync", "Processing");
17162
18266
  if (!this.hasPendingChanges) return void logger_logger.progressEnd("sync");
@@ -17174,6 +18278,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17174
18278
  newCommitId: this.config.getCommitId(),
17175
18279
  fileCount
17176
18280
  });
18281
+ if (result.anvilYamlDependenciesChanged) this.emit("anvil-yaml-dependencies-changed", void 0);
17177
18282
  this.emit("save-complete", {
17178
18283
  newCommitId: this.config.getCommitId(),
17179
18284
  fileCount
@@ -17264,13 +18369,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17264
18369
  }
17265
18370
  async getStagedChanges() {
17266
18371
  try {
17267
- return await getStagedFileChanges(this.config.gitService.getGit());
18372
+ return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
17268
18373
  } catch (e) {
17269
18374
  logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
17270
18375
  return [];
17271
18376
  }
17272
18377
  }
17273
18378
  async getUnstagedChanges(status) {
18379
+ const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
17274
18380
  const renames = status.renamed;
17275
18381
  const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
17276
18382
  const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
@@ -17290,7 +18396,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17290
18396
  const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
17291
18397
  const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
17292
18398
  return [
17293
- ...status.modified.map((f)=>({
18399
+ ...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
17294
18400
  path: f,
17295
18401
  type: "change"
17296
18402
  })),
@@ -17361,11 +18467,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17361
18467
  async processBatchedChanges(changes) {
17362
18468
  const saveArray = [];
17363
18469
  const originalFilePaths = [];
18470
+ let anvilYamlDependenciesChanged = false;
17364
18471
  const deduplicatedChanges = deduplicateFormChanges(changes);
17365
18472
  for (const change of deduplicatedChanges){
17366
18473
  if ("anvil.yaml" === change.path) {
17367
18474
  const previousAnvilYaml = await this.getHeadAnvilYaml();
17368
18475
  const anvilChanges = await getAnvilYamlChanges(this.config.repoPath, previousAnvilYaml);
18476
+ const previousDependencies = normalizeAnvilYamlDependenciesValue(previousAnvilYaml?.dependencies);
18477
+ const currentDependencies = await readAnvilYamlDependencies(this.config.repoPath);
18478
+ if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) anvilYamlDependenciesChanged = true;
17369
18479
  if (0 === anvilChanges.length) {
17370
18480
  logger_logger.verbose(external_chalk_default().gray(" Ignoring anvil.yaml: No changes detected"));
17371
18481
  continue;
@@ -17417,7 +18527,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17417
18527
  }
17418
18528
  await this.sendSavesToAnvil(saveArray, originalFilePaths);
17419
18529
  return {
17420
- skipped: false
18530
+ skipped: false,
18531
+ anvilYamlDependenciesChanged
17421
18532
  };
17422
18533
  }
17423
18534
  logSaveResult(change, savePath, content) {
@@ -17493,7 +18604,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17493
18604
  await this.handleConflict(json_resp, originalFilePaths, retryCount);
17494
18605
  return;
17495
18606
  }
17496
- if (200 === resp.status) return void await this.handleSuccess(json_resp);
18607
+ if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
17497
18608
  logger_logger.error(`Unexpected response status: ${resp.status}`);
17498
18609
  throw {
17499
18610
  type: "server_error",
@@ -17596,13 +18707,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17596
18707
  reason: "Unknown conflict"
17597
18708
  };
17598
18709
  }
17599
- async handleSuccess(json_resp) {
18710
+ async handleSuccess(json_resp, originalFilePaths) {
17600
18711
  const version = json_resp.version;
17601
- this.config.setCommitId(version);
18712
+ const oldCommitId = json_resp.previous_commit ?? null;
18713
+ const commitIdBeforePostSaveSync = this.config.getCommitId();
18714
+ this.emit("saved", {
18715
+ oldCommitId,
18716
+ newCommitId: version,
18717
+ branch: this.config.getCurrentBranch()
18718
+ });
17602
18719
  if (this.postSaveCallback) {
17603
18720
  logger_logger.progressUpdate("sync", "Syncing back");
17604
- await this.postSaveCallback();
18721
+ await this.postSaveCallback(new Set(originalFilePaths));
17605
18722
  }
18723
+ if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
17606
18724
  }
17607
18725
  async rerouteChanges(changes) {
17608
18726
  logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
@@ -17656,76 +18774,114 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17656
18774
  }
17657
18775
  class SyncManager extends Emitter {
17658
18776
  config;
17659
- syncInProgress = false;
18777
+ syncQueue = Promise.resolve();
17660
18778
  constructor(config){
17661
18779
  super();
17662
18780
  this.config = config;
17663
18781
  }
17664
18782
  async syncRemoteChanges(knownLocalChanges) {
17665
- if (this.syncInProgress) {
17666
- logger_logger.debug("Sync already in progress, skipping");
18783
+ const runSync = ()=>this.performSyncRemoteChanges(knownLocalChanges);
18784
+ const queuedSync = this.syncQueue.then(runSync, runSync);
18785
+ this.syncQueue = queuedSync.catch(()=>{});
18786
+ return queuedSync;
18787
+ }
18788
+ async performSyncRemoteChanges(knownLocalChanges) {
18789
+ const oldCommitId = this.config.getCommitId();
18790
+ let locallyChangedFiles;
18791
+ let initialStatus;
18792
+ if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18793
+ else {
18794
+ initialStatus = await this.config.gitService.getStatus();
18795
+ locallyChangedFiles = buildLocalChangesSet(initialStatus);
18796
+ }
18797
+ let syncMode;
18798
+ if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
18799
+ else {
18800
+ await this.fetchAndResetFromAnvil(locallyChangedFiles);
18801
+ syncMode = "preservation";
18802
+ }
18803
+ if ("clean-move" === syncMode) return {
18804
+ localOnlyChanges: [],
18805
+ conflicts: []
18806
+ };
18807
+ const newCommitId = await this.config.gitService.getCommitId();
18808
+ this.config.setCommitId(newCommitId);
18809
+ const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
18810
+ const statusAfter = await this.config.gitService.getStatus();
18811
+ const newChanges = extractChangesFromStatus(statusAfter);
18812
+ if (0 === newChanges.length) {
18813
+ logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
17667
18814
  return {
17668
18815
  localOnlyChanges: [],
17669
18816
  conflicts: []
17670
18817
  };
17671
18818
  }
17672
- this.syncInProgress = true;
17673
- try {
17674
- const oldCommitId = this.config.getCommitId();
17675
- let locallyChangedFiles;
17676
- if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18819
+ const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
18820
+ await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
18821
+ logConflictResolution(result);
18822
+ return {
18823
+ localOnlyChanges: result.localOnlyChanges,
18824
+ conflicts: result.conflicts
18825
+ };
18826
+ }
18827
+ async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
18828
+ const oldCommitId = this.config.getCommitId();
18829
+ await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
18830
+ }
18831
+ async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
18832
+ return this.withFetchedAnvilRef(async (tempRef)=>{
18833
+ const localHead = await this.config.gitService.getCommitId();
18834
+ if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
17677
18835
  else {
17678
- const status = await this.config.gitService.getStatus();
17679
- locallyChangedFiles = buildLocalChangesSet(status);
18836
+ const statusBeforeHardReset = await this.config.gitService.getStatus();
18837
+ if (!this.isCleanForFastForward(statusBeforeHardReset)) {
18838
+ for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
18839
+ await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
18840
+ return "preservation";
18841
+ }
18842
+ await this.config.gitService.reset(tempRef, "hard");
17680
18843
  }
17681
- await this.fetchAndResetFromAnvil();
17682
18844
  const newCommitId = await this.config.gitService.getCommitId();
17683
18845
  this.config.setCommitId(newCommitId);
17684
- const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
17685
- const statusAfter = await this.config.gitService.getStatus();
17686
- const newChanges = extractChangesFromStatus(statusAfter);
17687
- if (0 === newChanges.length) {
17688
- logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
17689
- return {
17690
- localOnlyChanges: [],
17691
- conflicts: []
17692
- };
17693
- }
17694
- const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
17695
- await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
17696
- logConflictResolution(result);
17697
- return {
17698
- localOnlyChanges: result.localOnlyChanges,
17699
- conflicts: result.conflicts
17700
- };
17701
- } finally{
17702
- this.syncInProgress = false;
17703
- }
18846
+ await this.config.editorYaml.reload();
18847
+ return "clean-move";
18848
+ });
17704
18849
  }
17705
- async fetchAndResetFromAnvil() {
18850
+ async withFetchedAnvilRef(callback) {
17706
18851
  const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
17707
18852
  this.config.setAuthToken(validToken);
17708
18853
  const httpUrl = anvil_api_getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
17709
18854
  const currentBranch = this.config.getCurrentBranch();
17710
18855
  const tempRef = `anvil-sync-temp-${Date.now()}`;
17711
- const oldCommitId = this.config.getCommitId();
17712
- await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18856
+ try {
18857
+ await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18858
+ return await callback(tempRef);
18859
+ } finally{
18860
+ await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18861
+ }
18862
+ }
18863
+ async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
17713
18864
  await this.config.gitService.reset(tempRef, "mixed");
17714
- await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
17715
18865
  const newCommitId = await this.config.gitService.getCommitId();
17716
18866
  await this.config.gitService.checkout([
17717
18867
  ".anvil_editor.yaml",
17718
18868
  "anvil.yaml"
17719
18869
  ]);
17720
18870
  await this.config.editorYaml.reload();
17721
- await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
18871
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
17722
18872
  await this.discardFormattingOnlyYamlChanges();
17723
18873
  }
17724
- async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
18874
+ isCleanForFastForward(status) {
18875
+ return status.isClean && 0 === status.modified.length && 0 === status.notAdded.length && 0 === status.created.length && 0 === status.deleted.length && 0 === status.staged.length && 0 === status.renamed.length;
18876
+ }
18877
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
17725
18878
  try {
17726
18879
  const status = await this.config.gitService.getStatus();
17727
18880
  const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
17728
- await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles);
18881
+ await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles, {
18882
+ oldCommitId,
18883
+ localChangesToPreserve
18884
+ });
17729
18885
  } catch (e) {
17730
18886
  logger_logger.verbose(external_chalk_default().gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
17731
18887
  }
@@ -17838,6 +18994,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17838
18994
  environment;
17839
18995
  envBranchChangeMode;
17840
18996
  isSwitchingForEnvironment = false;
18997
+ environmentSwitchTargetBranch = null;
18998
+ dependencyWatchEnabled;
18999
+ isRefreshingDependencies = false;
19000
+ hasPendingDependencyRefresh = false;
17841
19001
  pendingEnvironmentBranchSwitch = null;
17842
19002
  BRANCH_CHANGE_SETTLE_MS = 2000;
17843
19003
  LOCAL_CHANGE_POLL_MS = 2000;
@@ -17855,6 +19015,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17855
19015
  this.stagedOnly = options.stagedOnly ?? false;
17856
19016
  this.environment = options.environment;
17857
19017
  this.envBranchChangeMode = options.envBranchChangeMode ?? "manual";
19018
+ this.dependencyWatchEnabled = options.dependencyWatch ?? true;
17858
19019
  this.editSession = "anvil-sync-" + Math.random().toString(36).substring(2, 15);
17859
19020
  this.gitService = new git_GitService(this.repoPath);
17860
19021
  this.editorYaml = new EditorYamlManager(this.repoPath);
@@ -17874,6 +19035,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17874
19035
  async initialize() {
17875
19036
  await this.editorYaml.load();
17876
19037
  this.initializeProcessors();
19038
+ if (this.dependencyWatchEnabled) await this.refreshDependencies("startup");
17877
19039
  try {
17878
19040
  await this.connectWebSocket();
17879
19041
  } catch (error) {
@@ -17893,7 +19055,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17893
19055
  getAuthToken: ()=>this.authToken,
17894
19056
  setAuthToken: (token)=>{
17895
19057
  this.authToken = token;
17896
- this.wsClient?.updateAuthToken(token);
19058
+ this.wsClient?.updateAuthToken?.(token);
17897
19059
  },
17898
19060
  getCurrentBranch: ()=>this.currentBranch,
17899
19061
  getCommitId: ()=>this.commitId,
@@ -17913,7 +19075,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17913
19075
  getAuthToken: ()=>this.authToken,
17914
19076
  setAuthToken: (token)=>{
17915
19077
  this.authToken = token;
17916
- this.wsClient?.updateAuthToken(token);
19078
+ this.wsClient?.updateAuthToken?.(token);
17917
19079
  },
17918
19080
  getCurrentBranch: ()=>this.currentBranch,
17919
19081
  getCommitId: ()=>this.commitId,
@@ -17926,15 +19088,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17926
19088
  isPaused: ()=>this.isPausedForUserInput
17927
19089
  });
17928
19090
  this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
17929
- this.saveProcessor.setPostSaveCallback(()=>this.syncManager.fetchAndResetFromAnvil());
19091
+ this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
17930
19092
  this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
17931
19093
  this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
17932
19094
  this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
17933
19095
  this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
19096
+ this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
19097
+ this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
19098
+ this.queueDependencyRefresh("anvil.yaml dependencies changed");
19099
+ });
17934
19100
  this.saveProcessor.on("validation-failed", (data)=>this.emit("validation-failed", data));
17935
19101
  this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
17936
19102
  this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
17937
19103
  }
19104
+ async syncAfterSave(savedFilePaths) {
19105
+ const status = await this.gitService.getStatus();
19106
+ const locallyChangedFiles = buildLocalChangesSet(status);
19107
+ if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
19108
+ return this.syncManager.syncRemoteChanges(locallyChangedFiles);
19109
+ }
17938
19110
  async connectWebSocket() {
17939
19111
  this.wsClient = new WebSocketClient({
17940
19112
  appId: this.appId,
@@ -17943,7 +19115,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17943
19115
  currentBranch: this.currentBranch,
17944
19116
  editSession: this.editSession,
17945
19117
  username: this.username,
17946
- environment: this.environment
19118
+ environment: this.environment,
19119
+ dependencyWatch: this.dependencyWatchEnabled,
19120
+ dependencySubscriptions: this.dependencyWatchEnabled ? await this.getDependencyWatchSubscriptions() : []
17947
19121
  });
17948
19122
  this.wsClient.on("env-state", ({ envs })=>{
17949
19123
  for (const env of envs)this.handleEnvironmentBinding(env, "snapshot");
@@ -17951,7 +19125,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17951
19125
  this.wsClient.on("env-update", (env)=>{
17952
19126
  this.handleEnvironmentBinding(env, "update");
17953
19127
  });
17954
- this.wsClient.on("repo-update", async ({ ref, commitHash, saveSeq })=>{
19128
+ this.wsClient.on("repo-update", async ({ appId, ref, commitHash, saveSeq })=>{
19129
+ if (appId !== this.appId) return void await this.handleDependencyRepoUpdate(appId, ref, commitHash);
17955
19130
  if (saveSeq?.session === this.editSession) return void logger_logger.debug("Ignoring update from our own session");
17956
19131
  const branchName = ref.replace(/^refs\/heads\//, "");
17957
19132
  if (branchName !== this.currentBranch) return void logger_logger.debug(`Ignoring update to different branch: ${branchName} (current: ${this.currentBranch})`);
@@ -17973,6 +19148,55 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17973
19148
  });
17974
19149
  await this.wsClient.connect();
17975
19150
  }
19151
+ async handleDependencyRepoUpdate(appId, ref, commitHash) {
19152
+ if (!this.dependencyWatchEnabled) return;
19153
+ const branchName = ref.replace(/^refs\/heads\//, "");
19154
+ const subscription = this.wsClient?.getDependencySubscriptions().find((candidate)=>candidate.appId === appId && candidate.branch === branchName);
19155
+ if (!subscription) return void logger_logger.debug(`Ignoring update for unwatched dependency app ${appId} on ${branchName}`);
19156
+ logger_logger.verbose(external_chalk_default().cyan("Dependency update detected: ") + external_chalk_default().bold(subscription.packageName ?? appId) + external_chalk_default().gray(` (${branchName}, ${commitHash.substring(0, 8)})`));
19157
+ await this.queueDependencyRefresh(`dependency ${appId} updated`);
19158
+ }
19159
+ async queueDependencyRefresh(reason) {
19160
+ if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
19161
+ if (this.isRefreshingDependencies) {
19162
+ this.hasPendingDependencyRefresh = true;
19163
+ return;
19164
+ }
19165
+ this.isRefreshingDependencies = true;
19166
+ try {
19167
+ do {
19168
+ this.hasPendingDependencyRefresh = false;
19169
+ await this.refreshDependencies(reason);
19170
+ }while (this.hasPendingDependencyRefresh && !this.isCleanedUp);
19171
+ } finally{
19172
+ this.isRefreshingDependencies = false;
19173
+ }
19174
+ }
19175
+ async refreshDependencies(reason) {
19176
+ if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
19177
+ logger_logger.progress("dependency-refresh", "startup" === reason ? "Fetching dependencies" : "Refreshing dependencies");
19178
+ try {
19179
+ const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
19180
+ const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
19181
+ includeDocs: true
19182
+ });
19183
+ if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
19184
+ const nextSubscriptions = await this.getDependencyWatchSubscriptions();
19185
+ this.reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions);
19186
+ logger_logger.progressEnd("dependency-refresh", response.dependencies.length ? `Fetched ${response.dependencies.length} dependencies` : "Dependencies already up to date");
19187
+ } catch (error) {
19188
+ logger_logger.progressEnd("dependency-refresh", "Failed");
19189
+ logger_logger.warn(external_chalk_default().yellow(`Failed to refresh dependencies: ${errors_getErrorMessage(error)}`));
19190
+ }
19191
+ }
19192
+ async getDependencyWatchSubscriptions() {
19193
+ return getDependencyWatchSubscriptionsFromManifest(await readDepsCacheManifest(this.repoPath));
19194
+ }
19195
+ reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions) {
19196
+ if (dependencySubscriptionsEqual(previousSubscriptions, nextSubscriptions)) return;
19197
+ this.wsClient?.updateDependencySubscriptions(nextSubscriptions);
19198
+ if (this.wsClient?.isConnected() && previousSubscriptions.length > 0) this.wsClient.reconnect();
19199
+ }
17976
19200
  async handleEnvironmentBinding(env, source) {
17977
19201
  if (!this.environment?.envPid || env.envPid !== this.environment.envPid) return;
17978
19202
  this.environment = {
@@ -17995,6 +19219,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17995
19219
  async switchToEnvironmentBranch(targetBranch, source) {
17996
19220
  if (this.isCleanedUp) return;
17997
19221
  this.isSwitchingForEnvironment = true;
19222
+ this.environmentSwitchTargetBranch = targetBranch;
17998
19223
  const oldPausedState = this.isPausedForUserInput;
17999
19224
  try {
18000
19225
  const status = await this.gitService.getStatus();
@@ -18019,6 +19244,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18019
19244
  ]);
18020
19245
  }
18021
19246
  logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
19247
+ this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
19248
+ this.wsClient?.updateAuthToken?.(this.authToken);
18022
19249
  const fetchUrl = anvil_api_getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
18023
19250
  const envRemoteRef = environment_environmentRemoteRefForBranch(targetBranch);
18024
19251
  await this.gitService.fetch(fetchUrl, environment_environmentFetchRefSpecForBranch(targetBranch));
@@ -18034,6 +19261,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18034
19261
  this.lastBranchChangeTime = Date.now();
18035
19262
  this.fileWatcher?.setCurrentBranch(targetBranch);
18036
19263
  this.wsClient?.updateBranch(targetBranch);
19264
+ this.queueDependencyRefresh("branch changed");
18037
19265
  await this.initializeLocalChangeFingerprint();
18038
19266
  this.emit("branch-changed", {
18039
19267
  oldBranch,
@@ -18053,6 +19281,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18053
19281
  } finally{
18054
19282
  this.isPausedForUserInput = oldPausedState;
18055
19283
  this.isSwitchingForEnvironment = false;
19284
+ this.environmentSwitchTargetBranch = null;
18056
19285
  }
18057
19286
  const pendingSwitch = this.pendingEnvironmentBranchSwitch;
18058
19287
  this.pendingEnvironmentBranchSwitch = null;
@@ -18104,7 +19333,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18104
19333
  });
18105
19334
  }
18106
19335
  async syncRemoteChanges(knownLocalChanges) {
18107
- return this.syncManager.syncRemoteChanges(knownLocalChanges);
19336
+ const previousDependencies = this.dependencyWatchEnabled ? await readAnvilYamlDependencies(this.repoPath) : void 0;
19337
+ const result = await this.syncManager.syncRemoteChanges(knownLocalChanges);
19338
+ if (this.dependencyWatchEnabled && void 0 !== previousDependencies) {
19339
+ const currentDependencies = await readAnvilYamlDependencies(this.repoPath);
19340
+ if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) await this.queueDependencyRefresh("anvil.yaml dependencies changed");
19341
+ }
19342
+ return result;
18108
19343
  }
18109
19344
  cleanup() {
18110
19345
  this.isCleanedUp = true;
@@ -18134,15 +19369,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18134
19369
  getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
18135
19370
  });
18136
19371
  this.fileWatcher.on("branch-change", ({ newBranch })=>{
18137
- const oldBranch = this.currentBranch;
18138
- logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
18139
- this.currentBranch = newBranch;
18140
- this.lastBranchChangeTime = Date.now();
18141
- this.wsClient?.updateBranch(newBranch);
18142
- this.emit("branch-changed", {
18143
- oldBranch,
18144
- newBranch
18145
- });
19372
+ this.handleFileWatcherBranchChange(newBranch);
18146
19373
  });
18147
19374
  this.fileWatcher.on("index-change", ()=>{
18148
19375
  this.saveProcessor?.queueSave();
@@ -18159,6 +19386,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18159
19386
  this.startLocalChangeFallbackPolling();
18160
19387
  await new Promise(()=>{});
18161
19388
  }
19389
+ handleFileWatcherBranchChange(newBranch) {
19390
+ if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
19391
+ const oldBranch = this.currentBranch;
19392
+ logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19393
+ this.currentBranch = newBranch;
19394
+ this.lastBranchChangeTime = Date.now();
19395
+ this.wsClient?.updateBranch(newBranch);
19396
+ this.queueDependencyRefresh("branch changed");
19397
+ this.emit("external-branch-changed", {
19398
+ oldBranch,
19399
+ newBranch
19400
+ });
19401
+ this.emit("branch-changed", {
19402
+ oldBranch,
19403
+ newBranch
19404
+ });
19405
+ }
18162
19406
  startWatchingInBackground(onError) {
18163
19407
  this.startWatching().catch((error)=>{
18164
19408
  this.cleanup();
@@ -18284,6 +19528,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18284
19528
  return false;
18285
19529
  }
18286
19530
  }
19531
+ function dependencySubscriptionsEqual(left, right) {
19532
+ return subscriptionKeys(left).join("\n") === subscriptionKeys(right).join("\n");
19533
+ }
19534
+ function subscriptionKeys(subscriptions) {
19535
+ return subscriptions.map((subscription)=>`${subscription.appId}\0${subscription.branch}`).sort();
19536
+ }
18287
19537
  async function validation_validateAnvilApp(repoPath) {
18288
19538
  const anvilYamlPath = external_path_default().join(repoPath, "anvil.yaml");
18289
19539
  if (!external_fs_.existsSync(anvilYamlPath)) throw createValidationError.missingYaml(repoPath);
@@ -18501,6 +19751,367 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18501
19751
  }
18502
19752
  require("inquirer");
18503
19753
  require("ora");
19754
+ const defaultGitAuthDoctorDeps = {
19755
+ getAppAuthBinding: git_auth_getAppAuthBinding,
19756
+ getAccountsForUrl: auth_getAccountsForUrl,
19757
+ hasTokensForUrl: auth_hasTokensForUrl,
19758
+ runCredentialFill: runGitCredentialFill
19759
+ };
19760
+ function getHighestStatus(statuses) {
19761
+ if (statuses.includes("error")) return "error";
19762
+ if (statuses.includes("warning")) return "warning";
19763
+ return "ok";
19764
+ }
19765
+ function finding(status, code, message) {
19766
+ return {
19767
+ status,
19768
+ code,
19769
+ message
19770
+ };
19771
+ }
19772
+ function redactSecrets(value) {
19773
+ return value.replace(/^password=.*$/gim, "password=<redacted>").replace(/(https?:\/\/git:)[^@\s]+@/g, "$1<redacted>@").replace(/(access[_-]?token=)[^&\s]+/gi, "$1<redacted>").replace(/(refresh[_-]?token=)[^&\s]+/gi, "$1<redacted>");
19774
+ }
19775
+ function splitShellCommand(command) {
19776
+ const tokens = [];
19777
+ let current = "";
19778
+ let state = "normal";
19779
+ for(let i = 0; i < command.length; i++){
19780
+ const ch = command[i];
19781
+ if ("single" === state) {
19782
+ if ("'" === ch) state = "normal";
19783
+ else current += ch;
19784
+ continue;
19785
+ }
19786
+ if ("double" === state) {
19787
+ if ("\"" === ch) state = "normal";
19788
+ else if ("\\" === ch && i + 1 < command.length && [
19789
+ "$",
19790
+ "`",
19791
+ "\"",
19792
+ "\\",
19793
+ "\n"
19794
+ ].includes(command[i + 1])) current += command[++i];
19795
+ else current += ch;
19796
+ continue;
19797
+ }
19798
+ if (/\s/.test(ch)) {
19799
+ if (current) {
19800
+ tokens.push(current);
19801
+ current = "";
19802
+ }
19803
+ } else if ("'" === ch) state = "single";
19804
+ else if ("\"" === ch) state = "double";
19805
+ else if ("\\" === ch && i + 1 < command.length) current += command[++i];
19806
+ else current += ch;
19807
+ }
19808
+ if ("normal" !== state) throw new Error("Unterminated quote in helper command");
19809
+ if (current) tokens.push(current);
19810
+ return tokens;
19811
+ }
19812
+ function parseCredentialHelperCommand(command) {
19813
+ const trimmed = command.trim();
19814
+ if (!trimmed || !trimmed.startsWith("!")) return {};
19815
+ const tokens = splitShellCommand(trimmed.slice(1).trim());
19816
+ return {
19817
+ execPath: tokens[0],
19818
+ helperPath: tokens[1]
19819
+ };
19820
+ }
19821
+ function readHelperModulePath(helperPath) {
19822
+ try {
19823
+ const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
19824
+ const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
19825
+ return match?.[2];
19826
+ } catch {
19827
+ return;
19828
+ }
19829
+ }
19830
+ function inspectCredentialHelperCommand(raw) {
19831
+ const details = {
19832
+ raw
19833
+ };
19834
+ try {
19835
+ const parsed = parseCredentialHelperCommand(raw);
19836
+ details.execPath = parsed.execPath;
19837
+ details.helperPath = parsed.helperPath;
19838
+ } catch (e) {
19839
+ details.parseError = e.message;
19840
+ return details;
19841
+ }
19842
+ if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
19843
+ if (details.helperPath) {
19844
+ details.helperExists = external_fs_default().existsSync(details.helperPath);
19845
+ if (details.helperExists) {
19846
+ try {
19847
+ const stat = external_fs_default().statSync(details.helperPath);
19848
+ details.helperExecutable = (73 & stat.mode) !== 0;
19849
+ } catch {
19850
+ details.helperExecutable = false;
19851
+ }
19852
+ details.modulePath = readHelperModulePath(details.helperPath);
19853
+ if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
19854
+ }
19855
+ }
19856
+ return details;
19857
+ }
19858
+ function detectAnvilRemote(remote) {
19859
+ const fetchUrl = remote.refs.fetch;
19860
+ const out = {
19861
+ name: remote.name,
19862
+ fetchUrl
19863
+ };
19864
+ if (!fetchUrl) return out;
19865
+ const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
19866
+ if (httpMatch) {
19867
+ const [, protocol, host, appId] = httpMatch;
19868
+ return {
19869
+ ...out,
19870
+ appId,
19871
+ anvilUrl: config_normalizeAnvilUrl(`${protocol}://${host}`),
19872
+ transport: "https"
19873
+ };
19874
+ }
19875
+ const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
19876
+ if (sshMatch) {
19877
+ const [, , host, appId] = sshMatch;
19878
+ return {
19879
+ ...out,
19880
+ appId,
19881
+ anvilUrl: config_normalizeAnvilUrl(host),
19882
+ transport: "ssh"
19883
+ };
19884
+ }
19885
+ return out;
19886
+ }
19887
+ async function getLocalConfigValues(repoPath, key) {
19888
+ try {
19889
+ const output = await esm_default(repoPath).raw([
19890
+ "config",
19891
+ "--local",
19892
+ "--get-all",
19893
+ key
19894
+ ]);
19895
+ const values = output.split(/\r?\n/);
19896
+ if ("" === values[values.length - 1]) values.pop();
19897
+ return values;
19898
+ } catch {
19899
+ return [];
19900
+ }
19901
+ }
19902
+ function buildCredentialInput(anvilUrl, appId) {
19903
+ const url = new URL(anvilUrl);
19904
+ return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
19905
+ }
19906
+ async function runGitCredentialFill(repoPath, input) {
19907
+ return new Promise((resolve)=>{
19908
+ const child = (0, external_child_process_namespaceObject.spawn)("git", [
19909
+ "credential",
19910
+ "fill"
19911
+ ], {
19912
+ cwd: repoPath,
19913
+ env: {
19914
+ ...process.env,
19915
+ GIT_TERMINAL_PROMPT: "0"
19916
+ },
19917
+ stdio: [
19918
+ "pipe",
19919
+ "pipe",
19920
+ "pipe"
19921
+ ]
19922
+ });
19923
+ let stdout = "";
19924
+ let stderr = "";
19925
+ child.stdout.setEncoding("utf8");
19926
+ child.stderr.setEncoding("utf8");
19927
+ child.stdout.on("data", (chunk)=>{
19928
+ stdout += chunk;
19929
+ });
19930
+ child.stderr.on("data", (chunk)=>{
19931
+ stderr += chunk;
19932
+ });
19933
+ child.on("error", (error)=>{
19934
+ resolve({
19935
+ exitCode: null,
19936
+ stdout: redactSecrets(stdout),
19937
+ stderr: redactSecrets(stderr),
19938
+ error: redactSecrets(error.message)
19939
+ });
19940
+ });
19941
+ child.on("close", (code)=>{
19942
+ resolve({
19943
+ exitCode: code,
19944
+ stdout: redactSecrets(stdout),
19945
+ stderr: redactSecrets(stderr)
19946
+ });
19947
+ });
19948
+ child.stdin.end(input);
19949
+ });
19950
+ }
19951
+ async function buildAppReport(repoPath, remote, deps) {
19952
+ const appId = remote.appId;
19953
+ const anvilUrl = remote.anvilUrl;
19954
+ const url = new URL(anvilUrl);
19955
+ const credentialScope = `${url.protocol}//${url.host}`;
19956
+ const helperConfigKey = `credential.${credentialScope}.helper`;
19957
+ const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
19958
+ const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
19959
+ const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
19960
+ const findings = [];
19961
+ if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
19962
+ else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
19963
+ for (const helper of helperDetails){
19964
+ if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
19965
+ if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
19966
+ if (helper.helperPath) {
19967
+ if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
19968
+ else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
19969
+ } else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
19970
+ if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
19971
+ if (helper.modulePath && "cli.js" === external_path_default().basename(helper.modulePath)) findings.push(finding("error", "helper_module_cli_entry", `Generated helper points at the CLI entry instead of the API entry: ${helper.modulePath}`));
19972
+ }
19973
+ const binding = await deps.getAppAuthBinding(repoPath, appId);
19974
+ if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
19975
+ if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
19976
+ const authUrl = binding.url ? config_normalizeAnvilUrl(binding.url) : anvilUrl;
19977
+ let accounts = [];
19978
+ let authUsername = binding.username;
19979
+ let hasUsableAuth = false;
19980
+ try {
19981
+ accounts = await deps.getAccountsForUrl(authUrl);
19982
+ if (authUsername || 1 !== accounts.length) {
19983
+ if (!authUsername && accounts.length > 1) findings.push(finding("error", "multiple_accounts_without_binding", `Multiple accounts are logged in for ${authUrl}; bind one with anvil checkout --user.`));
19984
+ } else authUsername = accounts[0];
19985
+ hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
19986
+ if (!hasUsableAuth) findings.push(finding("error", "missing_usable_auth", authUsername ? `No usable auth tokens found for ${authUrl} as ${authUsername}.` : `No usable auth tokens found for ${authUrl}.`));
19987
+ } catch (e) {
19988
+ findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
19989
+ }
19990
+ const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
19991
+ const credentialSimulation = {
19992
+ status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
19993
+ exitCode: fillResult.exitCode,
19994
+ stdout: redactSecrets(fillResult.stdout),
19995
+ stderr: redactSecrets(fillResult.stderr),
19996
+ error: fillResult.error ? redactSecrets(fillResult.error) : void 0
19997
+ };
19998
+ if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
19999
+ const report = {
20000
+ appId,
20001
+ anvilUrl,
20002
+ remoteName: remote.name,
20003
+ remoteUrl: remote.fetchUrl,
20004
+ credentialScope,
20005
+ helperConfigKey,
20006
+ helperValues,
20007
+ helperDetails,
20008
+ binding: {
20009
+ url: binding.url,
20010
+ username: binding.username,
20011
+ hasUrl: !!binding.url,
20012
+ hasUsername: !!binding.username
20013
+ },
20014
+ auth: {
20015
+ url: authUrl,
20016
+ username: authUsername,
20017
+ accounts,
20018
+ hasUsableAuth
20019
+ },
20020
+ credentialSimulation,
20021
+ findings
20022
+ };
20023
+ return report;
20024
+ }
20025
+ async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
20026
+ const deps = options.deps ?? defaultGitAuthDoctorDeps;
20027
+ const inputPath = external_path_default().resolve(repoPathInput);
20028
+ const git = esm_default(inputPath);
20029
+ const repoPath = (await git.raw([
20030
+ "rev-parse",
20031
+ "--show-toplevel"
20032
+ ])).trim();
20033
+ const rawGitDir = (await git.raw([
20034
+ "rev-parse",
20035
+ "--git-dir"
20036
+ ])).trim();
20037
+ const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
20038
+ auth_setRepoContext(repoPath);
20039
+ const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
20040
+ const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
20041
+ const findings = [];
20042
+ if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
20043
+ const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
20044
+ const status = getHighestStatus([
20045
+ ...findings.map((item)=>item.status),
20046
+ ...apps.flatMap((app)=>app.findings.map((item)=>item.status))
20047
+ ]);
20048
+ return {
20049
+ repoPath,
20050
+ gitDir,
20051
+ remotes,
20052
+ apps,
20053
+ findings,
20054
+ status
20055
+ };
20056
+ }
20057
+ function formatStatus(status) {
20058
+ if ("ok" === status) return external_chalk_default().green("[ok]");
20059
+ if ("warning" === status) return external_chalk_default().yellow("[warn]");
20060
+ return external_chalk_default().red("[error]");
20061
+ }
20062
+ function formatGitAuthDoctorReport(report) {
20063
+ const lines = [];
20064
+ lines.push(external_chalk_default().bold("Git auth doctor"));
20065
+ lines.push(`Repository: ${report.repoPath}`);
20066
+ lines.push(`Git dir: ${report.gitDir}`);
20067
+ lines.push(`Overall: ${formatStatus(report.status)}`);
20068
+ lines.push("");
20069
+ lines.push(external_chalk_default().bold("Detected remotes:"));
20070
+ if (0 === report.remotes.length) lines.push(" (none)");
20071
+ else for (const remote of report.remotes){
20072
+ const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
20073
+ lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
20074
+ }
20075
+ for (const app of report.apps){
20076
+ lines.push("");
20077
+ lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
20078
+ lines.push(` URL: ${app.anvilUrl}`);
20079
+ lines.push(` Credential scope: ${app.credentialScope}`);
20080
+ lines.push(` Helper config: ${app.helperConfigKey}`);
20081
+ if (0 === app.helperValues.length) lines.push(" (none)");
20082
+ else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
20083
+ for (const helper of app.helperDetails){
20084
+ lines.push(` Helper command: ${helper.raw}`);
20085
+ lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
20086
+ lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
20087
+ if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
20088
+ }
20089
+ lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
20090
+ lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
20091
+ lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
20092
+ if (app.credentialSimulation) {
20093
+ lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
20094
+ if (app.credentialSimulation.stdout.trim()) {
20095
+ lines.push(" stdout:");
20096
+ for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
20097
+ }
20098
+ if (app.credentialSimulation.stderr.trim()) {
20099
+ lines.push(" stderr:");
20100
+ for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
20101
+ }
20102
+ }
20103
+ if (app.findings.length > 0) {
20104
+ lines.push(" Findings:");
20105
+ for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
20106
+ }
20107
+ }
20108
+ if (report.findings.length > 0) {
20109
+ lines.push("");
20110
+ lines.push(external_chalk_default().bold("Repository findings:"));
20111
+ for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
20112
+ }
20113
+ return lines.join("\n");
20114
+ }
18504
20115
  const gitCredential_defaultDeps = {
18505
20116
  getValidAuthToken: auth_getValidAuthToken,
18506
20117
  getAccountsForUrl: auth_getAccountsForUrl,
@@ -18803,7 +20414,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18803
20414
  authToken,
18804
20415
  currentBranch,
18805
20416
  commitId,
18806
- username
20417
+ username,
20418
+ dependencyWatch: false
18807
20419
  });
18808
20420
  await session.initialize();
18809
20421
  await session.syncRemoteChanges();
@@ -18900,7 +20512,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18900
20512
  stagedOnly,
18901
20513
  username,
18902
20514
  environment,
18903
- envBranchChangeMode: options.envBranchChangeMode ?? "manual"
20515
+ envBranchChangeMode: options.envBranchChangeMode ?? "manual",
20516
+ dependencyWatch: options.dependencyWatch ?? true
18904
20517
  });
18905
20518
  session.on("branch-changed", (data)=>{
18906
20519
  logger_logger.debug("Event: branch-changed", data);
@@ -18956,6 +20569,7 @@ exports.ValidationIssue = __webpack_exports__.ValidationIssue;
18956
20569
  exports.ValidationTarget = __webpack_exports__.ValidationTarget;
18957
20570
  exports.WatchSession = __webpack_exports__.WatchSession;
18958
20571
  exports.branchFromEnvironmentVersion = __webpack_exports__.branchFromEnvironmentVersion;
20572
+ exports.buildGitAuthDoctorReport = __webpack_exports__.buildGitAuthDoctorReport;
18959
20573
  exports.checkUncommittedChanges = __webpack_exports__.checkUncommittedChanges;
18960
20574
  exports.clearInMemoryTokens = __webpack_exports__.clearInMemoryTokens;
18961
20575
  exports.createWatchDiagnostic = __webpack_exports__.createWatchDiagnostic;
@@ -18967,6 +20581,7 @@ exports.executeCheckout = __webpack_exports__.executeCheckout;
18967
20581
  exports.executeGitCredentialOperation = __webpack_exports__.executeGitCredentialOperation;
18968
20582
  exports.filterCandidates = __webpack_exports__.filterCandidates;
18969
20583
  exports.formatCandidateLabel = __webpack_exports__.formatCandidateLabel;
20584
+ exports.formatGitAuthDoctorReport = __webpack_exports__.formatGitAuthDoctorReport;
18970
20585
  exports.formatValidationPath = __webpack_exports__.formatValidationPath;
18971
20586
  exports.getAccountsForUrl = __webpack_exports__.getAccountsForUrl;
18972
20587
  exports.getAllConfig = __webpack_exports__.getAllConfig;
@@ -18982,6 +20597,7 @@ exports.getSettableConfigKeys = __webpack_exports__.getSettableConfigKeys;
18982
20597
  exports.getValidAuthToken = __webpack_exports__.getValidAuthToken;
18983
20598
  exports.getWebSocketUrl = __webpack_exports__.getWebSocketUrl;
18984
20599
  exports.hasTokensForUrl = __webpack_exports__.hasTokensForUrl;
20600
+ exports.inspectCredentialHelperCommand = __webpack_exports__.inspectCredentialHelperCommand;
18985
20601
  exports.isCommandAvailable = __webpack_exports__.isCommandAvailable;
18986
20602
  exports.isDirectoryNonEmpty = __webpack_exports__.isDirectoryNonEmpty;
18987
20603
  exports.isPathInsideGitRepo = __webpack_exports__.isPathInsideGitRepo;
@@ -18992,6 +20608,7 @@ exports.lookupRemoteInfoForAppId = __webpack_exports__.lookupRemoteInfoForAppId;
18992
20608
  exports.normalizeAnvilUrl = __webpack_exports__.normalizeAnvilUrl;
18993
20609
  exports.parseCheckoutInput = __webpack_exports__.parseCheckoutInput;
18994
20610
  exports.parseConfigSetValue = __webpack_exports__.parseConfigSetValue;
20611
+ exports.parseCredentialHelperCommand = __webpack_exports__.parseCredentialHelperCommand;
18995
20612
  exports.pollDeviceAuthorization = __webpack_exports__.pollDeviceAuthorization;
18996
20613
  exports.preferredEditors = __webpack_exports__.preferredEditors;
18997
20614
  exports.readEnvironmentPid = __webpack_exports__.readEnvironmentPid;
@@ -19011,6 +20628,7 @@ exports.validateAppId = __webpack_exports__.validateAppId;
19011
20628
  exports.validateBranchSyncStatus = __webpack_exports__.validateBranchSyncStatus;
19012
20629
  exports.validateCheckoutDestination = __webpack_exports__.validateCheckoutDestination;
19013
20630
  exports.validateFormTemplate = __webpack_exports__.validateFormTemplate;
20631
+ exports.validateFormTemplateHtml = __webpack_exports__.validateFormTemplateHtml;
19014
20632
  exports.validatePath = __webpack_exports__.validatePath;
19015
20633
  exports.verifyAndStoreTokens = __webpack_exports__.verifyAndStoreTokens;
19016
20634
  exports.verifyAuth = __webpack_exports__.verifyAuth;
@@ -19043,6 +20661,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19043
20661
  "ValidationTarget",
19044
20662
  "WatchSession",
19045
20663
  "branchFromEnvironmentVersion",
20664
+ "buildGitAuthDoctorReport",
19046
20665
  "checkUncommittedChanges",
19047
20666
  "clearInMemoryTokens",
19048
20667
  "createWatchDiagnostic",
@@ -19054,6 +20673,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19054
20673
  "executeGitCredentialOperation",
19055
20674
  "filterCandidates",
19056
20675
  "formatCandidateLabel",
20676
+ "formatGitAuthDoctorReport",
19057
20677
  "formatValidationPath",
19058
20678
  "getAccountsForUrl",
19059
20679
  "getAllConfig",
@@ -19069,6 +20689,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19069
20689
  "getValidAuthToken",
19070
20690
  "getWebSocketUrl",
19071
20691
  "hasTokensForUrl",
20692
+ "inspectCredentialHelperCommand",
19072
20693
  "isCommandAvailable",
19073
20694
  "isDirectoryNonEmpty",
19074
20695
  "isPathInsideGitRepo",
@@ -19079,6 +20700,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19079
20700
  "normalizeAnvilUrl",
19080
20701
  "parseCheckoutInput",
19081
20702
  "parseConfigSetValue",
20703
+ "parseCredentialHelperCommand",
19082
20704
  "pollDeviceAuthorization",
19083
20705
  "preferredEditors",
19084
20706
  "readEnvironmentPid",
@@ -19098,6 +20720,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
19098
20720
  "validateBranchSyncStatus",
19099
20721
  "validateCheckoutDestination",
19100
20722
  "validateFormTemplate",
20723
+ "validateFormTemplateHtml",
19101
20724
  "validatePath",
19102
20725
  "verifyAndStoreTokens",
19103
20726
  "verifyAuth",