@anvil-works/anvil-cli 0.7.0-canary.16 → 0.7.0-canary.18

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.
package/README.md CHANGED
@@ -170,17 +170,18 @@ Useful options:
170
170
 
171
171
  ### `anvil validate`
172
172
 
173
- Check an Anvil app file before syncing, or when the CLI reports a validation error.
173
+ Check an Anvil app file or directory before syncing, or when the CLI reports a validation error.
174
174
 
175
175
  ```bash
176
- anvil validate <file>
176
+ anvil validate <path>
177
177
  ```
178
178
 
179
- The command chooses the validator from the file path. It supports:
179
+ When given a file, the command chooses the validator from the file path. When given a directory, it recursively validates supported files under that directory. It supports:
180
180
 
181
181
  - `anvil.yaml`
182
182
  - `.yaml` files under `client_code/`, including `client_code/**/form_template.yaml`
183
183
  - `.html` files under `client_code/`, including `client_code/**/form_template.html` and module-form `client_code/<Form>.html`
184
+ - `.py` files under `client_code/` and `server_code/`
184
185
 
185
186
  Common examples:
186
187
 
@@ -190,10 +191,15 @@ anvil validate client_code/Form1/form_template.yaml
190
191
  anvil validate client_code/Form1.yaml
191
192
  anvil validate client_code/Form1/form_template.html
192
193
  anvil validate client_code/Form1.html
194
+ anvil validate client_code/Helpers.py
195
+ anvil validate server_code/ServerModule.py
196
+ anvil validate .
193
197
  ```
194
198
 
195
199
  HTML validation parses the template with `@anvil-works/form-template-parser`, checks optional YAML frontmatter, and reports semantic issues such as duplicate explicit component names or invalid component types.
196
200
 
201
+ Python validation checks syntax using a local Python interpreter (`ANVIL_PYTHON`, `python3`, or `python`). Form Python under `client_code/` also checks for the matching designer-template import and form class inheritance when a sibling form template exists.
202
+
197
203
  When the file is valid, the command prints a success message and exits with status `0`. When the file is invalid, it prints path-specific validation issues and exits with a non-zero status.
198
204
 
199
205
  Use `--json` with `anvil validate` to emit structured NDJSON output for scripts or LLM tooling.
@@ -1 +1 @@
1
- {"version":3,"file":"WatchSession.d.ts","sourceRoot":"","sources":["../src/WatchSession.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"WatchSession.d.ts","sourceRoot":"","sources":["../src/WatchSession.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -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;
@@ -16249,6 +16288,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16249
16288
  ok: true
16250
16289
  };
16251
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() {
16360
+ const candidates = [
16361
+ process.env.ANVIL_PYTHON,
16362
+ "python3",
16363
+ "python"
16364
+ ].filter((candidate)=>"string" == typeof candidate && candidate.length > 0);
16365
+ for (const candidate of candidates){
16366
+ const result = (0, external_child_process_namespaceObject.spawnSync)(candidate, [
16367
+ "--version"
16368
+ ], {
16369
+ encoding: "utf8"
16370
+ });
16371
+ if (!result.error && 0 === result.status) return candidate;
16372
+ }
16373
+ return null;
16374
+ }
16375
+ function isClientFormPython(filePath) {
16376
+ if ("__init__.py" === external_path_default().basename(filePath)) {
16377
+ const formDir = external_path_default().dirname(filePath);
16378
+ return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
16379
+ }
16380
+ const parsed = external_path_default().parse(filePath);
16381
+ 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`));
16382
+ }
16383
+ function expectedFormClassName(filePath) {
16384
+ if (!isClientFormPython(filePath)) return null;
16385
+ if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
16386
+ return external_path_default().basename(filePath, ".py");
16387
+ }
16388
+ function isUnderDirectory(filePath, directoryPath) {
16389
+ const relativePath = external_path_default().relative(directoryPath, filePath);
16390
+ return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
16391
+ }
16392
+ function validatePython(filePath, fileContent, appRoot) {
16393
+ const interpreter = findPythonInterpreter();
16394
+ if (!interpreter) return {
16395
+ ok: false,
16396
+ message: "Python file could not be validated",
16397
+ issues: [
16398
+ {
16399
+ path: "root",
16400
+ message: "Python validation requires python3 or python on PATH, or ANVIL_PYTHON to be set"
16401
+ }
16402
+ ]
16403
+ };
16404
+ const absolutePath = external_path_default().resolve(filePath);
16405
+ const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
16406
+ const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
16407
+ const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter, [
16408
+ "-c",
16409
+ PYTHON_VALIDATOR_SCRIPT,
16410
+ filePath,
16411
+ formClassName ?? ""
16412
+ ], {
16413
+ encoding: "utf8",
16414
+ input: fileContent,
16415
+ maxBuffer: 1048576
16416
+ });
16417
+ if (result.error || 0 !== result.status) return {
16418
+ ok: false,
16419
+ message: "Python file could not be validated",
16420
+ issues: [
16421
+ {
16422
+ path: "root",
16423
+ message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
16424
+ }
16425
+ ]
16426
+ };
16427
+ let parsed;
16428
+ try {
16429
+ parsed = JSON.parse(result.stdout);
16430
+ } catch (error) {
16431
+ return {
16432
+ ok: false,
16433
+ message: "Python file could not be validated",
16434
+ issues: [
16435
+ {
16436
+ path: "root",
16437
+ message: error instanceof Error ? error.message : String(error)
16438
+ }
16439
+ ]
16440
+ };
16441
+ }
16442
+ const issues = parsed.issues ?? [];
16443
+ if (issues.length > 0) return {
16444
+ ok: false,
16445
+ message: "Python file is invalid for Anvil",
16446
+ issues
16447
+ };
16448
+ return {
16449
+ ok: true
16450
+ };
16451
+ }
16252
16452
  const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16253
16453
  const DB_ACCESS_LEVELS = new Set([
16254
16454
  "none",
@@ -16646,6 +16846,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16646
16846
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
16647
16847
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16648
16848
  if (relativeToClientCode.endsWith(".html")) return "form_template.html";
16849
+ if (relativeToClientCode.endsWith(".py")) return "python";
16850
+ }
16851
+ const serverCodePrefix = "server_code/";
16852
+ if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
16853
+ const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
16854
+ const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
16855
+ if (relativeToServerCode.endsWith(".py")) return "python";
16649
16856
  }
16650
16857
  return null;
16651
16858
  }
@@ -16753,6 +16960,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16753
16960
  target
16754
16961
  };
16755
16962
  }
16963
+ if ("python" === target) {
16964
+ if (existingPath && appRoot) {
16965
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
16966
+ const serverCodeRoot = external_path_default().join(appRoot, "server_code");
16967
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
16968
+ const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
16969
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
16970
+ const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
16971
+ if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
16972
+ ok: false,
16973
+ target,
16974
+ message: "File is not an Anvil app Python file",
16975
+ issues: [
16976
+ {
16977
+ path: "",
16978
+ message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
16979
+ }
16980
+ ]
16981
+ };
16982
+ }
16983
+ const result = validatePython(filePath, fileContent, appRoot);
16984
+ return result.ok ? {
16985
+ ok: true,
16986
+ target
16987
+ } : {
16988
+ ...result,
16989
+ target
16990
+ };
16991
+ }
16756
16992
  return {
16757
16993
  ok: false,
16758
16994
  target: "unknown",
@@ -16760,7 +16996,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16760
16996
  issues: [
16761
16997
  {
16762
16998
  path: "",
16763
- message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, and client_code/**/*.html"
16999
+ message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
16764
17000
  }
16765
17001
  ]
16766
17002
  };
@@ -17845,6 +18081,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17845
18081
  }
17846
18082
  }
17847
18083
  function authDiagnostic(error) {
18084
+ if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
18085
+ code: error.type,
18086
+ recoverability: "requires_user_action",
18087
+ requiresUserAction: true,
18088
+ suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
18089
+ };
17848
18090
  return {
17849
18091
  code: error.type,
17850
18092
  recoverability: "requires_user_action",
@@ -18761,7 +19003,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18761
19003
  getAuthToken: ()=>this.authToken,
18762
19004
  setAuthToken: (token)=>{
18763
19005
  this.authToken = token;
18764
- this.wsClient?.updateAuthToken(token);
19006
+ this.wsClient?.updateAuthToken?.(token);
18765
19007
  },
18766
19008
  getCurrentBranch: ()=>this.currentBranch,
18767
19009
  getCommitId: ()=>this.commitId,
@@ -18781,7 +19023,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18781
19023
  getAuthToken: ()=>this.authToken,
18782
19024
  setAuthToken: (token)=>{
18783
19025
  this.authToken = token;
18784
- this.wsClient?.updateAuthToken(token);
19026
+ this.wsClient?.updateAuthToken?.(token);
18785
19027
  },
18786
19028
  getCurrentBranch: ()=>this.currentBranch,
18787
19029
  getCommitId: ()=>this.commitId,
@@ -18949,6 +19191,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18949
19191
  ]);
18950
19192
  }
18951
19193
  logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
19194
+ this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
19195
+ this.wsClient?.updateAuthToken?.(this.authToken);
18952
19196
  const fetchUrl = getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
18953
19197
  const envRemoteRef = environmentRemoteRefForBranch(targetBranch);
18954
19198
  await this.gitService.fetch(fetchUrl, environmentFetchRefSpecForBranch(targetBranch));
@@ -22874,6 +23118,123 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
22874
23118
  }
22875
23119
  }).catch(()=>{});
22876
23120
  }
23121
+ function collectValidationFiles(dir) {
23122
+ const files = [];
23123
+ const ignoredDirectories = new Set([
23124
+ ".anvil",
23125
+ ".git",
23126
+ "node_modules",
23127
+ "__pycache__"
23128
+ ]);
23129
+ function walk(currentDir) {
23130
+ for (const entry of (0, external_fs_.readdirSync)(currentDir, {
23131
+ withFileTypes: true
23132
+ })){
23133
+ const entryPath = (0, external_path_namespaceObject.join)(currentDir, entry.name);
23134
+ if (entry.isDirectory()) {
23135
+ if (!ignoredDirectories.has(entry.name)) walk(entryPath);
23136
+ continue;
23137
+ }
23138
+ if (entry.isFile() && inferValidationTarget(entryPath)) files.push(entryPath);
23139
+ }
23140
+ }
23141
+ walk(dir);
23142
+ return files.sort();
23143
+ }
23144
+ function validateSingleFile(file) {
23145
+ let fileContent;
23146
+ try {
23147
+ fileContent = (0, external_fs_.readFileSync)(file, "utf8");
23148
+ } catch (error) {
23149
+ return {
23150
+ file,
23151
+ type: "unknown",
23152
+ valid: false,
23153
+ error: `Failed to read ${file}: ${error.message}`,
23154
+ issues: [
23155
+ {
23156
+ path: "",
23157
+ message: error.message
23158
+ }
23159
+ ]
23160
+ };
23161
+ }
23162
+ const result = validatePath(file, fileContent);
23163
+ if (result.ok) return {
23164
+ file,
23165
+ type: result.target,
23166
+ valid: true
23167
+ };
23168
+ return {
23169
+ file,
23170
+ type: result.target,
23171
+ valid: false,
23172
+ error: result.message,
23173
+ issues: result.issues
23174
+ };
23175
+ }
23176
+ function logValidationFailure(fileResult) {
23177
+ logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
23178
+ for (const issue of fileResult.issues ?? []){
23179
+ const issuePath = issue.path.length > 0 ? issue.path : "root";
23180
+ logger_logger.error(` - ${issuePath}: ${issue.message}`);
23181
+ }
23182
+ }
23183
+ function handleValidateFile(file) {
23184
+ const fileResult = validateSingleFile(file);
23185
+ if (fileResult.valid) {
23186
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23187
+ data: {
23188
+ file: fileResult.file,
23189
+ type: fileResult.type,
23190
+ valid: true
23191
+ }
23192
+ });
23193
+ else logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23194
+ return;
23195
+ }
23196
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23197
+ error: fileResult.error,
23198
+ data: {
23199
+ file: fileResult.file,
23200
+ type: fileResult.type,
23201
+ issues: fileResult.issues ?? []
23202
+ }
23203
+ });
23204
+ else logValidationFailure(fileResult);
23205
+ process.exitCode = 1;
23206
+ }
23207
+ function handleValidateDirectory(dir) {
23208
+ const files = collectValidationFiles(dir);
23209
+ if (0 === files.length) {
23210
+ const error = `No supported files found under ${dir}`;
23211
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23212
+ error,
23213
+ data: {
23214
+ file: dir,
23215
+ valid: false,
23216
+ files: []
23217
+ }
23218
+ });
23219
+ else logger_logger.error(error);
23220
+ process.exitCode = 1;
23221
+ return;
23222
+ }
23223
+ const fileResults = files.map(validateSingleFile);
23224
+ const failedResults = fileResults.filter((result)=>!result.valid);
23225
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
23226
+ error: failedResults.length > 0 ? "Validation failed" : void 0,
23227
+ data: {
23228
+ file: dir,
23229
+ valid: 0 === failedResults.length,
23230
+ count: fileResults.length,
23231
+ files: fileResults
23232
+ }
23233
+ });
23234
+ else for (const fileResult of fileResults)if (fileResult.valid) logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23235
+ else logValidationFailure(fileResult);
23236
+ if (failedResults.length > 0) process.exitCode = 1;
23237
+ }
22877
23238
  function getUpdateInstructions(platform) {
22878
23239
  if ("win32" === platform) return [
22879
23240
  "try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
@@ -22987,43 +23348,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
22987
23348
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
22988
23349
  await handleUpdateCommand();
22989
23350
  });
22990
- program.command("validate").description("Validate an Anvil app file by path (anvil.yaml, client_code/**/*.yaml, or client_code/**/*.html)").argument("<file>", "Path to file").action((file)=>{
22991
- let fileContent;
23351
+ 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)=>{
23352
+ let pathStat;
22992
23353
  try {
22993
- fileContent = (0, external_fs_.readFileSync)(file, "utf8");
23354
+ pathStat = (0, external_fs_.statSync)(file);
22994
23355
  } catch (error) {
22995
- logger_logger.error(`Failed to read ${file}: ${error.message}`);
23356
+ logger_logger.error(`Failed to access ${file}: ${error.message}`);
22996
23357
  process.exitCode = 1;
22997
23358
  return;
22998
23359
  }
22999
- const result = validatePath(file, fileContent);
23000
- if (result.ok) {
23001
- if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23002
- data: {
23003
- file,
23004
- type: result.target,
23005
- valid: true
23006
- }
23007
- });
23008
- else logger_logger.success(`${file} is valid (${result.target})`);
23009
- return;
23010
- }
23011
- if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23012
- error: result.message,
23013
- data: {
23014
- file,
23015
- type: result.target,
23016
- issues: result.issues
23017
- }
23018
- });
23019
- else {
23020
- logger_logger.error(`${file} failed validation: ${result.message}`);
23021
- for (const issue of result.issues){
23022
- const issuePath = issue.path.length > 0 ? issue.path : "root";
23023
- logger_logger.error(` - ${issuePath}: ${issue.message}`);
23024
- }
23025
- }
23026
- process.exitCode = 1;
23360
+ if (pathStat.isDirectory()) return void handleValidateDirectory(file);
23361
+ handleValidateFile(file);
23027
23362
  });
23028
23363
  if (watchCommand) {
23029
23364
  const watchOptions = watchCommand.options.map((opt)=>{
package/dist/index.js CHANGED
@@ -13885,6 +13885,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13885
13885
  function auth_setRepoContext(repoPath) {
13886
13886
  repoContext = repoPath;
13887
13887
  }
13888
+ const inFlightRefreshes = new Map();
13888
13889
  async function auth_verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
13889
13890
  try {
13890
13891
  const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
@@ -13938,53 +13939,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
13938
13939
  }
13939
13940
  async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
13940
13941
  const normalized = config_normalizeAnvilUrl(anvilUrl);
13941
- let tokens;
13942
- let repoAuth;
13943
- if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
13944
- else {
13945
- repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13946
- tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
13947
- }
13942
+ const source = await resolveTokenSource(normalized, username);
13943
+ const tokens = getSourceTokens(source);
13948
13944
  if (!tokens.authToken && !tokens.refreshToken) throw createAuthError.required("Not logged in. Please log in first.");
13949
13945
  const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
13950
- if (tokens.refreshToken && isExpired) try {
13946
+ if (tokens.refreshToken && isExpired) {
13947
+ const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
13948
+ const existingRefresh = inFlightRefreshes.get(refreshKey);
13949
+ if (existingRefresh) return existingRefresh;
13950
+ const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
13951
+ inFlightRefreshes.delete(refreshKey);
13952
+ });
13953
+ inFlightRefreshes.set(refreshKey, refreshPromise);
13954
+ return refreshPromise;
13955
+ }
13956
+ if (tokens.authToken) return tokens.authToken;
13957
+ throw createAuthError.required("No valid token available");
13958
+ }
13959
+ async function resolveTokenSource(normalizedUrl, username) {
13960
+ if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
13961
+ type: "memory",
13962
+ auth: inMemoryAuth
13963
+ };
13964
+ const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
13965
+ if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
13966
+ type: "repo",
13967
+ auth: repoAuth,
13968
+ repoPath: repoContext
13969
+ };
13970
+ const tokens = getTokensAutoSelect(normalizedUrl, username);
13971
+ return {
13972
+ type: "store",
13973
+ tokens,
13974
+ accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
13975
+ };
13976
+ }
13977
+ function getSourceTokens(source) {
13978
+ return "store" === source.type ? source.tokens : source.auth.tokens;
13979
+ }
13980
+ function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
13981
+ if (username) return username;
13982
+ const urlTokens = getUrlTokens(normalizedUrl);
13983
+ if (!urlTokens) return;
13984
+ const accounts = Object.keys(urlTokens);
13985
+ if (1 === accounts.length) return accounts[0];
13986
+ return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
13987
+ }
13988
+ function getRefreshKey(normalizedUrl, username, source, refreshToken) {
13989
+ switch(source.type){
13990
+ case "memory":
13991
+ return `memory:${normalizedUrl}:${source.auth.username}`;
13992
+ case "repo":
13993
+ return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
13994
+ case "store":
13995
+ return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
13996
+ }
13997
+ }
13998
+ async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
13999
+ try {
14000
+ const tokens = getSourceTokens(source);
13951
14001
  const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
13952
- const tokenData = await refreshAccessToken(tokens.refreshToken, normalized, clientId);
13953
- const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
14002
+ const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
13954
14003
  const newTokens = {
13955
14004
  authToken: tokenData.access_token,
13956
14005
  refreshToken: tokenData.refresh_token,
13957
- authTokenExpiresAt: newExpiresAt,
14006
+ authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
13958
14007
  clientId
13959
14008
  };
13960
- if (inMemoryAuth) inMemoryAuth.tokens = newTokens;
13961
- else if (repoAuth) {
14009
+ if ("memory" === source.type) source.auth.tokens = newTokens;
14010
+ else if ("repo" === source.type) {
13962
14011
  const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
13963
14012
  if (!encryptionKey) throw createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
13964
- repoAuth.tokens = newTokens;
13965
- await writeAuthToFile(repoContext, repoAuth, encryptionKey);
14013
+ source.auth.tokens = newTokens;
14014
+ await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
13966
14015
  } else {
13967
- let accountUsername = username;
13968
- if (!accountUsername) {
13969
- const urlTokens = getUrlTokens(normalized);
13970
- if (urlTokens) {
13971
- const accounts = Object.keys(urlTokens);
13972
- accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
13973
- }
13974
- }
13975
- if (!accountUsername) {
13976
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14016
+ if (!source.accountUsername) {
14017
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13977
14018
  throw createAuthError.required("Cannot determine which account to refresh. Please log in again.");
13978
14019
  }
13979
- setAccountTokens(normalized, accountUsername, newTokens);
14020
+ setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
13980
14021
  }
13981
14022
  return tokenData.access_token;
13982
14023
  } catch (e) {
13983
- await clearTokensForAccount(normalized, username, tokens.refreshToken);
14024
+ await clearTokensForAccount(normalizedUrl, username, refreshToken);
13984
14025
  throw e;
13985
14026
  }
13986
- if (tokens.authToken) return tokens.authToken;
13987
- throw createAuthError.required("No valid token available");
13988
14027
  }
13989
14028
  async function clearTokensForAccount(url, username, refreshToken) {
13990
14029
  if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
@@ -16278,6 +16317,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16278
16317
  ok: true
16279
16318
  };
16280
16319
  }
16320
+ const PYTHON_VALIDATOR_SCRIPT = String.raw`
16321
+ import ast
16322
+ import json
16323
+ import sys
16324
+
16325
+
16326
+ def issue_path(error):
16327
+ if getattr(error, "lineno", None) is None:
16328
+ return "root"
16329
+ if getattr(error, "offset", None) is None:
16330
+ return f"line {error.lineno}"
16331
+ return f"line {error.lineno}, column {error.offset}"
16332
+
16333
+
16334
+ def base_name(node):
16335
+ if isinstance(node, ast.Name):
16336
+ return node.id
16337
+ if isinstance(node, ast.Attribute):
16338
+ return node.attr
16339
+ return None
16340
+
16341
+
16342
+ def imports_template_from_anvil_designer(node, template_name):
16343
+ if node.level < 1:
16344
+ return False
16345
+ if node.module not in ("_anvil_designer", None):
16346
+ return False
16347
+ return any(alias.name == template_name for alias in node.names)
16348
+
16349
+
16350
+ file_path = sys.argv[1]
16351
+ class_name = sys.argv[2]
16352
+ template_name = f"{class_name}Template" if class_name else ""
16353
+ source = sys.stdin.read()
16354
+ issues = []
16355
+
16356
+ try:
16357
+ compile(source, file_path, "exec")
16358
+ except SyntaxError as error:
16359
+ issues.append({"path": issue_path(error), "message": error.msg})
16360
+ print(json.dumps({"issues": issues}))
16361
+ sys.exit(0)
16362
+
16363
+ if class_name:
16364
+ tree = ast.parse(source, filename=file_path)
16365
+ has_template_import = False
16366
+ has_form_class = False
16367
+
16368
+ for node in ast.walk(tree):
16369
+ if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
16370
+ has_template_import = True
16371
+ elif isinstance(node, ast.ClassDef) and node.name == class_name:
16372
+ if any(base_name(base) == template_name for base in node.bases):
16373
+ has_form_class = True
16374
+
16375
+ if not has_template_import:
16376
+ issues.append({
16377
+ "path": "root",
16378
+ "message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
16379
+ })
16380
+ if not has_form_class:
16381
+ issues.append({
16382
+ "path": "root",
16383
+ "message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
16384
+ })
16385
+
16386
+ print(json.dumps({"issues": issues}))
16387
+ `;
16388
+ function findPythonInterpreter() {
16389
+ const candidates = [
16390
+ process.env.ANVIL_PYTHON,
16391
+ "python3",
16392
+ "python"
16393
+ ].filter((candidate)=>"string" == typeof candidate && candidate.length > 0);
16394
+ for (const candidate of candidates){
16395
+ const result = (0, external_child_process_namespaceObject.spawnSync)(candidate, [
16396
+ "--version"
16397
+ ], {
16398
+ encoding: "utf8"
16399
+ });
16400
+ if (!result.error && 0 === result.status) return candidate;
16401
+ }
16402
+ return null;
16403
+ }
16404
+ function isClientFormPython(filePath) {
16405
+ if ("__init__.py" === external_path_default().basename(filePath)) {
16406
+ const formDir = external_path_default().dirname(filePath);
16407
+ return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
16408
+ }
16409
+ const parsed = external_path_default().parse(filePath);
16410
+ 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`));
16411
+ }
16412
+ function expectedFormClassName(filePath) {
16413
+ if (!isClientFormPython(filePath)) return null;
16414
+ if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
16415
+ return external_path_default().basename(filePath, ".py");
16416
+ }
16417
+ function isUnderDirectory(filePath, directoryPath) {
16418
+ const relativePath = external_path_default().relative(directoryPath, filePath);
16419
+ return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
16420
+ }
16421
+ function validatePython(filePath, fileContent, appRoot) {
16422
+ const interpreter = findPythonInterpreter();
16423
+ if (!interpreter) return {
16424
+ ok: false,
16425
+ message: "Python file could not be validated",
16426
+ issues: [
16427
+ {
16428
+ path: "root",
16429
+ message: "Python validation requires python3 or python on PATH, or ANVIL_PYTHON to be set"
16430
+ }
16431
+ ]
16432
+ };
16433
+ const absolutePath = external_path_default().resolve(filePath);
16434
+ const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
16435
+ const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
16436
+ const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter, [
16437
+ "-c",
16438
+ PYTHON_VALIDATOR_SCRIPT,
16439
+ filePath,
16440
+ formClassName ?? ""
16441
+ ], {
16442
+ encoding: "utf8",
16443
+ input: fileContent,
16444
+ maxBuffer: 1048576
16445
+ });
16446
+ if (result.error || 0 !== result.status) return {
16447
+ ok: false,
16448
+ message: "Python file could not be validated",
16449
+ issues: [
16450
+ {
16451
+ path: "root",
16452
+ message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
16453
+ }
16454
+ ]
16455
+ };
16456
+ let parsed;
16457
+ try {
16458
+ parsed = JSON.parse(result.stdout);
16459
+ } catch (error) {
16460
+ return {
16461
+ ok: false,
16462
+ message: "Python file could not be validated",
16463
+ issues: [
16464
+ {
16465
+ path: "root",
16466
+ message: error instanceof Error ? error.message : String(error)
16467
+ }
16468
+ ]
16469
+ };
16470
+ }
16471
+ const issues = parsed.issues ?? [];
16472
+ if (issues.length > 0) return {
16473
+ ok: false,
16474
+ message: "Python file is invalid for Anvil",
16475
+ issues
16476
+ };
16477
+ return {
16478
+ ok: true
16479
+ };
16480
+ }
16281
16481
  const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16282
16482
  const DB_ACCESS_LEVELS = new Set([
16283
16483
  "none",
@@ -16675,6 +16875,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16675
16875
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
16676
16876
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16677
16877
  if (relativeToClientCode.endsWith(".html")) return "form_template.html";
16878
+ if (relativeToClientCode.endsWith(".py")) return "python";
16879
+ }
16880
+ const serverCodePrefix = "server_code/";
16881
+ if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
16882
+ const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
16883
+ const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
16884
+ if (relativeToServerCode.endsWith(".py")) return "python";
16678
16885
  }
16679
16886
  return null;
16680
16887
  }
@@ -16782,6 +16989,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16782
16989
  target
16783
16990
  };
16784
16991
  }
16992
+ if ("python" === target) {
16993
+ if (existingPath && appRoot) {
16994
+ const clientCodeRoot = external_path_default().join(appRoot, "client_code");
16995
+ const serverCodeRoot = external_path_default().join(appRoot, "server_code");
16996
+ const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
16997
+ const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
16998
+ const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
16999
+ const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
17000
+ if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
17001
+ ok: false,
17002
+ target,
17003
+ message: "File is not an Anvil app Python file",
17004
+ issues: [
17005
+ {
17006
+ path: "",
17007
+ message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
17008
+ }
17009
+ ]
17010
+ };
17011
+ }
17012
+ const result = validatePython(filePath, fileContent, appRoot);
17013
+ return result.ok ? {
17014
+ ok: true,
17015
+ target
17016
+ } : {
17017
+ ...result,
17018
+ target
17019
+ };
17020
+ }
16785
17021
  return {
16786
17022
  ok: false,
16787
17023
  target: "unknown",
@@ -16789,7 +17025,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16789
17025
  issues: [
16790
17026
  {
16791
17027
  path: "",
16792
- message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, and client_code/**/*.html"
17028
+ message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
16793
17029
  }
16794
17030
  ]
16795
17031
  };
@@ -17874,6 +18110,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
17874
18110
  }
17875
18111
  }
17876
18112
  function authDiagnostic(error) {
18113
+ if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
18114
+ code: error.type,
18115
+ recoverability: "requires_user_action",
18116
+ requiresUserAction: true,
18117
+ suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
18118
+ };
17877
18119
  return {
17878
18120
  code: error.type,
17879
18121
  recoverability: "requires_user_action",
@@ -18790,7 +19032,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18790
19032
  getAuthToken: ()=>this.authToken,
18791
19033
  setAuthToken: (token)=>{
18792
19034
  this.authToken = token;
18793
- this.wsClient?.updateAuthToken(token);
19035
+ this.wsClient?.updateAuthToken?.(token);
18794
19036
  },
18795
19037
  getCurrentBranch: ()=>this.currentBranch,
18796
19038
  getCommitId: ()=>this.commitId,
@@ -18810,7 +19052,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18810
19052
  getAuthToken: ()=>this.authToken,
18811
19053
  setAuthToken: (token)=>{
18812
19054
  this.authToken = token;
18813
- this.wsClient?.updateAuthToken(token);
19055
+ this.wsClient?.updateAuthToken?.(token);
18814
19056
  },
18815
19057
  getCurrentBranch: ()=>this.currentBranch,
18816
19058
  getCommitId: ()=>this.commitId,
@@ -18978,6 +19220,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18978
19220
  ]);
18979
19221
  }
18980
19222
  logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
19223
+ this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
19224
+ this.wsClient?.updateAuthToken?.(this.authToken);
18981
19225
  const fetchUrl = anvil_api_getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
18982
19226
  const envRemoteRef = environment_environmentRemoteRefForBranch(targetBranch);
18983
19227
  await this.gitService.fetch(fetchUrl, environment_environmentFetchRefSpecForBranch(targetBranch));
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuDpC,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,EAAE,CAezE;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAwFzD;AAiBD,wBAAgB,YAAY,IAAI,OAAO,CAmHtC"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2LpC,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,EAAE,CAezE;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAwFzD;AAiBD,wBAAgB,YAAY,IAAI,OAAO,CAkGtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,WAAW,CAAC;AAS9E,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAMjD,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACzB;AAQD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAMjG;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAYD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD;AAMD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACpC,YAAY,EAAE,MAAM,EACpB,QAAQ,SAAuB,EAC/B,QAAQ,SAAuB,GAChC,OAAO,CAAC,SAAS,CAAC,CA2BpB;AAMD;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY3F;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,SAAuB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkF3G;AA0BD;;;GAGG;AACH,wBAAsB,oBAAoB,CACtC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0CxF;AAMD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYtE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/D"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,WAAW,CAAC;AAS9E,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAMjD,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACzB;AAaD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAMjG;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAYD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD;AAQD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACpC,YAAY,EAAE,MAAM,EACpB,QAAQ,SAAuB,EAC/B,QAAQ,SAAuB,GAChC,OAAO,CAAC,SAAS,CAAC,CA2BpB;AAMD;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY3F;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,SAAuB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC3G;AA8HD;;;GAGG;AACH,wBAAsB,oBAAoB,CACtC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0CxF;AAMD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYtE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/D"}
@@ -0,0 +1,10 @@
1
+ import type { ValidationIssue } from "./validators";
2
+ export type PythonValidationResult = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ message: string;
7
+ issues: ValidationIssue[];
8
+ };
9
+ export declare function validatePython(filePath: string, fileContent: string, appRoot: string | null): PythonValidationResult;
10
+ //# sourceMappingURL=validatePython.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validatePython.d.ts","sourceRoot":"","sources":["../src/validatePython.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,sBAAsB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAqHR,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,sBAAsB,CAiEpH"}
@@ -16,7 +16,7 @@ export type AnvilYamlValidationResult = {
16
16
  message: string;
17
17
  issues: ValidationIssue[];
18
18
  };
19
- export type ValidationTarget = "anvil.yaml" | "form_template.yaml" | "form_template.html";
19
+ export type ValidationTarget = "anvil.yaml" | "form_template.yaml" | "form_template.html" | "python";
20
20
  export type ValidatePathResult = {
21
21
  ok: true;
22
22
  target: ValidationTarget;
@@ -43,6 +43,7 @@ export declare function findAnvilAppRoot(filePath: string): string | null;
43
43
  * - `anvil.yaml`
44
44
  * - any form YAML under `client_code/` with a `.yaml` suffix
45
45
  * - any form HTML under `client_code/` with a `.html` suffix
46
+ * - any Python file under `client_code/` or `server_code/`
46
47
  */
47
48
  export declare function validatePath(filePath: string, fileContent: string): ValidatePathResult;
48
49
  //# sourceMappingURL=validators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,eAAe,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,yBAAyB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAE1F,MAAM,MAAM,kBAAkB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACtC;IACI,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAoER,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,UAEhD;AAgBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,4BAA4B,CA0BtF;AAuZD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,yBAAyB,CAkFhF;AAED,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAiBxE;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAOjC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAoGtF"}
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,eAAe,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,yBAAyB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,oBAAoB,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAErG,MAAM,MAAM,kBAAkB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACtC;IACI,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAoER,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,UAEhD;AAgBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,4BAA4B,CA0BtF;AAuZD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,yBAAyB,CAkFhF;AAED,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA4BxE;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAOjC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAoItF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvil-works/anvil-cli",
3
- "version": "0.7.0-canary.16",
3
+ "version": "0.7.0-canary.18",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/api.d.ts",