tlaplus-cli 0.6.0__tar.gz → 0.7.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 (91) hide show
  1. tlaplus_cli-0.7.0/PKG-INFO +51 -0
  2. tlaplus_cli-0.7.0/README.md +21 -0
  3. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/pyproject.toml +2 -2
  4. tlaplus_cli-0.7.0/src/tlaplus_cli/cmd/modules/__init__.py +16 -0
  5. tlaplus_cli-0.7.0/src/tlaplus_cli/cmd/modules/add.py +138 -0
  6. tlaplus_cli-0.7.0/src/tlaplus_cli/cmd/modules/list.py +45 -0
  7. tlaplus_cli-0.7.0/src/tlaplus_cli/cmd/modules/path.py +68 -0
  8. tlaplus_cli-0.7.0/src/tlaplus_cli/cmd/modules/remove.py +20 -0
  9. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tlc.py +103 -1
  10. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/java/classpath.py +27 -4
  11. tlaplus_cli-0.7.0/src/tlaplus_cli/tlc/compiler.py +12 -0
  12. tlaplus_cli-0.7.0/src/tlaplus_cli/tlc/components.py +287 -0
  13. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/formatter.py +19 -80
  14. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/runner.py +82 -6
  15. tlaplus_cli-0.7.0/src/tlaplus_cli.egg-info/PKG-INFO +51 -0
  16. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli.egg-info/SOURCES.txt +5 -18
  17. tlaplus_cli-0.6.0/PKG-INFO +0 -296
  18. tlaplus_cli-0.6.0/README.md +0 -266
  19. tlaplus_cli-0.6.0/src/tlaplus_cli/cmd/modules/__init__.py +0 -5
  20. tlaplus_cli-0.6.0/src/tlaplus_cli/cmd/modules/build.py +0 -41
  21. tlaplus_cli-0.6.0/src/tlaplus_cli/cmd/modules/install.py +0 -75
  22. tlaplus_cli-0.6.0/src/tlaplus_cli/cmd/modules/lib.py +0 -46
  23. tlaplus_cli-0.6.0/src/tlaplus_cli/cmd/modules/path.py +0 -72
  24. tlaplus_cli-0.6.0/src/tlaplus_cli/tlc/compiler.py +0 -81
  25. tlaplus_cli-0.6.0/src/tlaplus_cli.egg-info/PKG-INFO +0 -296
  26. tlaplus_cli-0.6.0/tests/test_build_both.py +0 -33
  27. tlaplus_cli-0.6.0/tests/test_build_modules_path.py +0 -50
  28. tlaplus_cli-0.6.0/tests/test_cli.py +0 -39
  29. tlaplus_cli-0.6.0/tests/test_config.py +0 -59
  30. tlaplus_cli-0.6.0/tests/test_config_suite.py +0 -74
  31. tlaplus_cli-0.6.0/tests/test_modules_build_lib.py +0 -88
  32. tlaplus_cli-0.6.0/tests/test_modules_build_show_command.py +0 -27
  33. tlaplus_cli-0.6.0/tests/test_modules_install_community.py +0 -86
  34. tlaplus_cli-0.6.0/tests/test_modules_lib.py +0 -106
  35. tlaplus_cli-0.6.0/tests/test_modules_path.py +0 -101
  36. tlaplus_cli-0.6.0/tests/test_modules_path_resolved.py +0 -33
  37. tlaplus_cli-0.6.0/tests/test_settings.py +0 -38
  38. tlaplus_cli-0.6.0/tests/test_tlc_modules_path.py +0 -50
  39. tlaplus_cli-0.6.0/tests/test_tlc_show_command.py +0 -25
  40. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/LICENSE +0 -0
  41. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/MANIFEST.in +0 -0
  42. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/setup.cfg +0 -0
  43. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/__init__.py +0 -0
  44. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cache/__init__.py +0 -0
  45. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cache/github.py +0 -0
  46. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cli.py +0 -0
  47. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/__init__.py +0 -0
  48. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/check_java.py +0 -0
  49. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/config/__init__.py +0 -0
  50. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/config/edit.py +0 -0
  51. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/config/list.py +0 -0
  52. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/fetch_cache/__init__.py +0 -0
  53. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/fetch_cache/clear.py +0 -0
  54. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/__init__.py +0 -0
  55. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/dir.py +0 -0
  56. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/install.py +0 -0
  57. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/list.py +0 -0
  58. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/meta/__init__.py +0 -0
  59. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/meta/sync.py +0 -0
  60. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/path.py +0 -0
  61. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/pin.py +0 -0
  62. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/uninstall.py +0 -0
  63. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/cmd/tools/upgrade.py +0 -0
  64. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/config/__init__.py +0 -0
  65. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/config/loader.py +0 -0
  66. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/config/schema.py +0 -0
  67. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/java/__init__.py +0 -0
  68. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/java/inspector.py +0 -0
  69. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/project/__init__.py +0 -0
  70. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/project/core.py +0 -0
  71. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/py.typed +0 -0
  72. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/resources/__init__.py +0 -0
  73. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/resources/default_config.yaml +0 -0
  74. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/__init__.py +0 -0
  75. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/codes.py +0 -0
  76. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/models.py +0 -0
  77. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/parser.py +0 -0
  78. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/sany.py +0 -0
  79. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/tlc/values.py +0 -0
  80. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/ui.py +0 -0
  81. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/__init__.py +0 -0
  82. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/api.py +0 -0
  83. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/downloader.py +0 -0
  84. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/metadata.py +0 -0
  85. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/paths.py +0 -0
  86. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/resolver.py +0 -0
  87. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli/versioning/schema.py +0 -0
  88. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
  89. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
  90. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli.egg-info/requires.txt +0 -0
  91. {tlaplus_cli-0.6.0 → tlaplus_cli-0.7.0}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: tlaplus-cli
3
+ Version: 0.7.0
4
+ Summary: TLA+ tools: download TLC, compile custom modules, run model checker
5
+ Author-email: Denis Nikolskiy <codeomatics@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/nikolskiy/tlaplus-cli
8
+ Project-URL: Repository, https://github.com/nikolskiy/tlaplus-cli
9
+ Project-URL: Issues, https://github.com/nikolskiy/tlaplus-cli/issues
10
+ Project-URL: Changelog, https://github.com/nikolskiy/tlaplus-cli/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: typer<1.0.0,>=0.22.0
23
+ Requires-Dist: requests<3.0.0,>=2.31.0
24
+ Requires-Dist: pyyaml<7.0,>=6.0
25
+ Requires-Dist: platformdirs<5.0,>=4.0
26
+ Requires-Dist: pydantic<3.0.0,>=2.12.5
27
+ Requires-Dist: truststore<1.0.0,>=0.9.0
28
+ Requires-Dist: rich<14.0.0,>=13.7.0
29
+ Dynamic: license-file
30
+
31
+ # TLA+ CLI
32
+
33
+ Command-line tool for working with TLA+ specifications and the TLC model checker.
34
+
35
+ ## Project goals
36
+
37
+ 1. Simplify managing TLA+ tools such as TLC, community modules, custom modules, etc.
38
+ 2. Run TLA tools in a single CLI interface.
39
+ 3. Manage and display the tools outputs.
40
+
41
+ ## Features
42
+
43
+ * [Managing TLA+ Tools](docs/managing-tlaplus-tools/managing-tla2tools.md): Verify Java requirements, manage/pin `tla2tools.jar` versions, and handle cache settings.
44
+ * [Running TLC](docs/tlc/tlc.md): Run the TLC model checker, inspect commands, and execute model checking with custom modules.
45
+ * [Custom Java Modules](docs/java-modules-for-tlc/README.md): Compile, register, and manage custom Java operator overrides for TLC using automated CLI commands.
46
+
47
+ ## Installation
48
+
49
+ For detailed installation instructions, please refer to:
50
+ * [Installation via uv tool](docs/installation/uv-tool.md)
51
+ * [Nix Flakes Installation & Development](docs/installation/nix.md)
@@ -0,0 +1,21 @@
1
+ # TLA+ CLI
2
+
3
+ Command-line tool for working with TLA+ specifications and the TLC model checker.
4
+
5
+ ## Project goals
6
+
7
+ 1. Simplify managing TLA+ tools such as TLC, community modules, custom modules, etc.
8
+ 2. Run TLA tools in a single CLI interface.
9
+ 3. Manage and display the tools outputs.
10
+
11
+ ## Features
12
+
13
+ * [Managing TLA+ Tools](docs/managing-tlaplus-tools/managing-tla2tools.md): Verify Java requirements, manage/pin `tla2tools.jar` versions, and handle cache settings.
14
+ * [Running TLC](docs/tlc/tlc.md): Run the TLC model checker, inspect commands, and execute model checking with custom modules.
15
+ * [Custom Java Modules](docs/java-modules-for-tlc/README.md): Compile, register, and manage custom Java operator overrides for TLC using automated CLI commands.
16
+
17
+ ## Installation
18
+
19
+ For detailed installation instructions, please refer to:
20
+ * [Installation via uv tool](docs/installation/uv-tool.md)
21
+ * [Nix Flakes Installation & Development](docs/installation/nix.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tlaplus-cli"
7
- version = "0.6.0"
7
+ version = "0.7.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"
@@ -68,7 +68,7 @@ dev = [
68
68
 
69
69
  [tool.pytest.ini_options]
70
70
  addopts = "--cov=tlaplus_cli --cov-report=term-missing"
71
- testpaths = ["tests"]
71
+ testpaths = ["tests/unit"]
72
72
 
73
73
  [tool.ruff]
74
74
  line-length = 120
@@ -0,0 +1,16 @@
1
+ import typer
2
+
3
+ app = typer.Typer(name="modules", help="Manage TLA+ Java modules.", no_args_is_help=True)
4
+
5
+
6
+ # Import functions for new commands
7
+ from .add import add_module # noqa: E402
8
+ from .list import list_modules # noqa: E402
9
+ from .path import show_modules_path # noqa: E402
10
+ from .remove import remove_module # noqa: E402
11
+
12
+ # Register new commands with the typer app
13
+ app.command(name="add")(add_module)
14
+ app.command(name="list")(list_modules)
15
+ app.command(name="remove")(remove_module)
16
+ app.command(name="path")(show_modules_path)
@@ -0,0 +1,138 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from tlaplus_cli.config.loader import load_config
12
+ from tlaplus_cli.java.classpath import ClasspathResolver, ResolveMode
13
+ from tlaplus_cli.tlc.compiler import get_tlc_jar_path
14
+ from tlaplus_cli.versioning.paths import get_modules_dir
15
+
16
+
17
+ def find_override_classes(java_files: list[Path]) -> list[str]:
18
+ """Find all class names implementing ITLCOverrides in Java files."""
19
+ override_classes = []
20
+ package_pattern = re.compile(r"package\s+([a-zA-Z0-9_.]+)\s*;")
21
+ class_pattern = re.compile(
22
+ r"\bclass\s+([a-zA-Z0-9_]+)\b[^{]*\bimplements\b[^{]*\b(tlc2\.overrides\.)?ITLCOverrides\b"
23
+ )
24
+
25
+ for jf in java_files:
26
+ try:
27
+ content = jf.read_text(encoding="utf-8", errors="ignore")
28
+ except OSError:
29
+ continue
30
+
31
+ # Strip comments to prevent false matches
32
+ content_no_comments = re.sub(r"//.*", "", content)
33
+ content_no_comments = re.sub(r"/\*.*?\*/", "", content_no_comments, flags=re.DOTALL)
34
+
35
+ package_match = package_pattern.search(content_no_comments)
36
+ package_name = package_match.group(1) if package_match else ""
37
+
38
+ for match in class_pattern.finditer(content_no_comments):
39
+ class_name = match.group(1)
40
+ full_class_name = f"{package_name}.{class_name}" if package_name else class_name
41
+ override_classes.append(full_class_name)
42
+
43
+ return override_classes
44
+
45
+
46
+ def add_module( # noqa: PLR0912, PLR0915
47
+ path: str = typer.Argument(..., help="Path to the module directory to add."),
48
+ ) -> None:
49
+ """Compile custom Java overrides and add/update the module in the cache."""
50
+ source_dir = Path(path).resolve()
51
+ if not source_dir.is_dir():
52
+ typer.echo(f"Error: Path does not exist or is not a directory: {source_dir}", err=True)
53
+ raise typer.Exit(1)
54
+
55
+ config = load_config()
56
+ module_name = source_dir.name
57
+ target_dir = get_modules_dir() / module_name
58
+
59
+ # Overwrite if exists
60
+ if target_dir.exists():
61
+ shutil.rmtree(target_dir)
62
+ target_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ # 1. Copy directories (lib, modules, tlc2, and test/tests)
65
+ def copy_subfolder(subname: str) -> None:
66
+ src = source_dir / subname
67
+ if src.is_dir():
68
+ shutil.copytree(src, target_dir / subname)
69
+
70
+ typer.echo("Copying subfolders (lib, modules, test, tlc2)...")
71
+ copy_subfolder("lib")
72
+ copy_subfolder("modules")
73
+ copy_subfolder("tlc2")
74
+
75
+ if (source_dir / "test").is_dir():
76
+ copy_subfolder("test")
77
+ elif (source_dir / "tests").is_dir():
78
+ copy_subfolder("tests")
79
+
80
+ # 2. Compile Java overrides in cached version if tlc2/overrides exists
81
+ java_files = []
82
+ for candidate in [target_dir / "modules" / "tlc2" / "overrides", target_dir / "tlc2" / "overrides"]:
83
+ if candidate.is_dir():
84
+ java_files.extend(list(candidate.rglob("*.java")))
85
+
86
+ if java_files:
87
+ # Build classpath including tool_jar and local libs from the cached folder
88
+ cached_lib = target_dir / "lib"
89
+ extra_libs = []
90
+ if cached_lib.is_dir():
91
+ extra_libs.extend([str(j.absolute()) for j in cached_lib.glob("*.jar")])
92
+
93
+ tool_jar = get_tlc_jar_path()
94
+ if not tool_jar.exists():
95
+ typer.echo("Error: tla2tools.jar not found. Run 'tla tools install' first.", err=True)
96
+ shutil.rmtree(target_dir, ignore_errors=True)
97
+ raise typer.Exit(1)
98
+
99
+ resolver = ClasspathResolver(config, project_root=None, tool_jar=tool_jar, extra_libs=extra_libs)
100
+ classpath = os.pathsep.join(resolver.resolve(ResolveMode.COMPILE))
101
+
102
+ classes_dir = target_dir / "classes"
103
+ classes_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ cmd = ["javac", "-cp", classpath, "-d", str(classes_dir), *[str(f) for f in java_files]]
106
+ typer.echo("Compiling Java files in cached tlc2/overrides...")
107
+ try:
108
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
109
+ except FileNotFoundError as err:
110
+ typer.echo("Error: 'javac' not found. Ensure JDK is installed.", err=True)
111
+ shutil.rmtree(target_dir, ignore_errors=True)
112
+ raise typer.Exit(1) from err
113
+ except subprocess.CalledProcessError as err:
114
+ typer.echo("Compilation failed!", err=True)
115
+ typer.echo(err.stdout, err=True)
116
+ typer.echo(err.stderr, err=True)
117
+ shutil.rmtree(target_dir, ignore_errors=True)
118
+ raise typer.Exit(1) from err
119
+
120
+ # Write service file
121
+ meta_inf = classes_dir / "META-INF" / "services"
122
+ meta_inf.mkdir(parents=True, exist_ok=True)
123
+ service_file = meta_inf / "tlc2.overrides.ITLCOverrides"
124
+
125
+ override_classes = find_override_classes(java_files)
126
+ if not override_classes:
127
+ override_classes = [config.tlc.overrides_class]
128
+
129
+ with service_file.open("w") as f:
130
+ for cls in override_classes:
131
+ f.write(f"{cls}\n")
132
+
133
+ # 3. Create metadata.json
134
+ metadata = {"name": module_name, "built_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
135
+ with (target_dir / "metadata.json").open("w") as f:
136
+ json.dump(metadata, f, indent=2)
137
+
138
+ typer.echo(f"Module '{module_name}' successfully added to cache.")
@@ -0,0 +1,45 @@
1
+ import json
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from tlaplus_cli.versioning.paths import get_modules_dir
8
+
9
+
10
+ def list_modules() -> None:
11
+ """List all custom TLA+ modules added to the cache."""
12
+ modules_dir = get_modules_dir()
13
+
14
+ if not modules_dir.is_dir():
15
+ typer.echo("No modules currently added.")
16
+ return
17
+
18
+ modules_data = []
19
+ for item in sorted(modules_dir.iterdir()):
20
+ if item.is_dir():
21
+ metadata_file = item / "metadata.json"
22
+ if metadata_file.exists():
23
+ try:
24
+ meta = json.loads(metadata_file.read_text())
25
+ name = meta.get("name", item.name)
26
+ built_at = meta.get("built_at", "Unknown")
27
+ modules_data.append((name, built_at))
28
+ except Exception:
29
+ modules_data.append((item.name, "Corrupt Metadata"))
30
+ else:
31
+ modules_data.append((item.name, "Unknown"))
32
+
33
+ if not modules_data:
34
+ typer.echo("No modules currently added.")
35
+ return
36
+
37
+ table = Table()
38
+ table.add_column("Module Name", style="cyan")
39
+ table.add_column("Last Built/Modified", style="magenta")
40
+
41
+ for name, built_at in modules_data:
42
+ table.add_row(name, built_at)
43
+
44
+ console = Console()
45
+ console.print(table)
@@ -0,0 +1,68 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from tlaplus_cli.config.loader import load_config
7
+ from tlaplus_cli.java.classpath import ClasspathResolver, ResolveMode, SubdirType
8
+ from tlaplus_cli.tlc.compiler import get_tlc_jar_path
9
+ from tlaplus_cli.versioning.paths import get_modules_dir
10
+
11
+
12
+ def show_modules_path() -> None: # noqa: PLR0912
13
+ """Display the active paths that will be included when running TLC."""
14
+ config_obj = load_config()
15
+ tool_jar = get_tlc_jar_path()
16
+
17
+ resolver = ClasspathResolver(config_obj, project_root=None, tool_jar=tool_jar)
18
+
19
+ # 1. Source Paths (-DTLA-Library)
20
+ tla_library = resolver.get_tla_library_property()
21
+ typer.echo("Source Paths (-DTLA-Library):")
22
+ if tla_library:
23
+ for p in tla_library.split(os.pathsep):
24
+ typer.echo(f" - {p}")
25
+ else:
26
+ typer.echo(" None")
27
+
28
+ # 2. Classes Paths
29
+ classes_paths = []
30
+ project_classes = resolver.get_project_path(SubdirType.CLASSES)
31
+ if project_classes:
32
+ classes_paths.append(str(project_classes.absolute()))
33
+
34
+ managed_dir = get_modules_dir()
35
+ if managed_dir.is_dir():
36
+ for item in sorted(managed_dir.iterdir()):
37
+ if item.is_dir():
38
+ mod_classes = item / "classes"
39
+ if mod_classes.is_dir():
40
+ classes_paths.append(str(mod_classes.absolute()))
41
+
42
+ typer.echo("\nClasses Paths:")
43
+ if classes_paths:
44
+ for p in classes_paths:
45
+ typer.echo(f" - {p}")
46
+ else:
47
+ typer.echo(" None")
48
+
49
+ # 3. Library Paths (JARs)
50
+ libs = resolver.resolve(ResolveMode.RUNTIME)
51
+ tool_jar_abs = str(tool_jar.absolute()) if tool_jar.exists() else ""
52
+
53
+ # Filter out classes directories and tool_jar to show only external library JARs
54
+ only_libs = []
55
+ for p in libs:
56
+ if p == tool_jar_abs:
57
+ continue
58
+ p_path = Path(p)
59
+ if p_path.name == "classes" and p_path.is_dir():
60
+ continue
61
+ only_libs.append(p)
62
+
63
+ typer.echo("\nLibrary Paths (JARs):")
64
+ if only_libs:
65
+ for p in only_libs:
66
+ typer.echo(f" - {p}")
67
+ else:
68
+ typer.echo(" None")
@@ -0,0 +1,20 @@
1
+ import shutil
2
+
3
+ import typer
4
+
5
+ from tlaplus_cli.versioning.paths import get_modules_dir
6
+
7
+
8
+ def remove_module(module_name: str = typer.Argument(..., help="Name of the module to remove from cache.")) -> None:
9
+ """Remove a custom module from the cache."""
10
+ target_dir = get_modules_dir() / module_name
11
+ if not target_dir.is_dir():
12
+ typer.echo(f"Error: Module '{module_name}' not found in cache.", err=True)
13
+ raise typer.Exit(1)
14
+
15
+ try:
16
+ shutil.rmtree(target_dir)
17
+ typer.echo(f"Module '{module_name}' successfully deleted from cache.")
18
+ except OSError as e:
19
+ typer.echo(f"Error: Failed to delete module: {e}", err=True)
20
+ raise typer.Exit(1) from e
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  import typer
2
4
  from rich.console import Console
3
5
  from rich.live import Live
@@ -7,6 +9,7 @@ from tlaplus_cli.config.loader import load_config
7
9
  from tlaplus_cli.java.classpath import ClasspathResolver
8
10
  from tlaplus_cli.project import find_project_root
9
11
  from tlaplus_cli.tlc.compiler import get_tlc_jar_path
12
+ from tlaplus_cli.tlc.components import StatsTableFormatter, StatusFooterRenderable
10
13
  from tlaplus_cli.tlc.formatter import TlcFormatter
11
14
  from tlaplus_cli.tlc.models import CheckState, ModelCheckResult
12
15
  from tlaplus_cli.tlc.runner import (
@@ -78,6 +81,7 @@ def tlc( # noqa: PLR0912, PLR0915
78
81
  help="Refresh interval for the output (seconds).",
79
82
  ),
80
83
  coverage: bool = typer.Option(False, "--coverage", help="Enable code coverage visualization."),
84
+ raw: bool = typer.Option(False, "--raw", help="Run TLC in raw mode without structured tool mode formatting."),
81
85
  ) -> None:
82
86
  """Run TLC model checker on a TLA+ specification."""
83
87
  # version argument is handled by version_callback (is_eager=True)
@@ -96,16 +100,80 @@ def tlc( # noqa: PLR0912, PLR0915
96
100
  typer.echo(" ".join(cmd))
97
101
  raise typer.Exit(0)
98
102
 
103
+ if raw:
104
+ exit_code = run_tlc(spec, coverage=coverage, raw=True)
105
+ raise typer.Exit(exit_code)
106
+
99
107
  tlc_version = get_tlc_version() or "unknown"
100
108
  formatter = TlcFormatter(tlc_version=tlc_version)
101
109
  layout = formatter.create_layout()
102
110
 
111
+ is_tty = sys.stdout.isatty()
112
+
103
113
  try:
104
114
  final_result: list[ModelCheckResult] = []
115
+ printed_logs_count = 0
116
+ printed_stats_count = 0
117
+ stats_header_printed = False
118
+ stats_border_printed = False
119
+
120
+ if is_tty:
121
+ table_formatter = StatsTableFormatter()
122
+ footer_renderable = StatusFooterRenderable(ModelCheckResult())
123
+ console = Console()
124
+
125
+ with Live(footer_renderable, refresh_per_second=4, screen=False) as live:
126
+
127
+ def wrapped_callback(res: ModelCheckResult) -> None:
128
+ nonlocal printed_logs_count, printed_stats_count, stats_header_printed, stats_border_printed
129
+
130
+ # Keep tests happy by updating the formatter
131
+ formatter.update(layout, res)
132
+
133
+ if not final_result:
134
+ final_result.append(res)
135
+ else:
136
+ final_result[0] = res
137
+
138
+ # Update the live status footer dynamic data
139
+ footer_renderable.result = res
105
140
 
106
- with Live(layout, refresh_per_second=4, screen=False):
141
+ # Stream any new logs
142
+ if len(res.output_lines) > printed_logs_count:
143
+ new_logs = res.output_lines[printed_logs_count:]
144
+ for log in new_logs:
145
+ live.console.print(log.strip())
146
+ printed_logs_count = len(res.output_lines)
147
+
148
+ # Stream any new progress stats
149
+ if len(res.initial_states_stat) > printed_stats_count:
150
+ new_stats = res.initial_states_stat[printed_stats_count:]
151
+ for stat in new_stats:
152
+ if not stats_header_printed:
153
+ live.console.print("\nState Space Progress:\n")
154
+ live.console.print(table_formatter.get_header())
155
+ live.console.print(table_formatter.get_border())
156
+ stats_header_printed = True
157
+
158
+ row_str = table_formatter.format_row(stat)
159
+ live.console.print(row_str)
160
+ printed_stats_count = len(res.initial_states_stat)
161
+
162
+ # Print bottom border when execution changes state to non-running
163
+ if res.state != CheckState.Running and stats_header_printed and not stats_border_printed:
164
+ live.console.print(table_formatter.get_border())
165
+ stats_border_printed = True
166
+
167
+ exit_code = run_tlc(spec, callback=wrapped_callback, coverage=coverage)
168
+ else:
169
+ # Non-TTY mode: no Live block
170
+ table_formatter = StatsTableFormatter()
171
+ print(f"Starting TLC model checker (version {tlc_version}).")
107
172
 
108
173
  def wrapped_callback(res: ModelCheckResult) -> None:
174
+ nonlocal printed_logs_count, printed_stats_count, stats_header_printed, stats_border_printed
175
+
176
+ # Keep tests happy by updating the formatter
109
177
  formatter.update(layout, res)
110
178
 
111
179
  if not final_result:
@@ -113,8 +181,42 @@ def tlc( # noqa: PLR0912, PLR0915
113
181
  else:
114
182
  final_result[0] = res
115
183
 
184
+ # Stream any new logs
185
+ if len(res.output_lines) > printed_logs_count:
186
+ new_logs = res.output_lines[printed_logs_count:]
187
+ for log in new_logs:
188
+ print(log.strip())
189
+ printed_logs_count = len(res.output_lines)
190
+
191
+ # Stream any new progress stats
192
+ if len(res.initial_states_stat) > printed_stats_count:
193
+ new_stats = res.initial_states_stat[printed_stats_count:]
194
+ for stat in new_stats:
195
+ if not stats_header_printed:
196
+ print("\nState Space Progress:\n")
197
+ print(table_formatter.get_header())
198
+ print(table_formatter.get_border())
199
+ stats_header_printed = True
200
+
201
+ row_str = table_formatter.format_row(stat)
202
+ print(row_str)
203
+ printed_stats_count = len(res.initial_states_stat)
204
+
205
+ # Print bottom border when execution changes state to non-running
206
+ if res.state != CheckState.Running and stats_header_printed and not stats_border_printed:
207
+ print(table_formatter.get_border())
208
+ stats_border_printed = True
209
+
116
210
  exit_code = run_tlc(spec, callback=wrapped_callback, coverage=coverage)
117
211
 
212
+ # Fallback print of border if not printed yet
213
+ if stats_header_printed and not stats_border_printed:
214
+ if is_tty:
215
+ console.print(table_formatter.get_border())
216
+ else:
217
+ print(table_formatter.get_border())
218
+ stats_border_printed = True
219
+
118
220
  if final_result:
119
221
  res = final_result[0]
120
222
 
@@ -84,7 +84,7 @@ class ClasspathResolver:
84
84
 
85
85
  return None
86
86
 
87
- def _resolve_paths(self, cli_paths: list[str], subdir: SubdirType, config_path: str | None) -> list[str]:
87
+ def _resolve_paths(self, cli_paths: list[str], subdir: SubdirType, config_path: str | None) -> list[str]: # noqa: PLR0912
88
88
  """
89
89
  Resolves a set of paths following the hierarchy.
90
90
  Deduplicates while preserving order.
@@ -106,10 +106,19 @@ class ClasspathResolver:
106
106
  # 4. Managed Modules (Global Cache)
107
107
  managed_dir = get_modules_dir()
108
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():
109
+ # Add all subdirectories of managed_dir to raw_paths so they are expanded for JARs/TLA files
110
+ for item in sorted(managed_dir.iterdir()):
111
111
  if item.is_dir():
112
- raw_paths.append(str(item.absolute()))
112
+ if subdir == SubdirType.LIB:
113
+ mod_lib = item / "lib"
114
+ if mod_lib.is_dir():
115
+ raw_paths.append(str(mod_lib.absolute()))
116
+ raw_paths.append(str(item.absolute())) # Legacy fallback
117
+ elif subdir == SubdirType.MODULES:
118
+ mod_modules = item / "modules"
119
+ if mod_modules.is_dir():
120
+ raw_paths.append(str(mod_modules.absolute()))
121
+ raw_paths.append(str(item.absolute())) # Legacy fallback
113
122
 
114
123
  # Deduplicate while preserving order
115
124
  seen = set()
@@ -141,10 +150,24 @@ class ClasspathResolver:
141
150
 
142
151
  # In RUNTIME mode, custom classes shadow everything
143
152
  if mode == ResolveMode.RUNTIME:
153
+ # Project-local classes
144
154
  classes_dir = self.get_project_path(SubdirType.CLASSES)
145
155
  if classes_dir:
146
156
  classpath.append(str(classes_dir.absolute()))
147
157
 
158
+ # Module source directories containing .tla files
159
+ modules = self._resolve_paths(self.extra_modules, SubdirType.MODULES, self.config.module_path)
160
+ classpath.extend(modules)
161
+
162
+ # Cached module classes are included as dependencies in all modes (RUNTIME & COMPILE)
163
+ managed_dir = get_modules_dir()
164
+ if managed_dir.is_dir():
165
+ for item in sorted(managed_dir.iterdir()):
166
+ if item.is_dir():
167
+ mod_classes = item / "classes"
168
+ if mod_classes.is_dir():
169
+ classpath.append(str(mod_classes.absolute()))
170
+
148
171
  # Tool JAR
149
172
  if self.tool_jar:
150
173
  classpath.append(str(self.tool_jar.absolute()))
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+
3
+ from tlaplus_cli.config.loader import cache_dir
4
+ from tlaplus_cli.versioning import get_pinned_version_dir
5
+
6
+
7
+ def get_tlc_jar_path() -> Path:
8
+ """Resolve the path to tla2tools.jar using the fallback chain: pinned -> legacy."""
9
+ pinned_dir = get_pinned_version_dir()
10
+ pinned_jar = pinned_dir / "tla2tools.jar" if pinned_dir else None
11
+ legacy = cache_dir() / "tla2tools.jar"
12
+ return pinned_jar if (pinned_jar and pinned_jar.exists()) else legacy