@anvil-works/anvil-cli 0.7.0-canary.17 → 0.7.0-canary.19

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.
@@ -7,6 +7,10 @@ export interface WatchSessionEvents {
7
7
  oldBranch: string;
8
8
  newBranch: string;
9
9
  };
10
+ "external-branch-changed": {
11
+ oldBranch: string;
12
+ newBranch: string;
13
+ };
10
14
  "env-branch-switch-started": {
11
15
  envPid: string;
12
16
  oldBranch: string;
@@ -113,6 +117,7 @@ export declare class WatchSession extends Emitter<WatchSessionEvents> {
113
117
  private environment?;
114
118
  private envBranchChangeMode;
115
119
  private isSwitchingForEnvironment;
120
+ private environmentSwitchTargetBranch;
116
121
  private dependencyWatchEnabled;
117
122
  private isRefreshingDependencies;
118
123
  private hasPendingDependencyRefresh;
@@ -158,6 +163,7 @@ export declare class WatchSession extends Emitter<WatchSessionEvents> {
158
163
  }>;
159
164
  cleanup(): void;
160
165
  startWatching(): Promise<void>;
166
+ private handleFileWatcherBranchChange;
161
167
  startWatchingInBackground(onError?: (error: unknown) => void): void;
162
168
  private initializeLocalChangeFingerprint;
163
169
  private startLocalChangeFallbackPolling;
@@ -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
@@ -16288,6 +16288,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16288
16288
  ok: true
16289
16289
  };
16290
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
+ }
16291
16452
  const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16292
16453
  const DB_ACCESS_LEVELS = new Set([
16293
16454
  "none",
@@ -16685,6 +16846,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16685
16846
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
16686
16847
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16687
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";
16688
16856
  }
16689
16857
  return null;
16690
16858
  }
@@ -16792,6 +16960,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16792
16960
  target
16793
16961
  };
16794
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
+ }
16795
16992
  return {
16796
16993
  ok: false,
16797
16994
  target: "unknown",
@@ -16799,7 +16996,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16799
16996
  issues: [
16800
16997
  {
16801
16998
  path: "",
16802
- 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"
16803
17000
  }
16804
17001
  ]
16805
17002
  };
@@ -18746,6 +18943,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18746
18943
  environment;
18747
18944
  envBranchChangeMode;
18748
18945
  isSwitchingForEnvironment = false;
18946
+ environmentSwitchTargetBranch = null;
18749
18947
  dependencyWatchEnabled;
18750
18948
  isRefreshingDependencies = false;
18751
18949
  hasPendingDependencyRefresh = false;
@@ -18970,6 +19168,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18970
19168
  async switchToEnvironmentBranch(targetBranch, source) {
18971
19169
  if (this.isCleanedUp) return;
18972
19170
  this.isSwitchingForEnvironment = true;
19171
+ this.environmentSwitchTargetBranch = targetBranch;
18973
19172
  const oldPausedState = this.isPausedForUserInput;
18974
19173
  try {
18975
19174
  const status = await this.gitService.getStatus();
@@ -19031,6 +19230,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19031
19230
  } finally{
19032
19231
  this.isPausedForUserInput = oldPausedState;
19033
19232
  this.isSwitchingForEnvironment = false;
19233
+ this.environmentSwitchTargetBranch = null;
19034
19234
  }
19035
19235
  const pendingSwitch = this.pendingEnvironmentBranchSwitch;
19036
19236
  this.pendingEnvironmentBranchSwitch = null;
@@ -19118,16 +19318,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19118
19318
  getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
19119
19319
  });
19120
19320
  this.fileWatcher.on("branch-change", ({ newBranch })=>{
19121
- const oldBranch = this.currentBranch;
19122
- logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19123
- this.currentBranch = newBranch;
19124
- this.lastBranchChangeTime = Date.now();
19125
- this.wsClient?.updateBranch(newBranch);
19126
- this.queueDependencyRefresh("branch changed");
19127
- this.emit("branch-changed", {
19128
- oldBranch,
19129
- newBranch
19130
- });
19321
+ this.handleFileWatcherBranchChange(newBranch);
19131
19322
  });
19132
19323
  this.fileWatcher.on("index-change", ()=>{
19133
19324
  this.saveProcessor?.queueSave();
@@ -19144,6 +19335,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19144
19335
  this.startLocalChangeFallbackPolling();
19145
19336
  await new Promise(()=>{});
19146
19337
  }
19338
+ handleFileWatcherBranchChange(newBranch) {
19339
+ if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
19340
+ const oldBranch = this.currentBranch;
19341
+ logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19342
+ this.currentBranch = newBranch;
19343
+ this.lastBranchChangeTime = Date.now();
19344
+ this.wsClient?.updateBranch(newBranch);
19345
+ this.queueDependencyRefresh("branch changed");
19346
+ this.emit("external-branch-changed", {
19347
+ oldBranch,
19348
+ newBranch
19349
+ });
19350
+ this.emit("branch-changed", {
19351
+ oldBranch,
19352
+ newBranch
19353
+ });
19354
+ }
19147
19355
  startWatchingInBackground(onError) {
19148
19356
  this.startWatching().catch((error)=>{
19149
19357
  this.cleanup();
@@ -22921,6 +23129,123 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
22921
23129
  }
22922
23130
  }).catch(()=>{});
22923
23131
  }
23132
+ function collectValidationFiles(dir) {
23133
+ const files = [];
23134
+ const ignoredDirectories = new Set([
23135
+ ".anvil",
23136
+ ".git",
23137
+ "node_modules",
23138
+ "__pycache__"
23139
+ ]);
23140
+ function walk(currentDir) {
23141
+ for (const entry of (0, external_fs_.readdirSync)(currentDir, {
23142
+ withFileTypes: true
23143
+ })){
23144
+ const entryPath = (0, external_path_namespaceObject.join)(currentDir, entry.name);
23145
+ if (entry.isDirectory()) {
23146
+ if (!ignoredDirectories.has(entry.name)) walk(entryPath);
23147
+ continue;
23148
+ }
23149
+ if (entry.isFile() && inferValidationTarget(entryPath)) files.push(entryPath);
23150
+ }
23151
+ }
23152
+ walk(dir);
23153
+ return files.sort();
23154
+ }
23155
+ function validateSingleFile(file) {
23156
+ let fileContent;
23157
+ try {
23158
+ fileContent = (0, external_fs_.readFileSync)(file, "utf8");
23159
+ } catch (error) {
23160
+ return {
23161
+ file,
23162
+ type: "unknown",
23163
+ valid: false,
23164
+ error: `Failed to read ${file}: ${error.message}`,
23165
+ issues: [
23166
+ {
23167
+ path: "",
23168
+ message: error.message
23169
+ }
23170
+ ]
23171
+ };
23172
+ }
23173
+ const result = validatePath(file, fileContent);
23174
+ if (result.ok) return {
23175
+ file,
23176
+ type: result.target,
23177
+ valid: true
23178
+ };
23179
+ return {
23180
+ file,
23181
+ type: result.target,
23182
+ valid: false,
23183
+ error: result.message,
23184
+ issues: result.issues
23185
+ };
23186
+ }
23187
+ function logValidationFailure(fileResult) {
23188
+ logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
23189
+ for (const issue of fileResult.issues ?? []){
23190
+ const issuePath = issue.path.length > 0 ? issue.path : "root";
23191
+ logger_logger.error(` - ${issuePath}: ${issue.message}`);
23192
+ }
23193
+ }
23194
+ function handleValidateFile(file) {
23195
+ const fileResult = validateSingleFile(file);
23196
+ if (fileResult.valid) {
23197
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23198
+ data: {
23199
+ file: fileResult.file,
23200
+ type: fileResult.type,
23201
+ valid: true
23202
+ }
23203
+ });
23204
+ else logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23205
+ return;
23206
+ }
23207
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23208
+ error: fileResult.error,
23209
+ data: {
23210
+ file: fileResult.file,
23211
+ type: fileResult.type,
23212
+ issues: fileResult.issues ?? []
23213
+ }
23214
+ });
23215
+ else logValidationFailure(fileResult);
23216
+ process.exitCode = 1;
23217
+ }
23218
+ function handleValidateDirectory(dir) {
23219
+ const files = collectValidationFiles(dir);
23220
+ if (0 === files.length) {
23221
+ const error = `No supported files found under ${dir}`;
23222
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23223
+ error,
23224
+ data: {
23225
+ file: dir,
23226
+ valid: false,
23227
+ files: []
23228
+ }
23229
+ });
23230
+ else logger_logger.error(error);
23231
+ process.exitCode = 1;
23232
+ return;
23233
+ }
23234
+ const fileResults = files.map(validateSingleFile);
23235
+ const failedResults = fileResults.filter((result)=>!result.valid);
23236
+ if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
23237
+ error: failedResults.length > 0 ? "Validation failed" : void 0,
23238
+ data: {
23239
+ file: dir,
23240
+ valid: 0 === failedResults.length,
23241
+ count: fileResults.length,
23242
+ files: fileResults
23243
+ }
23244
+ });
23245
+ else for (const fileResult of fileResults)if (fileResult.valid) logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
23246
+ else logValidationFailure(fileResult);
23247
+ if (failedResults.length > 0) process.exitCode = 1;
23248
+ }
22924
23249
  function getUpdateInstructions(platform) {
22925
23250
  if ("win32" === platform) return [
22926
23251
  "try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
@@ -23034,43 +23359,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
23034
23359
  program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
23035
23360
  await handleUpdateCommand();
23036
23361
  });
23037
- 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)=>{
23038
- let fileContent;
23362
+ 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)=>{
23363
+ let pathStat;
23039
23364
  try {
23040
- fileContent = (0, external_fs_.readFileSync)(file, "utf8");
23365
+ pathStat = (0, external_fs_.statSync)(file);
23041
23366
  } catch (error) {
23042
- logger_logger.error(`Failed to read ${file}: ${error.message}`);
23367
+ logger_logger.error(`Failed to access ${file}: ${error.message}`);
23043
23368
  process.exitCode = 1;
23044
23369
  return;
23045
23370
  }
23046
- const result = validatePath(file, fileContent);
23047
- if (result.ok) {
23048
- if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
23049
- data: {
23050
- file,
23051
- type: result.target,
23052
- valid: true
23053
- }
23054
- });
23055
- else logger_logger.success(`${file} is valid (${result.target})`);
23056
- return;
23057
- }
23058
- if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
23059
- error: result.message,
23060
- data: {
23061
- file,
23062
- type: result.target,
23063
- issues: result.issues
23064
- }
23065
- });
23066
- else {
23067
- logger_logger.error(`${file} failed validation: ${result.message}`);
23068
- for (const issue of result.issues){
23069
- const issuePath = issue.path.length > 0 ? issue.path : "root";
23070
- logger_logger.error(` - ${issuePath}: ${issue.message}`);
23071
- }
23072
- }
23073
- process.exitCode = 1;
23371
+ if (pathStat.isDirectory()) return void handleValidateDirectory(file);
23372
+ handleValidateFile(file);
23074
23373
  });
23075
23374
  if (watchCommand) {
23076
23375
  const watchOptions = watchCommand.options.map((opt)=>{
package/dist/index.js CHANGED
@@ -16317,6 +16317,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16317
16317
  ok: true
16318
16318
  };
16319
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
+ }
16320
16481
  const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
16321
16482
  const DB_ACCESS_LEVELS = new Set([
16322
16483
  "none",
@@ -16714,6 +16875,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16714
16875
  const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
16715
16876
  if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
16716
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";
16717
16885
  }
16718
16886
  return null;
16719
16887
  }
@@ -16821,6 +16989,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16821
16989
  target
16822
16990
  };
16823
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
+ }
16824
17021
  return {
16825
17022
  ok: false,
16826
17023
  target: "unknown",
@@ -16828,7 +17025,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
16828
17025
  issues: [
16829
17026
  {
16830
17027
  path: "",
16831
- 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"
16832
17029
  }
16833
17030
  ]
16834
17031
  };
@@ -18775,6 +18972,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18775
18972
  environment;
18776
18973
  envBranchChangeMode;
18777
18974
  isSwitchingForEnvironment = false;
18975
+ environmentSwitchTargetBranch = null;
18778
18976
  dependencyWatchEnabled;
18779
18977
  isRefreshingDependencies = false;
18780
18978
  hasPendingDependencyRefresh = false;
@@ -18999,6 +19197,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
18999
19197
  async switchToEnvironmentBranch(targetBranch, source) {
19000
19198
  if (this.isCleanedUp) return;
19001
19199
  this.isSwitchingForEnvironment = true;
19200
+ this.environmentSwitchTargetBranch = targetBranch;
19002
19201
  const oldPausedState = this.isPausedForUserInput;
19003
19202
  try {
19004
19203
  const status = await this.gitService.getStatus();
@@ -19060,6 +19259,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19060
19259
  } finally{
19061
19260
  this.isPausedForUserInput = oldPausedState;
19062
19261
  this.isSwitchingForEnvironment = false;
19262
+ this.environmentSwitchTargetBranch = null;
19063
19263
  }
19064
19264
  const pendingSwitch = this.pendingEnvironmentBranchSwitch;
19065
19265
  this.pendingEnvironmentBranchSwitch = null;
@@ -19147,16 +19347,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19147
19347
  getCurrentBranch: async ()=>this.gitService.getCurrentBranch()
19148
19348
  });
19149
19349
  this.fileWatcher.on("branch-change", ({ newBranch })=>{
19150
- const oldBranch = this.currentBranch;
19151
- logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19152
- this.currentBranch = newBranch;
19153
- this.lastBranchChangeTime = Date.now();
19154
- this.wsClient?.updateBranch(newBranch);
19155
- this.queueDependencyRefresh("branch changed");
19156
- this.emit("branch-changed", {
19157
- oldBranch,
19158
- newBranch
19159
- });
19350
+ this.handleFileWatcherBranchChange(newBranch);
19160
19351
  });
19161
19352
  this.fileWatcher.on("index-change", ()=>{
19162
19353
  this.saveProcessor?.queueSave();
@@ -19173,6 +19364,23 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
19173
19364
  this.startLocalChangeFallbackPolling();
19174
19365
  await new Promise(()=>{});
19175
19366
  }
19367
+ handleFileWatcherBranchChange(newBranch) {
19368
+ if (this.isSwitchingForEnvironment && newBranch === this.environmentSwitchTargetBranch) return void logger_logger.debug(`Ignoring FileWatcher branch-change during environment switch: ${newBranch}`);
19369
+ const oldBranch = this.currentBranch;
19370
+ logger_logger.verbose(external_chalk_default().gray(" Pausing sync for 2 seconds to allow branch to settle..."));
19371
+ this.currentBranch = newBranch;
19372
+ this.lastBranchChangeTime = Date.now();
19373
+ this.wsClient?.updateBranch(newBranch);
19374
+ this.queueDependencyRefresh("branch changed");
19375
+ this.emit("external-branch-changed", {
19376
+ oldBranch,
19377
+ newBranch
19378
+ });
19379
+ this.emit("branch-changed", {
19380
+ oldBranch,
19381
+ newBranch
19382
+ });
19383
+ }
19176
19384
  startWatchingInBackground(onError) {
19177
19385
  this.startWatching().catch((error)=>{
19178
19386
  this.cleanup();
@@ -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"}
@@ -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.17",
3
+ "version": "0.7.0-canary.19",
4
4
  "description": "CLI tool for developing Anvil apps locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/api.d.ts",