tlaplus-cli 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {tlaplus_cli-0.4.1/src/tlaplus_cli.egg-info → tlaplus_cli-0.5.0}/PKG-INFO +15 -3
  2. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/README.md +14 -2
  3. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/pyproject.toml +1 -1
  4. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cli.py +1 -1
  5. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/modules/__init__.py +1 -1
  6. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/modules/build.py +12 -1
  7. tlaplus_cli-0.5.0/src/tlaplus_cli/cmd/modules/install.py +75 -0
  8. tlaplus_cli-0.5.0/src/tlaplus_cli/cmd/modules/path.py +72 -0
  9. tlaplus_cli-0.5.0/src/tlaplus_cli/cmd/tlc.py +139 -0
  10. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/meta/sync.py +2 -1
  11. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/uninstall.py +3 -2
  12. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/config/schema.py +1 -0
  13. tlaplus_cli-0.5.0/src/tlaplus_cli/java/classpath.py +174 -0
  14. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/resources/default_config.yaml +4 -0
  15. tlaplus_cli-0.5.0/src/tlaplus_cli/tlc/codes.py +314 -0
  16. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/tlc/compiler.py +34 -11
  17. tlaplus_cli-0.5.0/src/tlaplus_cli/tlc/models.py +130 -0
  18. tlaplus_cli-0.5.0/src/tlaplus_cli/tlc/parser.py +256 -0
  19. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/tlc/runner.py +54 -29
  20. tlaplus_cli-0.5.0/src/tlaplus_cli/tlc/sany.py +94 -0
  21. tlaplus_cli-0.5.0/src/tlaplus_cli/tlc/values.py +186 -0
  22. tlaplus_cli-0.5.0/src/tlaplus_cli/ui.py +21 -0
  23. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/api.py +9 -6
  24. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/downloader.py +15 -18
  25. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/paths.py +10 -4
  26. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0/src/tlaplus_cli.egg-info}/PKG-INFO +15 -3
  27. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli.egg-info/SOURCES.txt +13 -1
  28. tlaplus_cli-0.5.0/tests/test_build_both.py +33 -0
  29. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_modules_build_lib.py +4 -4
  30. tlaplus_cli-0.5.0/tests/test_modules_build_show_command.py +27 -0
  31. tlaplus_cli-0.5.0/tests/test_modules_install_community.py +86 -0
  32. tlaplus_cli-0.5.0/tests/test_modules_path_resolved.py +33 -0
  33. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_tlc_modules_path.py +11 -4
  34. tlaplus_cli-0.5.0/tests/test_tlc_show_command.py +25 -0
  35. tlaplus_cli-0.4.1/src/tlaplus_cli/cmd/modules/path.py +0 -39
  36. tlaplus_cli-0.4.1/src/tlaplus_cli/cmd/tlc.py +0 -51
  37. tlaplus_cli-0.4.1/src/tlaplus_cli/ui.py +0 -6
  38. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/LICENSE +0 -0
  39. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/MANIFEST.in +0 -0
  40. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/setup.cfg +0 -0
  41. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/__init__.py +0 -0
  42. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cache/__init__.py +0 -0
  43. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cache/github.py +0 -0
  44. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/__init__.py +0 -0
  45. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/check_java.py +0 -0
  46. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/config/__init__.py +0 -0
  47. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/config/edit.py +0 -0
  48. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/config/list.py +0 -0
  49. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/fetch_cache/__init__.py +0 -0
  50. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/fetch_cache/clear.py +0 -0
  51. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/modules/lib.py +0 -0
  52. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/__init__.py +0 -0
  53. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/dir.py +0 -0
  54. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/install.py +0 -0
  55. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/list.py +0 -0
  56. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/meta/__init__.py +0 -0
  57. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/path.py +0 -0
  58. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/pin.py +0 -0
  59. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/cmd/tools/upgrade.py +0 -0
  60. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/config/__init__.py +0 -0
  61. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/config/loader.py +0 -0
  62. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/java/__init__.py +0 -0
  63. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/java/inspector.py +0 -0
  64. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/project/__init__.py +0 -0
  65. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/project/core.py +0 -0
  66. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/py.typed +0 -0
  67. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/resources/__init__.py +0 -0
  68. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/tlc/__init__.py +0 -0
  69. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/__init__.py +0 -0
  70. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/metadata.py +0 -0
  71. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/resolver.py +0 -0
  72. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli/versioning/schema.py +0 -0
  73. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
  74. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
  75. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli.egg-info/requires.txt +0 -0
  76. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
  77. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_build_modules_path.py +0 -0
  78. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_cli.py +0 -0
  79. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_config.py +0 -0
  80. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_config_suite.py +0 -0
  81. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_modules_lib.py +0 -0
  82. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_modules_path.py +0 -0
  83. {tlaplus_cli-0.4.1 → tlaplus_cli-0.5.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tlaplus-cli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: TLA+ tools: download TLC, compile custom modules, run model checker
5
5
  Author-email: Denis Nikolskiy <codeomatics@gmail.com>
6
6
  License-Expression: MIT
@@ -156,6 +156,12 @@ To check the currently pinned `tla2tools.jar` path and its TLC version:
156
156
  tla tlc --version
157
157
  ```
158
158
 
159
+ To inspect the exact Java command that will be executed without running it:
160
+
161
+ ```bash
162
+ tla tlc <spec_name> --show-command
163
+ ```
164
+
159
165
  ### Compile Custom Java Modules
160
166
 
161
167
  Java modules (overrides) are compiled using the pinned version of the toolset.
@@ -177,8 +183,9 @@ tla modules path /path/to/custom/modules
177
183
 
178
184
  View or reset the custom modules path:
179
185
  ```bash
180
- tla modules path # View current path
181
- tla modules path none # Reset to default
186
+ tla modules path # View current path
187
+ tla modules path none # Reset to default
188
+ tla modules path --resolved # Show fully resolved paths for current context
182
189
  ```
183
190
 
184
191
  Configure a persistent custom modules dependencies (lib) path:
@@ -200,6 +207,11 @@ Verbose output:
200
207
  tla modules build --verbose
201
208
  ```
202
209
 
210
+ Show compilation command:
211
+ ```bash
212
+ tla modules build --show-command
213
+ ```
214
+
203
215
  The build command:
204
216
  1. Resolves the project root (defaults to `workspace.root` from config).
205
217
  2. Includes `lib/*.jar` files from the project root in the `javac` classpath.
@@ -127,6 +127,12 @@ To check the currently pinned `tla2tools.jar` path and its TLC version:
127
127
  tla tlc --version
128
128
  ```
129
129
 
130
+ To inspect the exact Java command that will be executed without running it:
131
+
132
+ ```bash
133
+ tla tlc <spec_name> --show-command
134
+ ```
135
+
130
136
  ### Compile Custom Java Modules
131
137
 
132
138
  Java modules (overrides) are compiled using the pinned version of the toolset.
@@ -148,8 +154,9 @@ tla modules path /path/to/custom/modules
148
154
 
149
155
  View or reset the custom modules path:
150
156
  ```bash
151
- tla modules path # View current path
152
- tla modules path none # Reset to default
157
+ tla modules path # View current path
158
+ tla modules path none # Reset to default
159
+ tla modules path --resolved # Show fully resolved paths for current context
153
160
  ```
154
161
 
155
162
  Configure a persistent custom modules dependencies (lib) path:
@@ -171,6 +178,11 @@ Verbose output:
171
178
  tla modules build --verbose
172
179
  ```
173
180
 
181
+ Show compilation command:
182
+ ```bash
183
+ tla modules build --show-command
184
+ ```
185
+
174
186
  The build command:
175
187
  1. Resolves the project root (defaults to `workspace.root` from config).
176
188
  2. Includes `lib/*.jar` files from the project root in the `javac` classpath.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tlaplus-cli"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "TLA+ tools: download TLC, compile custom modules, run model checker"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -30,7 +30,7 @@ def version_callback(value: bool) -> None:
30
30
 
31
31
  @app.callback()
32
32
  def root(
33
- version: bool = typer.Option( # noqa: ARG001
33
+ _: bool = typer.Option(
34
34
  None,
35
35
  "--version",
36
36
  "-v",
@@ -2,4 +2,4 @@ import typer
2
2
 
3
3
  app = typer.Typer(name="modules", help="Manage TLA+ Java modules.", no_args_is_help=True)
4
4
 
5
- from . import build, lib, path # noqa: F401, E402
5
+ from . import build, install, lib, path # noqa: F401, E402
@@ -4,17 +4,28 @@ from pathlib import Path
4
4
  import typer
5
5
 
6
6
  from tlaplus_cli.cmd.modules import app
7
- from tlaplus_cli.tlc.compiler import compile_modules
7
+ from tlaplus_cli.tlc.compiler import build_compile_command, compile_modules
8
8
 
9
9
 
10
10
  @app.command(name="build")
11
11
  def build(
12
12
  path: str | None = typer.Argument(None, help="Project root directory (defaults to workspace root)."),
13
13
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show compilation output."),
14
+ show_command: bool = typer.Option(False, "--show-command", help="Print the command instead of executing it."),
14
15
  ) -> None:
15
16
  """Compile custom Java modules."""
16
17
  base_dir = Path(path).resolve() if path is not None else None
17
18
 
19
+ if show_command:
20
+ try:
21
+ cmd = build_compile_command(base_dir)
22
+ except FileNotFoundError as e:
23
+ typer.echo(f"Error: {e}", err=True)
24
+ raise typer.Exit(1) from None
25
+ if cmd:
26
+ typer.echo(" ".join(cmd))
27
+ return
28
+
18
29
  typer.echo("Compiling Java files ...")
19
30
  try:
20
31
  classes_dir = compile_modules(base_dir, verbose)
@@ -0,0 +1,75 @@
1
+ import requests
2
+ import typer
3
+
4
+ from tlaplus_cli import ui
5
+ from tlaplus_cli.cmd.modules import app
6
+ from tlaplus_cli.config.loader import load_config
7
+ from tlaplus_cli.versioning import (
8
+ download_version,
9
+ download_version_from_url,
10
+ fetch_remote_versions,
11
+ is_url,
12
+ )
13
+ from tlaplus_cli.versioning.paths import get_modules_dir
14
+
15
+
16
+ @app.command(name="install-community")
17
+ def install_community(
18
+ version: str = typer.Argument(
19
+ None, help="Version tag (e.g. 'v202412171801') or a direct URL to CommunityModules.jar."
20
+ ),
21
+ force: bool = typer.Option(False, "--force", "-f", help="Re-download if already installed."),
22
+ ) -> None:
23
+ """Download and install a specific version of CommunityModules."""
24
+ base_dir = get_modules_dir()
25
+ jar_name = "CommunityModules.jar"
26
+
27
+ if version and is_url(version):
28
+ try:
29
+ version_dir = download_version_from_url(version, base_dir=base_dir, jar_name=jar_name)
30
+ except (requests.RequestException, OSError, ValueError) as e:
31
+ ui.error(f"Failed to download: {e}")
32
+ raise typer.Exit(1) from e
33
+ else:
34
+ ui.success(f"Successfully installed from URL to {version_dir}")
35
+ return
36
+
37
+ config = load_config()
38
+ if not config.tla.community_modules:
39
+ ui.error("Community modules URLs not configured in settings.")
40
+ raise typer.Exit(1)
41
+
42
+ versions, status = fetch_remote_versions(
43
+ config.tla.community_modules.tags,
44
+ config.tla.community_modules.releases,
45
+ config.tla.community_modules.per_page,
46
+ asset_name=jar_name,
47
+ )
48
+
49
+ if not versions:
50
+ ui.error(f"Could not fetch remote versions (status: {status.value})")
51
+ raise typer.Exit(1)
52
+
53
+ target = None
54
+ if version:
55
+ target = next((v for v in versions if v.name == version), None)
56
+ if not target:
57
+ ui.error(f"Version {version} not found.")
58
+ raise typer.Exit(1)
59
+ else:
60
+ # Default to latest non-prerelease
61
+ target = next((v for v in versions if not v.prerelease), versions[0])
62
+ ui.info(f"No version specified, selecting latest stable release: {target.name}")
63
+
64
+ # Check if already installed
65
+ target_dir = base_dir / f"{target.name}-{target.short_sha}"
66
+ if target_dir.exists() and not force:
67
+ ui.info(f"Version {target.name} is already installed at {target_dir}")
68
+ return
69
+
70
+ try:
71
+ version_dir = download_version(target, force=force, base_dir=base_dir, jar_name=jar_name)
72
+ ui.success(f"Successfully installed {target.name} to {version_dir}")
73
+ except (requests.RequestException, OSError) as e:
74
+ ui.error(f"Failed to download: {e}")
75
+ raise typer.Exit(1) from e
@@ -0,0 +1,72 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+
5
+ from tlaplus_cli.cmd.modules import app
6
+ from tlaplus_cli.config.loader import load_config, save_config, workspace_root
7
+ from tlaplus_cli.java.classpath import ClasspathResolver, ResolveMode, SubdirType
8
+ from tlaplus_cli.tlc.compiler import get_tlc_jar_path
9
+
10
+
11
+ @app.command(name="path")
12
+ def set_modules_path(
13
+ path: str | None = typer.Argument(None, help="Path to the custom Java modules directory, or 'none' to reset."),
14
+ resolved: bool = typer.Option(False, "--resolved", help="Show the actual resolved paths for the current context."),
15
+ ) -> None:
16
+ """Configure or view custom Java modules source path."""
17
+ config_obj = load_config()
18
+
19
+ if resolved:
20
+ resolver = ClasspathResolver(config_obj, project_root=workspace_root(), tool_jar=get_tlc_jar_path())
21
+
22
+ # Source path (modules)
23
+ tla_library = resolver.get_tla_library_property()
24
+ typer.echo(f"Source Path: {tla_library or 'None'}")
25
+
26
+ # Classes path
27
+ classes_dir = resolver.get_project_path(SubdirType.CLASSES)
28
+ typer.echo(f"Classes Path: {classes_dir or 'None'}")
29
+
30
+ # Library path (JARs)
31
+ libs = resolver.resolve(ResolveMode.RUNTIME)
32
+ # Filter out classes_dir and tool_jar to show only libs
33
+ tool_jar = get_tlc_jar_path()
34
+ only_libs = [p for p in libs if p != str(classes_dir) and p != str(tool_jar)]
35
+
36
+ lib_str = "None"
37
+ if only_libs:
38
+ lib_str = f"{len(only_libs)} JARs"
39
+
40
+ typer.echo(f"Library Path: {lib_str}")
41
+ if only_libs:
42
+ for lib in only_libs:
43
+ typer.echo(f" - {lib}")
44
+
45
+ # Tool Jar
46
+ typer.echo(f"Tool Jar: {tool_jar}")
47
+ return
48
+
49
+ if path is None:
50
+ if config_obj.module_path:
51
+ typer.echo(f"Current modules path: {config_obj.module_path}")
52
+ else:
53
+ default_path = workspace_root() / config_obj.workspace.modules_dir
54
+ typer.echo("Custom modules path is not set.")
55
+ typer.echo(f"Defaulting to: {default_path}")
56
+ return
57
+
58
+ if path.lower() == "none":
59
+ config_obj.module_path = None
60
+ save_config(config_obj)
61
+ typer.echo("Custom modules path reset to default.")
62
+ return
63
+
64
+ p = Path(path).resolve()
65
+
66
+ if not p.is_dir():
67
+ typer.echo(f"Error: Path does not exist or is not a directory: {p}", err=True)
68
+ raise typer.Exit(1)
69
+
70
+ config_obj.module_path = str(p)
71
+ save_config(config_obj)
72
+ typer.echo(f"Modules path updated to: {p}")
@@ -0,0 +1,139 @@
1
+ import typer
2
+
3
+ from tlaplus_cli import ui
4
+ from tlaplus_cli.config.loader import load_config
5
+ from tlaplus_cli.java.classpath import ClasspathResolver
6
+ from tlaplus_cli.project import find_project_root
7
+ from tlaplus_cli.tlc.compiler import get_tlc_jar_path
8
+ from tlaplus_cli.tlc.models import CheckState, CheckStatus, ModelCheckResult
9
+ from tlaplus_cli.tlc.runner import (
10
+ build_tlc_command,
11
+ get_tlc_version,
12
+ resolve_spec_file,
13
+ run_tlc,
14
+ )
15
+
16
+
17
+ def version_callback(value: bool) -> None:
18
+ if value:
19
+ jar_path = get_tlc_jar_path()
20
+ typer.echo(f"tla2tools.jar path: {jar_path}")
21
+
22
+ if not jar_path.exists():
23
+ ui.error(f"tla2tools.jar not found at {jar_path}")
24
+ typer.echo("Run 'tla tools install' first.", err=True)
25
+ raise typer.Exit(1)
26
+
27
+ version_str = get_tlc_version()
28
+ if version_str:
29
+ typer.echo(version_str)
30
+
31
+ raise typer.Exit(0)
32
+
33
+
34
+ def print_progress(result: ModelCheckResult) -> None:
35
+ """Callback to print progress from ModelCheckResult."""
36
+ if result.status == CheckStatus.SuccessorStatesComputing and result.initial_states_stat:
37
+ last_stat = result.initial_states_stat[-1]
38
+ # Use carriage return to update the same line for progress
39
+ msg = f"Progress: {last_stat.total} states generated, {last_stat.distinct} distinct states found"
40
+ typer.echo(f"\r{msg}", err=True, nl=False)
41
+ elif result.status == CheckStatus.Finished:
42
+ # Final newline after progress
43
+ typer.echo("", err=True)
44
+
45
+
46
+ def _show_diagnostic_info(spec: str, spec_name: str) -> None:
47
+ """Show diagnostic info before running TLC."""
48
+ version_str = get_tlc_version() or "unknown"
49
+ typer.echo(f"Running TLC ({version_str}) on {spec_name} ...")
50
+
51
+ # Check if we have TLA-Library
52
+ try:
53
+ config = load_config()
54
+ spec_file, _ = resolve_spec_file(spec)
55
+ project_root = find_project_root(
56
+ spec_file,
57
+ modules_dir=config.workspace.modules_dir,
58
+ classes_dir=config.workspace.classes_dir,
59
+ )
60
+ jar_path = get_tlc_jar_path()
61
+ resolver = ClasspathResolver(config, project_root=project_root, tool_jar=jar_path)
62
+ tla_library = resolver.get_tla_library_property()
63
+ if tla_library:
64
+ ui.info(f"TLA-Library: {tla_library}")
65
+ except Exception:
66
+ pass # nosec
67
+
68
+
69
+ # PLR0912: This command handler orchestrates multiple output types (errors, warnings, results, progress);
70
+ # splitting it would fragment the CLI's primary user-facing logic.
71
+ def tlc( # noqa: PLR0912
72
+ spec: str = typer.Argument(help="Name of the TLA+ specification (without .tla extension)."),
73
+ # Typer requires the parameter in the signature to register the CLI option;
74
+ # since the logic is in the eager callback, the value is not used here.
75
+ version: bool | None = typer.Option( # noqa: ARG001
76
+ None,
77
+ "--version",
78
+ help="Print the path to tla2tools.jar and its version.",
79
+ callback=version_callback,
80
+ is_eager=True,
81
+ ),
82
+ show_command: bool = typer.Option(False, "--show-command", help="Print the command instead of executing it."),
83
+ ) -> None:
84
+ """Run TLC model checker on a TLA+ specification."""
85
+ # version argument is handled by version_callback (is_eager=True)
86
+ try:
87
+ _, spec_name = resolve_spec_file(spec)
88
+ except FileNotFoundError as e:
89
+ ui.error(str(e))
90
+ raise typer.Exit(1) from None
91
+
92
+ if show_command:
93
+ try:
94
+ cmd = build_tlc_command(spec)
95
+ except FileNotFoundError as e:
96
+ ui.error(str(e))
97
+ raise typer.Exit(1) from None
98
+ typer.echo(" ".join(cmd))
99
+ raise typer.Exit(0)
100
+
101
+ _show_diagnostic_info(spec, spec_name)
102
+
103
+ try:
104
+ # We need the parser to get the final result, so we'll wrap run_tlc
105
+ # or capture the result from the last callback.
106
+ final_result: list[ModelCheckResult] = []
107
+
108
+ def wrapped_callback(res: ModelCheckResult) -> None:
109
+ print_progress(res)
110
+ if not final_result:
111
+ final_result.append(res)
112
+ else:
113
+ final_result[0] = res
114
+
115
+ exit_code = run_tlc(spec, callback=wrapped_callback)
116
+
117
+ if final_result:
118
+ res = final_result[0]
119
+ if res.state == CheckState.Success:
120
+ ui.success("Model checking completed successfully.")
121
+ elif res.state == CheckState.Error:
122
+ ui.error(f"Model checking failed with {len(res.errors)} error(s).")
123
+ for err in res.errors:
124
+ for line in err.lines:
125
+ typer.echo(f" {line}", err=True)
126
+
127
+ if res.warnings:
128
+ ui.warn(f"Found {len(res.warnings)} warning(s).")
129
+ for warn_info in res.warnings:
130
+ for line in warn_info.lines:
131
+ typer.echo(f" {line}", err=True)
132
+
133
+ if res.duration:
134
+ ui.info(f"Duration: {res.duration}ms")
135
+ except FileNotFoundError as e:
136
+ ui.error(str(e))
137
+ raise typer.Exit(1) from None
138
+
139
+ raise typer.Exit(exit_code)
@@ -2,6 +2,7 @@ import typer
2
2
 
3
3
  from tlaplus_cli.cmd.tools.meta import app
4
4
  from tlaplus_cli.config.loader import load_config
5
+ from tlaplus_cli.ui import warn
5
6
  from tlaplus_cli.versioning import (
6
7
  fetch_remote_versions,
7
8
  list_local_versions,
@@ -28,6 +29,6 @@ def meta_sync() -> None:
28
29
  write_version_metadata(lv.path, target)
29
30
  typer.echo(f"Synced metadata for {lv.path.name}")
30
31
  else:
31
- typer.echo(f"⚠ Warning: Could not find remote data for {lv.name}", err=True)
32
+ warn(f"Could not find remote data for {lv.name}")
32
33
 
33
34
  typer.echo("Metadata sync complete.")
@@ -5,6 +5,7 @@ import typer
5
5
 
6
6
  from tlaplus_cli.cmd.tools import app
7
7
  from tlaplus_cli.config.loader import cache_dir
8
+ from tlaplus_cli.ui import warn
8
9
  from tlaplus_cli.versioning import (
9
10
  clear_pin,
10
11
  get_pinned_version_dir,
@@ -24,12 +25,12 @@ def _resolve_uninstall_targets(versions: list[str]) -> list[Path]:
24
25
  if legacy.exists():
25
26
  targets.append(legacy)
26
27
  else:
27
- typer.echo("⚠ Warning: Default (legacy) tla2tools.jar not found.", err=True)
28
+ warn("Default (legacy) tla2tools.jar not found.")
28
29
  continue
29
30
 
30
31
  matching = sorted([lv for lv in local_versions if lv.name == v], key=lambda x: x.path.name)
31
32
  if not matching:
32
- typer.echo(f"⚠ Warning: Version {v} not found locally.", err=True)
33
+ warn(f"Version {v} not found locally.")
33
34
  continue
34
35
 
35
36
  if len(matching) > 1:
@@ -13,6 +13,7 @@ class TlaUrls(BaseModel):
13
13
 
14
14
  class TlaConfig(BaseModel):
15
15
  urls: TlaUrls
16
+ community_modules: TlaUrls | None = None
16
17
 
17
18
 
18
19
  class WorkspaceConfig(BaseModel):
@@ -0,0 +1,174 @@
1
+ import os
2
+ from enum import Enum, auto
3
+ from pathlib import Path
4
+
5
+ from tlaplus_cli.config.schema import Settings
6
+ from tlaplus_cli.versioning.paths import get_modules_dir
7
+
8
+
9
+ class ResolveMode(Enum):
10
+ """Modes for classpath resolution."""
11
+
12
+ RUNTIME = auto() # For 'java -cp ... tlc2.TLC' (classes first)
13
+ COMPILE = auto() # For 'javac -cp ...' (tool jar first, no output classes)
14
+
15
+
16
+ class SubdirType(Enum):
17
+ """Types of project-local subdirectories."""
18
+
19
+ MODULES = auto()
20
+ CLASSES = auto()
21
+ LIB = auto()
22
+
23
+
24
+ class ClasspathResolver:
25
+ """
26
+ Centralized component for resolving Java classpaths and system properties.
27
+ Follows a strict Specific-to-General hierarchy: CLI > Project > Config > Global.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ config: Settings,
33
+ project_root: Path | None = None,
34
+ tool_jar: Path | None = None,
35
+ extra_libs: list[str] | None = None,
36
+ extra_modules: list[str] | None = None,
37
+ ):
38
+ """
39
+ Initialize the resolver.
40
+
41
+ Args:
42
+ config: The global settings.
43
+ project_root: The root of the TLA+ project (where .tla file is).
44
+ tool_jar: Path to tla2tools.jar.
45
+ extra_libs: Explicit library paths from CLI.
46
+ extra_modules: Explicit module paths from CLI.
47
+ """
48
+ self.config = config
49
+ self.project_root = project_root
50
+ self.tool_jar = tool_jar
51
+ self.extra_libs = extra_libs or []
52
+ self.extra_modules = extra_modules or []
53
+
54
+ def get_project_path(self, subdir: SubdirType) -> Path | None:
55
+ """Helper to find a project-local directory."""
56
+ if not self.project_root:
57
+ return None
58
+
59
+ # 1. Try explicit workspace config mapping
60
+ if subdir == SubdirType.MODULES:
61
+ path = self.project_root / self.config.workspace.modules_dir
62
+ if path.is_dir():
63
+ return path
64
+ elif subdir == SubdirType.CLASSES:
65
+ path = self.project_root / self.config.workspace.classes_dir
66
+ if path.is_dir():
67
+ return path
68
+
69
+ # 2. Try direct subdir (new standard)
70
+ subdir_name = {
71
+ SubdirType.MODULES: "modules",
72
+ SubdirType.CLASSES: "classes",
73
+ SubdirType.LIB: "lib",
74
+ }[subdir]
75
+ path = self.project_root / subdir_name
76
+ if path.is_dir():
77
+ return path
78
+
79
+ # 3. Try legacy/nested 'lib' inside modules_dir
80
+ if subdir == SubdirType.LIB:
81
+ path = self.project_root / self.config.workspace.modules_dir / "lib"
82
+ if path.is_dir():
83
+ return path
84
+
85
+ return None
86
+
87
+ def _resolve_paths(self, cli_paths: list[str], subdir: SubdirType, config_path: str | None) -> list[str]:
88
+ """
89
+ Resolves a set of paths following the hierarchy.
90
+ Deduplicates while preserving order.
91
+ """
92
+ raw_paths = []
93
+
94
+ # 1. CLI Arguments
95
+ raw_paths.extend(cli_paths)
96
+
97
+ # 2. Project-Local
98
+ project_path = self.get_project_path(subdir)
99
+ if project_path:
100
+ raw_paths.append(str(project_path.absolute()))
101
+
102
+ # 3. Config
103
+ if config_path:
104
+ raw_paths.append(str(Path(config_path).absolute()))
105
+
106
+ # 4. Managed Modules (Global Cache)
107
+ managed_dir = get_modules_dir()
108
+ if managed_dir.is_dir():
109
+ # Add all subdirectories of managed_dir to raw_paths so they are expanded for JARs
110
+ for item in managed_dir.iterdir():
111
+ if item.is_dir():
112
+ raw_paths.append(str(item.absolute()))
113
+
114
+ # Deduplicate while preserving order
115
+ seen = set()
116
+ deduped = []
117
+ for p in raw_paths:
118
+ # Expand directories to include JARs if it's a lib path
119
+ path_obj = Path(p)
120
+ if path_obj.is_dir() and subdir == SubdirType.LIB:
121
+ for jar in sorted(path_obj.glob("*.jar")):
122
+ jar_abs = str(jar.absolute())
123
+ if jar_abs not in seen:
124
+ seen.add(jar_abs)
125
+ deduped.append(jar_abs)
126
+ else:
127
+ p_abs = str(path_obj.absolute())
128
+ if p_abs not in seen:
129
+ seen.add(p_abs)
130
+ deduped.append(p_abs)
131
+
132
+ return deduped
133
+
134
+ def resolve(self, mode: ResolveMode) -> list[str]:
135
+ """
136
+ Resolve all necessary paths for the classpath based on the requested mode.
137
+ Deduplicates results while preserving the Specific-to-General order
138
+ and mode-specific requirements.
139
+ """
140
+ classpath = []
141
+
142
+ # In RUNTIME mode, custom classes shadow everything
143
+ if mode == ResolveMode.RUNTIME:
144
+ classes_dir = self.get_project_path(SubdirType.CLASSES)
145
+ if classes_dir:
146
+ classpath.append(str(classes_dir.absolute()))
147
+
148
+ # Tool JAR
149
+ if self.tool_jar:
150
+ classpath.append(str(self.tool_jar.absolute()))
151
+
152
+ # Libraries (CLI > Project > Config)
153
+ libs = self._resolve_paths(self.extra_libs, SubdirType.LIB, self.config.module_lib_path)
154
+ classpath.extend(libs)
155
+
156
+ # Final deduplication (e.g. if tool_jar is also in libs)
157
+ seen = set()
158
+ final_cp = []
159
+ for p in classpath:
160
+ if p not in seen:
161
+ seen.add(p)
162
+ final_cp.append(p)
163
+
164
+ return final_cp
165
+
166
+ def get_tla_library_property(self) -> str | None:
167
+ """
168
+ Resolve paths for -DTLA-Library system property.
169
+ Deduplicates, orders, and joins multiple valid directories.
170
+ """
171
+ modules = self._resolve_paths(self.extra_modules, SubdirType.MODULES, self.config.module_path)
172
+ if not modules:
173
+ return None
174
+ return os.pathsep.join(modules)
@@ -3,6 +3,10 @@ tla:
3
3
  tags: https://api.github.com/repos/tlaplus/tlaplus/tags
4
4
  releases: https://api.github.com/repos/tlaplus/tlaplus/releases
5
5
  per_page: 30
6
+ community_modules:
7
+ tags: https://api.github.com/repos/tlaplus/CommunityModules/tags
8
+ releases: https://api.github.com/repos/tlaplus/CommunityModules/releases
9
+ per_page: 30
6
10
 
7
11
  # Path to the TLA+ workspace (specs, custom modules, compiled classes).
8
12
  # Relative paths are resolved from the current working directory.