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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/cli.js CHANGED
@@ -8946,7 +8946,7 @@ var __webpack_exports__ = {};
8946
8946
  commands,
8947
8947
  format: "utf-8",
8948
8948
  onError: esm_onError,
8949
- parser
8949
+ parser: esm_parser
8950
8950
  };
8951
8951
  }
8952
8952
  function checkIsRepoRootTask() {
@@ -8972,13 +8972,13 @@ var __webpack_exports__ = {};
8972
8972
  commands,
8973
8973
  format: "utf-8",
8974
8974
  onError: esm_onError,
8975
- parser
8975
+ parser: esm_parser
8976
8976
  };
8977
8977
  }
8978
8978
  function isNotRepoMessage(error) {
8979
8979
  return /(Not a git repository|Kein Git-Repository)/i.test(String(error));
8980
8980
  }
8981
- var CheckRepoActions, esm_onError, parser;
8981
+ var CheckRepoActions, esm_onError, esm_parser;
8982
8982
  var init_check_is_repo = __esm({
8983
8983
  "src/lib/tasks/check-is-repo.ts" () {
8984
8984
  "use strict";
@@ -8993,7 +8993,7 @@ var __webpack_exports__ = {};
8993
8993
  if (128 === exitCode && isNotRepoMessage(error)) return done(Buffer.from("false"));
8994
8994
  fail(error);
8995
8995
  };
8996
- parser = (text)=>"true" === text.trim();
8996
+ esm_parser = (text)=>"true" === text.trim();
8997
8997
  }
8998
8998
  });
8999
8999
  function cleanSummaryParser(dryRun, text) {
@@ -13718,7 +13718,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13718
13718
  }
13719
13719
  function getCredentialHelperModulePath(options) {
13720
13720
  const currentModulePath = options?.currentModulePath ?? __filename;
13721
- if (isAnvilCliPackageFile(currentModulePath)) return currentModulePath;
13721
+ if (isAnvilCliPackageFile(currentModulePath) && "cli.js" !== external_path_default().basename(currentModulePath)) return currentModulePath;
13722
13722
  return (options?.resolvePackageEntry ?? (()=>requireFromHere.resolve("@anvil-works/anvil-cli")))();
13723
13723
  }
13724
13724
  async function getRepositoryGitDir(repoPath, git = esm_default(repoPath)) {
@@ -13895,6 +13895,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13895
13895
  function setRepoContext(repoPath) {
13896
13896
  repoContext = repoPath;
13897
13897
  }
13898
+ const inFlightRefreshes = new Map();
13898
13899
  async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13899
13900
  try {
13900
13901
  const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
@@ -13948,53 +13949,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13948
13949
  }
13949
13950
  async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
13950
13951
  const normalized = normalizeAnvilUrl(anvilUrl);
13951
- let tokens;
13952
- let repoAuth;
13953
- if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
13954
- else {
13955
- repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13956
- tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
13957
- }
13952
+ const source = await resolveTokenSource(normalized, username);
13953
+ const tokens = getSourceTokens(source);
13958
13954
  if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
13959
13955
  const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
13960
- if (tokens.refreshToken && isExpired) try {
13956
+ if (tokens.refreshToken && isExpired) {
13957
+ const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13958
+ const existingRefresh = inFlightRefreshes.get(refreshKey);
13959
+ if (existingRefresh) return existingRefresh;
13960
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
13961
+ inFlightRefreshes.delete(refreshKey);
13962
+ });
13963
+ inFlightRefreshes.set(refreshKey, refreshPromise);
13964
+ return refreshPromise;
13965
+ }
13966
+ if (tokens.authToken) return tokens.authToken;
13967
+ throw errors_createAuthError.required("No valid token available");
13968
+ }
13969
+ async function resolveTokenSource(normalizedUrl, username) {
13970
+ if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
13971
+ type: "memory",
13972
+ auth: inMemoryAuth
13973
+ };
13974
+ const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13975
+ if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13976
+ type: "repo",
13977
+ auth: repoAuth,
13978
+ repoPath: repoContext
13979
+ };
13980
+ const tokens = getTokensAutoSelect(normalizedUrl, username);
13981
+ return {
13982
+ type: "store",
13983
+ tokens,
13984
+ accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
13985
+ };
13986
+ }
13987
+ function getSourceTokens(source) {
13988
+ return "store" === source.type ? source.tokens : source.auth.tokens;
13989
+ }
13990
+ function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13991
+ if (username) return username;
13992
+ const urlTokens = getUrlTokens(normalizedUrl);
13993
+ if (!urlTokens) return;
13994
+ const accounts = Object.keys(urlTokens);
13995
+ if (1 === accounts.length) return accounts[0];
13996
+ return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
13997
+ }
13998
+ function getRefreshKey(normalizedUrl, username, source, refreshToken) {
13999
+ switch(source.type){
14000
+ case "memory":
14001
+ return `memory:${normalizedUrl}:${source.auth.username}`;
14002
+ case "repo":
14003
+ return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
14004
+ case "store":
14005
+ return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
14006
+ }
14007
+ }
14008
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
14009
+ try {
14010
+ const tokens = getSourceTokens(source);
13961
14011
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
13962
- const tokenData = await refreshAccessToken(tokens.refreshToken, normalized, clientId);
13963
- const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
14012
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
13964
14013
  const newTokens = {
13965
14014
  authToken: tokenData.access_token,
13966
14015
  refreshToken: tokenData.refresh_token,
13967
- authTokenExpiresAt: newExpiresAt,
14016
+ authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
13968
14017
  clientId
13969
14018
  };
13970
- if (inMemoryAuth) inMemoryAuth.tokens = newTokens;
13971
- else if (repoAuth) {
14019
+ if ("memory" === source.type) source.auth.tokens = newTokens;
14020
+ else if ("repo" === source.type) {
13972
14021
  const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
13973
14022
  if (!encryptionKey) throw errors_createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
13974
- repoAuth.tokens = newTokens;
13975
- await writeAuthToFile(repoContext, repoAuth, encryptionKey);
14023
+ source.auth.tokens = newTokens;
14024
+ await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
13976
14025
  } else {
13977
- let accountUsername = username;
13978
- if (!accountUsername) {
13979
- const urlTokens = getUrlTokens(normalized);
13980
- if (urlTokens) {
13981
- const accounts = Object.keys(urlTokens);
13982
- accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
13983
- }
13984
- }
13985
- if (!accountUsername) {
13986
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14026
+ if (!source.accountUsername) {
14027
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13987
14028
  throw errors_createAuthError.required("Cannot determine which account to refresh. Please log in again.");
13988
14029
  }
13989
- setAccountTokens(normalized, accountUsername, newTokens);
14030
+ setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
13990
14031
  }
13991
14032
  return tokenData.access_token;
13992
14033
  } catch (e) {
13993
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14034
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13994
14035
  throw e;
13995
14036
  }
13996
- if (tokens.authToken) return tokens.authToken;
13997
- throw errors_createAuthError.required("No valid token available");
13998
14037
  }
13999
14038
  async function clearTokensForAccount(url, username, refreshToken) {
14000
14039
  if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
@@ -14275,6 +14314,334 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14275
14314
  } catch (_e) {}
14276
14315
  return out;
14277
14316
  }
14317
+ const promises_namespaceObject = require("fs/promises");
14318
+ var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
14319
+ function normalizeLineEndings(content) {
14320
+ return content.replace(/\r\n/g, "\n");
14321
+ }
14322
+ function pythonifyName(name) {
14323
+ return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14324
+ }
14325
+ function extractPythonName(relativePath, skipParts = 1) {
14326
+ const parts = relativePath.slice(0, -3).split("/");
14327
+ const isPackage = relativePath.endsWith("__init__.py");
14328
+ const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14329
+ return relevantParts.map(pythonifyName).join(".");
14330
+ }
14331
+ function deepEqual(a, b) {
14332
+ if (a === b) return true;
14333
+ if (null == a || null == b) return false;
14334
+ if (typeof a !== typeof b) return false;
14335
+ if ("object" != typeof a) return a === b;
14336
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
14337
+ if (Array.isArray(a)) {
14338
+ if (a.length !== b.length) return false;
14339
+ return a.every((item, index)=>deepEqual(item, b[index]));
14340
+ }
14341
+ const keysA = Object.keys(a);
14342
+ const keysB = Object.keys(b);
14343
+ if (keysA.length !== keysB.length) return false;
14344
+ return keysA.every((key)=>deepEqual(a[key], b[key]));
14345
+ }
14346
+ function parseHtmlWithFrontmatter(htmlContent) {
14347
+ const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14348
+ const match = htmlContent.match(frontmatterPattern);
14349
+ if (match) {
14350
+ const yamlStr = match[1];
14351
+ const html = match[2];
14352
+ const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14353
+ return {
14354
+ frontmatter,
14355
+ html
14356
+ };
14357
+ }
14358
+ return {
14359
+ frontmatter: {},
14360
+ html: htmlContent
14361
+ };
14362
+ }
14363
+ const ANVIL_GITIGNORE_ENTRY = "/.anvil/";
14364
+ async function ensureProjectGitignoreIgnoresAnvilDir(projectRoot) {
14365
+ const rootGitignorePath = external_path_default().join(projectRoot, ".gitignore");
14366
+ try {
14367
+ const existing = await external_fs_.promises.readFile(rootGitignorePath, "utf8");
14368
+ if (existing.includes(ANVIL_GITIGNORE_ENTRY)) return false;
14369
+ const prefix = existing.endsWith("\n") || 0 === existing.length ? "" : "\n";
14370
+ await external_fs_.promises.writeFile(rootGitignorePath, `${existing}${prefix}${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14371
+ return true;
14372
+ } catch (error) {
14373
+ if ("ENOENT" !== error.code) throw error;
14374
+ await external_fs_.promises.writeFile(rootGitignorePath, `${ANVIL_GITIGNORE_ENTRY}\n`, "utf8");
14375
+ return true;
14376
+ }
14377
+ }
14378
+ const ANVIL_DEPS_DIR = ".anvil/deps";
14379
+ const ANVIL_DEPS_MANIFEST = "manifest.json";
14380
+ const DEPS_MANIFEST_VERSION = 1;
14381
+ function formatFetchError(prefix, status, errorBody) {
14382
+ if (!errorBody) return `${prefix}: ${status}`;
14383
+ try {
14384
+ const parsed = JSON.parse(errorBody);
14385
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
14386
+ } catch {}
14387
+ return `${prefix}: ${status} ${errorBody}`;
14388
+ }
14389
+ async function fetchAllDependencies(appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14390
+ const { commit, includeDocs = true, knownDependencies = [] } = options;
14391
+ const token = await auth_getValidAuthToken(anvilUrl);
14392
+ const body = {
14393
+ include_docs: includeDocs,
14394
+ known_dependencies: knownDependencies
14395
+ };
14396
+ if (commit) body.commit = commit;
14397
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/resolved-dependencies`, {
14398
+ method: "POST",
14399
+ headers: {
14400
+ Authorization: `Bearer ${token}`,
14401
+ "Content-Type": "application/json"
14402
+ },
14403
+ body: JSON.stringify(body)
14404
+ });
14405
+ if (!resp.ok) {
14406
+ const errorBody = await resp.text();
14407
+ throw new Error(formatFetchError("Failed to fetch dependencies", resp.status, errorBody));
14408
+ }
14409
+ return await resp.json();
14410
+ }
14411
+ async function refreshDependencyCache(projectRoot, appId, anvilUrl = resolveAnvilUrl(), options = {}) {
14412
+ const includeDocs = options.includeDocs ?? true;
14413
+ const knownDependencies = await getKnownDependenciesForCache(projectRoot, includeDocs);
14414
+ const response = await fetchAllDependencies(appId, anvilUrl, {
14415
+ commit: options.commit,
14416
+ includeDocs,
14417
+ knownDependencies
14418
+ });
14419
+ if (!response.resolved_dependencies.length) {
14420
+ await clearDepsCache(projectRoot);
14421
+ return {
14422
+ response,
14423
+ addedGitignoreEntry: false
14424
+ };
14425
+ }
14426
+ const addedGitignoreEntry = await ensureProjectGitignoreIgnoresAnvilDir(projectRoot);
14427
+ await writeAllDepsToCache(projectRoot, response, {
14428
+ includeDocs
14429
+ });
14430
+ return {
14431
+ response,
14432
+ addedGitignoreEntry
14433
+ };
14434
+ }
14435
+ function depsDir(projectRoot) {
14436
+ return external_path_default().join(projectRoot, ANVIL_DEPS_DIR);
14437
+ }
14438
+ function depsManifestPath(projectRoot) {
14439
+ return external_path_default().join(depsDir(projectRoot), ANVIL_DEPS_MANIFEST);
14440
+ }
14441
+ function isResolvedDependencyManifestEntry(value) {
14442
+ if (!value || "object" != typeof value) return false;
14443
+ const entry = value;
14444
+ return "string" == typeof entry.app_id && entry.app_id.length > 0 && "string" == typeof entry.commit_id && entry.commit_id.length > 0 && (void 0 === entry.branch || "string" == typeof entry.branch);
14445
+ }
14446
+ function isCachedDependencyManifestEntry(value) {
14447
+ if (!isResolvedDependencyManifestEntry(value)) return false;
14448
+ const entry = value;
14449
+ return "string" == typeof entry.package_name && entry.package_name.length > 0;
14450
+ }
14451
+ function isDepsCacheManifest(value) {
14452
+ if (!value || "object" != typeof value) return false;
14453
+ const manifest = value;
14454
+ return manifest.version === DEPS_MANIFEST_VERSION && "boolean" == typeof manifest.includeDocs && Array.isArray(manifest.resolved_dependencies) && manifest.resolved_dependencies.every(isResolvedDependencyManifestEntry) && Array.isArray(manifest.cached_dependencies) && manifest.cached_dependencies.every(isCachedDependencyManifestEntry);
14455
+ }
14456
+ async function readDepsCacheManifest(projectRoot) {
14457
+ try {
14458
+ const raw = await promises_default().readFile(depsManifestPath(projectRoot), "utf-8");
14459
+ const parsed = JSON.parse(raw);
14460
+ return isDepsCacheManifest(parsed) ? parsed : void 0;
14461
+ } catch {
14462
+ return;
14463
+ }
14464
+ }
14465
+ async function getKnownDependenciesForCache(projectRoot, includeDocs) {
14466
+ const manifest = await readDepsCacheManifest(projectRoot);
14467
+ if (!manifest || manifest.includeDocs !== includeDocs) return [];
14468
+ return manifest.resolved_dependencies.map(({ app_id, commit_id })=>({
14469
+ app_id,
14470
+ commit_id
14471
+ }));
14472
+ }
14473
+ function getDependencyWatchSubscriptionsFromManifest(manifest) {
14474
+ if (!manifest) return [];
14475
+ const cachedByAppId = new Map(manifest.cached_dependencies.map((dep)=>[
14476
+ dep.app_id,
14477
+ dep
14478
+ ]));
14479
+ return manifest.resolved_dependencies.flatMap((dep)=>{
14480
+ if (!dep.branch) return [];
14481
+ return [
14482
+ {
14483
+ appId: dep.app_id,
14484
+ branch: dep.branch,
14485
+ packageName: cachedByAppId.get(dep.app_id)?.package_name
14486
+ }
14487
+ ];
14488
+ });
14489
+ }
14490
+ function normalizeAnvilYamlDependenciesFromContent(content) {
14491
+ const parsed = external_js_yaml_default().load(content);
14492
+ if (!parsed || "object" != typeof parsed || !Array.isArray(parsed.dependencies)) return [];
14493
+ return normalizeAnvilYamlDependenciesValue(parsed.dependencies);
14494
+ }
14495
+ function normalizeAnvilYamlDependenciesValue(value) {
14496
+ if (!Array.isArray(value)) return [];
14497
+ return normalizeForComparison(value);
14498
+ }
14499
+ async function readAnvilYamlDependencies(projectRoot) {
14500
+ try {
14501
+ const content = await promises_default().readFile(external_path_default().join(projectRoot, "anvil.yaml"), "utf-8");
14502
+ return normalizeAnvilYamlDependenciesFromContent(content);
14503
+ } catch {
14504
+ return [];
14505
+ }
14506
+ }
14507
+ function anvilYamlDependenciesEqual(left, right) {
14508
+ return deepEqual(left, right);
14509
+ }
14510
+ function normalizeForComparison(value) {
14511
+ if (Array.isArray(value)) return value.map(normalizeForComparison);
14512
+ if (value && "object" == typeof value) return Object.fromEntries(Object.entries(value).sort(([left], [right])=>left.localeCompare(right)).map(([key, entryValue])=>[
14513
+ key,
14514
+ normalizeForComparison(entryValue)
14515
+ ]));
14516
+ return value;
14517
+ }
14518
+ function cachedManifestEntry(dep, packageName) {
14519
+ return {
14520
+ app_id: dep.app_id,
14521
+ commit_id: dep.commit_id,
14522
+ ...dep.branch ? {
14523
+ branch: dep.branch
14524
+ } : {},
14525
+ package_name: packageName
14526
+ };
14527
+ }
14528
+ function safeDependencyPath(filePath, kind) {
14529
+ const normalizedPath = external_path_default().normalize(filePath);
14530
+ if (external_path_default().isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) throw new Error(`Invalid dependency ${kind} path: ${filePath}`);
14531
+ return normalizedPath;
14532
+ }
14533
+ async function writeDepToCache(projectRoot, dep) {
14534
+ const depDir = external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name);
14535
+ await promises_default().rm(depDir, {
14536
+ recursive: true,
14537
+ force: true
14538
+ });
14539
+ await promises_default().mkdir(external_path_default().join(depDir, "client_code"), {
14540
+ recursive: true
14541
+ });
14542
+ await promises_default().mkdir(external_path_default().join(depDir, "server_code"), {
14543
+ recursive: true
14544
+ });
14545
+ await promises_default().writeFile(external_path_default().join(depDir, "anvil.yaml"), external_js_yaml_default().dump({
14546
+ package_name: dep.package_name
14547
+ }));
14548
+ for (const mod of dep.modules ?? [])if (mod.is_package) {
14549
+ const modDir = external_path_default().join(depDir, "client_code", mod.name);
14550
+ await promises_default().mkdir(modDir, {
14551
+ recursive: true
14552
+ });
14553
+ await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14554
+ } else await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${mod.name}.py`), mod.code);
14555
+ for (const form of dep.forms ?? []){
14556
+ const { code, class_name, is_package, ...formTemplate } = form;
14557
+ if (is_package) {
14558
+ const formDir = external_path_default().join(depDir, "client_code", class_name);
14559
+ await promises_default().mkdir(formDir, {
14560
+ recursive: true
14561
+ });
14562
+ await promises_default().writeFile(external_path_default().join(formDir, "__init__.py"), code);
14563
+ await promises_default().writeFile(external_path_default().join(formDir, "form_template.yaml"), external_js_yaml_default().dump(formTemplate));
14564
+ } else {
14565
+ await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.py`), code);
14566
+ await promises_default().writeFile(external_path_default().join(depDir, "client_code", `${class_name}.yaml`), external_js_yaml_default().dump(formTemplate));
14567
+ }
14568
+ }
14569
+ for (const mod of dep.server_modules ?? [])if (mod.is_package) {
14570
+ const modDir = external_path_default().join(depDir, "server_code", mod.name);
14571
+ await promises_default().mkdir(modDir, {
14572
+ recursive: true
14573
+ });
14574
+ await promises_default().writeFile(external_path_default().join(modDir, "__init__.py"), mod.code);
14575
+ } else await promises_default().writeFile(external_path_default().join(depDir, "server_code", `${mod.name}.py`), mod.code);
14576
+ for (const asset of dep.assets ?? []){
14577
+ const targetPath = external_path_default().join(depDir, "theme", "assets", safeDependencyPath(asset.name, "asset"));
14578
+ await promises_default().mkdir(external_path_default().dirname(targetPath), {
14579
+ recursive: true
14580
+ });
14581
+ await promises_default().writeFile(targetPath, Buffer.from(asset.content, "base64"));
14582
+ }
14583
+ for (const doc of dep.docs ?? []){
14584
+ const targetPath = external_path_default().join(depDir, safeDependencyPath(doc.path, "doc"));
14585
+ await promises_default().mkdir(external_path_default().dirname(targetPath), {
14586
+ recursive: true
14587
+ });
14588
+ await promises_default().writeFile(targetPath, doc.content);
14589
+ }
14590
+ }
14591
+ async function writeAllDepsToCache(projectRoot, response, options = {}) {
14592
+ const includeDocs = options.includeDocs ?? true;
14593
+ const manifest = await readDepsCacheManifest(projectRoot);
14594
+ const canReuseExistingCache = manifest?.includeDocs === includeDocs;
14595
+ if (!canReuseExistingCache) await clearDepsCache(projectRoot);
14596
+ const previousCached = canReuseExistingCache ? manifest.cached_dependencies : [];
14597
+ const resolvedAppIds = new Set(response.resolved_dependencies.map((dep)=>dep.app_id));
14598
+ for (const dep of previousCached)if (!resolvedAppIds.has(dep.app_id)) await promises_default().rm(external_path_default().join(projectRoot, ANVIL_DEPS_DIR, dep.package_name), {
14599
+ recursive: true,
14600
+ force: true
14601
+ });
14602
+ for (const dep of response.dependencies)await writeDepToCache(projectRoot, dep);
14603
+ const returnedByAppId = new Map(response.dependencies.map((dep)=>[
14604
+ dep.app_id,
14605
+ dep
14606
+ ]));
14607
+ const previousByAppId = new Map(previousCached.map((dep)=>[
14608
+ dep.app_id,
14609
+ dep
14610
+ ]));
14611
+ const cachedDependencies = response.resolved_dependencies.map((dep)=>{
14612
+ const returned = returnedByAppId.get(dep.app_id);
14613
+ if (returned) return cachedManifestEntry(returned, returned.package_name);
14614
+ const previous = previousByAppId.get(dep.app_id);
14615
+ if (!previous) return;
14616
+ return cachedManifestEntry(dep, previous.package_name);
14617
+ }).filter((dep)=>void 0 !== dep);
14618
+ const newManifest = {
14619
+ version: DEPS_MANIFEST_VERSION,
14620
+ includeDocs,
14621
+ resolved_dependencies: response.resolved_dependencies,
14622
+ cached_dependencies: cachedDependencies
14623
+ };
14624
+ await promises_default().mkdir(depsDir(projectRoot), {
14625
+ recursive: true
14626
+ });
14627
+ await promises_default().writeFile(depsManifestPath(projectRoot), `${JSON.stringify(newManifest, null, 2)}\n`);
14628
+ }
14629
+ async function clearDepsCache(projectRoot) {
14630
+ await promises_default().rm(depsDir(projectRoot), {
14631
+ recursive: true,
14632
+ force: true
14633
+ });
14634
+ }
14635
+ async function getCachedPackages(projectRoot) {
14636
+ try {
14637
+ const entries = await promises_default().readdir(depsDir(projectRoot), {
14638
+ withFileTypes: true
14639
+ });
14640
+ return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
14641
+ } catch {
14642
+ return [];
14643
+ }
14644
+ }
14278
14645
  class GitService {
14279
14646
  git;
14280
14647
  repoPath;
@@ -14361,6 +14728,77 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14361
14728
  throw errors_createGitError.commandFailed("status", e.message);
14362
14729
  }
14363
14730
  }
14731
+ async discardModeOnlyChanges(paths) {
14732
+ const discarded = [];
14733
+ for (const relativePath of paths)try {
14734
+ const headMode = await this.getHeadFileMode(relativePath);
14735
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14736
+ const contentUnchanged = await this.hasSameContentAsHead(relativePath);
14737
+ if (!contentUnchanged) continue;
14738
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14739
+ discarded.push(relativePath);
14740
+ } catch (e) {
14741
+ throw errors_createGitError.commandFailed("discard mode-only changes", e.message);
14742
+ }
14743
+ return discarded;
14744
+ }
14745
+ async discardStagedModeOnlyChanges(paths) {
14746
+ const discarded = [];
14747
+ for (const relativePath of paths)try {
14748
+ const headMode = await this.getHeadFileMode(relativePath);
14749
+ if ("100644" !== headMode && "100755" !== headMode) continue;
14750
+ const indexContentUnchanged = await this.hasSameIndexContentAsHead(relativePath);
14751
+ if (!indexContentUnchanged) continue;
14752
+ await this.git.raw([
14753
+ "reset",
14754
+ "-q",
14755
+ "HEAD",
14756
+ "--",
14757
+ relativePath
14758
+ ]);
14759
+ await this.restoreExecutableBit(relativePath, "100755" === headMode);
14760
+ discarded.push(relativePath);
14761
+ } catch (e) {
14762
+ throw errors_createGitError.commandFailed("discard staged mode-only changes", e.message);
14763
+ }
14764
+ return discarded;
14765
+ }
14766
+ async getHeadFileMode(relativePath) {
14767
+ const output = await this.git.raw([
14768
+ "ls-tree",
14769
+ "HEAD",
14770
+ "--",
14771
+ relativePath
14772
+ ]);
14773
+ const match = output.match(/^(\d{6})\s/);
14774
+ return match?.[1] ?? null;
14775
+ }
14776
+ async hasSameContentAsHead(relativePath) {
14777
+ const headHash = (await this.git.revparse([
14778
+ `HEAD:${relativePath}`
14779
+ ])).trim();
14780
+ const worktreeHash = (await this.git.raw([
14781
+ "hash-object",
14782
+ "--",
14783
+ relativePath
14784
+ ])).trim();
14785
+ return headHash === worktreeHash;
14786
+ }
14787
+ async hasSameIndexContentAsHead(relativePath) {
14788
+ const headHash = (await this.git.revparse([
14789
+ `HEAD:${relativePath}`
14790
+ ])).trim();
14791
+ const indexHash = (await this.git.revparse([
14792
+ `:${relativePath}`
14793
+ ])).trim();
14794
+ return headHash === indexHash;
14795
+ }
14796
+ async restoreExecutableBit(relativePath, executable) {
14797
+ const filePath = external_path_default().join(this.repoPath, relativePath);
14798
+ const stat = await external_fs_.promises.stat(filePath);
14799
+ const mode = executable ? 73 | stat.mode : -74 & stat.mode;
14800
+ if ((511 & stat.mode) !== (511 & mode)) await external_fs_.promises.chmod(filePath, mode);
14801
+ }
14364
14802
  async hasUncommittedChanges() {
14365
14803
  const status = await this.getStatus();
14366
14804
  return !status.isClean;
@@ -14500,6 +14938,32 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14500
14938
  throw errors_createGitError.commandFailed("rebase --abort", e.message);
14501
14939
  }
14502
14940
  }
14941
+ async isAncestor(ancestorRef, descendantRef) {
14942
+ try {
14943
+ const mergeBase = (await this.git.raw([
14944
+ "merge-base",
14945
+ ancestorRef,
14946
+ descendantRef
14947
+ ])).trim();
14948
+ const ancestorCommit = (await this.git.revparse([
14949
+ ancestorRef
14950
+ ])).trim();
14951
+ return mergeBase === ancestorCommit;
14952
+ } catch {
14953
+ return false;
14954
+ }
14955
+ }
14956
+ async mergeFastForward(ref) {
14957
+ try {
14958
+ await this.git.raw([
14959
+ "merge",
14960
+ "--ff-only",
14961
+ ref
14962
+ ]);
14963
+ } catch (e) {
14964
+ throw errors_createGitError.commandFailed("merge --ff-only", e.message);
14965
+ }
14966
+ }
14503
14967
  async deleteRef(ref) {
14504
14968
  try {
14505
14969
  await this.git.raw([
@@ -14544,11 +15008,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14544
15008
  } catch (error) {}
14545
15009
  }
14546
15010
  }
14547
- async function getStagedFileChanges(git) {
15011
+ async function getStagedFileChanges(git, gitService) {
14548
15012
  try {
14549
15013
  const status = await git.status();
14550
15014
  const renames = status.renamed || [];
14551
15015
  const stagedFiles = status.files.filter((f)=>" " !== f.index && "?" !== f.index);
15016
+ const stagedModeOnlyPaths = new Set(gitService ? await gitService.discardStagedModeOnlyChanges(stagedFiles.filter((f)=>"R" !== f.index && "D" !== f.index).map((f)=>f.path)) : []);
14552
15017
  const stagedRenames = [];
14553
15018
  for (const f of stagedFiles)if ("R" === f.index) {
14554
15019
  const rename = renames.find((r)=>r.to === f.path);
@@ -14561,19 +15026,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14561
15026
  const stagedRenamedFromPaths = new Set(stagedRenames.map((r)=>r.from));
14562
15027
  const stagedRenamedToPaths = new Set(stagedRenames.map((r)=>r.path));
14563
15028
  const changes = [];
14564
- for (const f of stagedFiles)if ("R" !== f.index) {
14565
- if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
14566
- path: f.path,
14567
- type: "unlink"
14568
- });
14569
- else if ("A" === f.index || "?" === f.index) changes.push({
14570
- path: f.path,
14571
- type: "add"
14572
- });
14573
- else changes.push({
14574
- path: f.path,
14575
- type: "change"
14576
- });
15029
+ for (const f of stagedFiles){
15030
+ if ("R" !== f.index) {
15031
+ if (!stagedModeOnlyPaths.has(f.path)) {
15032
+ if (!(stagedRenamedFromPaths.has(f.path) || stagedRenamedToPaths.has(f.path))) if ("D" === f.index) changes.push({
15033
+ path: f.path,
15034
+ type: "unlink"
15035
+ });
15036
+ else if ("A" === f.index || "?" === f.index) changes.push({
15037
+ path: f.path,
15038
+ type: "add"
15039
+ });
15040
+ else changes.push({
15041
+ path: f.path,
15042
+ type: "change"
15043
+ });
15044
+ }
15045
+ }
14577
15046
  }
14578
15047
  changes.push(...stagedRenames);
14579
15048
  return changes;
@@ -14581,50 +15050,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14581
15050
  throw errors_createGitError.commandFailed("status", e.message);
14582
15051
  }
14583
15052
  }
14584
- function normalizeLineEndings(content) {
14585
- return content.replace(/\r\n/g, "\n");
14586
- }
14587
- function pythonifyName(name) {
14588
- return name.replace(/[^A-z0-9]/g, "_").replace(/^[0-9]/, "_$&");
14589
- }
14590
- function extractPythonName(relativePath, skipParts = 1) {
14591
- const parts = relativePath.slice(0, -3).split("/");
14592
- const isPackage = relativePath.endsWith("__init__.py");
14593
- const relevantParts = isPackage ? parts.slice(skipParts, -1) : parts.slice(skipParts);
14594
- return relevantParts.map(pythonifyName).join(".");
14595
- }
14596
- function deepEqual(a, b) {
14597
- if (a === b) return true;
14598
- if (null == a || null == b) return false;
14599
- if (typeof a !== typeof b) return false;
14600
- if ("object" != typeof a) return a === b;
14601
- if (Array.isArray(a) !== Array.isArray(b)) return false;
14602
- if (Array.isArray(a)) {
14603
- if (a.length !== b.length) return false;
14604
- return a.every((item, index)=>deepEqual(item, b[index]));
14605
- }
14606
- const keysA = Object.keys(a);
14607
- const keysB = Object.keys(b);
14608
- if (keysA.length !== keysB.length) return false;
14609
- return keysA.every((key)=>deepEqual(a[key], b[key]));
14610
- }
14611
- function parseHtmlWithFrontmatter(htmlContent) {
14612
- const frontmatterPattern = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
14613
- const match = htmlContent.match(frontmatterPattern);
14614
- if (match) {
14615
- const yamlStr = match[1];
14616
- const html = match[2];
14617
- const frontmatter = yamlStr ? external_js_yaml_default().load(yamlStr) ?? {} : {};
14618
- return {
14619
- frontmatter,
14620
- html
14621
- };
14622
- }
14623
- return {
14624
- frontmatter: {},
14625
- html: htmlContent
14626
- };
14627
- }
14628
15053
  const external_chokidar_namespaceObject = require("chokidar");
14629
15054
  var external_chokidar_default = /*#__PURE__*/ __webpack_require__.n(external_chokidar_namespaceObject);
14630
15055
  class FileWatcher extends Emitter {
@@ -14774,6 +15199,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14774
15199
  editSession;
14775
15200
  username;
14776
15201
  environment;
15202
+ dependencyWatchEnabled;
15203
+ dependencySubscriptions;
15204
+ activeDependencySubscriptions = new Map();
15205
+ watchedPrimaryBranch = null;
15206
+ nextSubscriptionId = 1;
15207
+ pendingDependencySubscriptionById = new Map();
14777
15208
  reconnectAttempts = 0;
14778
15209
  connectAttempts = 0;
14779
15210
  reconnectTimer = null;
@@ -14796,6 +15227,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14796
15227
  this.editSession = options.editSession;
14797
15228
  this.username = options.username;
14798
15229
  this.environment = options.environment;
15230
+ this.dependencyWatchEnabled = options.dependencyWatch ?? true;
15231
+ this.dependencySubscriptions = dedupeDependencySubscriptions(options.dependencySubscriptions ?? []);
14799
15232
  this.sessionId = Math.random().toString(36).substring(2, 10);
14800
15233
  this.reconnectDelayMs = this.RECONNECT_DELAY_BASE_MS;
14801
15234
  }
@@ -14821,6 +15254,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14821
15254
  const subscribeMsg = this.buildSubscribeMessage(this.currentBranch);
14822
15255
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing: ${JSON.stringify(subscribeMsg)}`);
14823
15256
  this.ws?.send(JSON.stringify(subscribeMsg));
15257
+ this.watchedPrimaryBranch = this.currentBranch;
15258
+ this.activeDependencySubscriptions.clear();
15259
+ this.subscribeToDependencies();
14824
15260
  this.emit("connected", void 0);
14825
15261
  });
14826
15262
  this.ws.on("unexpected-response", (request, response)=>{
@@ -14843,6 +15279,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14843
15279
  try {
14844
15280
  const msg = JSON.parse(data.toString());
14845
15281
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Message: ${JSON.stringify(msg)}`);
15282
+ this.handleDependencySubscriptionReply(msg);
14846
15283
  this.handleMessage(msg);
14847
15284
  } catch (error) {
14848
15285
  logger_logger.error(external_chalk_default().red(`Failed to parse WebSocket message: ${error.message}`));
@@ -14893,7 +15330,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14893
15330
  if (!ref_update) return void logger_logger.debug("Ignoring REPO_UPDATE with no ref_update");
14894
15331
  const { ref } = ref_update;
14895
15332
  const commitHash = "string" == typeof commit ? commit : commit?.v;
15333
+ const appId = this.getMessageAppId(msg);
14896
15334
  this.emit("repo-update", {
15335
+ appId,
14897
15336
  ref,
14898
15337
  commitHash: commitHash || "unknown",
14899
15338
  saveSeq: ref_update?.["save-seq"]
@@ -14908,11 +15347,26 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14908
15347
  }
14909
15348
  updateBranch(branch) {
14910
15349
  this.currentBranch = branch;
14911
- if (this.ws?.readyState === external_ws_default().OPEN) this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
15350
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15351
+ this.ws.send(JSON.stringify(this.buildSubscribeMessage(branch)));
15352
+ this.watchedPrimaryBranch = branch;
15353
+ this.subscribeToDependencies();
15354
+ }
14912
15355
  }
14913
15356
  updateEnvironment(environment) {
14914
15357
  this.environment = environment;
14915
15358
  }
15359
+ updateDependencySubscriptions(subscriptions) {
15360
+ const previousSubscriptions = this.dependencySubscriptions;
15361
+ this.dependencySubscriptions = dedupeDependencySubscriptions(subscriptions);
15362
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15363
+ this.unsubscribeStaleDependencies(previousSubscriptions, this.dependencySubscriptions);
15364
+ this.subscribeToDependencies();
15365
+ }
15366
+ }
15367
+ getDependencySubscriptions() {
15368
+ return this.dependencySubscriptions;
15369
+ }
14916
15370
  getEditSession() {
14917
15371
  return this.editSession;
14918
15372
  }
@@ -14923,7 +15377,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14923
15377
  clearTimeout(this.reconnectTimer);
14924
15378
  this.reconnectTimer = null;
14925
15379
  }
15380
+ if (this.ws?.readyState === external_ws_default().OPEN) {
15381
+ this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(this.appId)));
15382
+ this.unwatchDependencySubscriptions([
15383
+ ...this.activeDependencySubscriptions.values()
15384
+ ]);
15385
+ }
15386
+ this.teardownSocket();
15387
+ }
15388
+ reconnect() {
15389
+ if (this.isClosing) return;
14926
15390
  this.teardownSocket();
15391
+ this.connect().catch((error)=>{
15392
+ this.emit("error", {
15393
+ error: error instanceof Error ? error : new Error(String(error))
15394
+ });
15395
+ });
14927
15396
  }
14928
15397
  isConnected() {
14929
15398
  return this.ws?.readyState === external_ws_default().OPEN;
@@ -14956,7 +15425,73 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
14956
15425
  } : {}
14957
15426
  };
14958
15427
  }
14959
- getConnectionContext() {
15428
+ buildUnsubscribeMessage(appId) {
15429
+ return {
15430
+ cmd: "UNWATCH_APP",
15431
+ app: appId
15432
+ };
15433
+ }
15434
+ subscribeToDependencies() {
15435
+ if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
15436
+ for (const subscription of this.dependencySubscriptions){
15437
+ const key = this.dependencySubscriptionKey(subscription);
15438
+ if (this.activeDependencySubscriptions.has(key)) continue;
15439
+ const id = this.nextSubscriptionId++;
15440
+ this.pendingDependencySubscriptionById.set(id, subscription);
15441
+ this.activeDependencySubscriptions.set(key, subscription);
15442
+ const subscribeMsg = {
15443
+ id,
15444
+ cmd: "WATCH_APP",
15445
+ app: subscription.appId,
15446
+ only_branch: subscription.branch
15447
+ };
15448
+ logger_logger.debug(`[WebSocket ${this.sessionId}]`, `Subscribing dependency: ${JSON.stringify(subscribeMsg)}`);
15449
+ logger_logger.verbose(external_chalk_default().gray(`Watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch})`));
15450
+ this.ws.send(JSON.stringify(subscribeMsg));
15451
+ }
15452
+ }
15453
+ handleDependencySubscriptionReply(msg) {
15454
+ if ("number" != typeof msg.id) return;
15455
+ const subscription = this.pendingDependencySubscriptionById.get(msg.id);
15456
+ if (!subscription) return;
15457
+ this.pendingDependencySubscriptionById.delete(msg.id);
15458
+ if (!msg.error) return;
15459
+ this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
15460
+ logger_logger.warn(external_chalk_default().yellow(`Could not watch dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId}, branch ${subscription.branch}): ${this.formatDependencySubscriptionError(msg.error)}`));
15461
+ }
15462
+ formatDependencySubscriptionError(error) {
15463
+ if ("string" == typeof error) return error;
15464
+ if (error && "object" == typeof error && "string" == typeof error.message) return error.message;
15465
+ return JSON.stringify(error);
15466
+ }
15467
+ getMessageAppId(msg) {
15468
+ if ("string" == typeof msg.app) return msg.app;
15469
+ if ("string" == typeof msg.app_id) return msg.app_id;
15470
+ if ("string" == typeof msg.ref_update?.app) return msg.ref_update.app;
15471
+ if ("string" == typeof msg.ref_update?.app_id) return msg.ref_update.app_id;
15472
+ return this.appId;
15473
+ }
15474
+ unsubscribeStaleDependencies(previousSubscriptions, nextSubscriptions) {
15475
+ const nextAppIds = new Set(nextSubscriptions.map((subscription)=>subscription.appId));
15476
+ const staleSubscriptions = previousSubscriptions.filter((subscription)=>!nextAppIds.has(subscription.appId));
15477
+ this.unwatchDependencySubscriptions(staleSubscriptions);
15478
+ const nextKeys = new Set(nextSubscriptions.map((subscription)=>this.dependencySubscriptionKey(subscription)));
15479
+ for (const subscription of previousSubscriptions){
15480
+ const key = this.dependencySubscriptionKey(subscription);
15481
+ if (!nextKeys.has(key)) this.activeDependencySubscriptions.delete(key);
15482
+ }
15483
+ }
15484
+ unwatchDependencySubscriptions(subscriptions) {
15485
+ if (!this.dependencyWatchEnabled || this.ws?.readyState !== external_ws_default().OPEN) return;
15486
+ const unwatchedAppIds = new Set();
15487
+ for (const subscription of subscriptions)if (!unwatchedAppIds.has(subscription.appId)) {
15488
+ unwatchedAppIds.add(subscription.appId);
15489
+ logger_logger.verbose(external_chalk_default().gray(`Stopped watching dependency ${subscription.packageName ?? subscription.appId} (${subscription.appId})`));
15490
+ this.ws.send(JSON.stringify(this.buildUnsubscribeMessage(subscription.appId)));
15491
+ }
15492
+ for (const subscription of subscriptions)this.activeDependencySubscriptions.delete(this.dependencySubscriptionKey(subscription));
15493
+ }
15494
+ getConnectionContext() {
14960
15495
  return `app ${this.appId}, branch ${this.currentBranch}, session ${this.sessionId}`;
14961
15496
  }
14962
15497
  getConnectionTarget(wsUrl) {
@@ -15027,6 +15562,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15027
15562
  }
15028
15563
  teardownSocket() {
15029
15564
  this.stopHeartbeat();
15565
+ this.pendingDependencySubscriptionById.clear();
15566
+ this.activeDependencySubscriptions.clear();
15567
+ this.watchedPrimaryBranch = null;
15030
15568
  const socket = this.ws;
15031
15569
  this.ws = null;
15032
15570
  if (!socket) return;
@@ -15042,6 +15580,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15042
15580
  logger_logger.debug(`[WebSocket ${this.sessionId}]`, "Error closing WebSocket (ignoring):", e);
15043
15581
  }
15044
15582
  }
15583
+ dependencySubscriptionKey(subscription) {
15584
+ return `${subscription.appId}\0${subscription.branch}`;
15585
+ }
15045
15586
  }
15046
15587
  function normalizeEnvironmentBinding(msg) {
15047
15588
  return {
@@ -15050,6 +15591,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15050
15591
  tableMappingId: msg.tableMappingId ?? msg.table_mapping_id ?? null
15051
15592
  };
15052
15593
  }
15594
+ function dedupeDependencySubscriptions(subscriptions) {
15595
+ const seen = new Set();
15596
+ const deduped = [];
15597
+ for (const subscription of subscriptions){
15598
+ const key = `${subscription.appId}\0${subscription.branch}`;
15599
+ if (!seen.has(key)) {
15600
+ seen.add(key);
15601
+ deduped.push(subscription);
15602
+ }
15603
+ }
15604
+ return deduped;
15605
+ }
15053
15606
  async function detectRemoteChanges(gitService, oldCommitId, newCommitId) {
15054
15607
  try {
15055
15608
  const files = await gitService.diffNames(oldCommitId, newCommitId);
@@ -15100,7 +15653,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15100
15653
  ...status.modified,
15101
15654
  ...status.notAdded,
15102
15655
  ...status.created,
15103
- ...status.deleted
15656
+ ...status.deleted,
15657
+ ...(status.renamed ?? []).flatMap((r)=>[
15658
+ r.from,
15659
+ r.to
15660
+ ])
15104
15661
  ]);
15105
15662
  }
15106
15663
  function extractChangesFromStatus(status) {
@@ -15142,12 +15699,22 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15142
15699
  result.localOnlyChanges.forEach((c)=>logger_logger.verbose(external_chalk_default().gray(` ${c.path}`)));
15143
15700
  }
15144
15701
  }
15145
- async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles) {
15702
+ async function deleteFilesRemovedOnAnvil(gitService, unstagedFiles, remotelyChangedFiles, options = {}) {
15146
15703
  const filesToRemove = [];
15147
- for (const file of unstagedFiles)if (remotelyChangedFiles.has(file)) try {
15148
- await gitService.show(`HEAD:${file}`);
15149
- } catch (e) {
15150
- filesToRemove.push(file);
15704
+ const localChangesToPreserve = options.localChangesToPreserve ?? new Set();
15705
+ for (const file of unstagedFiles)if (!localChangesToPreserve.has(file)) {
15706
+ try {
15707
+ await gitService.show(`HEAD:${file}`);
15708
+ continue;
15709
+ } catch (e) {}
15710
+ if (options.oldCommitId) {
15711
+ try {
15712
+ await gitService.show(`${options.oldCommitId}:${file}`);
15713
+ filesToRemove.push(file);
15714
+ } catch (e) {}
15715
+ continue;
15716
+ }
15717
+ if (remotelyChangedFiles.has(file)) filesToRemove.push(file);
15151
15718
  }
15152
15719
  if (filesToRemove.length > 0) try {
15153
15720
  await gitService.clean(filesToRemove);
@@ -15159,6 +15726,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15159
15726
  return filesToRemove;
15160
15727
  }
15161
15728
  const external_crypto_namespaceObject = require("crypto");
15729
+ const form_template_parser_namespaceObject = require("@anvil-works/form-template-parser");
15162
15730
  const BUILTIN_COMPONENT_TYPES = new Set([
15163
15731
  "Label",
15164
15732
  "RichText",
@@ -15189,59 +15757,156 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15189
15757
  "CustomComponent",
15190
15758
  "LayoutSlot",
15191
15759
  "FlowPanel",
15192
- "HtmlTemplate"
15193
- ]);
15194
- const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
15195
- "/runtime/services/tables.yml",
15196
- "/runtime/services/google.yml",
15197
- "/runtime/services/uplink.yml",
15198
- "/runtime/services/stripe.yml",
15199
- "/runtime/services/segment.yml",
15200
- "/runtime/services/facebook.yml",
15201
- "/runtime/services/anvil/users.yml",
15202
- "/runtime/services/anvil/secrets.yml",
15203
- "/runtime/services/anvil/saml.yml",
15204
- "/runtime/services/anvil/microsoft.yml",
15205
- "/runtime/services/anvil/email.yml",
15206
- "/runtime/services/anvil/files.yml"
15760
+ "HtmlTemplate",
15761
+ "HtmlComponent"
15207
15762
  ]);
15208
15763
  const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15209
- const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
15210
- const DB_ACCESS_LEVELS = new Set([
15211
- "none",
15212
- "search",
15213
- "full"
15214
- ]);
15215
- const DB_BASIC_COLUMN_TYPES = new Set([
15216
- "string",
15217
- "number",
15218
- "bool",
15219
- "date",
15220
- "datetime",
15221
- "media",
15222
- "simpleObject"
15223
- ]);
15224
- const DB_LINK_COLUMN_TYPES = new Set([
15225
- "link_single",
15226
- "link_multiple"
15227
- ]);
15228
- const DB_TABLE_INDEX_TYPES = new Set([
15229
- "b_tree",
15230
- "trigram",
15231
- "full_text"
15232
- ]);
15233
- const SCHEDULE_EVERY_VALUES = new Set([
15234
- "minute",
15235
- "hour",
15236
- "day",
15237
- "week",
15238
- "month"
15764
+ const PYTHON_KEYWORDS = new Set([
15765
+ "False",
15766
+ "None",
15767
+ "True",
15768
+ "and",
15769
+ "as",
15770
+ "assert",
15771
+ "async",
15772
+ "await",
15773
+ "break",
15774
+ "class",
15775
+ "continue",
15776
+ "def",
15777
+ "del",
15778
+ "elif",
15779
+ "else",
15780
+ "except",
15781
+ "finally",
15782
+ "for",
15783
+ "from",
15784
+ "global",
15785
+ "if",
15786
+ "import",
15787
+ "in",
15788
+ "is",
15789
+ "lambda",
15790
+ "nonlocal",
15791
+ "not",
15792
+ "or",
15793
+ "pass",
15794
+ "raise",
15795
+ "return",
15796
+ "try",
15797
+ "while",
15798
+ "with",
15799
+ "yield",
15800
+ "match",
15801
+ "case"
15239
15802
  ]);
15240
- const CLIENT_VERSIONS = new Set([
15241
- "2",
15242
- "3"
15803
+ function isValidPythonIdentifier(value) {
15804
+ return IDENTIFIER_RE.test(value) && !PYTHON_KEYWORDS.has(value);
15805
+ }
15806
+ function isParserGeneratedComponentName(name) {
15807
+ if (!name.startsWith("$")) return false;
15808
+ const suffix = name.slice(1);
15809
+ return suffix.length > 0 && isValidPythonIdentifier(suffix);
15810
+ }
15811
+ function pushIssue(issues, path, message) {
15812
+ issues.push({
15813
+ path,
15814
+ message
15815
+ });
15816
+ }
15817
+ function validateComponentName(value, path, issues) {
15818
+ if (void 0 === value || "string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15819
+ if (isParserGeneratedComponentName(value)) return;
15820
+ if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
15821
+ if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15822
+ }
15823
+ const componentTypeSpec_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15824
+ const DEP_ID_RE = /^dep_[A-Za-z0-9]+$/;
15825
+ const componentTypeSpec_PYTHON_KEYWORDS = new Set([
15826
+ "False",
15827
+ "None",
15828
+ "True",
15829
+ "and",
15830
+ "as",
15831
+ "assert",
15832
+ "async",
15833
+ "await",
15834
+ "break",
15835
+ "class",
15836
+ "continue",
15837
+ "def",
15838
+ "del",
15839
+ "elif",
15840
+ "else",
15841
+ "except",
15842
+ "finally",
15843
+ "for",
15844
+ "from",
15845
+ "global",
15846
+ "if",
15847
+ "import",
15848
+ "in",
15849
+ "is",
15850
+ "lambda",
15851
+ "nonlocal",
15852
+ "not",
15853
+ "or",
15854
+ "pass",
15855
+ "raise",
15856
+ "return",
15857
+ "try",
15858
+ "while",
15859
+ "with",
15860
+ "yield",
15861
+ "match",
15862
+ "case"
15243
15863
  ]);
15244
- const PYTHON_KEYWORDS = new Set([
15864
+ const LEGACY_FORM_TYPE_REFERENCE_MESSAGE = "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)";
15865
+ 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)";
15866
+ function isValidIdentifierSegment(value) {
15867
+ return value.length > 0 && componentTypeSpec_IDENTIFIER_RE.test(value) && !componentTypeSpec_PYTHON_KEYWORDS.has(value);
15868
+ }
15869
+ function isValidLegacyFormTypeReference(typeName) {
15870
+ if (!typeName.startsWith("form:")) return false;
15871
+ const rest = typeName.slice(5);
15872
+ if (0 === rest.length) return false;
15873
+ let classPath = rest;
15874
+ if (rest.startsWith("dep_")) {
15875
+ const splitIndex = rest.indexOf(":");
15876
+ if (splitIndex <= 0) return false;
15877
+ const depId = rest.slice(0, splitIndex);
15878
+ if (!DEP_ID_RE.test(depId)) return false;
15879
+ classPath = rest.slice(splitIndex + 1);
15880
+ }
15881
+ if (0 === classPath.length) return false;
15882
+ return classPath.split(".").every((part)=>isValidIdentifierSegment(part));
15883
+ }
15884
+ function isValidPackageQualifiedComponentType(typeName) {
15885
+ if (typeName.startsWith("anvil.")) return false;
15886
+ if (!typeName.includes(".")) return false;
15887
+ const parts = typeName.split(".");
15888
+ if (parts.length < 2) return false;
15889
+ return parts.every((part)=>isValidIdentifierSegment(part));
15890
+ }
15891
+ function componentTypeSpec_pushIssue(issues, path, message) {
15892
+ issues.push({
15893
+ path,
15894
+ message
15895
+ });
15896
+ }
15897
+ function validateComponentTypeSpec(value, path, issues, allowedSingleSegmentTypes) {
15898
+ if ("string" != typeof value || 0 === value.length) return void componentTypeSpec_pushIssue(issues, path, "must be a non-empty string");
15899
+ const typeName = value;
15900
+ if (typeName.startsWith("form:")) {
15901
+ if (!isValidLegacyFormTypeReference(typeName)) componentTypeSpec_pushIssue(issues, path, LEGACY_FORM_TYPE_REFERENCE_MESSAGE);
15902
+ return;
15903
+ }
15904
+ if (isValidPackageQualifiedComponentType(typeName)) return;
15905
+ if (!isValidIdentifierSegment(typeName)) return void componentTypeSpec_pushIssue(issues, path, "must be a valid identifier");
15906
+ if (!allowedSingleSegmentTypes.has(typeName)) componentTypeSpec_pushIssue(issues, path, COMPONENT_TYPE_REFERENCE_MESSAGE);
15907
+ }
15908
+ const formTemplateValidation_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15909
+ const formTemplateValidation_PYTHON_KEYWORDS = new Set([
15245
15910
  "False",
15246
15911
  "None",
15247
15912
  "True",
@@ -15295,58 +15960,29 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15295
15960
  "padding",
15296
15961
  "spacing"
15297
15962
  ]);
15298
- function isValidFormReference(typeName) {
15299
- if (!typeName.startsWith("form:")) return false;
15300
- const rest = typeName.slice(5);
15301
- if (0 === rest.length) return false;
15302
- let classPath = rest;
15303
- if (rest.startsWith("dep_")) {
15304
- const splitIndex = rest.indexOf(":");
15305
- if (splitIndex <= 0) return false;
15306
- const depId = rest.slice(0, splitIndex);
15307
- if (!DEP_ID_RE.test(depId)) return false;
15308
- classPath = rest.slice(splitIndex + 1);
15309
- }
15310
- if (0 === classPath.length) return false;
15311
- return classPath.split(".").every((part)=>IDENTIFIER_RE.test(part));
15312
- }
15313
- function isBuiltinComponentType(typeName) {
15314
- return BUILTIN_COMPONENT_TYPES.has(typeName);
15315
- }
15316
15963
  function isPlainObject(value) {
15317
15964
  return "object" == typeof value && null !== value && !Array.isArray(value);
15318
15965
  }
15319
- function pushIssue(issues, path, message) {
15966
+ function formTemplateValidation_pushIssue(issues, path, message) {
15320
15967
  issues.push({
15321
15968
  path,
15322
15969
  message
15323
15970
  });
15324
15971
  }
15325
- function formatValidationPath(path) {
15326
- return path || "root";
15327
- }
15328
15972
  function validateIdentifier(value, path, issues) {
15329
- if ("string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15330
- if (!IDENTIFIER_RE.test(value)) return void pushIssue(issues, path, "must be a valid identifier");
15331
- if (PYTHON_KEYWORDS.has(value)) pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15973
+ if ("string" != typeof value || 0 === value.length) return void formTemplateValidation_pushIssue(issues, path, "must be a non-empty string");
15974
+ if (!formTemplateValidation_IDENTIFIER_RE.test(value)) return void formTemplateValidation_pushIssue(issues, path, "must be a valid identifier");
15975
+ if (formTemplateValidation_PYTHON_KEYWORDS.has(value)) formTemplateValidation_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15332
15976
  }
15333
15977
  function validateComponentType(value, path, issues) {
15334
- if ("string" != typeof value || 0 === value.length) return void pushIssue(issues, path, "must be a non-empty string");
15335
- const typeName = value;
15336
- if (typeName.startsWith("form:")) {
15337
- if (!isValidFormReference(typeName)) pushIssue(issues, path, "must be a valid form: reference (form:Module.Form, form:dep_xxx:Package.Form)");
15338
- return;
15339
- }
15340
- if (!IDENTIFIER_RE.test(typeName)) return void pushIssue(issues, path, "must be a valid identifier");
15341
- if (PYTHON_KEYWORDS.has(typeName)) return void pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15342
- if (!isBuiltinComponentType(typeName)) pushIssue(issues, path, "must be a known Anvil component type (see componentIcons.ts) or a form: reference");
15978
+ validateComponentTypeSpec(value, path, issues, BUILTIN_COMPONENT_TYPES);
15343
15979
  }
15344
15980
  function validatePropertiesObject(value, path, issues) {
15345
- if (void 0 !== value && !isPlainObject(value)) pushIssue(issues, path, "must be an object");
15981
+ if (void 0 !== value && !isPlainObject(value)) formTemplateValidation_pushIssue(issues, path, "must be an object");
15346
15982
  }
15347
15983
  function validateEventBindingsObject(value, path, issues) {
15348
15984
  if (void 0 === value) return;
15349
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15985
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15350
15986
  for (const [eventName, handlerName] of Object.entries(value)){
15351
15987
  validateIdentifier(eventName, `${path}.${eventName}.name`, issues);
15352
15988
  validateIdentifier(handlerName, `${path}.${eventName}`, issues);
@@ -15354,13 +15990,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15354
15990
  }
15355
15991
  function validateDataBindings(value, path, issues) {
15356
15992
  if (void 0 === value) return;
15357
- if (!Array.isArray(value)) return void pushIssue(issues, path, "must be an array");
15993
+ if (!Array.isArray(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an array");
15358
15994
  value.forEach((binding, index)=>{
15359
15995
  const bindingPath = `${path}[${index}]`;
15360
- if (!isPlainObject(binding)) return void pushIssue(issues, bindingPath, "must be an object");
15361
- if ("string" != typeof binding.property || 0 === binding.property.length) pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
15362
- if ("string" != typeof binding.code || 0 === binding.code.length) pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
15363
- if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
15996
+ if (!isPlainObject(binding)) return void formTemplateValidation_pushIssue(issues, bindingPath, "must be an object");
15997
+ if ("string" != typeof binding.property || 0 === binding.property.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.property`, "must be a non-empty string");
15998
+ if ("string" != typeof binding.code || 0 === binding.code.length) formTemplateValidation_pushIssue(issues, `${bindingPath}.code`, "must be a non-empty string");
15999
+ if (void 0 !== binding.writeback && "boolean" != typeof binding.writeback) formTemplateValidation_pushIssue(issues, `${bindingPath}.writeback`, "must be a boolean");
15364
16000
  });
15365
16001
  }
15366
16002
  function validateComponentArray(components, pathPrefix, issues, seenNames) {
@@ -15368,146 +16004,563 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15368
16004
  const childPath = `${pathPrefix}[${index}]`;
15369
16005
  validateComponentNode(component, childPath, issues, seenNames);
15370
16006
  if (isPlainObject(component) && "string" == typeof component.name && component.name.length > 0) {
15371
- if (seenNames.has(component.name)) pushIssue(issues, `${childPath}.name`, "must be unique across form components");
16007
+ if (seenNames.has(component.name)) formTemplateValidation_pushIssue(issues, `${childPath}.name`, "must be unique across form components");
15372
16008
  seenNames.add(component.name);
15373
16009
  }
15374
16010
  });
15375
16011
  }
15376
16012
  function validateComponentNode(value, path, issues, seenNames) {
15377
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16013
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15378
16014
  validateComponentType(value.type, `${path}.type`, issues);
15379
- if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) pushIssue(issues, `${path}.name`, "must be a non-empty string");
15380
- else validateIdentifier(value.name, `${path}.name`, issues);
16015
+ validateComponentName(value.name, `${path}.name`, issues);
15381
16016
  validatePropertiesObject(value.properties, `${path}.properties`, issues);
15382
16017
  validatePropertiesObject(value.layout_properties, `${path}.layout_properties`, issues);
15383
16018
  validateEventBindingsObject(value.event_bindings, `${path}.event_bindings`, issues);
15384
16019
  validateDataBindings(value.data_bindings, `${path}.data_bindings`, issues);
15385
16020
  if (void 0 !== value.components) if (Array.isArray(value.components)) validateComponentArray(value.components, `${path}.components`, issues, seenNames);
15386
- else pushIssue(issues, `${path}.components`, "must be an array");
16021
+ else formTemplateValidation_pushIssue(issues, `${path}.components`, "must be an array");
15387
16022
  }
15388
16023
  function validatePropertyDefinition(value, path, issues) {
15389
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16024
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15390
16025
  validateIdentifier(value.name, `${path}.name`, issues);
15391
- if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) pushIssue(issues, `${path}.type`, "must be a supported property type");
15392
- if (void 0 !== value.description && "string" != typeof value.description) pushIssue(issues, `${path}.description`, "must be a string");
15393
- if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
15394
- if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
16026
+ if ("string" != typeof value.type || !PROPERTY_TYPES.has(value.type)) formTemplateValidation_pushIssue(issues, `${path}.type`, "must be a supported property type");
16027
+ if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
16028
+ if (void 0 !== value.default_binding_prop && "boolean" != typeof value.default_binding_prop) formTemplateValidation_pushIssue(issues, `${path}.default_binding_prop`, "must be a boolean");
16029
+ if (void 0 !== value.allow_binding_writeback && "boolean" != typeof value.allow_binding_writeback) formTemplateValidation_pushIssue(issues, `${path}.allow_binding_writeback`, "must be a boolean");
15395
16030
  if (void 0 !== value.binding_writeback_events) if (Array.isArray(value.binding_writeback_events)) value.binding_writeback_events.forEach((eventName, index)=>{
15396
- if ("string" != typeof eventName || 0 === eventName.length) pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
16031
+ if ("string" != typeof eventName || 0 === eventName.length) formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events[${index}]`, "must be a non-empty string");
15397
16032
  });
15398
- else pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
16033
+ else formTemplateValidation_pushIssue(issues, `${path}.binding_writeback_events`, "must be an array");
15399
16034
  if ("enum" === value.type) {
15400
- 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");
16035
+ 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");
15401
16036
  }
15402
16037
  }
15403
16038
  function validateEventDefinition(value, path, issues) {
15404
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
16039
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
15405
16040
  validateIdentifier(value.name, `${path}.name`, issues);
15406
- if (void 0 !== value.description && "string" != typeof value.description) pushIssue(issues, `${path}.description`, "must be a string");
15407
- if (void 0 !== value.default_event && "boolean" != typeof value.default_event) pushIssue(issues, `${path}.default_event`, "must be a boolean");
16041
+ if (void 0 !== value.description && "string" != typeof value.description) formTemplateValidation_pushIssue(issues, `${path}.description`, "must be a string");
16042
+ if (void 0 !== value.default_event && "boolean" != typeof value.default_event) formTemplateValidation_pushIssue(issues, `${path}.default_event`, "must be a boolean");
15408
16043
  if (void 0 !== value.parameters) if (Array.isArray(value.parameters)) value.parameters.forEach((parameter, index)=>{
15409
- if (!isPlainObject(parameter)) return void pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
16044
+ if (!isPlainObject(parameter)) return void formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}]`, "must be an object");
15410
16045
  validateIdentifier(parameter.name, `${path}.parameters[${index}].name`, issues);
15411
- if (void 0 !== parameter.description && "string" != typeof parameter.description) pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
16046
+ if (void 0 !== parameter.description && "string" != typeof parameter.description) formTemplateValidation_pushIssue(issues, `${path}.parameters[${index}].description`, "must be a string");
15412
16047
  });
15413
- else pushIssue(issues, `${path}.parameters`, "must be an array");
16048
+ else formTemplateValidation_pushIssue(issues, `${path}.parameters`, "must be an array");
15414
16049
  }
15415
16050
  function validateSlotDefinition(value, path, issues, seenNames) {
15416
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15417
- if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) pushIssue(issues, `${path}.index`, "must be a non-negative integer");
16051
+ if (!isPlainObject(value)) return void formTemplateValidation_pushIssue(issues, path, "must be an object");
16052
+ if ("number" != typeof value.index || !Number.isInteger(value.index) || value.index < 0) formTemplateValidation_pushIssue(issues, `${path}.index`, "must be a non-negative integer");
15418
16053
  validatePropertiesObject(value.set_layout_properties, `${path}.set_layout_properties`, issues);
15419
16054
  if (isPlainObject(value.target)) {
15420
- if ("container" !== value.target.type && "slot" !== value.target.type) pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
15421
- validateIdentifier(value.target.name, `${path}.target.name`, issues);
15422
- } else pushIssue(issues, `${path}.target`, "must be an object");
16055
+ if ("container" !== value.target.type && "slot" !== value.target.type) formTemplateValidation_pushIssue(issues, `${path}.target.type`, "must be 'container' or 'slot'");
16056
+ if ("container" !== value.target.type || "" !== value.target.name) validateIdentifier(value.target.name, `${path}.target.name`, issues);
16057
+ } else formTemplateValidation_pushIssue(issues, `${path}.target`, "must be an object");
15423
16058
  if (void 0 !== value.template) validateComponentNode(value.template, `${path}.template`, issues, seenNames);
15424
16059
  }
16060
+ function prefixedPath(pathPrefix, path) {
16061
+ return pathPrefix.length > 0 ? `${pathPrefix}.${path}` : path;
16062
+ }
16063
+ function validateFormTemplateMetadata(value, options = {}) {
16064
+ const issues = [];
16065
+ if (!isPlainObject(value)) {
16066
+ formTemplateValidation_pushIssue(issues, options.pathPrefix ?? "", "must be a YAML object");
16067
+ return issues;
16068
+ }
16069
+ const pathPrefix = options.pathPrefix ?? "";
16070
+ const path = (suffix)=>prefixedPath(pathPrefix, suffix);
16071
+ if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component"), "must be a boolean");
16072
+ if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "must be a boolean");
16073
+ if (true === value.custom_component_container && true !== value.custom_component) formTemplateValidation_pushIssue(issues, path("custom_component_container"), "requires custom_component: true");
16074
+ const properties = value.properties;
16075
+ if (void 0 !== properties) if (Array.isArray(properties)) {
16076
+ const seenNames = new Set();
16077
+ let defaultBindingPropCount = 0;
16078
+ properties.forEach((property, index)=>{
16079
+ validatePropertyDefinition(property, path(`properties[${index}]`), issues);
16080
+ if (!isPlainObject(property) || "string" != typeof property.name) return;
16081
+ if (seenNames.has(property.name)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].name`), "must be unique");
16082
+ seenNames.add(property.name);
16083
+ if (property.default_binding_prop) defaultBindingPropCount++;
16084
+ });
16085
+ if (defaultBindingPropCount > 1) formTemplateValidation_pushIssue(issues, path("properties"), "can only define one default_binding_prop");
16086
+ } else formTemplateValidation_pushIssue(issues, path("properties"), "must be an array");
16087
+ const events = value.events;
16088
+ const eventNames = new Set();
16089
+ if (void 0 !== events) if (Array.isArray(events)) {
16090
+ let defaultEventCount = 0;
16091
+ events.forEach((event, index)=>{
16092
+ validateEventDefinition(event, path(`events[${index}]`), issues);
16093
+ if (!isPlainObject(event) || "string" != typeof event.name) return;
16094
+ if (eventNames.has(event.name)) formTemplateValidation_pushIssue(issues, path(`events[${index}].name`), "must be unique");
16095
+ eventNames.add(event.name);
16096
+ if (event.default_event) defaultEventCount++;
16097
+ });
16098
+ if (defaultEventCount > 1) formTemplateValidation_pushIssue(issues, path("events"), "can only define one default_event");
16099
+ } else formTemplateValidation_pushIssue(issues, path("events"), "must be an array");
16100
+ if (Array.isArray(properties)) properties.forEach((property, index)=>{
16101
+ if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
16102
+ property.binding_writeback_events.forEach((eventName, eventIndex)=>{
16103
+ if ("string" == typeof eventName && !eventNames.has(eventName)) formTemplateValidation_pushIssue(issues, path(`properties[${index}].binding_writeback_events[${eventIndex}]`), "must reference a defined event");
16104
+ });
16105
+ });
16106
+ if (void 0 !== value.slots) {
16107
+ const slotsPath = path("slots");
16108
+ if (isPlainObject(value.slots)) if ("empty-marker" === options.slotsPolicy) {
16109
+ if (Object.keys(value.slots).length > 0) formTemplateValidation_pushIssue(issues, slotsPath, "must be empty when defined in frontmatter");
16110
+ } else {
16111
+ const componentNames = options.componentNames ?? new Set();
16112
+ for (const [slotName, slotDef] of Object.entries(value.slots)){
16113
+ validateIdentifier(slotName, path(`slots.${slotName}.name`), issues);
16114
+ validateSlotDefinition(slotDef, path(`slots.${slotName}`), issues, componentNames);
16115
+ }
16116
+ }
16117
+ else formTemplateValidation_pushIssue(issues, slotsPath, "must be an object");
16118
+ }
16119
+ if (void 0 !== value.toolbox_item && !isPlainObject(value.toolbox_item)) formTemplateValidation_pushIssue(issues, path("toolbox_item"), "must be an object");
16120
+ if (void 0 !== value.layout_metadata && !isPlainObject(value.layout_metadata)) formTemplateValidation_pushIssue(issues, path("layout_metadata"), "must be an object");
16121
+ if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
16122
+ if ("number" != typeof value.item_type.table_id) formTemplateValidation_pushIssue(issues, path("item_type.table_id"), "must be a number");
16123
+ } else formTemplateValidation_pushIssue(issues, path("item_type"), "must be an object");
16124
+ return issues;
16125
+ }
15425
16126
  function validateFormTemplateData(value) {
15426
16127
  const issues = [];
15427
16128
  const componentNames = new Set();
15428
16129
  if (!isPlainObject(value)) {
15429
- pushIssue(issues, "", "must be a YAML object");
16130
+ formTemplateValidation_pushIssue(issues, "", "must be a YAML object");
15430
16131
  return issues;
15431
16132
  }
15432
16133
  const hasContainer = void 0 !== value.container;
15433
16134
  const hasLayout = void 0 !== value.layout;
15434
16135
  const hasComponents = void 0 !== value.components;
15435
16136
  const hasComponentsBySlot = void 0 !== value.components_by_slot;
15436
- if (hasContainer && hasLayout) pushIssue(issues, "", "cannot define both container and layout");
15437
- if (!hasContainer && !hasLayout) pushIssue(issues, "", "must define either container or layout");
15438
- if (void 0 !== value.is_package && "boolean" != typeof value.is_package) pushIssue(issues, "is_package", "must be a boolean");
16137
+ if (hasContainer && hasLayout) formTemplateValidation_pushIssue(issues, "", "cannot define both container and layout");
16138
+ if (!hasContainer && !hasLayout) formTemplateValidation_pushIssue(issues, "", "must define either container or layout");
16139
+ if (void 0 !== value.is_package && "boolean" != typeof value.is_package) formTemplateValidation_pushIssue(issues, "is_package", "must be a boolean");
15439
16140
  if (hasContainer) if (isPlainObject(value.container)) {
15440
16141
  validateComponentType(value.container.type, "container.type", issues);
15441
16142
  validatePropertiesObject(value.container.properties, "container.properties", issues);
15442
16143
  validatePropertiesObject(value.container.layout_properties, "container.layout_properties", issues);
15443
16144
  validateEventBindingsObject(value.container.event_bindings, "container.event_bindings", issues);
15444
16145
  validateDataBindings(value.container.data_bindings, "container.data_bindings", issues);
15445
- } else pushIssue(issues, "container", "must be an object");
16146
+ } else formTemplateValidation_pushIssue(issues, "container", "must be an object");
15446
16147
  if (hasLayout) if (isPlainObject(value.layout)) {
15447
16148
  validateComponentType(value.layout.type, "layout.type", issues);
15448
16149
  validatePropertiesObject(value.layout.properties, "layout.properties", issues);
15449
16150
  validateEventBindingsObject(value.layout.event_bindings, "layout.event_bindings", issues);
15450
16151
  validateEventBindingsObject(value.layout.form_event_bindings, "layout.form_event_bindings", issues);
15451
16152
  validateDataBindings(value.layout.data_bindings, "layout.data_bindings", issues);
15452
- } else pushIssue(issues, "layout", "must be an object");
15453
- if (hasComponents && hasLayout) pushIssue(issues, "components", "can only be used with container-based forms");
15454
- else if (hasComponents && !hasContainer) pushIssue(issues, "components", "can only be used with container-based forms");
16153
+ } else formTemplateValidation_pushIssue(issues, "layout", "must be an object");
16154
+ if (hasComponents && hasLayout) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
16155
+ else if (hasComponents && !hasContainer) formTemplateValidation_pushIssue(issues, "components", "can only be used with container-based forms");
15455
16156
  else if (hasComponents) if (Array.isArray(value.components)) validateComponentArray(value.components, "components", issues, componentNames);
15456
- else pushIssue(issues, "components", "must be an array");
15457
- if (hasComponentsBySlot && hasContainer) pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
15458
- else if (hasComponentsBySlot && !hasLayout) pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
16157
+ else formTemplateValidation_pushIssue(issues, "components", "must be an array");
16158
+ if (hasComponentsBySlot && hasContainer) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
16159
+ else if (hasComponentsBySlot && !hasLayout) formTemplateValidation_pushIssue(issues, "components_by_slot", "can only be used with layout-based forms");
15459
16160
  else if (hasComponentsBySlot) if (isPlainObject(value.components_by_slot)) for (const [slotName, slotComponents] of Object.entries(value.components_by_slot)){
15460
16161
  if (!Array.isArray(slotComponents)) {
15461
- pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
16162
+ formTemplateValidation_pushIssue(issues, `components_by_slot.${slotName}`, "must be an array");
15462
16163
  continue;
15463
16164
  }
15464
16165
  validateComponentArray(slotComponents, `components_by_slot.${slotName}`, issues, componentNames);
15465
16166
  }
15466
- else pushIssue(issues, "components_by_slot", "must be an object");
15467
- if (void 0 !== value.custom_component && "boolean" != typeof value.custom_component) pushIssue(issues, "custom_component", "must be a boolean");
15468
- if (void 0 !== value.custom_component_container && "boolean" != typeof value.custom_component_container) pushIssue(issues, "custom_component_container", "must be a boolean");
15469
- if (true === value.custom_component_container && true !== value.custom_component) pushIssue(issues, "custom_component_container", "requires custom_component: true");
15470
- const properties = value.properties;
15471
- if (void 0 !== properties) if (Array.isArray(properties)) {
15472
- const seenNames = new Set();
15473
- let defaultBindingPropCount = 0;
15474
- properties.forEach((property, index)=>{
15475
- validatePropertyDefinition(property, `properties[${index}]`, issues);
15476
- if (!isPlainObject(property) || "string" != typeof property.name) return;
15477
- if (seenNames.has(property.name)) pushIssue(issues, `properties[${index}].name`, "must be unique");
15478
- seenNames.add(property.name);
15479
- if (property.default_binding_prop) defaultBindingPropCount++;
15480
- });
15481
- if (defaultBindingPropCount > 1) pushIssue(issues, "properties", "can only define one default_binding_prop");
15482
- } else pushIssue(issues, "properties", "must be an array");
15483
- const events = value.events;
15484
- const eventNames = new Set();
15485
- if (void 0 !== events) if (Array.isArray(events)) {
15486
- let defaultEventCount = 0;
15487
- events.forEach((event, index)=>{
15488
- validateEventDefinition(event, `events[${index}]`, issues);
15489
- if (!isPlainObject(event) || "string" != typeof event.name) return;
15490
- if (eventNames.has(event.name)) pushIssue(issues, `events[${index}].name`, "must be unique");
15491
- eventNames.add(event.name);
15492
- if (event.default_event) defaultEventCount++;
15493
- });
15494
- if (defaultEventCount > 1) pushIssue(issues, "events", "can only define one default_event");
15495
- } else pushIssue(issues, "events", "must be an array");
15496
- if (Array.isArray(properties)) properties.forEach((property, index)=>{
15497
- if (!isPlainObject(property) || !Array.isArray(property.binding_writeback_events)) return;
15498
- property.binding_writeback_events.forEach((eventName, eventIndex)=>{
15499
- if ("string" == typeof eventName && !eventNames.has(eventName)) pushIssue(issues, `properties[${index}].binding_writeback_events[${eventIndex}]`, "must reference a defined event");
16167
+ else formTemplateValidation_pushIssue(issues, "components_by_slot", "must be an object");
16168
+ issues.push(...validateFormTemplateMetadata(value, {
16169
+ componentNames
16170
+ }));
16171
+ return issues;
16172
+ }
16173
+ const KNOWN_RUNTIME_SERVICE_SOURCES = new Set([
16174
+ "/runtime/services/tables.yml",
16175
+ "/runtime/services/google.yml",
16176
+ "/runtime/services/uplink.yml",
16177
+ "/runtime/services/stripe.yml",
16178
+ "/runtime/services/segment.yml",
16179
+ "/runtime/services/facebook.yml",
16180
+ "/runtime/services/anvil/users.yml",
16181
+ "/runtime/services/anvil/secrets.yml",
16182
+ "/runtime/services/anvil/saml.yml",
16183
+ "/runtime/services/anvil/microsoft.yml",
16184
+ "/runtime/services/anvil/email.yml",
16185
+ "/runtime/services/anvil/files.yml"
16186
+ ]);
16187
+ const FRONTMATTER_ALLOWED_KEYS = new Set([
16188
+ "custom_component",
16189
+ "custom_component_container",
16190
+ "properties",
16191
+ "events",
16192
+ "toolbox_item",
16193
+ "layout_metadata",
16194
+ "item_type",
16195
+ "slots"
16196
+ ]);
16197
+ const FRONTMATTER_DISALLOWED_KEYS = new Set([
16198
+ "container",
16199
+ "components",
16200
+ "layout",
16201
+ "components_by_slot",
16202
+ "code",
16203
+ "class_name",
16204
+ "is_package",
16205
+ "save_as_html",
16206
+ "serialized_html"
16207
+ ]);
16208
+ function validateFormTemplateHtml_isPlainObject(value) {
16209
+ return "object" == typeof value && null !== value && !Array.isArray(value);
16210
+ }
16211
+ function validateFormTemplateHtml_pushIssue(issues, issuePath, message) {
16212
+ issues.push({
16213
+ path: issuePath,
16214
+ message
16215
+ });
16216
+ }
16217
+ function splitHtmlFormTemplateFrontmatter(htmlContent) {
16218
+ const match = htmlContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
16219
+ if (!match) return {
16220
+ frontmatter: null,
16221
+ body: htmlContent
16222
+ };
16223
+ return {
16224
+ frontmatter: match[1],
16225
+ body: htmlContent.slice(match[0].length)
16226
+ };
16227
+ }
16228
+ function validateHtmlFrontmatter(frontmatterYaml) {
16229
+ const issues = [];
16230
+ if (0 === frontmatterYaml.trim().length) {
16231
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must not be empty; omit frontmatter entirely when unused");
16232
+ return issues;
16233
+ }
16234
+ let data;
16235
+ try {
16236
+ data = external_js_yaml_namespaceObject.load(frontmatterYaml);
16237
+ } catch (error) {
16238
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", error instanceof Error ? error.message : String(error));
16239
+ return issues;
16240
+ }
16241
+ if (!validateFormTemplateHtml_isPlainObject(data)) {
16242
+ validateFormTemplateHtml_pushIssue(issues, "frontmatter", "must be a YAML object");
16243
+ return issues;
16244
+ }
16245
+ 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");
16246
+ else if (!FRONTMATTER_ALLOWED_KEYS.has(key)) validateFormTemplateHtml_pushIssue(issues, `frontmatter.${key}`, "is not an allowed frontmatter field");
16247
+ issues.push(...validateFormTemplateMetadata(data, {
16248
+ pathPrefix: "frontmatter",
16249
+ slotsPolicy: "empty-marker"
16250
+ }));
16251
+ return issues;
16252
+ }
16253
+ function validateParsedHtmlTemplate(parsed) {
16254
+ return validateFormTemplateData(parsed);
16255
+ }
16256
+ function validateFormTemplateHtml(htmlContent) {
16257
+ const { frontmatter, body } = splitHtmlFormTemplateFrontmatter(htmlContent);
16258
+ if (null !== frontmatter) {
16259
+ const frontmatterIssues = validateHtmlFrontmatter(frontmatter);
16260
+ if (frontmatterIssues.length > 0) return {
16261
+ ok: false,
16262
+ message: "form_template.html is invalid for Anvil",
16263
+ issues: frontmatterIssues
16264
+ };
16265
+ }
16266
+ let parsed;
16267
+ try {
16268
+ parsed = (0, form_template_parser_namespaceObject.parseSerializedHtml)(body);
16269
+ } catch (error) {
16270
+ return {
16271
+ ok: false,
16272
+ message: "form_template.html could not be parsed",
16273
+ issues: [
16274
+ {
16275
+ path: "",
16276
+ message: error instanceof Error ? error.message : String(error)
16277
+ }
16278
+ ]
16279
+ };
16280
+ }
16281
+ const issues = validateParsedHtmlTemplate(parsed);
16282
+ if (issues.length > 0) return {
16283
+ ok: false,
16284
+ message: "form_template.html is invalid for Anvil",
16285
+ issues
16286
+ };
16287
+ return {
16288
+ ok: true
16289
+ };
16290
+ }
16291
+ const PYTHON_VALIDATOR_SCRIPT = String.raw`
16292
+ import ast
16293
+ import json
16294
+ import sys
16295
+
16296
+
16297
+ def issue_path(error):
16298
+ if getattr(error, "lineno", None) is None:
16299
+ return "root"
16300
+ if getattr(error, "offset", None) is None:
16301
+ return f"line {error.lineno}"
16302
+ return f"line {error.lineno}, column {error.offset}"
16303
+
16304
+
16305
+ def base_name(node):
16306
+ if isinstance(node, ast.Name):
16307
+ return node.id
16308
+ if isinstance(node, ast.Attribute):
16309
+ return node.attr
16310
+ return None
16311
+
16312
+
16313
+ def imports_template_from_anvil_designer(node, template_name):
16314
+ if node.level < 1:
16315
+ return False
16316
+ if node.module not in ("_anvil_designer", None):
16317
+ return False
16318
+ return any(alias.name == template_name for alias in node.names)
16319
+
16320
+
16321
+ file_path = sys.argv[1]
16322
+ class_name = sys.argv[2]
16323
+ template_name = f"{class_name}Template" if class_name else ""
16324
+ source = sys.stdin.read()
16325
+ issues = []
16326
+
16327
+ try:
16328
+ compile(source, file_path, "exec")
16329
+ except SyntaxError as error:
16330
+ issues.append({"path": issue_path(error), "message": error.msg})
16331
+ print(json.dumps({"issues": issues}))
16332
+ sys.exit(0)
16333
+
16334
+ if class_name:
16335
+ tree = ast.parse(source, filename=file_path)
16336
+ has_template_import = False
16337
+ has_form_class = False
16338
+
16339
+ for node in ast.walk(tree):
16340
+ if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
16341
+ has_template_import = True
16342
+ elif isinstance(node, ast.ClassDef) and node.name == class_name:
16343
+ if any(base_name(base) == template_name for base in node.bases):
16344
+ has_form_class = True
16345
+
16346
+ if not has_template_import:
16347
+ issues.append({
16348
+ "path": "root",
16349
+ "message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
16350
+ })
16351
+ if not has_form_class:
16352
+ issues.append({
16353
+ "path": "root",
16354
+ "message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
16355
+ })
16356
+
16357
+ print(json.dumps({"issues": issues}))
16358
+ `;
16359
+ function findPythonInterpreter(env = process.env, probe = external_child_process_namespaceObject.spawnSync) {
16360
+ const candidates = [
16361
+ ..."string" == typeof env.ANVIL_PYTHON && env.ANVIL_PYTHON.length > 0 ? [
16362
+ {
16363
+ command: env.ANVIL_PYTHON,
16364
+ args: []
16365
+ }
16366
+ ] : [],
16367
+ {
16368
+ command: "uv",
16369
+ args: [
16370
+ "run",
16371
+ "--isolated",
16372
+ "--no-python-downloads",
16373
+ "python"
16374
+ ]
16375
+ },
16376
+ {
16377
+ command: "python3",
16378
+ args: []
16379
+ },
16380
+ {
16381
+ command: "python",
16382
+ args: []
16383
+ }
16384
+ ];
16385
+ for (const candidate of candidates){
16386
+ const result = probe(candidate.command, [
16387
+ ...candidate.args,
16388
+ "--version"
16389
+ ], {
16390
+ encoding: "utf8"
15500
16391
  });
16392
+ if (!result.error && 0 === result.status) return candidate;
16393
+ }
16394
+ return null;
16395
+ }
16396
+ function isClientFormPython(filePath) {
16397
+ if ("__init__.py" === external_path_default().basename(filePath)) {
16398
+ const formDir = external_path_default().dirname(filePath);
16399
+ return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
16400
+ }
16401
+ const parsed = external_path_default().parse(filePath);
16402
+ 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`));
16403
+ }
16404
+ function expectedFormClassName(filePath) {
16405
+ if (!isClientFormPython(filePath)) return null;
16406
+ if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
16407
+ return external_path_default().basename(filePath, ".py");
16408
+ }
16409
+ function isUnderDirectory(filePath, directoryPath) {
16410
+ const relativePath = external_path_default().relative(directoryPath, filePath);
16411
+ return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
16412
+ }
16413
+ function validatePython(filePath, fileContent, appRoot) {
16414
+ const interpreter = findPythonInterpreter();
16415
+ if (!interpreter) return {
16416
+ ok: false,
16417
+ message: "Python file could not be validated",
16418
+ issues: [
16419
+ {
16420
+ path: "root",
16421
+ message: "Python validation requires uv, python3, or python on PATH, or ANVIL_PYTHON to be set"
16422
+ }
16423
+ ]
16424
+ };
16425
+ const absolutePath = external_path_default().resolve(filePath);
16426
+ const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
16427
+ const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
16428
+ const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter.command, [
16429
+ ...interpreter.args,
16430
+ "-c",
16431
+ PYTHON_VALIDATOR_SCRIPT,
16432
+ filePath,
16433
+ formClassName ?? ""
16434
+ ], {
16435
+ encoding: "utf8",
16436
+ input: fileContent,
16437
+ maxBuffer: 1048576
16438
+ });
16439
+ if (result.error || 0 !== result.status) return {
16440
+ ok: false,
16441
+ message: "Python file could not be validated",
16442
+ issues: [
16443
+ {
16444
+ path: "root",
16445
+ message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
16446
+ }
16447
+ ]
16448
+ };
16449
+ let parsed;
16450
+ try {
16451
+ parsed = JSON.parse(result.stdout);
16452
+ } catch (error) {
16453
+ return {
16454
+ ok: false,
16455
+ message: "Python file could not be validated",
16456
+ issues: [
16457
+ {
16458
+ path: "root",
16459
+ message: error instanceof Error ? error.message : String(error)
16460
+ }
16461
+ ]
16462
+ };
16463
+ }
16464
+ const issues = parsed.issues ?? [];
16465
+ if (issues.length > 0) return {
16466
+ ok: false,
16467
+ message: "Python file is invalid for Anvil",
16468
+ issues
16469
+ };
16470
+ return {
16471
+ ok: true
16472
+ };
16473
+ }
16474
+ const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16475
+ const DB_ACCESS_LEVELS = new Set([
16476
+ "none",
16477
+ "search",
16478
+ "full"
16479
+ ]);
16480
+ const DB_BASIC_COLUMN_TYPES = new Set([
16481
+ "string",
16482
+ "number",
16483
+ "bool",
16484
+ "date",
16485
+ "datetime",
16486
+ "media",
16487
+ "simpleObject"
16488
+ ]);
16489
+ const DB_LINK_COLUMN_TYPES = new Set([
16490
+ "link_single",
16491
+ "link_multiple"
16492
+ ]);
16493
+ const DB_TABLE_INDEX_TYPES = new Set([
16494
+ "b_tree",
16495
+ "trigram",
16496
+ "full_text"
16497
+ ]);
16498
+ const SCHEDULE_EVERY_VALUES = new Set([
16499
+ "minute",
16500
+ "hour",
16501
+ "day",
16502
+ "week",
16503
+ "month"
16504
+ ]);
16505
+ const CLIENT_VERSIONS = new Set([
16506
+ "2",
16507
+ "3"
16508
+ ]);
16509
+ const validators_PYTHON_KEYWORDS = new Set([
16510
+ "False",
16511
+ "None",
16512
+ "True",
16513
+ "and",
16514
+ "as",
16515
+ "assert",
16516
+ "async",
16517
+ "await",
16518
+ "break",
16519
+ "class",
16520
+ "continue",
16521
+ "def",
16522
+ "del",
16523
+ "elif",
16524
+ "else",
16525
+ "except",
16526
+ "finally",
16527
+ "for",
16528
+ "from",
16529
+ "global",
16530
+ "if",
16531
+ "import",
16532
+ "in",
16533
+ "is",
16534
+ "lambda",
16535
+ "nonlocal",
16536
+ "not",
16537
+ "or",
16538
+ "pass",
16539
+ "raise",
16540
+ "return",
16541
+ "try",
16542
+ "while",
16543
+ "with",
16544
+ "yield",
16545
+ "match",
16546
+ "case"
16547
+ ]);
16548
+ function validators_isPlainObject(value) {
16549
+ return "object" == typeof value && null !== value && !Array.isArray(value);
16550
+ }
16551
+ function validators_pushIssue(issues, path, message) {
16552
+ issues.push({
16553
+ path,
16554
+ message
15501
16555
  });
15502
- if (void 0 !== value.slots) if (isPlainObject(value.slots)) for (const [slotName, slotDef] of Object.entries(value.slots)){
15503
- validateIdentifier(slotName, `slots.${slotName}.name`, issues);
15504
- validateSlotDefinition(slotDef, `slots.${slotName}`, issues, componentNames);
15505
- }
15506
- else pushIssue(issues, "slots", "must be an object");
15507
- if (void 0 !== value.item_type) if (isPlainObject(value.item_type)) {
15508
- if ("number" != typeof value.item_type.table_id) pushIssue(issues, "item_type.table_id", "must be a number");
15509
- } else pushIssue(issues, "item_type", "must be an object");
15510
- return issues;
16556
+ }
16557
+ function formatValidationPath(path) {
16558
+ return path || "root";
16559
+ }
16560
+ function validators_validateIdentifier(value, path, issues) {
16561
+ if ("string" != typeof value || 0 === value.length) return void validators_pushIssue(issues, path, "must be a non-empty string");
16562
+ if (!validators_IDENTIFIER_RE.test(value)) return void validators_pushIssue(issues, path, "must be a valid identifier");
16563
+ if (validators_PYTHON_KEYWORDS.has(value)) validators_pushIssue(issues, path, "must be a valid Python identifier (not a keyword)");
15511
16564
  }
15512
16565
  function validateFormTemplate(yamlContent) {
15513
16566
  try {
@@ -15534,7 +16587,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15534
16587
  };
15535
16588
  }
15536
16589
  }
15537
- const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>isPlainObject(input), "must be an object");
16590
+ const serviceConfigObject = external_valibot_namespaceObject.custom((input)=>validators_isPlainObject(input), "must be an object");
15538
16591
  const AnvilServiceEntrySchema = external_valibot_namespaceObject.looseObject({
15539
16592
  source: external_valibot_namespaceObject.pipe(external_valibot_namespaceObject.string(), external_valibot_namespaceObject.nonEmpty()),
15540
16593
  client_config: external_valibot_namespaceObject.optional(serviceConfigObject),
@@ -15562,14 +16615,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15562
16615
  function validateRuntimeOptions(value, path) {
15563
16616
  const issues = [];
15564
16617
  if (void 0 === value) return issues;
15565
- if (!isPlainObject(value)) return issues;
16618
+ if (!validators_isPlainObject(value)) return issues;
15566
16619
  if (void 0 !== value.client_version) {
15567
- if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
16620
+ if ("string" != typeof value.client_version || !CLIENT_VERSIONS.has(value.client_version)) validators_pushIssue(issues, `${path}.client_version`, 'must be "2" or "3"');
15568
16621
  }
15569
16622
  if (void 0 !== value.server_spec && null !== value.server_spec) {
15570
- if (!isPlainObject(value.server_spec)) return issues;
15571
- 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");
15572
- if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) pushIssue(issues, `${path}.server_spec.base`, "must be a string");
16623
+ if (!validators_isPlainObject(value.server_spec)) return issues;
16624
+ 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");
16625
+ if (void 0 !== value.server_spec.base && "string" != typeof value.server_spec.base) validators_pushIssue(issues, `${path}.server_spec.base`, "must be a string");
15573
16626
  }
15574
16627
  return issues;
15575
16628
  }
@@ -15577,27 +16630,27 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15577
16630
  const issues = [];
15578
16631
  if (void 0 === value) return issues;
15579
16632
  if (null === value) {
15580
- pushIssue(issues, path, "must be an object, an empty array, or omitted");
16633
+ validators_pushIssue(issues, path, "must be an object, an empty array, or omitted");
15581
16634
  return issues;
15582
16635
  }
15583
16636
  if (Array.isArray(value)) {
15584
16637
  if (0 === value.length) return issues;
15585
- pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
16638
+ validators_pushIssue(issues, path, "must be an object mapping table names to schemas, or an empty array");
15586
16639
  return issues;
15587
16640
  }
15588
- if (!isPlainObject(value)) {
15589
- pushIssue(issues, path, "must be an object mapping table names to schemas");
16641
+ if (!validators_isPlainObject(value)) {
16642
+ validators_pushIssue(issues, path, "must be an object mapping table names to schemas");
15590
16643
  return issues;
15591
16644
  }
15592
16645
  for (const tableName of Object.keys(value)){
15593
16646
  const tablePath = `${path}.${tableName}`;
15594
- if (!IDENTIFIER_RE.test(tableName)) {
15595
- pushIssue(issues, tablePath, "must be a valid Python identifier");
16647
+ if (!validators_IDENTIFIER_RE.test(tableName)) {
16648
+ validators_pushIssue(issues, tablePath, "must be a valid Python identifier");
15596
16649
  continue;
15597
16650
  }
15598
16651
  const table = value[tableName];
15599
- if (!isPlainObject(table)) {
15600
- pushIssue(issues, tablePath, "must be an object");
16652
+ if (!validators_isPlainObject(table)) {
16653
+ validators_pushIssue(issues, tablePath, "must be an object");
15601
16654
  continue;
15602
16655
  }
15603
16656
  for (const req of [
@@ -15605,61 +16658,61 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15605
16658
  "server",
15606
16659
  "title",
15607
16660
  "columns"
15608
- ])if (void 0 === table[req]) pushIssue(issues, `${tablePath}.${req}`, "is required");
16661
+ ])if (void 0 === table[req]) validators_pushIssue(issues, `${tablePath}.${req}`, "is required");
15609
16662
  if (void 0 !== table.client) {
15610
- if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
16663
+ if ("string" != typeof table.client || !DB_ACCESS_LEVELS.has(table.client)) validators_pushIssue(issues, `${tablePath}.client`, 'must be "none", "search", or "full"');
15611
16664
  }
15612
16665
  if (void 0 !== table.server) {
15613
- if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
16666
+ if ("string" != typeof table.server || !DB_ACCESS_LEVELS.has(table.server)) validators_pushIssue(issues, `${tablePath}.server`, 'must be "none", "search", or "full"');
15614
16667
  }
15615
16668
  if (void 0 !== table.title) {
15616
- if ("string" != typeof table.title || 0 === table.title.length) pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
16669
+ if ("string" != typeof table.title || 0 === table.title.length) validators_pushIssue(issues, `${tablePath}.title`, "must be a non-empty string");
15617
16670
  }
15618
16671
  if (void 0 !== table.columns) if (Array.isArray(table.columns)) table.columns.forEach((column, index)=>{
15619
16672
  validateDbSchemaColumn(column, `${tablePath}.columns[${index}]`, issues);
15620
16673
  });
15621
- else pushIssue(issues, `${tablePath}.columns`, "must be an array");
16674
+ else validators_pushIssue(issues, `${tablePath}.columns`, "must be an array");
15622
16675
  if (void 0 !== table.indexes) if (Array.isArray(table.indexes)) table.indexes.forEach((indexEntry, index)=>{
15623
16676
  validateDbTableIndex(indexEntry, `${tablePath}.indexes[${index}]`, issues);
15624
16677
  });
15625
- else pushIssue(issues, `${tablePath}.indexes`, "must be an array");
16678
+ else validators_pushIssue(issues, `${tablePath}.indexes`, "must be an array");
15626
16679
  }
15627
16680
  return issues;
15628
16681
  }
15629
16682
  function validateDbTableIndex(value, path, issues) {
15630
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15631
- if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) pushIssue(issues, `${path}.columns`, "must be an array of strings");
15632
- if (void 0 === value.type) pushIssue(issues, `${path}.type`, "is required");
15633
- 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"');
16683
+ if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
16684
+ if (!Array.isArray(value.columns) || value.columns.some((c)=>"string" != typeof c)) validators_pushIssue(issues, `${path}.columns`, "must be an array of strings");
16685
+ if (void 0 === value.type) validators_pushIssue(issues, `${path}.type`, "is required");
16686
+ 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"');
15634
16687
  }
15635
16688
  function validateDbSchemaColumn(value, path, issues) {
15636
- if (!isPlainObject(value)) return void pushIssue(issues, path, "must be an object");
15637
- if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) pushIssue(issues, `${path}.name`, "must be a non-empty string");
15638
- else if (!IDENTIFIER_RE.test(value.name)) pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
15639
- if (void 0 === value.type) return void pushIssue(issues, `${path}.type`, "is required");
15640
- if ("string" != typeof value.type) return void pushIssue(issues, `${path}.type`, "must be a string");
16689
+ if (!validators_isPlainObject(value)) return void validators_pushIssue(issues, path, "must be an object");
16690
+ if (void 0 === value.name || "string" != typeof value.name || 0 === value.name.length) validators_pushIssue(issues, `${path}.name`, "must be a non-empty string");
16691
+ else if (!validators_IDENTIFIER_RE.test(value.name)) validators_pushIssue(issues, `${path}.name`, "must be a valid Python identifier");
16692
+ if (void 0 === value.type) return void validators_pushIssue(issues, `${path}.type`, "is required");
16693
+ if ("string" != typeof value.type) return void validators_pushIssue(issues, `${path}.type`, "must be a string");
15641
16694
  const colType = value.type;
15642
16695
  if (DB_LINK_COLUMN_TYPES.has(colType)) {
15643
- 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)");
15644
- else if (!IDENTIFIER_RE.test(value.target)) pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
15645
- } else if (!DB_BASIC_COLUMN_TYPES.has(colType)) pushIssue(issues, `${path}.type`, `must be a basic column type (${[
16696
+ 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)");
16697
+ else if (!validators_IDENTIFIER_RE.test(value.target)) validators_pushIssue(issues, `${path}.target`, "must be a valid Python identifier");
16698
+ } else if (!DB_BASIC_COLUMN_TYPES.has(colType)) validators_pushIssue(issues, `${path}.type`, `must be a basic column type (${[
15646
16699
  ...DB_BASIC_COLUMN_TYPES
15647
16700
  ].join(", ")}) or ${[
15648
16701
  ...DB_LINK_COLUMN_TYPES
15649
16702
  ].join(", ")}`);
15650
- if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
15651
- if (void 0 !== value.admin_ui && null !== value.admin_ui) if (isPlainObject(value.admin_ui)) {
16703
+ if (void 0 !== value.client_hidden && "boolean" != typeof value.client_hidden) validators_pushIssue(issues, `${path}.client_hidden`, "must be a boolean");
16704
+ if (void 0 !== value.admin_ui && null !== value.admin_ui) if (validators_isPlainObject(value.admin_ui)) {
15652
16705
  const ui = value.admin_ui;
15653
- if (void 0 !== ui.width && "number" != typeof ui.width) pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
15654
- if (void 0 !== ui.order && "number" != typeof ui.order) pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
15655
- } else pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
16706
+ if (void 0 !== ui.width && "number" != typeof ui.width) validators_pushIssue(issues, `${path}.admin_ui.width`, "must be a number");
16707
+ if (void 0 !== ui.order && "number" != typeof ui.order) validators_pushIssue(issues, `${path}.admin_ui.order`, "must be a number");
16708
+ } else validators_pushIssue(issues, `${path}.admin_ui`, "must be an object or null");
15656
16709
  if (void 0 !== value.indexes) {
15657
- if (!Array.isArray(value.indexes)) return void pushIssue(issues, `${path}.indexes`, "must be an array");
16710
+ if (!Array.isArray(value.indexes)) return void validators_pushIssue(issues, `${path}.indexes`, "must be an array");
15658
16711
  value.indexes.forEach((entry, i)=>{
15659
16712
  const idxPath = `${path}.indexes[${i}]`;
15660
- if (!isPlainObject(entry)) return void pushIssue(issues, idxPath, "must be an object");
15661
- if (void 0 === entry.type) pushIssue(issues, `${idxPath}.type`, "is required");
15662
- 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"');
16713
+ if (!validators_isPlainObject(entry)) return void validators_pushIssue(issues, idxPath, "must be an object");
16714
+ if (void 0 === entry.type) validators_pushIssue(issues, `${idxPath}.type`, "is required");
16715
+ 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"');
15663
16716
  });
15664
16717
  }
15665
16718
  }
@@ -15668,8 +16721,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15668
16721
  if (void 0 === services) return issues;
15669
16722
  if (!Array.isArray(services)) return issues;
15670
16723
  services.forEach((service, index)=>{
15671
- if (!isPlainObject(service) || "string" != typeof service.source) return;
15672
- if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
16724
+ if (!validators_isPlainObject(service) || "string" != typeof service.source) return;
16725
+ if (!KNOWN_RUNTIME_SERVICE_SOURCES.has(service.source)) validators_pushIssue(issues, `${path}[${index}].source`, "must be a known runtime service source path");
15673
16726
  });
15674
16727
  return issues;
15675
16728
  }
@@ -15677,9 +16730,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15677
16730
  const issues = [];
15678
16731
  if (void 0 === services || !Array.isArray(services)) return issues;
15679
16732
  services.forEach((service, index)=>{
15680
- if (!isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
15681
- if (!isPlainObject(service.client_config)) return;
15682
- 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");
16733
+ if (!validators_isPlainObject(service) || "/runtime/services/anvil/users.yml" !== service.source) return;
16734
+ if (!validators_isPlainObject(service.client_config)) return;
16735
+ 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");
15683
16736
  });
15684
16737
  return issues;
15685
16738
  }
@@ -15687,25 +16740,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15687
16740
  const issues = [];
15688
16741
  if (void 0 === value) return issues;
15689
16742
  if (!Array.isArray(value)) {
15690
- pushIssue(issues, path, "must be an array");
16743
+ validators_pushIssue(issues, path, "must be an array");
15691
16744
  return issues;
15692
16745
  }
15693
16746
  value.forEach((task, index)=>{
15694
16747
  const taskPath = `${path}[${index}]`;
15695
- if (!isPlainObject(task)) return void pushIssue(issues, taskPath, "must be an object");
15696
- if ("string" != typeof task.job_id || 0 === task.job_id.length) pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
15697
- validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
15698
- if (!isPlainObject(task.time_spec)) return void pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
16748
+ if (!validators_isPlainObject(task)) return void validators_pushIssue(issues, taskPath, "must be an object");
16749
+ if ("string" != typeof task.job_id || 0 === task.job_id.length) validators_pushIssue(issues, `${taskPath}.job_id`, "must be a non-empty string");
16750
+ validators_validateIdentifier(task.task_name, `${taskPath}.task_name`, issues);
16751
+ if (!validators_isPlainObject(task.time_spec)) return void validators_pushIssue(issues, `${taskPath}.time_spec`, "must be an object");
15699
16752
  const timeSpec = task.time_spec;
15700
- 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"');
16753
+ 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"');
15701
16754
  const interval = timeSpec.n;
15702
- if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
15703
- if (void 0 !== timeSpec.at) if (isPlainObject(timeSpec.at)) {
16755
+ if (void 0 !== interval && ("number" != typeof interval || !Number.isInteger(interval) || interval <= 0)) validators_pushIssue(issues, `${taskPath}.time_spec.n`, "must be a positive integer");
16756
+ if (void 0 !== timeSpec.at) if (validators_isPlainObject(timeSpec.at)) {
15704
16757
  const at = timeSpec.at;
15705
- if (void 0 !== at.minute && !Number.isInteger(at.minute)) pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
15706
- if (void 0 !== at.hour && !Number.isInteger(at.hour)) pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
15707
- if (void 0 !== at.day && !Number.isInteger(at.day)) pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
15708
- } else pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
16758
+ if (void 0 !== at.minute && !Number.isInteger(at.minute)) validators_pushIssue(issues, `${taskPath}.time_spec.at.minute`, "must be an integer");
16759
+ if (void 0 !== at.hour && !Number.isInteger(at.hour)) validators_pushIssue(issues, `${taskPath}.time_spec.at.hour`, "must be an integer");
16760
+ if (void 0 !== at.day && !Number.isInteger(at.day)) validators_pushIssue(issues, `${taskPath}.time_spec.at.day`, "must be an integer");
16761
+ } else validators_pushIssue(issues, `${taskPath}.time_spec.at`, "must be an object");
15709
16762
  });
15710
16763
  return issues;
15711
16764
  }
@@ -15717,13 +16770,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15717
16770
  dependencies: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(external_valibot_namespaceObject.any())),
15718
16771
  runtime_options: RuntimeOptionsSchema,
15719
16772
  services: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.array(AnvilServiceEntrySchema)),
15720
- startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.looseObject({
16773
+ startup: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.looseObject({
15721
16774
  type: external_valibot_namespaceObject.picklist([
15722
16775
  "module",
15723
16776
  "form"
15724
16777
  ]),
15725
16778
  module: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.string())
15726
- })),
16779
+ }))),
15727
16780
  startup_form: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.nullable(external_valibot_namespaceObject.string())),
15728
16781
  db_schema: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
15729
16782
  scheduled_tasks: external_valibot_namespaceObject.optional(external_valibot_namespaceObject.any()),
@@ -15814,6 +16867,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15814
16867
  const startIndex = normalizedPath.lastIndexOf(clientCodePrefix);
15815
16868
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
15816
16869
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16870
+ if (relativeToClientCode.endsWith(".html")) return "form_template.html";
16871
+ if (relativeToClientCode.endsWith(".py")) return "python";
16872
+ }
16873
+ const serverCodePrefix = "server_code/";
16874
+ if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
16875
+ const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
16876
+ const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
16877
+ if (relativeToServerCode.endsWith(".py")) return "python";
15817
16878
  }
15818
16879
  return null;
15819
16880
  }
@@ -15833,7 +16894,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15833
16894
  currentDir = parentDir;
15834
16895
  }
15835
16896
  }
15836
- function validatePath(filePath, yamlContent) {
16897
+ function validatePath(filePath, fileContent) {
15837
16898
  const target = inferValidationTarget(filePath);
15838
16899
  const existingPath = resolveExistingPath(filePath);
15839
16900
  const appRoot = findAnvilAppRoot(filePath);
@@ -15860,7 +16921,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15860
16921
  }
15861
16922
  ]
15862
16923
  };
15863
- const result = validateAnvilYaml(yamlContent);
16924
+ const result = validateAnvilYaml(fileContent);
15864
16925
  return result.ok ? {
15865
16926
  ok: true,
15866
16927
  target
@@ -15886,7 +16947,62 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15886
16947
  ]
15887
16948
  };
15888
16949
  }
15889
- const result = validateFormTemplate(yamlContent);
16950
+ const result = validateFormTemplate(fileContent);
16951
+ return result.ok ? {
16952
+ ok: true,
16953
+ target
16954
+ } : {
16955
+ ...result,
16956
+ target
16957
+ };
16958
+ }
16959
+ if ("form_template.html" === target) {
16960
+ if (existingPath && appRoot) {
16961
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
16962
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
16963
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
16964
+ if (!isInsideClientCode || !existingPath.endsWith(".html")) return {
16965
+ ok: false,
16966
+ target,
16967
+ message: "File is not a client_code HTML form",
16968
+ issues: [
16969
+ {
16970
+ path: "",
16971
+ message: `expected a .html file under ${clientCodeRoot}`
16972
+ }
16973
+ ]
16974
+ };
16975
+ }
16976
+ const result = validateFormTemplateHtml(fileContent);
16977
+ return result.ok ? {
16978
+ ok: true,
16979
+ target
16980
+ } : {
16981
+ ...result,
16982
+ target
16983
+ };
16984
+ }
16985
+ if ("python" === target) {
16986
+ if (existingPath && appRoot) {
16987
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
16988
+ const serverCodeRoot = external_path_default().join(appRoot, "server_code");
16989
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
16990
+ const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
16991
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
16992
+ const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
16993
+ if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
16994
+ ok: false,
16995
+ target,
16996
+ message: "File is not an Anvil app Python file",
16997
+ issues: [
16998
+ {
16999
+ path: "",
17000
+ message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
17001
+ }
17002
+ ]
17003
+ };
17004
+ }
17005
+ const result = validatePython(filePath, fileContent, appRoot);
15890
17006
  return result.ok ? {
15891
17007
  ok: true,
15892
17008
  target
@@ -15902,7 +17018,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15902
17018
  issues: [
15903
17019
  {
15904
17020
  path: "",
15905
- message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, and client_code/**/*.yaml"
17021
+ message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
15906
17022
  }
15907
17023
  ]
15908
17024
  };
@@ -15941,9 +17057,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15941
17057
  };
15942
17058
  return null;
15943
17059
  }
17060
+ function formatValidationFailure(fileName, validationResult) {
17061
+ const issueSummary = validationResult.issues.map((issue)=>` - ${formatValidationPath(issue.path)}: ${issue.message}`).join("\n");
17062
+ return issueSummary ? `${fileName} validation failed: ${validationResult.message}\n${issueSummary}` : `${fileName} validation failed: ${validationResult.message}`;
17063
+ }
15944
17064
  async function loadFormTemplateData(relativePath, templateInfo, context) {
15945
17065
  const { repoPath, stagedOnly = false } = context;
15946
- const { kind, format } = templateInfo;
17066
+ const { format } = templateInfo;
15947
17067
  try {
15948
17068
  const content = await readFileContent(repoPath, relativePath, stagedOnly);
15949
17069
  if ("yaml" === format) {
@@ -15956,26 +17076,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
15956
17076
  reason: issueSummary ? `${validationResult.message}. ${issueSummary}` : validationResult.message
15957
17077
  };
15958
17078
  }
15959
- const parsed = external_js_yaml_default().load(content);
15960
- const template = {
15961
- ...parsed,
15962
- is_package: "package" === kind
15963
- };
15964
- return {
15965
- ok: true,
15966
- data: {
15967
- template
15968
- }
15969
- };
15970
17079
  }
15971
- const { frontmatter, html } = parseHtmlWithFrontmatter(content);
15972
- const template = frontmatter ?? {};
15973
17080
  return {
15974
17081
  ok: true,
15975
- data: {
15976
- template,
15977
- html
15978
- }
17082
+ source: (0, form_template_parser_namespaceObject.parseFormTemplateSource)(content, {
17083
+ format
17084
+ })
15979
17085
  };
15980
17086
  } catch (error) {
15981
17087
  return {
@@ -16682,20 +17788,6 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16682
17788
  ]
16683
17789
  });
16684
17790
  }
16685
- function buildFormDataFromTemplate(template, html, options) {
16686
- const { class_name: _templateClassName, code: _templateCode, is_package: _templateIsPackage, ...templateRest } = template && "object" == typeof template ? template : {};
16687
- const formData = {
16688
- ...templateRest,
16689
- class_name: options.className,
16690
- code: options.code,
16691
- is_package: options.isPackage
16692
- };
16693
- if (void 0 !== html) {
16694
- formData.save_as_html = true;
16695
- formData.serialized_html = html;
16696
- }
16697
- return formData;
16698
- }
16699
17791
  async function buildFormTemplateData(relativePath, templateInfo, context) {
16700
17792
  const { repoPath, stagedOnly = false } = context;
16701
17793
  const { kind } = templateInfo;
@@ -16719,10 +17811,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16719
17811
  reason: "package" === kind ? "Form __init__.py file not found - cannot save template without code" : "Form .py file not found - cannot save template without code"
16720
17812
  }
16721
17813
  };
16722
- const templateData = loadResult.data.template ?? {};
16723
- const html = loadResult.data.html;
16724
- const templateObject = templateData && "object" == typeof templateData && !Array.isArray(templateData) ? templateData : {};
16725
- const formData = buildFormDataFromTemplate(templateObject, html, {
17814
+ const formData = (0, form_template_parser_namespaceObject.buildFormTemplateSavePayload)(loadResult.source, {
16726
17815
  className,
16727
17816
  code: codeContent,
16728
17817
  isPackage: "package" === kind
@@ -16907,7 +17996,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16907
17996
  }
16908
17997
  };
16909
17998
  const validationResult = validateAnvilYaml(yamlContent);
16910
- if (!validationResult.ok) throw new Error("anvil.yaml validation failed - see errors above");
17999
+ if (!validationResult.ok) throw new Error(formatValidationFailure("anvil.yaml", validationResult));
16911
18000
  const changes = [];
16912
18001
  for (const [key, value] of Object.entries(parsedYaml))if (!previousYaml || !deepEqual(previousYaml[key], value)) changes.push({
16913
18002
  key,
@@ -17014,6 +18103,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17014
18103
  }
17015
18104
  }
17016
18105
  function authDiagnostic(error) {
18106
+ if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
18107
+ code: error.type,
18108
+ recoverability: "requires_user_action",
18109
+ requiresUserAction: true,
18110
+ suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
18111
+ };
17017
18112
  return {
17018
18113
  code: error.type,
17019
18114
  recoverability: "requires_user_action",
@@ -17076,6 +18171,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17076
18171
  class SaveProcessor extends Emitter {
17077
18172
  hasPendingChanges = false;
17078
18173
  debounceTimer = null;
18174
+ saveLoopPromise = null;
17079
18175
  saveSequence = 0;
17080
18176
  SAVE_DEBOUNCE_MS = 1000;
17081
18177
  MAX_RETRIES = 3;
@@ -17099,6 +18195,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17099
18195
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
17100
18196
  if (isFirstChange) logger_logger.progress("sync", "Change detected");
17101
18197
  this.debounceTimer = setTimeout(()=>{
18198
+ this.debounceTimer = null;
17102
18199
  this.processSaveBatchWithEvents().catch((error)=>{
17103
18200
  logger_logger.error(`Save failed: ${errors_getErrorMessage(error)}`);
17104
18201
  });
@@ -17123,6 +18220,18 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17123
18220
  }
17124
18221
  }
17125
18222
  async processSaveBatchWithEvents() {
18223
+ if (this.saveLoopPromise) return this.saveLoopPromise;
18224
+ this.saveLoopPromise = this.drainSaveQueue();
18225
+ try {
18226
+ await this.saveLoopPromise;
18227
+ } finally{
18228
+ this.saveLoopPromise = null;
18229
+ }
18230
+ }
18231
+ async drainSaveQueue() {
18232
+ while(this.hasPendingChanges && !this.config.isCleanedUp() && !this.config.isPaused())await this.processOneSaveBatchWithEvents();
18233
+ }
18234
+ async processOneSaveBatchWithEvents() {
17126
18235
  if (this.config.isCleanedUp()) return;
17127
18236
  logger_logger.progressUpdate("sync", "Processing");
17128
18237
  if (!this.hasPendingChanges) return void logger_logger.progressEnd("sync");
@@ -17140,6 +18249,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17140
18249
  newCommitId: this.config.getCommitId(),
17141
18250
  fileCount
17142
18251
  });
18252
+ if (result.anvilYamlDependenciesChanged) this.emit("anvil-yaml-dependencies-changed", void 0);
17143
18253
  this.emit("save-complete", {
17144
18254
  newCommitId: this.config.getCommitId(),
17145
18255
  fileCount
@@ -17230,13 +18340,14 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17230
18340
  }
17231
18341
  async getStagedChanges() {
17232
18342
  try {
17233
- return await getStagedFileChanges(this.config.gitService.getGit());
18343
+ return await getStagedFileChanges(this.config.gitService.getGit(), this.config.gitService);
17234
18344
  } catch (e) {
17235
18345
  logger_logger.error(external_chalk_default().red(`Error getting staged changes: ${errors_getErrorMessage(e)}`));
17236
18346
  return [];
17237
18347
  }
17238
18348
  }
17239
18349
  async getUnstagedChanges(status) {
18350
+ const modeOnlyChanges = new Set(await this.config.gitService.discardModeOnlyChanges(status.modified));
17240
18351
  const renames = status.renamed;
17241
18352
  const stagedRenamedFromPaths = new Set(renames.map((r)=>r.from));
17242
18353
  const stagedRenamedToPaths = new Set(renames.map((r)=>r.to));
@@ -17256,7 +18367,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17256
18367
  const allRenamedFromPaths = new Set(allRenames.map((r)=>r.from));
17257
18368
  const allRenamedToPaths = new Set(allRenames.map((r)=>r.to));
17258
18369
  return [
17259
- ...status.modified.map((f)=>({
18370
+ ...status.modified.filter((f)=>!modeOnlyChanges.has(f)).map((f)=>({
17260
18371
  path: f,
17261
18372
  type: "change"
17262
18373
  })),
@@ -17327,11 +18438,15 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17327
18438
  async processBatchedChanges(changes) {
17328
18439
  const saveArray = [];
17329
18440
  const originalFilePaths = [];
18441
+ let anvilYamlDependenciesChanged = false;
17330
18442
  const deduplicatedChanges = deduplicateFormChanges(changes);
17331
18443
  for (const change of deduplicatedChanges){
17332
18444
  if ("anvil.yaml" === change.path) {
17333
18445
  const previousAnvilYaml = await this.getHeadAnvilYaml();
17334
18446
  const anvilChanges = await getAnvilYamlChanges(this.config.repoPath, previousAnvilYaml);
18447
+ const previousDependencies = normalizeAnvilYamlDependenciesValue(previousAnvilYaml?.dependencies);
18448
+ const currentDependencies = await readAnvilYamlDependencies(this.config.repoPath);
18449
+ if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) anvilYamlDependenciesChanged = true;
17335
18450
  if (0 === anvilChanges.length) {
17336
18451
  logger_logger.verbose(external_chalk_default().gray(" Ignoring anvil.yaml: No changes detected"));
17337
18452
  continue;
@@ -17383,7 +18498,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17383
18498
  }
17384
18499
  await this.sendSavesToAnvil(saveArray, originalFilePaths);
17385
18500
  return {
17386
- skipped: false
18501
+ skipped: false,
18502
+ anvilYamlDependenciesChanged
17387
18503
  };
17388
18504
  }
17389
18505
  logSaveResult(change, savePath, content) {
@@ -17459,7 +18575,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17459
18575
  await this.handleConflict(json_resp, originalFilePaths, retryCount);
17460
18576
  return;
17461
18577
  }
17462
- if (200 === resp.status) return void await this.handleSuccess(json_resp);
18578
+ if (200 === resp.status) return void await this.handleSuccess(json_resp, originalFilePaths);
17463
18579
  logger_logger.error(`Unexpected response status: ${resp.status}`);
17464
18580
  throw {
17465
18581
  type: "server_error",
@@ -17562,13 +18678,20 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17562
18678
  reason: "Unknown conflict"
17563
18679
  };
17564
18680
  }
17565
- async handleSuccess(json_resp) {
18681
+ async handleSuccess(json_resp, originalFilePaths) {
17566
18682
  const version = json_resp.version;
17567
- this.config.setCommitId(version);
18683
+ const oldCommitId = json_resp.previous_commit ?? null;
18684
+ const commitIdBeforePostSaveSync = this.config.getCommitId();
18685
+ this.emit("saved", {
18686
+ oldCommitId,
18687
+ newCommitId: version,
18688
+ branch: this.config.getCurrentBranch()
18689
+ });
17568
18690
  if (this.postSaveCallback) {
17569
18691
  logger_logger.progressUpdate("sync", "Syncing back");
17570
- await this.postSaveCallback();
18692
+ await this.postSaveCallback(new Set(originalFilePaths));
17571
18693
  }
18694
+ if (this.config.getCommitId() === commitIdBeforePostSaveSync) this.config.setCommitId(version);
17572
18695
  }
17573
18696
  async rerouteChanges(changes) {
17574
18697
  logger_logger.verbose(external_chalk_default().blue(`Re-routing ${changes.length} local change(s)...`));
@@ -17622,76 +18745,114 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17622
18745
  }
17623
18746
  class SyncManager extends Emitter {
17624
18747
  config;
17625
- syncInProgress = false;
18748
+ syncQueue = Promise.resolve();
17626
18749
  constructor(config){
17627
18750
  super();
17628
18751
  this.config = config;
17629
18752
  }
17630
18753
  async syncRemoteChanges(knownLocalChanges) {
17631
- if (this.syncInProgress) {
17632
- logger_logger.debug("Sync already in progress, skipping");
18754
+ const runSync = ()=>this.performSyncRemoteChanges(knownLocalChanges);
18755
+ const queuedSync = this.syncQueue.then(runSync, runSync);
18756
+ this.syncQueue = queuedSync.catch(()=>{});
18757
+ return queuedSync;
18758
+ }
18759
+ async performSyncRemoteChanges(knownLocalChanges) {
18760
+ const oldCommitId = this.config.getCommitId();
18761
+ let locallyChangedFiles;
18762
+ let initialStatus;
18763
+ if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18764
+ else {
18765
+ initialStatus = await this.config.gitService.getStatus();
18766
+ locallyChangedFiles = buildLocalChangesSet(initialStatus);
18767
+ }
18768
+ let syncMode;
18769
+ if (!knownLocalChanges && initialStatus && this.isCleanForFastForward(initialStatus)) syncMode = await this.fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles);
18770
+ else {
18771
+ await this.fetchAndResetFromAnvil(locallyChangedFiles);
18772
+ syncMode = "preservation";
18773
+ }
18774
+ if ("clean-move" === syncMode) return {
18775
+ localOnlyChanges: [],
18776
+ conflicts: []
18777
+ };
18778
+ const newCommitId = await this.config.gitService.getCommitId();
18779
+ this.config.setCommitId(newCommitId);
18780
+ const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
18781
+ const statusAfter = await this.config.gitService.getStatus();
18782
+ const newChanges = extractChangesFromStatus(statusAfter);
18783
+ if (0 === newChanges.length) {
18784
+ logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
17633
18785
  return {
17634
18786
  localOnlyChanges: [],
17635
18787
  conflicts: []
17636
18788
  };
17637
18789
  }
17638
- this.syncInProgress = true;
17639
- try {
17640
- const oldCommitId = this.config.getCommitId();
17641
- let locallyChangedFiles;
17642
- if (knownLocalChanges) locallyChangedFiles = knownLocalChanges;
18790
+ const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
18791
+ await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
18792
+ logConflictResolution(result);
18793
+ return {
18794
+ localOnlyChanges: result.localOnlyChanges,
18795
+ conflicts: result.conflicts
18796
+ };
18797
+ }
18798
+ async fetchAndResetFromAnvil(localChangesToPreserve = new Set()) {
18799
+ const oldCommitId = this.config.getCommitId();
18800
+ await this.withFetchedAnvilRef((tempRef)=>this.resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve));
18801
+ }
18802
+ async fetchAndMoveCleanTreeToAnvilHead(oldCommitId, locallyChangedFiles) {
18803
+ return this.withFetchedAnvilRef(async (tempRef)=>{
18804
+ const localHead = await this.config.gitService.getCommitId();
18805
+ if (await this.config.gitService.isAncestor(localHead, tempRef)) await this.config.gitService.mergeFastForward(tempRef);
17643
18806
  else {
17644
- const status = await this.config.gitService.getStatus();
17645
- locallyChangedFiles = buildLocalChangesSet(status);
18807
+ const statusBeforeHardReset = await this.config.gitService.getStatus();
18808
+ if (!this.isCleanForFastForward(statusBeforeHardReset)) {
18809
+ for (const filePath of buildLocalChangesSet(statusBeforeHardReset))locallyChangedFiles.add(filePath);
18810
+ await this.resetToFetchedAnvilRef(tempRef, oldCommitId, locallyChangedFiles);
18811
+ return "preservation";
18812
+ }
18813
+ await this.config.gitService.reset(tempRef, "hard");
17646
18814
  }
17647
- await this.fetchAndResetFromAnvil();
17648
18815
  const newCommitId = await this.config.gitService.getCommitId();
17649
18816
  this.config.setCommitId(newCommitId);
17650
- const remoteChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
17651
- const statusAfter = await this.config.gitService.getStatus();
17652
- const newChanges = extractChangesFromStatus(statusAfter);
17653
- if (0 === newChanges.length) {
17654
- logger_logger.verbose(external_chalk_default().gray(" No local changes - working directory is clean"));
17655
- return {
17656
- localOnlyChanges: [],
17657
- conflicts: []
17658
- };
17659
- }
17660
- const result = categorizeChanges(newChanges, locallyChangedFiles, remoteChangedFiles);
17661
- await acceptRemoteChanges(this.config.gitService, result.remoteOnlyChanges);
17662
- logConflictResolution(result);
17663
- return {
17664
- localOnlyChanges: result.localOnlyChanges,
17665
- conflicts: result.conflicts
17666
- };
17667
- } finally{
17668
- this.syncInProgress = false;
17669
- }
18817
+ await this.config.editorYaml.reload();
18818
+ return "clean-move";
18819
+ });
17670
18820
  }
17671
- async fetchAndResetFromAnvil() {
18821
+ async withFetchedAnvilRef(callback) {
17672
18822
  const validToken = await auth_getValidAuthToken(this.config.anvilUrl, this.config.username);
17673
18823
  this.config.setAuthToken(validToken);
17674
18824
  const httpUrl = getGitFetchUrl(this.config.appId, this.config.getAuthToken(), this.config.anvilUrl);
17675
18825
  const currentBranch = this.config.getCurrentBranch();
17676
18826
  const tempRef = `anvil-sync-temp-${Date.now()}`;
17677
- const oldCommitId = this.config.getCommitId();
17678
- await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18827
+ try {
18828
+ await this.config.gitService.fetch(httpUrl, `+${currentBranch}:${tempRef}`);
18829
+ return await callback(tempRef);
18830
+ } finally{
18831
+ await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
18832
+ }
18833
+ }
18834
+ async resetToFetchedAnvilRef(tempRef, oldCommitId, localChangesToPreserve) {
17679
18835
  await this.config.gitService.reset(tempRef, "mixed");
17680
- await this.config.gitService.deleteRef(`refs/heads/${tempRef}`);
17681
18836
  const newCommitId = await this.config.gitService.getCommitId();
17682
18837
  await this.config.gitService.checkout([
17683
18838
  ".anvil_editor.yaml",
17684
18839
  "anvil.yaml"
17685
18840
  ]);
17686
18841
  await this.config.editorYaml.reload();
17687
- await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId);
18842
+ await this.deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve);
17688
18843
  await this.discardFormattingOnlyYamlChanges();
17689
18844
  }
17690
- async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId) {
18845
+ isCleanForFastForward(status) {
18846
+ 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;
18847
+ }
18848
+ async deleteFilesRemovedOnAnvilLocally(oldCommitId, newCommitId, localChangesToPreserve) {
17691
18849
  try {
17692
18850
  const status = await this.config.gitService.getStatus();
17693
18851
  const remotelyChangedFiles = await detectRemoteChanges(this.config.gitService, oldCommitId, newCommitId);
17694
- await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles);
18852
+ await deleteFilesRemovedOnAnvil(this.config.gitService, status.notAdded, remotelyChangedFiles, {
18853
+ oldCommitId,
18854
+ localChangesToPreserve
18855
+ });
17695
18856
  } catch (e) {
17696
18857
  logger_logger.verbose(external_chalk_default().gray(` Failed to check git status: ${errors_getErrorMessage(e)}`));
17697
18858
  }
@@ -17804,6 +18965,10 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17804
18965
  environment;
17805
18966
  envBranchChangeMode;
17806
18967
  isSwitchingForEnvironment = false;
18968
+ environmentSwitchTargetBranch = null;
18969
+ dependencyWatchEnabled;
18970
+ isRefreshingDependencies = false;
18971
+ hasPendingDependencyRefresh = false;
17807
18972
  pendingEnvironmentBranchSwitch = null;
17808
18973
  BRANCH_CHANGE_SETTLE_MS = 2000;
17809
18974
  LOCAL_CHANGE_POLL_MS = 2000;
@@ -17821,6 +18986,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17821
18986
  this.stagedOnly = options.stagedOnly ?? false;
17822
18987
  this.environment = options.environment;
17823
18988
  this.envBranchChangeMode = options.envBranchChangeMode ?? "manual";
18989
+ this.dependencyWatchEnabled = options.dependencyWatch ?? true;
17824
18990
  this.editSession = "anvil-sync-" + Math.random().toString(36).substring(2, 15);
17825
18991
  this.gitService = new GitService(this.repoPath);
17826
18992
  this.editorYaml = new EditorYamlManager(this.repoPath);
@@ -17840,6 +19006,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17840
19006
  async initialize() {
17841
19007
  await this.editorYaml.load();
17842
19008
  this.initializeProcessors();
19009
+ if (this.dependencyWatchEnabled) await this.refreshDependencies("startup");
17843
19010
  try {
17844
19011
  await this.connectWebSocket();
17845
19012
  } catch (error) {
@@ -17859,7 +19026,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17859
19026
  getAuthToken: ()=>this.authToken,
17860
19027
  setAuthToken: (token)=>{
17861
19028
  this.authToken = token;
17862
- this.wsClient?.updateAuthToken(token);
19029
+ this.wsClient?.updateAuthToken?.(token);
17863
19030
  },
17864
19031
  getCurrentBranch: ()=>this.currentBranch,
17865
19032
  getCommitId: ()=>this.commitId,
@@ -17879,7 +19046,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17879
19046
  getAuthToken: ()=>this.authToken,
17880
19047
  setAuthToken: (token)=>{
17881
19048
  this.authToken = token;
17882
- this.wsClient?.updateAuthToken(token);
19049
+ this.wsClient?.updateAuthToken?.(token);
17883
19050
  },
17884
19051
  getCurrentBranch: ()=>this.currentBranch,
17885
19052
  getCommitId: ()=>this.commitId,
@@ -17892,15 +19059,25 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17892
19059
  isPaused: ()=>this.isPausedForUserInput
17893
19060
  });
17894
19061
  this.saveProcessor.setSyncCallback((filePaths)=>this.syncManager.syncRemoteChanges(filePaths));
17895
- this.saveProcessor.setPostSaveCallback(()=>this.syncManager.fetchAndResetFromAnvil());
19062
+ this.saveProcessor.setPostSaveCallback((filePaths)=>this.syncAfterSave(filePaths));
17896
19063
  this.saveProcessor.on("save-started", (data)=>this.emit("save-started", data));
17897
19064
  this.saveProcessor.on("save-succeeded", (data)=>this.emit("save-succeeded", data));
17898
19065
  this.saveProcessor.on("save-failed", (data)=>this.emit("save-failed", data));
17899
19066
  this.saveProcessor.on("save-complete", (data)=>this.emit("save-complete", data));
19067
+ this.saveProcessor.on("saved", (data)=>this.emit("saved", data));
19068
+ this.saveProcessor.on("anvil-yaml-dependencies-changed", ()=>{
19069
+ this.queueDependencyRefresh("anvil.yaml dependencies changed");
19070
+ });
17900
19071
  this.saveProcessor.on("validation-failed", (data)=>this.emit("validation-failed", data));
17901
19072
  this.saveProcessor.on("sync-conflict", (data)=>this.emit("sync-conflict", data));
17902
19073
  this.saveProcessor.on("max-retries-exceeded", (data)=>this.emit("max-retries-exceeded", data));
17903
19074
  }
19075
+ async syncAfterSave(savedFilePaths) {
19076
+ const status = await this.gitService.getStatus();
19077
+ const locallyChangedFiles = buildLocalChangesSet(status);
19078
+ if (!this.stagedOnly) for (const filePath of savedFilePaths)locallyChangedFiles.add(filePath);
19079
+ return this.syncManager.syncRemoteChanges(locallyChangedFiles);
19080
+ }
17904
19081
  async connectWebSocket() {
17905
19082
  this.wsClient = new WebSocketClient({
17906
19083
  appId: this.appId,
@@ -17909,7 +19086,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17909
19086
  currentBranch: this.currentBranch,
17910
19087
  editSession: this.editSession,
17911
19088
  username: this.username,
17912
- environment: this.environment
19089
+ environment: this.environment,
19090
+ dependencyWatch: this.dependencyWatchEnabled,
19091
+ dependencySubscriptions: this.dependencyWatchEnabled ? await this.getDependencyWatchSubscriptions() : []
17913
19092
  });
17914
19093
  this.wsClient.on("env-state", ({ envs })=>{
17915
19094
  for (const env of envs)this.handleEnvironmentBinding(env, "snapshot");
@@ -17917,7 +19096,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17917
19096
  this.wsClient.on("env-update", (env)=>{
17918
19097
  this.handleEnvironmentBinding(env, "update");
17919
19098
  });
17920
- this.wsClient.on("repo-update", async ({ ref, commitHash, saveSeq })=>{
19099
+ this.wsClient.on("repo-update", async ({ appId, ref, commitHash, saveSeq })=>{
19100
+ if (appId !== this.appId) return void await this.handleDependencyRepoUpdate(appId, ref, commitHash);
17921
19101
  if (saveSeq?.session === this.editSession) return void logger_logger.debug("Ignoring update from our own session");
17922
19102
  const branchName = ref.replace(/^refs\/heads\//, "");
17923
19103
  if (branchName !== this.currentBranch) return void logger_logger.debug(`Ignoring update to different branch: ${branchName} (current: ${this.currentBranch})`);
@@ -17939,6 +19119,55 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17939
19119
  });
17940
19120
  await this.wsClient.connect();
17941
19121
  }
19122
+ async handleDependencyRepoUpdate(appId, ref, commitHash) {
19123
+ if (!this.dependencyWatchEnabled) return;
19124
+ const branchName = ref.replace(/^refs\/heads\//, "");
19125
+ const subscription = this.wsClient?.getDependencySubscriptions().find((candidate)=>candidate.appId === appId && candidate.branch === branchName);
19126
+ if (!subscription) return void logger_logger.debug(`Ignoring update for unwatched dependency app ${appId} on ${branchName}`);
19127
+ 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)})`));
19128
+ await this.queueDependencyRefresh(`dependency ${appId} updated`);
19129
+ }
19130
+ async queueDependencyRefresh(reason) {
19131
+ if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
19132
+ if (this.isRefreshingDependencies) {
19133
+ this.hasPendingDependencyRefresh = true;
19134
+ return;
19135
+ }
19136
+ this.isRefreshingDependencies = true;
19137
+ try {
19138
+ do {
19139
+ this.hasPendingDependencyRefresh = false;
19140
+ await this.refreshDependencies(reason);
19141
+ }while (this.hasPendingDependencyRefresh && !this.isCleanedUp);
19142
+ } finally{
19143
+ this.isRefreshingDependencies = false;
19144
+ }
19145
+ }
19146
+ async refreshDependencies(reason) {
19147
+ if (!this.dependencyWatchEnabled || this.isCleanedUp) return;
19148
+ logger_logger.progress("dependency-refresh", "startup" === reason ? "Fetching dependencies" : "Refreshing dependencies");
19149
+ try {
19150
+ const previousSubscriptions = this.wsClient?.getDependencySubscriptions() ?? [];
19151
+ const { response, addedGitignoreEntry } = await refreshDependencyCache(this.repoPath, this.appId, this.anvilUrl, {
19152
+ includeDocs: true
19153
+ });
19154
+ if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
19155
+ const nextSubscriptions = await this.getDependencyWatchSubscriptions();
19156
+ this.reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions);
19157
+ logger_logger.progressEnd("dependency-refresh", response.dependencies.length ? `Fetched ${response.dependencies.length} dependencies` : "Dependencies already up to date");
19158
+ } catch (error) {
19159
+ logger_logger.progressEnd("dependency-refresh", "Failed");
19160
+ logger_logger.warn(external_chalk_default().yellow(`Failed to refresh dependencies: ${errors_getErrorMessage(error)}`));
19161
+ }
19162
+ }
19163
+ async getDependencyWatchSubscriptions() {
19164
+ return getDependencyWatchSubscriptionsFromManifest(await readDepsCacheManifest(this.repoPath));
19165
+ }
19166
+ reconcileDependencySubscriptions(previousSubscriptions, nextSubscriptions) {
19167
+ if (dependencySubscriptionsEqual(previousSubscriptions, nextSubscriptions)) return;
19168
+ this.wsClient?.updateDependencySubscriptions(nextSubscriptions);
19169
+ if (this.wsClient?.isConnected() && previousSubscriptions.length > 0) this.wsClient.reconnect();
19170
+ }
17942
19171
  async handleEnvironmentBinding(env, source) {
17943
19172
  if (!this.environment?.envPid || env.envPid !== this.environment.envPid) return;
17944
19173
  this.environment = {
@@ -17961,6 +19190,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17961
19190
  async switchToEnvironmentBranch(targetBranch, source) {
17962
19191
  if (this.isCleanedUp) return;
17963
19192
  this.isSwitchingForEnvironment = true;
19193
+ this.environmentSwitchTargetBranch = targetBranch;
17964
19194
  const oldPausedState = this.isPausedForUserInput;
17965
19195
  try {
17966
19196
  const status = await this.gitService.getStatus();
@@ -17985,6 +19215,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17985
19215
  ]);
17986
19216
  }
17987
19217
  logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
19218
+ this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
19219
+ this.wsClient?.updateAuthToken?.(this.authToken);
17988
19220
  const fetchUrl = getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
17989
19221
  const envRemoteRef = environmentRemoteRefForBranch(targetBranch);
17990
19222
  await this.gitService.fetch(fetchUrl, environmentFetchRefSpecForBranch(targetBranch));
@@ -18000,6 +19232,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18000
19232
  this.lastBranchChangeTime = Date.now();
18001
19233
  this.fileWatcher?.setCurrentBranch(targetBranch);
18002
19234
  this.wsClient?.updateBranch(targetBranch);
19235
+ this.queueDependencyRefresh("branch changed");
18003
19236
  await this.initializeLocalChangeFingerprint();
18004
19237
  this.emit("branch-changed", {
18005
19238
  oldBranch,
@@ -18019,6 +19252,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18019
19252
  } finally{
18020
19253
  this.isPausedForUserInput = oldPausedState;
18021
19254
  this.isSwitchingForEnvironment = false;
19255
+ this.environmentSwitchTargetBranch = null;
18022
19256
  }
18023
19257
  const pendingSwitch = this.pendingEnvironmentBranchSwitch;
18024
19258
  this.pendingEnvironmentBranchSwitch = null;
@@ -18070,7 +19304,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18070
19304
  });
18071
19305
  }
18072
19306
  async syncRemoteChanges(knownLocalChanges) {
18073
- return this.syncManager.syncRemoteChanges(knownLocalChanges);
19307
+ const previousDependencies = this.dependencyWatchEnabled ? await readAnvilYamlDependencies(this.repoPath) : void 0;
19308
+ const result = await this.syncManager.syncRemoteChanges(knownLocalChanges);
19309
+ if (this.dependencyWatchEnabled && void 0 !== previousDependencies) {
19310
+ const currentDependencies = await readAnvilYamlDependencies(this.repoPath);
19311
+ if (!anvilYamlDependenciesEqual(previousDependencies, currentDependencies)) await this.queueDependencyRefresh("anvil.yaml dependencies changed");
19312
+ }
19313
+ return result;
18074
19314
  }
18075
19315
  cleanup() {
18076
19316
  this.isCleanedUp = true;
@@ -18100,15 +19340,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18100
19340
  getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
18101
19341
  });
18102
19342
  this.fileWatcher.on("branch-change", ({ newBranch })=>{
18103
- const oldBranch = this.currentBranch;
18104
- logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
18105
- this.currentBranch = newBranch;
18106
- this.lastBranchChangeTime = Date.now();
18107
- this.wsClient?.updateBranch(newBranch);
18108
- this.emit("branch-changed", {
18109
- oldBranch,
18110
- newBranch
18111
- });
19343
+ this.handleFileWatcherBranchChange(newBranch);
18112
19344
  });
18113
19345
  this.fileWatcher.on("index-change", ()=>{
18114
19346
  this.saveProcessor?.queueSave();
@@ -18125,6 +19357,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18125
19357
  this.startLocalChangeFallbackPolling();
18126
19358
  await new Promise(()=>{});
18127
19359
  }
19360
+ handleFileWatcherBranchChange(newBranch) {
19361
+ if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
19362
+ const oldBranch = this.currentBranch;
19363
+ logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19364
+ this.currentBranch = newBranch;
19365
+ this.lastBranchChangeTime = Date.now();
19366
+ this.wsClient?.updateBranch(newBranch);
19367
+ this.queueDependencyRefresh("branch changed");
19368
+ this.emit("external-branch-changed", {
19369
+ oldBranch,
19370
+ newBranch
19371
+ });
19372
+ this.emit("branch-changed", {
19373
+ oldBranch,
19374
+ newBranch
19375
+ });
19376
+ }
18128
19377
  startWatchingInBackground(onError) {
18129
19378
  this.startWatching().catch((error)=>{
18130
19379
  this.cleanup();
@@ -18250,6 +19499,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18250
19499
  return false;
18251
19500
  }
18252
19501
  }
19502
+ function dependencySubscriptionsEqual(left, right) {
19503
+ return subscriptionKeys(left).join("\n") === subscriptionKeys(right).join("\n");
19504
+ }
19505
+ function subscriptionKeys(subscriptions) {
19506
+ return subscriptions.map((subscription)=>`${subscription.appId}\0${subscription.branch}`).sort();
19507
+ }
18253
19508
  async function validateAnvilApp(repoPath) {
18254
19509
  const anvilYamlPath = external_path_default().join(repoPath, "anvil.yaml");
18255
19510
  if (!external_fs_.existsSync(anvilYamlPath)) throw createValidationError.missingYaml(repoPath);
@@ -19628,38 +20883,419 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19628
20883
  await checkout_openPathInEditorOrDefault(result.destinationPath, preferredEditorCommand);
19629
20884
  logger_logger.info(external_chalk_default().gray(`Opened ${result.destinationDisplay}`));
19630
20885
  }
19631
- if (preferredEditorCommand && !options.open) if (isCommandAvailable(preferredEditorCommand)) logger_logger.info(external_chalk_default().cyan(`Next: ${preferredEditorCommand} ${result.destinationDisplay}`));
19632
- else logger_logger.warn(`Preferred editor command '${preferredEditorCommand}' was not found in PATH. Run 'anvil configure' to choose an installed editor.`);
20886
+ if (preferredEditorCommand && !options.open) if (isCommandAvailable(preferredEditorCommand)) logger_logger.info(external_chalk_default().cyan(`Next: ${preferredEditorCommand} ${result.destinationDisplay}`));
20887
+ else logger_logger.warn(`Preferred editor command '${preferredEditorCommand}' was not found in PATH. Run 'anvil configure' to choose an installed editor.`);
20888
+ }
20889
+ async function checkout_openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps) {
20890
+ await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
20891
+ }
20892
+ function registerCheckoutCommand(program) {
20893
+ const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
20894
+ try {
20895
+ const globalOptions = command?.optsWithGlobals() || options || {};
20896
+ if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
20897
+ await executeCheckoutInteractive({
20898
+ input,
20899
+ directory,
20900
+ open: options?.open,
20901
+ branch: options?.branch,
20902
+ depth: options?.depth,
20903
+ singleBranch: options?.singleBranch,
20904
+ origin: options?.origin,
20905
+ quiet: options?.quiet,
20906
+ verbose: globalOptions.verbose,
20907
+ url: options?.url,
20908
+ user: options?.user,
20909
+ force: options?.force,
20910
+ query: options?.query
20911
+ }, defaultCheckoutDeps);
20912
+ } catch (e) {
20913
+ logger_logger.error("Error: " + errors_getErrorMessage(e));
20914
+ process.exit(1);
20915
+ }
20916
+ });
20917
+ checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
20918
+ }
20919
+ const defaultGitAuthDoctorDeps = {
20920
+ getAppAuthBinding: getAppAuthBinding,
20921
+ getAccountsForUrl: auth_getAccountsForUrl,
20922
+ hasTokensForUrl: hasTokensForUrl,
20923
+ runCredentialFill: runGitCredentialFill
20924
+ };
20925
+ function getHighestStatus(statuses) {
20926
+ if (statuses.includes("error")) return "error";
20927
+ if (statuses.includes("warning")) return "warning";
20928
+ return "ok";
20929
+ }
20930
+ function finding(status, code, message) {
20931
+ return {
20932
+ status,
20933
+ code,
20934
+ message
20935
+ };
20936
+ }
20937
+ function redactSecrets(value) {
20938
+ 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>");
20939
+ }
20940
+ function splitShellCommand(command) {
20941
+ const tokens = [];
20942
+ let current = "";
20943
+ let state = "normal";
20944
+ for(let i = 0; i < command.length; i++){
20945
+ const ch = command[i];
20946
+ if ("single" === state) {
20947
+ if ("'" === ch) state = "normal";
20948
+ else current += ch;
20949
+ continue;
20950
+ }
20951
+ if ("double" === state) {
20952
+ if ("\"" === ch) state = "normal";
20953
+ else if ("\\" === ch && i + 1 < command.length && [
20954
+ "$",
20955
+ "`",
20956
+ "\"",
20957
+ "\\",
20958
+ "\n"
20959
+ ].includes(command[i + 1])) current += command[++i];
20960
+ else current += ch;
20961
+ continue;
20962
+ }
20963
+ if (/\s/.test(ch)) {
20964
+ if (current) {
20965
+ tokens.push(current);
20966
+ current = "";
20967
+ }
20968
+ } else if ("'" === ch) state = "single";
20969
+ else if ("\"" === ch) state = "double";
20970
+ else if ("\\" === ch && i + 1 < command.length) current += command[++i];
20971
+ else current += ch;
20972
+ }
20973
+ if ("normal" !== state) throw new Error("Unterminated quote in helper command");
20974
+ if (current) tokens.push(current);
20975
+ return tokens;
20976
+ }
20977
+ function parseCredentialHelperCommand(command) {
20978
+ const trimmed = command.trim();
20979
+ if (!trimmed || !trimmed.startsWith("!")) return {};
20980
+ const tokens = splitShellCommand(trimmed.slice(1).trim());
20981
+ return {
20982
+ execPath: tokens[0],
20983
+ helperPath: tokens[1]
20984
+ };
20985
+ }
20986
+ function readHelperModulePath(helperPath) {
20987
+ try {
20988
+ const helperSource = external_fs_default().readFileSync(helperPath, "utf8");
20989
+ const match = helperSource.match(/executeGitCredentialOperation\s*}\s*=\s*require\((["'])(.*?)\1\)/);
20990
+ return match?.[2];
20991
+ } catch {
20992
+ return;
20993
+ }
20994
+ }
20995
+ function inspectCredentialHelperCommand(raw) {
20996
+ const details = {
20997
+ raw
20998
+ };
20999
+ try {
21000
+ const parsed = parseCredentialHelperCommand(raw);
21001
+ details.execPath = parsed.execPath;
21002
+ details.helperPath = parsed.helperPath;
21003
+ } catch (e) {
21004
+ details.parseError = e.message;
21005
+ return details;
21006
+ }
21007
+ if (details.execPath) details.execExists = external_fs_default().existsSync(details.execPath);
21008
+ if (details.helperPath) {
21009
+ details.helperExists = external_fs_default().existsSync(details.helperPath);
21010
+ if (details.helperExists) {
21011
+ try {
21012
+ const stat = external_fs_default().statSync(details.helperPath);
21013
+ details.helperExecutable = (73 & stat.mode) !== 0;
21014
+ } catch {
21015
+ details.helperExecutable = false;
21016
+ }
21017
+ details.modulePath = readHelperModulePath(details.helperPath);
21018
+ if (details.modulePath) details.moduleExists = external_fs_default().existsSync(details.modulePath);
21019
+ }
21020
+ }
21021
+ return details;
21022
+ }
21023
+ function detectAnvilRemote(remote) {
21024
+ const fetchUrl = remote.refs.fetch;
21025
+ const out = {
21026
+ name: remote.name,
21027
+ fetchUrl
21028
+ };
21029
+ if (!fetchUrl) return out;
21030
+ const httpMatch = fetchUrl.match(/^(https?):\/\/(?:[^@/?#]+@)?([^/?#]+)\/git\/([A-Z0-9]+)\.git(?:[?#].*)?$/);
21031
+ if (httpMatch) {
21032
+ const [, protocol, host, appId] = httpMatch;
21033
+ return {
21034
+ ...out,
21035
+ appId,
21036
+ anvilUrl: normalizeAnvilUrl(`${protocol}://${host}`),
21037
+ transport: "https"
21038
+ };
21039
+ }
21040
+ const sshMatch = fetchUrl.match(/^ssh:\/\/(?:([^@]+)@)?([^:\/]+)(?::\d+)?\/(?:git\/)?([A-Z0-9]+)\.git(?:[?#].*)?$/);
21041
+ if (sshMatch) {
21042
+ const [, , host, appId] = sshMatch;
21043
+ return {
21044
+ ...out,
21045
+ appId,
21046
+ anvilUrl: normalizeAnvilUrl(host),
21047
+ transport: "ssh"
21048
+ };
21049
+ }
21050
+ return out;
21051
+ }
21052
+ async function getLocalConfigValues(repoPath, key) {
21053
+ try {
21054
+ const output = await esm_default(repoPath).raw([
21055
+ "config",
21056
+ "--local",
21057
+ "--get-all",
21058
+ key
21059
+ ]);
21060
+ const values = output.split(/\r?\n/);
21061
+ if ("" === values[values.length - 1]) values.pop();
21062
+ return values;
21063
+ } catch {
21064
+ return [];
21065
+ }
19633
21066
  }
19634
- async function checkout_openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps) {
19635
- await openPathInEditorOrDefault(destinationPath, preferredEditorCommand, deps);
21067
+ function buildCredentialInput(anvilUrl, appId) {
21068
+ const url = new URL(anvilUrl);
21069
+ return `protocol=${url.protocol.replace(/:$/, "")}\nhost=${url.host}\npath=git/${appId}.git\n\n`;
21070
+ }
21071
+ async function runGitCredentialFill(repoPath, input) {
21072
+ return new Promise((resolve)=>{
21073
+ const child = (0, external_child_process_namespaceObject.spawn)("git", [
21074
+ "credential",
21075
+ "fill"
21076
+ ], {
21077
+ cwd: repoPath,
21078
+ env: {
21079
+ ...process.env,
21080
+ GIT_TERMINAL_PROMPT: "0"
21081
+ },
21082
+ stdio: [
21083
+ "pipe",
21084
+ "pipe",
21085
+ "pipe"
21086
+ ]
21087
+ });
21088
+ let stdout = "";
21089
+ let stderr = "";
21090
+ child.stdout.setEncoding("utf8");
21091
+ child.stderr.setEncoding("utf8");
21092
+ child.stdout.on("data", (chunk)=>{
21093
+ stdout += chunk;
21094
+ });
21095
+ child.stderr.on("data", (chunk)=>{
21096
+ stderr += chunk;
21097
+ });
21098
+ child.on("error", (error)=>{
21099
+ resolve({
21100
+ exitCode: null,
21101
+ stdout: redactSecrets(stdout),
21102
+ stderr: redactSecrets(stderr),
21103
+ error: redactSecrets(error.message)
21104
+ });
21105
+ });
21106
+ child.on("close", (code)=>{
21107
+ resolve({
21108
+ exitCode: code,
21109
+ stdout: redactSecrets(stdout),
21110
+ stderr: redactSecrets(stderr)
21111
+ });
21112
+ });
21113
+ child.stdin.end(input);
21114
+ });
19636
21115
  }
19637
- function registerCheckoutCommand(program) {
19638
- const checkoutCommand = program.command("checkout [input] [directory]").description("Check out an Anvil app locally from editor URL, git URL, app ID, or interactive selection").alias("co").option("-O, --open", "Open destination after checkout").option("-b, --branch <BRANCH>", "Checkout a specific branch").option("--depth <N>", "Create a shallow clone with history truncated to N commits", (value)=>parseInt(value, 10)).option("--single-branch", "Clone only one branch").option("--origin <NAME>", "Use a custom remote name instead of origin").option("--quiet", "Suppress git clone progress output").option("--verbose", "Enable verbose git clone output").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").option("-U, --user <USERNAME>", "Specify which user account to use").option("-f, --force", "Override safety checks for destination path").option("-Q, --query <QUERY>", "Initial search query for interactive checkout picker").action(async (input, directory, options, command)=>{
21116
+ async function buildAppReport(repoPath, remote, deps) {
21117
+ const appId = remote.appId;
21118
+ const anvilUrl = remote.anvilUrl;
21119
+ const url = new URL(anvilUrl);
21120
+ const credentialScope = `${url.protocol}//${url.host}`;
21121
+ const helperConfigKey = `credential.${credentialScope}.helper`;
21122
+ const helperValues = await getLocalConfigValues(repoPath, helperConfigKey);
21123
+ const nonEmptyHelperValues = helperValues.filter((value)=>"" !== value.trim());
21124
+ const helperDetails = nonEmptyHelperValues.map(inspectCredentialHelperCommand);
21125
+ const findings = [];
21126
+ if (0 === helperValues.length) findings.push(finding("error", "missing_helper_config", `No local ${helperConfigKey} entries found.`));
21127
+ else if (0 === nonEmptyHelperValues.length) findings.push(finding("error", "missing_helper_command", `${helperConfigKey} only resets helpers; no Anvil helper command is configured.`));
21128
+ for (const helper of helperDetails){
21129
+ if (helper.parseError) findings.push(finding("error", "helper_parse_failed", `Could not parse helper command: ${helper.parseError}`));
21130
+ if (helper.execPath && false === helper.execExists) findings.push(finding("error", "helper_exec_missing", `Credential helper Node executable does not exist: ${helper.execPath}`));
21131
+ if (helper.helperPath) {
21132
+ if (false === helper.helperExists) findings.push(finding("error", "helper_file_missing", `Credential helper script does not exist: ${helper.helperPath}`));
21133
+ else if (false === helper.helperExecutable) findings.push(finding("warning", "helper_not_executable", `Credential helper script is not executable: ${helper.helperPath}`));
21134
+ } else findings.push(finding("error", "helper_path_missing", "Credential helper command does not include a helper script path."));
21135
+ if (helper.modulePath && false === helper.moduleExists) findings.push(finding("error", "helper_module_missing", `Generated helper points at a missing module: ${helper.modulePath}`));
21136
+ 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}`));
21137
+ }
21138
+ const binding = await deps.getAppAuthBinding(repoPath, appId);
21139
+ if (!binding.url) findings.push(finding("warning", "missing_app_binding_url", `No anvil.auth.${appId}.url binding is configured.`));
21140
+ if (!binding.username) findings.push(finding("warning", "missing_app_binding_username", `No anvil.auth.${appId}.username binding is configured.`));
21141
+ const authUrl = binding.url ? normalizeAnvilUrl(binding.url) : anvilUrl;
21142
+ let accounts = [];
21143
+ let authUsername = binding.username;
21144
+ let hasUsableAuth = false;
21145
+ try {
21146
+ accounts = await deps.getAccountsForUrl(authUrl);
21147
+ if (authUsername || 1 !== accounts.length) {
21148
+ 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.`));
21149
+ } else authUsername = accounts[0];
21150
+ hasUsableAuth = authUsername ? await deps.hasTokensForUrl(authUrl, authUsername) : accounts.length <= 1 && await deps.hasTokensForUrl(authUrl);
21151
+ 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}.`));
21152
+ } catch (e) {
21153
+ findings.push(finding("error", "auth_lookup_failed", `Auth lookup failed: ${redactSecrets(e.message)}`));
21154
+ }
21155
+ const fillResult = await deps.runCredentialFill(repoPath, buildCredentialInput(anvilUrl, appId));
21156
+ const credentialSimulation = {
21157
+ status: 0 === fillResult.exitCode && /^password=/im.test(fillResult.stdout) ? "ok" : "error",
21158
+ exitCode: fillResult.exitCode,
21159
+ stdout: redactSecrets(fillResult.stdout),
21160
+ stderr: redactSecrets(fillResult.stderr),
21161
+ error: fillResult.error ? redactSecrets(fillResult.error) : void 0
21162
+ };
21163
+ if ("ok" !== credentialSimulation.status) findings.push(finding("error", "credential_fill_failed", credentialSimulation.error || credentialSimulation.stderr.trim() || "git credential fill did not return a password."));
21164
+ const report = {
21165
+ appId,
21166
+ anvilUrl,
21167
+ remoteName: remote.name,
21168
+ remoteUrl: remote.fetchUrl,
21169
+ credentialScope,
21170
+ helperConfigKey,
21171
+ helperValues,
21172
+ helperDetails,
21173
+ binding: {
21174
+ url: binding.url,
21175
+ username: binding.username,
21176
+ hasUrl: !!binding.url,
21177
+ hasUsername: !!binding.username
21178
+ },
21179
+ auth: {
21180
+ url: authUrl,
21181
+ username: authUsername,
21182
+ accounts,
21183
+ hasUsableAuth
21184
+ },
21185
+ credentialSimulation,
21186
+ findings
21187
+ };
21188
+ return report;
21189
+ }
21190
+ async function buildGitAuthDoctorReport(repoPathInput = process.cwd(), options = {}) {
21191
+ const deps = options.deps ?? defaultGitAuthDoctorDeps;
21192
+ const inputPath = external_path_default().resolve(repoPathInput);
21193
+ const git = esm_default(inputPath);
21194
+ const repoPath = (await git.raw([
21195
+ "rev-parse",
21196
+ "--show-toplevel"
21197
+ ])).trim();
21198
+ const rawGitDir = (await git.raw([
21199
+ "rev-parse",
21200
+ "--git-dir"
21201
+ ])).trim();
21202
+ const gitDir = external_path_default().isAbsolute(rawGitDir) ? rawGitDir : external_path_default().resolve(inputPath, rawGitDir);
21203
+ setRepoContext(repoPath);
21204
+ const remotes = (await git.getRemotes(true)).map(detectAnvilRemote);
21205
+ const anvilRemotes = remotes.filter((remote)=>remote.appId && remote.anvilUrl && "https" === remote.transport);
21206
+ const findings = [];
21207
+ if (0 === anvilRemotes.length) findings.push(finding("error", "missing_anvil_https_remote", "No HTTPS Anvil git remotes were detected."));
21208
+ const apps = await Promise.all(anvilRemotes.map((remote)=>buildAppReport(repoPath, remote, deps)));
21209
+ const status = getHighestStatus([
21210
+ ...findings.map((item)=>item.status),
21211
+ ...apps.flatMap((app)=>app.findings.map((item)=>item.status))
21212
+ ]);
21213
+ return {
21214
+ repoPath,
21215
+ gitDir,
21216
+ remotes,
21217
+ apps,
21218
+ findings,
21219
+ status
21220
+ };
21221
+ }
21222
+ function formatStatus(status) {
21223
+ if ("ok" === status) return external_chalk_default().green("[ok]");
21224
+ if ("warning" === status) return external_chalk_default().yellow("[warn]");
21225
+ return external_chalk_default().red("[error]");
21226
+ }
21227
+ function formatGitAuthDoctorReport(report) {
21228
+ const lines = [];
21229
+ lines.push(external_chalk_default().bold("Git auth doctor"));
21230
+ lines.push(`Repository: ${report.repoPath}`);
21231
+ lines.push(`Git dir: ${report.gitDir}`);
21232
+ lines.push(`Overall: ${formatStatus(report.status)}`);
21233
+ lines.push("");
21234
+ lines.push(external_chalk_default().bold("Detected remotes:"));
21235
+ if (0 === report.remotes.length) lines.push(" (none)");
21236
+ else for (const remote of report.remotes){
21237
+ const appText = remote.appId && remote.anvilUrl ? ` -> ${remote.appId} on ${remote.anvilUrl}` : "";
21238
+ lines.push(` - ${remote.name}: ${remote.fetchUrl || "(no fetch URL)"}${appText}`);
21239
+ }
21240
+ for (const app of report.apps){
21241
+ lines.push("");
21242
+ lines.push(external_chalk_default().bold(`App ${app.appId} (${app.remoteName})`));
21243
+ lines.push(` URL: ${app.anvilUrl}`);
21244
+ lines.push(` Credential scope: ${app.credentialScope}`);
21245
+ lines.push(` Helper config: ${app.helperConfigKey}`);
21246
+ if (0 === app.helperValues.length) lines.push(" (none)");
21247
+ else for (const value of app.helperValues)lines.push(` - ${value || "(reset helper list)"}`);
21248
+ for (const helper of app.helperDetails){
21249
+ lines.push(` Helper command: ${helper.raw}`);
21250
+ lines.push(` Node: ${helper.execPath || "(missing)"} ${false === helper.execExists ? "[missing]" : ""}`.trimEnd());
21251
+ lines.push(` Script: ${helper.helperPath || "(missing)"} ${false === helper.helperExists ? "[missing]" : ""}`.trimEnd());
21252
+ if (helper.modulePath) lines.push(` Module: ${helper.modulePath} ${false === helper.moduleExists ? "[missing]" : ""}`.trimEnd());
21253
+ }
21254
+ lines.push(` Binding URL: ${app.binding.url || "(missing)"}`);
21255
+ lines.push(` Binding user: ${app.binding.username || "(missing)"}`);
21256
+ lines.push(` Auth tokens: ${app.auth.hasUsableAuth ? "available" : "not available"}${app.auth.username ? ` for ${app.auth.username}` : ""}`);
21257
+ if (app.credentialSimulation) {
21258
+ lines.push(` Credential fill: ${formatStatus(app.credentialSimulation.status)} exit=${app.credentialSimulation.exitCode}`);
21259
+ if (app.credentialSimulation.stdout.trim()) {
21260
+ lines.push(" stdout:");
21261
+ for (const line of app.credentialSimulation.stdout.trim().split(/\r?\n/))lines.push(` ${line}`);
21262
+ }
21263
+ if (app.credentialSimulation.stderr.trim()) {
21264
+ lines.push(" stderr:");
21265
+ for (const line of app.credentialSimulation.stderr.trim().split(/\r?\n/))lines.push(` ${line}`);
21266
+ }
21267
+ }
21268
+ if (app.findings.length > 0) {
21269
+ lines.push(" Findings:");
21270
+ for (const item of app.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
21271
+ }
21272
+ }
21273
+ if (report.findings.length > 0) {
21274
+ lines.push("");
21275
+ lines.push(external_chalk_default().bold("Repository findings:"));
21276
+ for (const item of report.findings)lines.push(` ${formatStatus(item.status)} ${item.code}: ${item.message}`);
21277
+ }
21278
+ return lines.join("\n");
21279
+ }
21280
+ function registerGitAuthCommand(program) {
21281
+ const gitAuthCommand = program.command("git-auth").description("Diagnose Anvil Git authentication");
21282
+ gitAuthCommand.command("doctor").description("Diagnose why plain Git commands cannot authenticate to Anvil").option("--repo <PATH>", "Repository path to diagnose", process.cwd()).action(async (options)=>{
19639
21283
  try {
19640
- const globalOptions = command?.optsWithGlobals() || options || {};
19641
- if ("number" == typeof options?.depth && (!Number.isFinite(options.depth) || options.depth <= 0)) throw new Error("--depth must be a positive integer");
19642
- await executeCheckoutInteractive({
19643
- input,
19644
- directory,
19645
- open: options?.open,
19646
- branch: options?.branch,
19647
- depth: options?.depth,
19648
- singleBranch: options?.singleBranch,
19649
- origin: options?.origin,
19650
- quiet: options?.quiet,
19651
- verbose: globalOptions.verbose,
19652
- url: options?.url,
19653
- user: options?.user,
19654
- force: options?.force,
19655
- query: options?.query
19656
- }, defaultCheckoutDeps);
21284
+ const report = await buildGitAuthDoctorReport(options.repo || process.cwd());
21285
+ if (getGlobalOutputConfig().jsonMode) logJsonResult("error" !== report.status, {
21286
+ data: report
21287
+ });
21288
+ else logger_logger.info(formatGitAuthDoctorReport(report));
21289
+ if ("error" === report.status) process.exitCode = 1;
19657
21290
  } catch (e) {
19658
- logger_logger.error("Error: " + errors_getErrorMessage(e));
19659
- process.exit(1);
21291
+ const message = redactSecrets(e.message);
21292
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
21293
+ error: message
21294
+ });
21295
+ else logger_logger.error(`Git auth doctor failed: ${message}`);
21296
+ process.exitCode = 1;
19660
21297
  }
19661
21298
  });
19662
- checkoutCommand.addHelpText("after", "\n" + external_chalk_default().bold("Examples:") + '\n anvil checkout http://localhost:3000/build/apps/APPID/code/assets/theme.css\n anvil checkout https://anvil.works/git/APPID.git\n anvil checkout APPID --url anvil.works\n anvil checkout APPID --branch master --depth 1 --single-branch\n anvil checkout APPID -O # open editor/file browser after checkout\n anvil checkout APPID my-local-folder --force\n anvil checkout # interactive search/select app list\n anvil checkout -Q "dashboard" # preseed picker search\n');
19663
21299
  }
19664
21300
  const gitCredential_defaultDeps = {
19665
21301
  getValidAuthToken: auth_getValidAuthToken,
@@ -19667,6 +21303,9 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19667
21303
  getAppAuthBinding: getAppAuthBinding,
19668
21304
  cwd: ()=>process.cwd()
19669
21305
  };
21306
+ function sanitizeCredentialError(message) {
21307
+ return message.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>");
21308
+ }
19670
21309
  function parseGitCredentialRequest(rawInput) {
19671
21310
  const out = {};
19672
21311
  const lines = rawInput.split(/\r?\n/);
@@ -19727,7 +21366,11 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19727
21366
  }).description("Internal helper command for Git HTTPS authentication").action(async (operation)=>{
19728
21367
  try {
19729
21368
  await executeGitCredentialOperation(operation);
19730
- } catch {
21369
+ } catch (e) {
21370
+ if ("1" === process.env["ANVIL_GIT_CREDENTIAL_DEBUG"]) {
21371
+ const message = e instanceof Error ? e.message : String(e);
21372
+ process.stderr.write(`anvil git-credential failed: ${sanitizeCredentialError(message)}\n`);
21373
+ }
19731
21374
  process.exit(1);
19732
21375
  }
19733
21376
  });
@@ -19745,7 +21388,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19745
21388
  authToken,
19746
21389
  currentBranch,
19747
21390
  commitId,
19748
- username
21391
+ username,
21392
+ dependencyWatch: false
19749
21393
  });
19750
21394
  await session.initialize();
19751
21395
  await session.syncRemoteChanges();
@@ -19842,7 +21486,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19842
21486
  stagedOnly,
19843
21487
  username,
19844
21488
  environment,
19845
- envBranchChangeMode: options.envBranchChangeMode ?? "manual"
21489
+ envBranchChangeMode: options.envBranchChangeMode ?? "manual",
21490
+ dependencyWatch: options.dependencyWatch ?? true
19846
21491
  });
19847
21492
  session.on("branch-changed", (data)=>{
19848
21493
  logger_logger.debug("Event: branch-changed", data);
@@ -20181,7 +21826,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20181
21826
  anvilUrl: options.anvilUrl,
20182
21827
  stagedOnly: options.stagedOnly,
20183
21828
  username: options.username,
20184
- envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual"
21829
+ envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual",
21830
+ dependencyWatch: options.dependencyWatch ?? true
20185
21831
  });
20186
21832
  logger_logger.progressEnd("verify");
20187
21833
  const currentCategory = getSyncStateCategory(freshSession.syncStatus);
@@ -20205,7 +21851,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20205
21851
  anvilUrl: options.anvilUrl,
20206
21852
  stagedOnly: options.stagedOnly,
20207
21853
  username: options.username,
20208
- envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual"
21854
+ envBranchChangeMode: options.autoMode ? "auto-clean-switch" : "manual",
21855
+ dependencyWatch: options.dependencyWatch ?? true
20209
21856
  });
20210
21857
  logger_logger.progressEnd("init");
20211
21858
  return await checkSyncStatusAndStart(newSession, options);
@@ -20455,7 +22102,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20455
22102
  anvilUrl,
20456
22103
  stagedOnly,
20457
22104
  username: options.username,
20458
- envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual"
22105
+ envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual",
22106
+ dependencyWatch: options.dependencyWatch ?? true
20459
22107
  });
20460
22108
  logger_logger.progressEnd("init");
20461
22109
  const started = await checkSyncStatusAndStart(newSession, options);
@@ -20542,7 +22190,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20542
22190
  logger_logger.error("'sync' has been renamed to 'watch'. Please use 'anvil watch' instead.");
20543
22191
  process.exit(1);
20544
22192
  }
20545
- const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
22193
+ const { path: repoPath = process.cwd(), appid: explicitAppId, useFirst = false, stagedOnly = false, dependencyWatch = true, autoMode = false, url: explicitUrl, user: explicitUsername, open: openAfterValidation = false } = options;
20546
22194
  setRepoContext(repoPath);
20547
22195
  try {
20548
22196
  const validationResult = await validateAnvilApp(repoPath);
@@ -20698,7 +22346,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20698
22346
  anvilUrl,
20699
22347
  stagedOnly,
20700
22348
  username,
20701
- envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual"
22349
+ envBranchChangeMode: autoMode ? "auto-clean-switch" : "manual",
22350
+ dependencyWatch
20702
22351
  });
20703
22352
  logger_logger.progressEnd("init");
20704
22353
  if (openAfterValidation) await openWatchPath(resolveWatchOpenPath(repoPath));
@@ -20709,6 +22358,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20709
22358
  appId: finalAppId,
20710
22359
  anvilUrl,
20711
22360
  stagedOnly,
22361
+ dependencyWatch,
20712
22362
  username
20713
22363
  });
20714
22364
  if (!started) process.exit(1);
@@ -20718,7 +22368,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20718
22368
  }
20719
22369
  }
20720
22370
  function registerWatchCommand(program) {
20721
- const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
22371
+ const watchCommand = program.command("watch [path]").description("Watch for file changes and sync to Anvil").alias("sync").alias("w").option("-A, --appid <APP_ID>", "Specify app ID directly").option("-f, --first", "Auto-select first detected app ID without confirmation").option("-s, --staged-only", "Only sync staged changes (use git add to stage files)").option("--no-dependency-watch", "Do not watch and refresh .anvil/deps dependencies").option("-a, --auto", "Auto mode: restart on branch changes and sync when behind").option("-O, --open", "Open watched path in preferred editor (or default app)").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL (e.g., anvil.works, localhost)").option("-U, --user <USERNAME>", "Specify which user account to use").action(async (path, options, command)=>{
20722
22372
  const globalOptions = command.optsWithGlobals();
20723
22373
  if (void 0 !== globalOptions.verbose && logger_logger instanceof CLILogger) logger_logger.setVerbose(globalOptions.verbose);
20724
22374
  await handleWatchCommand({
@@ -20726,6 +22376,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20726
22376
  appid: options.appid,
20727
22377
  useFirst: options.first,
20728
22378
  stagedOnly: options.stagedOnly,
22379
+ dependencyWatch: options.dependencyWatch,
20729
22380
  autoMode: options.auto,
20730
22381
  verbose: globalOptions.verbose,
20731
22382
  open: options.open,
@@ -20977,6 +22628,178 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
20977
22628
  }
20978
22629
  });
20979
22630
  }
22631
+ function formSpecContext_isPlainObject(value) {
22632
+ return "object" == typeof value && null !== value && !Array.isArray(value);
22633
+ }
22634
+ function collectFormSpecReferences(value, references) {
22635
+ if ("string" == typeof value) {
22636
+ if (value.startsWith("form:") || /^[^:]+:[^:]+$/.test(value)) references.add(value);
22637
+ return;
22638
+ }
22639
+ if (Array.isArray(value)) {
22640
+ for (const item of value)collectFormSpecReferences(item, references);
22641
+ return;
22642
+ }
22643
+ if (!formSpecContext_isPlainObject(value)) return;
22644
+ for (const [key, nested] of Object.entries(value)){
22645
+ if ("type" === key && "string" == typeof nested && nested.startsWith("form:")) references.add(nested);
22646
+ if ("item_template" === key && "string" == typeof nested) references.add(nested);
22647
+ collectFormSpecReferences(nested, references);
22648
+ }
22649
+ }
22650
+ function formYamlHasFormSpecReferences(form) {
22651
+ const references = new Set();
22652
+ collectFormSpecReferences(form, references);
22653
+ return references.size > 0;
22654
+ }
22655
+ async function readAnvilYamlConfig(appRoot) {
22656
+ try {
22657
+ const content = await external_fs_.promises.readFile(external_path_default().join(appRoot, "anvil.yaml"), "utf8");
22658
+ const parsed = external_js_yaml_default().load(content);
22659
+ return formSpecContext_isPlainObject(parsed) ? parsed : void 0;
22660
+ } catch {
22661
+ return;
22662
+ }
22663
+ }
22664
+ function resolveDependencyPackageName(dependency, dependencyIds, cachedPackageNamesByAppId) {
22665
+ const packageName = dependency.resolution_hints?.package_name;
22666
+ if (packageName) return packageName;
22667
+ const depId = dependency.dep_id;
22668
+ if (!depId) return;
22669
+ const appId = dependency.resolution_hints?.app_id ?? dependencyIds?.[depId];
22670
+ if (!appId) return;
22671
+ return cachedPackageNamesByAppId.get(appId);
22672
+ }
22673
+ async function buildFormSpecPackageContext(sourcePath) {
22674
+ const appRoot = findAnvilAppRoot(sourcePath);
22675
+ if (!appRoot) return;
22676
+ const config = await readAnvilYamlConfig(appRoot);
22677
+ if (!config) return;
22678
+ const manifest = await readDepsCacheManifest(appRoot);
22679
+ const cachedPackageNamesByAppId = new Map((manifest?.cached_dependencies ?? []).map((entry)=>[
22680
+ entry.app_id,
22681
+ entry.package_name
22682
+ ]));
22683
+ const dependencyPackageNamesByLogicalDepId = {};
22684
+ const knownPackageNames = new Set();
22685
+ for (const dependency of config.dependencies ?? []){
22686
+ const depId = dependency.dep_id;
22687
+ if (!depId) continue;
22688
+ const packageName = resolveDependencyPackageName(dependency, config.dependency_ids, cachedPackageNamesByAppId);
22689
+ dependencyPackageNamesByLogicalDepId[depId] = packageName;
22690
+ if (packageName) knownPackageNames.add(packageName);
22691
+ }
22692
+ return {
22693
+ appPackageName: config.package_name,
22694
+ knownPackageNames: [
22695
+ ...knownPackageNames
22696
+ ],
22697
+ dependencyPackageNamesByLogicalDepId
22698
+ };
22699
+ }
22700
+ class ConvertTemplateValidationError extends Error {
22701
+ issues;
22702
+ constructor(message, issues){
22703
+ super(message), this.issues = issues;
22704
+ this.name = "ConvertTemplateValidationError";
22705
+ }
22706
+ }
22707
+ function hasYamlExtension(filePath) {
22708
+ return /\.ya?ml$/i.test(filePath);
22709
+ }
22710
+ function getDefaultConvertedTemplatePath(inputPath) {
22711
+ return inputPath.replace(/\.ya?ml$/i, ".html");
22712
+ }
22713
+ function resolveConvertTemplatePaths(inputPath, options = {}) {
22714
+ const cwd = options.cwd ?? process.cwd();
22715
+ if (!hasYamlExtension(inputPath)) throw new Error("Input template must be a .yaml or .yml file");
22716
+ const sourcePath = external_path_default().resolve(cwd, inputPath);
22717
+ const outputPath = external_path_default().resolve(cwd, options.output ?? getDefaultConvertedTemplatePath(inputPath));
22718
+ if (sourcePath === outputPath) throw new Error("Output path must be different from the input path");
22719
+ return {
22720
+ sourcePath,
22721
+ outputPath
22722
+ };
22723
+ }
22724
+ async function pathExists(filePath) {
22725
+ try {
22726
+ await external_fs_.promises.access(filePath);
22727
+ return true;
22728
+ } catch {
22729
+ return false;
22730
+ }
22731
+ }
22732
+ async function writeFileAtomically(filePath, content) {
22733
+ const directory = external_path_default().dirname(filePath);
22734
+ await external_fs_.promises.mkdir(directory, {
22735
+ recursive: true
22736
+ });
22737
+ const tempPath = external_path_default().join(directory, `.${external_path_default().basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
22738
+ await external_fs_.promises.writeFile(tempPath, content, "utf8");
22739
+ await external_fs_.promises.rename(tempPath, filePath);
22740
+ }
22741
+ async function loadParser() {
22742
+ return import("@anvil-works/form-template-parser");
22743
+ }
22744
+ async function convertTemplateFile(inputPath, options = {}) {
22745
+ const { sourcePath, outputPath } = resolveConvertTemplatePaths(inputPath, options);
22746
+ if (!options.force && await pathExists(outputPath)) throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite it.`);
22747
+ const yamlContent = await external_fs_.promises.readFile(sourcePath, "utf8");
22748
+ const validationResult = validateFormTemplate(yamlContent);
22749
+ if (!validationResult.ok) throw new ConvertTemplateValidationError(validationResult.message, validationResult.issues);
22750
+ let template = external_js_yaml_default().load(yamlContent);
22751
+ const parser = await loadParser();
22752
+ const context = await buildFormSpecPackageContext(sourcePath);
22753
+ if (context) template = parser.copyFormYamlWithCanonicalizedFormSpecs(template, context).form;
22754
+ else if (formYamlHasFormSpecReferences(template)) logger_logger.warn("Skipping form-spec canonicalization: no anvil.yaml found for this template");
22755
+ const html = parser.serializeFormTemplateHtml(template, {
22756
+ allowReparse: true
22757
+ });
22758
+ await writeFileAtomically(outputPath, html.endsWith("\n") ? html : `${html}\n`);
22759
+ let removedSource = false;
22760
+ if (!options.keepSource) try {
22761
+ await external_fs_.promises.unlink(sourcePath);
22762
+ removedSource = true;
22763
+ } catch (error) {
22764
+ throw new Error(`Converted template written to ${outputPath}, but failed to remove source ${sourcePath}: ${error.message}`);
22765
+ }
22766
+ return {
22767
+ source: sourcePath,
22768
+ output: outputPath,
22769
+ removedSource
22770
+ };
22771
+ }
22772
+ function registerConvertTemplateCommand(program) {
22773
+ program.command("convert-template <file>").description("Convert an Anvil form template YAML file to HTML template format").option("-o, --output <path>", "Output HTML path").option("-f, --force", "Overwrite the output file if it already exists").option("--keep-source", "Keep the source YAML file after conversion").action(async (file, options)=>{
22774
+ try {
22775
+ const result = await convertTemplateFile(file, options);
22776
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
22777
+ data: result
22778
+ });
22779
+ else {
22780
+ logger_logger.success(`Converted ${result.source} to ${result.output}`);
22781
+ if (result.removedSource) logger_logger.info(`Removed source ${result.source}`);
22782
+ }
22783
+ } catch (error) {
22784
+ if (error instanceof ConvertTemplateValidationError) if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
22785
+ error: error.message,
22786
+ data: {
22787
+ file,
22788
+ issues: error.issues
22789
+ }
22790
+ });
22791
+ else {
22792
+ logger_logger.error(`${file} failed validation: ${error.message}`);
22793
+ for (const issue of error.issues)logger_logger.error(` - ${formatValidationPath(issue.path)}: ${issue.message}`);
22794
+ }
22795
+ else if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
22796
+ error: error.message
22797
+ });
22798
+ else logger_logger.error(error.message);
22799
+ process.exitCode = 1;
22800
+ }
22801
+ });
22802
+ }
20980
22803
  function getVersionInfo(version) {
20981
22804
  return {
20982
22805
  version,
@@ -21157,6 +22980,160 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
21157
22980
  }
21158
22981
  });
21159
22982
  }
22983
+ async function resolvePrimaryAppId(projectRoot, anvilUrl) {
22984
+ const remoteCandidates = await detectAppIdsFromAllRemotes(projectRoot);
22985
+ if (remoteCandidates.length > 0) return remoteCandidates[0].appId;
22986
+ const lookupCandidates = await detectAppIdsByCommitLookup(projectRoot, {
22987
+ anvilUrl,
22988
+ includeRemotes: false
22989
+ });
22990
+ return lookupCandidates[0]?.appId ?? null;
22991
+ }
22992
+ function registerDepsCommand(program) {
22993
+ const deps = program.command("deps").description("Manage app dependencies");
22994
+ deps.command("fetch").description("Fetch dependencies into .anvil/deps/").option("--commit <commit>", "Fetch deps at specific commit").option("--exclude-docs", "Exclude dependency docs from .anvil/deps/").action(async (options)=>{
22995
+ try {
22996
+ const projectRoot = process.cwd();
22997
+ const anvilUrl = resolveAnvilUrl();
22998
+ const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
22999
+ if (!mainAppId) {
23000
+ logger_logger.error("No Anvil app found in current directory");
23001
+ logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23002
+ process.exit(1);
23003
+ }
23004
+ logger_logger.info(`Fetching dependencies for app ${mainAppId}...`);
23005
+ const { response, addedGitignoreEntry } = await refreshDependencyCache(projectRoot, mainAppId, anvilUrl, {
23006
+ commit: options.commit,
23007
+ includeDocs: !options.excludeDocs
23008
+ });
23009
+ if (!response.resolved_dependencies.length) return void logger_logger.info("No dependencies to fetch");
23010
+ if (addedGitignoreEntry) logger_logger.info("Added /.anvil/ to .gitignore");
23011
+ if (!response.dependencies.length) return void logger_logger.success(`Dependencies already up to date (${response.resolved_dependencies.length} cached)`);
23012
+ logger_logger.success(`Fetched ${response.dependencies.length} dependencies:`);
23013
+ for (const dep of response.dependencies)logger_logger.info(` - ${dep.package_name}`);
23014
+ } catch (e) {
23015
+ logger_logger.error(e.message);
23016
+ process.exit(1);
23017
+ }
23018
+ });
23019
+ deps.command("status").description("Show cached dependencies").action(async ()=>{
23020
+ try {
23021
+ const cached = await getCachedPackages(process.cwd());
23022
+ if (!cached.length) return void logger_logger.info("No cached dependencies (run 'anvil deps fetch')");
23023
+ logger_logger.info("Cached dependencies:");
23024
+ for (const pkg of cached)logger_logger.info(` - ${pkg}`);
23025
+ } catch (e) {
23026
+ logger_logger.error(e.message);
23027
+ process.exit(1);
23028
+ }
23029
+ });
23030
+ deps.command("clear").description("Clear cached dependencies").action(async ()=>{
23031
+ try {
23032
+ await clearDepsCache(process.cwd());
23033
+ logger_logger.success("Cleared .anvil/deps/");
23034
+ } catch (e) {
23035
+ logger_logger.error(e.message);
23036
+ process.exit(1);
23037
+ }
23038
+ });
23039
+ }
23040
+ const TABLE_MAPPINGS_PATH = ".anvil/table-mappings.json";
23041
+ function tables_formatFetchError(prefix, status, errorBody) {
23042
+ if (!errorBody) return `${prefix}: ${status}`;
23043
+ try {
23044
+ const parsed = JSON.parse(errorBody);
23045
+ if ("string" == typeof parsed.error && parsed.error.trim()) return `${prefix}: ${parsed.error} (${status})`;
23046
+ } catch {}
23047
+ return `${prefix}: ${status} ${errorBody}`;
23048
+ }
23049
+ async function fetchTableMappings(appId, anvilUrl = resolveAnvilUrl()) {
23050
+ const token = await auth_getValidAuthToken(anvilUrl);
23051
+ const resp = await fetch(`${anvilUrl}/ide/api/_/apps/${appId}/db`, {
23052
+ headers: {
23053
+ Authorization: `Bearer ${token}`
23054
+ }
23055
+ });
23056
+ if (!resp.ok) {
23057
+ const errorBody = await resp.text();
23058
+ throw new Error(tables_formatFetchError("Failed to fetch table mappings", resp.status, errorBody));
23059
+ }
23060
+ const databases = await resp.json();
23061
+ const allTables = [];
23062
+ for (const db of databases)for (const table of db.tables || [])allTables.push({
23063
+ table_id: table.id,
23064
+ python_name: table.python_name,
23065
+ name: table.name
23066
+ });
23067
+ return {
23068
+ tables: allTables
23069
+ };
23070
+ }
23071
+ async function writeTableMappings(projectRoot, mappings) {
23072
+ const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23073
+ await promises_default().mkdir(external_path_default().dirname(filePath), {
23074
+ recursive: true
23075
+ });
23076
+ await promises_default().writeFile(filePath, JSON.stringify(mappings, null, 2));
23077
+ }
23078
+ async function readTableMappings(projectRoot) {
23079
+ const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23080
+ try {
23081
+ const content = await promises_default().readFile(filePath, "utf-8");
23082
+ return JSON.parse(content);
23083
+ } catch {
23084
+ return null;
23085
+ }
23086
+ }
23087
+ async function clearTableMappings(projectRoot) {
23088
+ const filePath = external_path_default().join(projectRoot, TABLE_MAPPINGS_PATH);
23089
+ await promises_default().rm(filePath, {
23090
+ force: true
23091
+ });
23092
+ }
23093
+ function registerTablesCommand(program) {
23094
+ const tables = program.command("tables").description("Manage table mappings");
23095
+ tables.command("fetch").description("Fetch table mappings into .anvil/table-mappings.json").action(async ()=>{
23096
+ try {
23097
+ const projectRoot = process.cwd();
23098
+ const anvilUrl = resolveAnvilUrl();
23099
+ const mainAppId = await resolvePrimaryAppId(projectRoot, anvilUrl);
23100
+ if (!mainAppId) {
23101
+ logger_logger.error("No Anvil app found in current directory");
23102
+ logger_logger.info("Make sure you're in a directory with an Anvil app git remote.");
23103
+ process.exit(1);
23104
+ }
23105
+ logger_logger.info("Fetching table mappings...");
23106
+ const response = await fetchTableMappings(mainAppId, anvilUrl);
23107
+ if (!response.tables.length) return void logger_logger.info("No tables found");
23108
+ if (await ensureProjectGitignoreIgnoresAnvilDir(projectRoot)) logger_logger.info("Added /.anvil/ to .gitignore");
23109
+ await writeTableMappings(projectRoot, response);
23110
+ logger_logger.success(`Fetched ${response.tables.length} table mappings`);
23111
+ } catch (e) {
23112
+ logger_logger.error(e.message);
23113
+ process.exit(1);
23114
+ }
23115
+ });
23116
+ tables.command("status").description("Show cached table mappings").action(async ()=>{
23117
+ try {
23118
+ const mappings = await readTableMappings(process.cwd());
23119
+ if (!mappings || !mappings.tables.length) return void logger_logger.info("No cached table mappings (run 'anvil tables fetch')");
23120
+ logger_logger.info("Cached table mappings:");
23121
+ for (const t of mappings.tables)logger_logger.info(` ${t.python_name} (${t.name}) - ID: ${t.table_id}`);
23122
+ } catch (e) {
23123
+ logger_logger.error(e.message);
23124
+ process.exit(1);
23125
+ }
23126
+ });
23127
+ tables.command("clear").description("Clear cached table mappings").action(async ()=>{
23128
+ try {
23129
+ await clearTableMappings(process.cwd());
23130
+ logger_logger.success("Cleared .anvil/table-mappings.json");
23131
+ } catch (e) {
23132
+ logger_logger.error(e.message);
23133
+ process.exit(1);
23134
+ }
23135
+ });
23136
+ }
21160
23137
  const program_packageJson = JSON.parse((0, external_fs_.readFileSync)((0, external_path_namespaceObject.join)(__dirname, "../package.json"), "utf-8"));
21161
23138
  const VERSION = program_packageJson.version;
21162
23139
  setLogger(new CLILogger({
@@ -21174,6 +23151,123 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
21174
23151
  }
21175
23152
  }).catch(()=>{});
21176
23153
  }
23154
+ function collectValidationFiles(dir) {
23155
+ const files = [];
23156
+ const ignoredDirectories = new Set([
23157
+ ".anvil",
23158
+ ".git",
23159
+ "node_modules",
23160
+ "__pycache__"
23161
+ ]);
23162
+ function walk(currentDir) {
23163
+ for (const entry of (0, external_fs_.readdirSync)(currentDir, {
23164
+ withFileTypes: true
23165
+ })){
23166
+ const entryPath = (0, external_path_namespaceObject.join)(currentDir, entry.name);
23167
+ if (entry.isDirectory()) {
23168
+ if (!ignoredDirectories.has(entry.name)) walk(entryPath);
23169
+ continue;
23170
+ }
23171
+ if (entry.isFile() && inferValidationTarget(entryPath)) files.push(entryPath);
23172
+ }
23173
+ }
23174
+ walk(dir);
23175
+ return files.sort();
23176
+ }
23177
+ function validateSingleFile(file) {
23178
+ let fileContent;
23179
+ try {
23180
+ fileContent = (0, external_fs_.readFileSync)(file, "utf8");
23181
+ } catch (error) {
23182
+ return {
23183
+ file,
23184
+ type: "unknown",
23185
+ valid: false,
23186
+ error: `Failed to read ${file}: ${error.message}`,
23187
+ issues: [
23188
+ {
23189
+ path: "",
23190
+ message: error.message
23191
+ }
23192
+ ]
23193
+ };
23194
+ }
23195
+ const result = validatePath(file, fileContent);
23196
+ if (result.ok) return {
23197
+ file,
23198
+ type: result.target,
23199
+ valid: true
23200
+ };
23201
+ return {
23202
+ file,
23203
+ type: result.target,
23204
+ valid: false,
23205
+ error: result.message,
23206
+ issues: result.issues
23207
+ };
23208
+ }
23209
+ function logValidationFailure(fileResult) {
23210
+ logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
23211
+ for (const issue of fileResult.issues ?? []){
23212
+ const issuePath = issue.path.length > 0 ? issue.path : "root";
23213
+ logger_logger.error(` - ${issuePath}: ${issue.message}`);
23214
+ }
23215
+ }
23216
+ function handleValidateFile(file) {
23217
+ const fileResult = validateSingleFile(file);
23218
+ if (fileResult.valid) {
23219
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23220
+ data: {
23221
+ file: fileResult.file,
23222
+ type: fileResult.type,
23223
+ valid: true
23224
+ }
23225
+ });
23226
+ else logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23227
+ return;
23228
+ }
23229
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23230
+ error: fileResult.error,
23231
+ data: {
23232
+ file: fileResult.file,
23233
+ type: fileResult.type,
23234
+ issues: fileResult.issues ?? []
23235
+ }
23236
+ });
23237
+ else logValidationFailure(fileResult);
23238
+ process.exitCode = 1;
23239
+ }
23240
+ function handleValidateDirectory(dir) {
23241
+ const files = collectValidationFiles(dir);
23242
+ if (0 === files.length) {
23243
+ const error = `No supported files found under ${dir}`;
23244
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23245
+ error,
23246
+ data: {
23247
+ file: dir,
23248
+ valid: false,
23249
+ files: []
23250
+ }
23251
+ });
23252
+ else logger_logger.error(error);
23253
+ process.exitCode = 1;
23254
+ return;
23255
+ }
23256
+ const fileResults = files.map(validateSingleFile);
23257
+ const failedResults = fileResults.filter((result)=>!result.valid);
23258
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
23259
+ error: failedResults.length > 0 ? "Validation failed" : void 0,
23260
+ data: {
23261
+ file: dir,
23262
+ valid: 0 === failedResults.length,
23263
+ count: fileResults.length,
23264
+ files: fileResults
23265
+ }
23266
+ });
23267
+ else for (const fileResult of fileResults)if (fileResult.valid) logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23268
+ else logValidationFailure(fileResult);
23269
+ if (failedResults.length > 0) process.exitCode = 1;
23270
+ }
21177
23271
  function getUpdateInstructions(platform) {
21178
23272
  if ("win32" === platform) return [
21179
23273
  "try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
@@ -21274,52 +23368,30 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
21274
23368
  });
21275
23369
  const watchCommand = registerWatchCommand(program);
21276
23370
  registerCheckoutCommand(program);
23371
+ registerGitAuthCommand(program);
21277
23372
  registerGitCredentialCommand(program);
21278
23373
  registerLoginCommand(program);
21279
23374
  registerLogoutCommand(program);
21280
23375
  registerConfigCommand(program);
23376
+ registerConvertTemplateCommand(program);
21281
23377
  registerVersionCommand(program, VERSION);
21282
23378
  registerConfigureCommand(program, VERSION);
23379
+ registerDepsCommand(program);
23380
+ registerTablesCommand(program);
21283
23381
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
21284
23382
  await handleUpdateCommand();
21285
23383
  });
21286
- program.command("validate").description("Validate YAML based on path (anvil.yaml or client_code/**/*.yaml)").argument("<file>", "Path to YAML file").action((file)=>{
21287
- let yamlContent;
23384
+ program.command("validate").description("Validate Anvil app files by path or directory (anvil.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, or server_code/**/*.py)").argument("<path>", "Path to file or directory").action((file)=>{
23385
+ let pathStat;
21288
23386
  try {
21289
- yamlContent = (0, external_fs_.readFileSync)(file, "utf8");
23387
+ pathStat = (0, external_fs_.statSync)(file);
21290
23388
  } catch (error) {
21291
- logger_logger.error(`Failed to read ${file}: ${error.message}`);
23389
+ logger_logger.error(`Failed to access ${file}: ${error.message}`);
21292
23390
  process.exitCode = 1;
21293
23391
  return;
21294
23392
  }
21295
- const result = validatePath(file, yamlContent);
21296
- if (result.ok) {
21297
- if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
21298
- data: {
21299
- file,
21300
- type: result.target,
21301
- valid: true
21302
- }
21303
- });
21304
- else logger_logger.success(`${file} is valid (${result.target})`);
21305
- return;
21306
- }
21307
- if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
21308
- error: result.message,
21309
- data: {
21310
- file,
21311
- type: result.target,
21312
- issues: result.issues
21313
- }
21314
- });
21315
- else {
21316
- logger_logger.error(`${file} failed validation: ${result.message}`);
21317
- for (const issue of result.issues){
21318
- const issuePath = issue.path.length > 0 ? issue.path : "root";
21319
- logger_logger.error(` - ${issuePath}: ${issue.message}`);
21320
- }
21321
- }
21322
- process.exitCode = 1;
23393
+ if (pathStat.isDirectory()) return void handleValidateDirectory(file);
23394
+ handleValidateFile(file);
21323
23395
  });
21324
23396
  if (watchCommand) {
21325
23397
  const watchOptions = watchCommand.options.map((opt)=>{