bg3-core 1.0.0b1__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 (48) hide show
  1. bg3_core-1.0.0b1/.gitignore +58 -0
  2. bg3_core-1.0.0b1/PKG-INFO +72 -0
  3. bg3_core-1.0.0b1/README.md +65 -0
  4. bg3_core-1.0.0b1/bg3_core/__init__.py +0 -0
  5. bg3_core-1.0.0b1/bg3_core/bump_mod_version.py +288 -0
  6. bg3_core-1.0.0b1/bg3_core/cli.py +2025 -0
  7. bg3_core-1.0.0b1/bg3_core/cli_common.py +204 -0
  8. bg3_core-1.0.0b1/bg3_core/env_paths.py +318 -0
  9. bg3_core-1.0.0b1/bg3_core/larian_parsers.py +379 -0
  10. bg3_core-1.0.0b1/bg3_core/library_catalog.py +379 -0
  11. bg3_core-1.0.0b1/bg3_core/lsf.py +443 -0
  12. bg3_core-1.0.0b1/bg3_core/lspk.py +387 -0
  13. bg3_core-1.0.0b1/bg3_core/manage_load_order.py +1528 -0
  14. bg3_core-1.0.0b1/bg3_core/manage_saves.py +648 -0
  15. bg3_core-1.0.0b1/bg3_core/mod_overrides.py +252 -0
  16. bg3_core-1.0.0b1/bg3_core/mod_resolver.py +436 -0
  17. bg3_core-1.0.0b1/bg3_core/modio_client.py +429 -0
  18. bg3_core-1.0.0b1/bg3_core/modio_create_collection.py +485 -0
  19. bg3_core-1.0.0b1/bg3_core/modio_extract.py +948 -0
  20. bg3_core-1.0.0b1/bg3_core/modio_search.py +381 -0
  21. bg3_core-1.0.0b1/bg3_core/modio_sync.py +472 -0
  22. bg3_core-1.0.0b1/bg3_core/modio_update.py +408 -0
  23. bg3_core-1.0.0b1/bg3_core/modio_update_metadata.py +384 -0
  24. bg3_core-1.0.0b1/bg3_core/purge_library.py +132 -0
  25. bg3_core-1.0.0b1/bg3_core/query_mod_database.py +507 -0
  26. bg3_core-1.0.0b1/bg3_core/render_modio_description.py +263 -0
  27. bg3_core-1.0.0b1/bg3_core/save_inspector.py +478 -0
  28. bg3_core-1.0.0b1/bg3_core/setup_environment.py +718 -0
  29. bg3_core-1.0.0b1/bg3_core/skills/bg3-create-modio-collection/SKILL.md +45 -0
  30. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/SKILL.md +50 -0
  31. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/references/library_catalog_spec.md +22 -0
  32. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/resources/document_subagent_template.json +11 -0
  33. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/SKILL.md +48 -0
  34. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/resources/return_message_templates.md +19 -0
  35. bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/resources/single_mod_readme_template.md +27 -0
  36. bg3_core-1.0.0b1/bg3_core/skills/bg3-manage-load-order/SKILL.md +47 -0
  37. bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/SKILL.md +52 -0
  38. bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/references/search_ranking_rubric.md +27 -0
  39. bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/resources/conflict_subagent_template.json +18 -0
  40. bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/SKILL.md +48 -0
  41. bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/references/catalog_sync_flow.md +19 -0
  42. bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/resources/update_subagent_template.json +11 -0
  43. bg3_core-1.0.0b1/bg3_core/skills/bg3-update-single-mod/SKILL.md +50 -0
  44. bg3_core-1.0.0b1/bg3_core/skills/bg3-update-single-mod/resources/update_status_messages.md +31 -0
  45. bg3_core-1.0.0b1/bg3_core/skills_installer.py +263 -0
  46. bg3_core-1.0.0b1/bg3_core/tools.py +693 -0
  47. bg3_core-1.0.0b1/bg3_core/update_library_catalog.py +314 -0
  48. bg3_core-1.0.0b1/pyproject.toml +17 -0
@@ -0,0 +1,58 @@
1
+ # ==============================================================================
2
+ # Baldur's Gate 3 Mod Studio & Mod.io Toolkit - Git Ignore Rules
3
+ # ==============================================================================
4
+
5
+ # Secrets & Environment Configurations
6
+ .env
7
+ *.env
8
+ .env.*.local
9
+
10
+ # Python runtime & caches
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .pytest_cache/
16
+ .coverage
17
+ htmlcov/
18
+ dist/
19
+ build/
20
+ *.egg-info/
21
+ .eggs/
22
+ .cache/
23
+
24
+ # Virtual Environments
25
+ .venv/
26
+ venv/
27
+ env/
28
+ ENV/
29
+
30
+ # IDE & Editor Files
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+
37
+ # Operating System Files
38
+ .DS_Store
39
+ Thumbs.db
40
+ desktop.ini
41
+
42
+ # Build artifacts, logs & archives
43
+ *.log
44
+ *.tmp
45
+ *.bak
46
+ *.pak
47
+ *.loca
48
+ *.zip
49
+
50
+ # Scratch / temporary experimental folders
51
+ scratch/
52
+
53
+ # Externalized User Content (Ignore any local studio folders or accidental spills)
54
+ build_guides/
55
+ mods/
56
+ my_mods/
57
+ BG3_Mod_Studio/
58
+ .agents/ledgers/artifacts-ledger.md
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: bg3-core
3
+ Version: 1.0.0b1
4
+ Summary: Baldur's Gate 3 Mod Studio Core Runtime, Parsers, and API Client
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+
8
+ # bg3-core
9
+
10
+ Foundational runtime, binary parsers, cross-platform path resolution, and mod.io REST API client for the **Baldur's Gate 3 Mod Studio** workspace.
11
+
12
+ ---
13
+
14
+ ## Domain Architecture & Scope
15
+
16
+ `bg3-core` serves as the foundational, leaf-level dependency package in the monorepo DAG. It is designed to be completely independent of higher-level domain packages (`bg3-authoring` and `bg3-theorycraft`), providing low-level binary engines and runtime utilities that require zero external binary tools (such as LSLib's `divine.exe`, Wine, or .NET runtimes).
17
+
18
+ ```mermaid
19
+ graph TD
20
+ Authoring["bg3-authoring"] --> Core["bg3-core"]
21
+ Theorycraft["bg3-theorycraft"] --> Core["bg3-core"]
22
+ CLI["bg3 CLI Router"] --> Core
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Core Modules & Exported Interfaces
28
+
29
+ | Module | Core Responsibility | Key Exported Symbols |
30
+ | :--- | :--- | :--- |
31
+ | `bg3_core.larian_parsers` | Larian binary formats, version encoding, and localization | `encode_version64()`, `decode_version64()`, `parse_loca_bytes()`, `compile_xml_bytes_to_loca()`, `split_larian_items()` |
32
+ | `bg3_core.lspk` | Pure-Python LSPK v18 archive reader, builder, and unpacker | `LspkArchive`, `build_lspk_archive()`, `lz4_block_decompress()`, `lz4_frame_or_block_decompress()`, `decompress_pak_file_data()` |
33
+ | `bg3_core.save_inspector` | Save game metadata extraction, AST audit, and mod pruning | `extract_save_metadata()`, `audit_against_modsettings()`, `prune_missing_mods_from_save()` |
34
+ | `bg3_core.manage_saves` | Savegame discovery, backup, retention purging, and rescue | `discover_save_games()`, `handle_clean_missing()` |
35
+ | `bg3_core.env_paths` | Cross-platform state, cache, config, and game path detection | `get_xdg_config_home()`, `get_xdg_cache_home()`, `get_xdg_data_home()`, `detect_bg3_appdata_dirs()`, `detect_bg3_install_dir()` |
36
+ | `bg3_core.cli_common` | Stream separation, POSIX exit codes, and JSON envelopes | `OutputEmitter`, `ConsoleColor`, `ExitCode`, `format_json_response()`, `confirm_action()` |
37
+ | `bg3_core.modio_client` | Authenticated mod.io v1 REST client | `ModioClient`, `ModioError`, `resolve_modio_token()` |
38
+ | `bg3_core.tools` | Dynamic CLI argument parser introspection and registry | `build_parser_ast_registry()`, `search_tools()`, `verify_registry()`, `audit_skills()` |
39
+ | `bg3_core.cli` | Central unified router for all workspace domains | `main()`, `build_parser()`, `dispatch_command()` |
40
+
41
+ ---
42
+
43
+ ## CLI Console Entrypoint
44
+
45
+ This package registers the primary console script for the entire studio:
46
+
47
+ ```toml
48
+ [project.scripts]
49
+ bg3 = "bg3_core.cli:main"
50
+ ```
51
+
52
+ ### Common Commands Driven by `bg3-core`:
53
+ - `bg3 tools list` — Discover all registered tools across packages.
54
+ - `bg3 tools search <query>` — Search tools by semantic intent.
55
+ - `bg3 tools info <tool>` — Inspect tool arguments and schemas.
56
+ - `bg3 tools verify --audit-skills` — Programmatic health audit of agent skills.
57
+ - `bg3 modio search <query>` — Search remote community mods on mod.io.
58
+ - `bg3 modio sync` — Synchronize subscribed mods and download `.pak` packages.
59
+ - `bg3 loadorder list` — Inspect active `modsettings.lsx` load order.
60
+ - `bg3 save inspect latest` — Inspect save metadata and audit mod requirements.
61
+ - `bg3 save clean-missing` — Prune missing mods to rescue corrupted saves.
62
+
63
+ ---
64
+
65
+ ## Cross-Platform Storage Architecture
66
+
67
+ All state directories comply strictly with the XDG Base Directory specification on Linux/macOS and APPDATA specifications on Windows:
68
+
69
+ - **Configuration**: `$XDG_CONFIG_HOME/bg3/env` (or `%APPDATA%\bg3\env`)
70
+ - **Cache**: `$XDG_CACHE_HOME/bg3/` (or `%LOCALAPPDATA%\bg3\`)
71
+ - **User Content**: `$XDG_DOCUMENTS_DIR/BG3_Mod_Studio/Mods/`
72
+ - **Game VFS**: Automatically detects Steam Native, Proton (`compatdata/1086940/pfx`), WSL, and Windows installations.
@@ -0,0 +1,65 @@
1
+ # bg3-core
2
+
3
+ Foundational runtime, binary parsers, cross-platform path resolution, and mod.io REST API client for the **Baldur's Gate 3 Mod Studio** workspace.
4
+
5
+ ---
6
+
7
+ ## Domain Architecture & Scope
8
+
9
+ `bg3-core` serves as the foundational, leaf-level dependency package in the monorepo DAG. It is designed to be completely independent of higher-level domain packages (`bg3-authoring` and `bg3-theorycraft`), providing low-level binary engines and runtime utilities that require zero external binary tools (such as LSLib's `divine.exe`, Wine, or .NET runtimes).
10
+
11
+ ```mermaid
12
+ graph TD
13
+ Authoring["bg3-authoring"] --> Core["bg3-core"]
14
+ Theorycraft["bg3-theorycraft"] --> Core["bg3-core"]
15
+ CLI["bg3 CLI Router"] --> Core
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Core Modules & Exported Interfaces
21
+
22
+ | Module | Core Responsibility | Key Exported Symbols |
23
+ | :--- | :--- | :--- |
24
+ | `bg3_core.larian_parsers` | Larian binary formats, version encoding, and localization | `encode_version64()`, `decode_version64()`, `parse_loca_bytes()`, `compile_xml_bytes_to_loca()`, `split_larian_items()` |
25
+ | `bg3_core.lspk` | Pure-Python LSPK v18 archive reader, builder, and unpacker | `LspkArchive`, `build_lspk_archive()`, `lz4_block_decompress()`, `lz4_frame_or_block_decompress()`, `decompress_pak_file_data()` |
26
+ | `bg3_core.save_inspector` | Save game metadata extraction, AST audit, and mod pruning | `extract_save_metadata()`, `audit_against_modsettings()`, `prune_missing_mods_from_save()` |
27
+ | `bg3_core.manage_saves` | Savegame discovery, backup, retention purging, and rescue | `discover_save_games()`, `handle_clean_missing()` |
28
+ | `bg3_core.env_paths` | Cross-platform state, cache, config, and game path detection | `get_xdg_config_home()`, `get_xdg_cache_home()`, `get_xdg_data_home()`, `detect_bg3_appdata_dirs()`, `detect_bg3_install_dir()` |
29
+ | `bg3_core.cli_common` | Stream separation, POSIX exit codes, and JSON envelopes | `OutputEmitter`, `ConsoleColor`, `ExitCode`, `format_json_response()`, `confirm_action()` |
30
+ | `bg3_core.modio_client` | Authenticated mod.io v1 REST client | `ModioClient`, `ModioError`, `resolve_modio_token()` |
31
+ | `bg3_core.tools` | Dynamic CLI argument parser introspection and registry | `build_parser_ast_registry()`, `search_tools()`, `verify_registry()`, `audit_skills()` |
32
+ | `bg3_core.cli` | Central unified router for all workspace domains | `main()`, `build_parser()`, `dispatch_command()` |
33
+
34
+ ---
35
+
36
+ ## CLI Console Entrypoint
37
+
38
+ This package registers the primary console script for the entire studio:
39
+
40
+ ```toml
41
+ [project.scripts]
42
+ bg3 = "bg3_core.cli:main"
43
+ ```
44
+
45
+ ### Common Commands Driven by `bg3-core`:
46
+ - `bg3 tools list` — Discover all registered tools across packages.
47
+ - `bg3 tools search <query>` — Search tools by semantic intent.
48
+ - `bg3 tools info <tool>` — Inspect tool arguments and schemas.
49
+ - `bg3 tools verify --audit-skills` — Programmatic health audit of agent skills.
50
+ - `bg3 modio search <query>` — Search remote community mods on mod.io.
51
+ - `bg3 modio sync` — Synchronize subscribed mods and download `.pak` packages.
52
+ - `bg3 loadorder list` — Inspect active `modsettings.lsx` load order.
53
+ - `bg3 save inspect latest` — Inspect save metadata and audit mod requirements.
54
+ - `bg3 save clean-missing` — Prune missing mods to rescue corrupted saves.
55
+
56
+ ---
57
+
58
+ ## Cross-Platform Storage Architecture
59
+
60
+ All state directories comply strictly with the XDG Base Directory specification on Linux/macOS and APPDATA specifications on Windows:
61
+
62
+ - **Configuration**: `$XDG_CONFIG_HOME/bg3/env` (or `%APPDATA%\bg3\env`)
63
+ - **Cache**: `$XDG_CACHE_HOME/bg3/` (or `%LOCALAPPDATA%\bg3\`)
64
+ - **User Content**: `$XDG_DOCUMENTS_DIR/BG3_Mod_Studio/Mods/`
65
+ - **Game VFS**: Automatically detects Steam Native, Proton (`compatdata/1086940/pfx`), WSL, and Windows installations.
File without changes
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ bump_mod_version.py
4
+ -------------------
5
+ Inspects and updates semantic version numbers and Larian Version64 integers
6
+ atomically across `Mods/<UUID>/meta.lsx` (and legacy `Projects/<UUID>/meta.lsx`),
7
+ and keeps `my_mods/<slug>/README.md` in sync so documentation and metadata never drift.
8
+
9
+ Features:
10
+ - Encodes and decodes 64-bit integer versions: (major << 55) | (minor << 47) | (revision << 31) | build
11
+ - Supports semantic increments: --build, --patch, --minor, --major, or explicit --set
12
+ - Atomic update across meta.lsx and README.md
13
+ - Clean regex-based replacement preserving exact XML and Markdown formatting
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+ from typing import List, Tuple
22
+
23
+ _REPO_ROOT = Path(__file__).resolve().parents[2]
24
+ if str(_REPO_ROOT) not in sys.path:
25
+ sys.path.insert(0, str(_REPO_ROOT))
26
+
27
+ from bg3_core.env_paths import MY_MODS_DIR, REPO_ROOT, resolve_mod_dir
28
+ from bg3_core.larian_parsers import (
29
+ calculate_version64,
30
+ decode_version64,
31
+ format_version_tuple,
32
+ parse_version_str,
33
+ )
34
+
35
+ WORKSPACE_ROOT = REPO_ROOT
36
+
37
+
38
+ def find_mod_directories(mod_query: str = "") -> List[Path]:
39
+ """Find mod directories in my_mods matching query or return all."""
40
+ if not mod_query:
41
+ if not MY_MODS_DIR.exists():
42
+ return []
43
+ return sorted([d for d in MY_MODS_DIR.iterdir() if d.is_dir()])
44
+
45
+ try:
46
+ target = resolve_mod_dir(mod_query)
47
+ if target and target.is_dir():
48
+ return [target]
49
+ except Exception:
50
+ pass
51
+
52
+ if not MY_MODS_DIR.exists():
53
+ return []
54
+
55
+ query_norm = mod_query.strip().lower().replace("-", "").replace("_", "")
56
+ matches = []
57
+ for d in MY_MODS_DIR.iterdir():
58
+ if d.is_dir():
59
+ name_norm = d.name.lower().replace("-", "").replace("_", "")
60
+ if query_norm in name_norm:
61
+ matches.append(d)
62
+ return sorted(matches)
63
+
64
+
65
+ def get_meta_files(mod_dir: Path) -> List[Path]:
66
+ """Find meta.lsx files in Mods/ (and legacy Projects/ with ModuleInfo) to update Version64."""
67
+ files = []
68
+ # Always include runtime module descriptors in Mods/
69
+ mods_dir = mod_dir / "Mods"
70
+ if mods_dir.exists():
71
+ for p in mods_dir.glob("*/meta.lsx"):
72
+ files.append(p)
73
+
74
+ # Only include Projects/ if it contains ModuleInfo (legacy scaffold)
75
+ proj_dir = mod_dir / "Projects"
76
+ if proj_dir.exists():
77
+ for p in proj_dir.glob("*/meta.lsx"):
78
+ try:
79
+ if '<node id="ModuleInfo">' in p.read_text(encoding="utf-8"):
80
+ files.append(p)
81
+ except Exception:
82
+ pass
83
+ return files
84
+
85
+
86
+ def read_mod_version(meta_path: Path) -> Tuple[int, int, int, int, int]:
87
+ """Extract Version64 integer and decoded tuple from a meta.lsx file."""
88
+ content = meta_path.read_text(encoding="utf-8")
89
+
90
+ # Locate ModuleInfo node first to avoid matching dependencies or PublishVersion
91
+ mod_info_match = re.search(r'<node\s+id="ModuleInfo">(.*?)</node>', content, re.DOTALL)
92
+ if mod_info_match:
93
+ target_text = mod_info_match.group(1)
94
+ else:
95
+ target_text = content
96
+
97
+ ver_match = re.search(r'<attribute\s+id="Version64"\s+type="int64"\s+value="(\d+)"\s*/>', target_text)
98
+ if not ver_match:
99
+ raise ValueError(f"Could not find Version64 attribute under ModuleInfo in {meta_path}")
100
+
101
+ raw_val = int(ver_match.group(1))
102
+ major, minor, rev, bld = decode_version64(raw_val)
103
+ return major, minor, rev, bld, raw_val
104
+
105
+
106
+ def update_meta_version(meta_path: Path, new_v64: int, dry_run: bool = False) -> bool:
107
+ """Updates Version64 under ModuleInfo in meta.lsx while preserving formatting."""
108
+ content = meta_path.read_text(encoding="utf-8")
109
+
110
+ def replacer(match: re.Match[str]) -> str:
111
+ node_content = match.group(0)
112
+ updated_node = re.sub(
113
+ r'(<attribute\s+id="Version64"\s+type="int64"\s+value=")\d+("\s*/>)',
114
+ rf"\g<1>{new_v64}\g<2>",
115
+ node_content,
116
+ count=1,
117
+ )
118
+ return updated_node
119
+
120
+ new_content, count = re.subn(
121
+ r'<node\s+id="ModuleInfo">.*?</node>',
122
+ replacer,
123
+ content,
124
+ count=1,
125
+ flags=re.DOTALL,
126
+ )
127
+
128
+ if count == 0:
129
+ return False
130
+
131
+ if not dry_run:
132
+ meta_path.write_text(new_content, encoding="utf-8")
133
+ return True
134
+
135
+
136
+ def update_readme_version(mod_dir: Path, new_version_str: str, dry_run: bool = False) -> bool:
137
+ """Update version line in README.md if present to avoid documentation drift."""
138
+ readme_path = mod_dir / "README.md"
139
+ if not readme_path.is_file():
140
+ return False
141
+
142
+ content = readme_path.read_text(encoding="utf-8")
143
+ pattern = r'(?i)(-\s*\*\*Version:\*\*\s*[`"]?|\*{0,2}Version:?\*{0,2}\s*[:=]?\s*[`"]?)(\d+(?:\.\d+)*)([`"]?)'
144
+ new_content, count = re.subn(
145
+ pattern,
146
+ rf"\g<1>{new_version_str}\g<3>",
147
+ content,
148
+ count=1,
149
+ )
150
+ if count == 0 or new_content == content:
151
+ return False
152
+
153
+ if not dry_run:
154
+ readme_path.write_text(new_content, encoding="utf-8")
155
+ return True
156
+
157
+
158
+ def bump_version(
159
+ mod_name: str,
160
+ action: str = "show",
161
+ set_value: str = "",
162
+ dry_run: bool = False,
163
+ as_json: bool = False,
164
+ ) -> dict:
165
+ matched = find_mod_directories(mod_name)
166
+ if not matched:
167
+ raise ValueError(f"No mod found in my_mods/ matching query '{mod_name}'.")
168
+
169
+ target_mod = matched[0]
170
+ meta_files = get_meta_files(target_mod)
171
+ if not meta_files:
172
+ raise ValueError(f"No meta.lsx files found in {target_mod} under Projects/ or Mods/.")
173
+
174
+ # Read current from first file
175
+ major, minor, rev, bld, raw_val = read_mod_version(meta_files[0])
176
+ old_version_str = format_version_tuple((major, minor, rev, bld))
177
+
178
+ new_major, new_minor, new_rev, new_bld = major, minor, rev, bld
179
+
180
+ if action == "build":
181
+ new_bld += 1
182
+ elif action == "patch":
183
+ new_rev += 1
184
+ new_bld = 0
185
+ elif action == "minor":
186
+ new_minor += 1
187
+ new_rev = 0
188
+ new_bld = 0
189
+ elif action == "major":
190
+ new_major += 1
191
+ new_minor = 0
192
+ new_rev = 0
193
+ new_bld = 0
194
+ elif action == "set":
195
+ new_major, new_minor, new_rev, new_bld = parse_version_str(set_value)
196
+
197
+ new_v64 = calculate_version64(new_major, new_minor, new_rev, new_bld)
198
+ new_version_str = format_version_tuple((new_major, new_minor, new_rev, new_bld))
199
+
200
+ def _rel_path(p: Path, base: Path) -> str:
201
+ try:
202
+ return str(p.relative_to(base))
203
+ except ValueError:
204
+ return str(p)
205
+
206
+ updated_files = []
207
+ if action != "show":
208
+ for mf in meta_files:
209
+ if update_meta_version(mf, new_v64, dry_run=dry_run):
210
+ updated_files.append(_rel_path(mf, WORKSPACE_ROOT))
211
+ if update_readme_version(target_mod, new_version_str, dry_run=dry_run):
212
+ updated_files.append(_rel_path(target_mod / "README.md", WORKSPACE_ROOT))
213
+
214
+ result = {
215
+ "mod_name": target_mod.name,
216
+ "mod_dir": _rel_path(target_mod, WORKSPACE_ROOT),
217
+ "action": action,
218
+ "previous_version": old_version_str,
219
+ "previous_version64": raw_val,
220
+ "new_version": new_version_str,
221
+ "new_version64": new_v64,
222
+ "updated_files": updated_files,
223
+ "dry_run": dry_run,
224
+ }
225
+
226
+ if as_json:
227
+ print(json.dumps(result, indent=2))
228
+ else:
229
+ if action == "show":
230
+ print(f"Mod '{target_mod.name}' version: {old_version_str} (Version64: {raw_val})")
231
+ for mf in meta_files:
232
+ print(f" └ {_rel_path(mf, WORKSPACE_ROOT)}")
233
+ else:
234
+ action_desc = "[DRY-RUN] Would update" if dry_run else "Successfully updated"
235
+ print(f"{action_desc} '{target_mod.name}': {old_version_str} -> {new_version_str} (Version64: {new_v64})")
236
+ for f in updated_files:
237
+ print(f" ✓ Updated {f}")
238
+
239
+ return result
240
+
241
+
242
+ def main() -> None:
243
+ parser = argparse.ArgumentParser(
244
+ description="Auto-generate and bump semantic versions and Larian Version64 integers in BG3 mods."
245
+ )
246
+ parser.add_argument("--mod", required=True, help="Mod name or folder in my_mods/ (e.g. paladins-call)")
247
+ group = parser.add_mutually_exclusive_group()
248
+ group.add_argument("--show", action="store_true", help="Display current version without modifying")
249
+ group.add_argument("--build", "-b", action="store_true", help="Increment build number (+1)")
250
+ group.add_argument("--patch", "-p", action="store_true", help="Increment patch/revision (+1) and reset build")
251
+ group.add_argument("--minor", "-m", action="store_true", help="Increment minor version (+1) and reset patch/build")
252
+ group.add_argument("--major", action="store_true", help="Increment major version (+1) and reset minor/patch/build")
253
+ group.add_argument("--set", type=str, help="Explicitly set version (e.g. 1.2.0.0)")
254
+
255
+ parser.add_argument("--dry-run", action="store_true", help="Preview version changes without writing to disk")
256
+ parser.add_argument("--json", action="store_true", help="Output result as JSON")
257
+
258
+ args = parser.parse_args()
259
+
260
+ action = "show"
261
+ set_val = ""
262
+ if args.build:
263
+ action = "build"
264
+ elif args.patch:
265
+ action = "patch"
266
+ elif args.minor:
267
+ action = "minor"
268
+ elif args.major:
269
+ action = "major"
270
+ elif args.set:
271
+ action = "set"
272
+ set_val = args.set
273
+
274
+ try:
275
+ bump_version(
276
+ mod_name=args.mod,
277
+ action=action,
278
+ set_value=set_val,
279
+ dry_run=args.dry_run,
280
+ as_json=args.json,
281
+ )
282
+ except Exception as e:
283
+ print(f"Error: {e}", file=sys.stderr)
284
+ sys.exit(1)
285
+
286
+
287
+ if __name__ == "__main__":
288
+ main()