bg3-authoring 1.0.0b1__py3-none-any.whl
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.
- bg3_authoring/__init__.py +0 -0
- bg3_authoring/add_mod_dependency.py +367 -0
- bg3_authoring/cli.py +32 -0
- bg3_authoring/create_mod_scaffold.py +469 -0
- bg3_authoring/diagnostics/__init__.py +0 -0
- bg3_authoring/diagnostics/check_mod_conflicts.py +637 -0
- bg3_authoring/diagnostics/diagnose_toolkit_logs.py +765 -0
- bg3_authoring/diagnostics/inspect_save_game.py +29 -0
- bg3_authoring/diagnostics/sync_localization.py +280 -0
- bg3_authoring/diagnostics/validate_mod.py +576 -0
- bg3_authoring/generate_compat_patch.py +400 -0
- bg3_authoring/inject_treasure_table.py +267 -0
- bg3_authoring/merge_staging_to_mod.py +297 -0
- bg3_authoring/packaging/__init__.py +0 -0
- bg3_authoring/packaging/pack_mod_pak.py +385 -0
- bg3_authoring/packaging/sync_game_data.py +773 -0
- bg3_authoring/scaffold_action_resource.py +222 -0
- bg3_authoring/scaffold_appearance_visual.py +238 -0
- bg3_authoring/scaffold_class_description.py +455 -0
- bg3_authoring/scaffold_default_values.py +250 -0
- bg3_authoring/scaffold_feat.py +269 -0
- bg3_authoring/scaffold_icon_uvs.py +235 -0
- bg3_authoring/scaffold_level_map.py +210 -0
- bg3_authoring/scaffold_origin.py +223 -0
- bg3_authoring/scaffold_progression_node.py +360 -0
- bg3_authoring/scaffold_race.py +298 -0
- bg3_authoring/scaffold_root_template.py +403 -0
- bg3_authoring/scaffold_spell_list.py +196 -0
- bg3_authoring/scaffold_stat_entry.py +455 -0
- bg3_authoring/scaffold_tag.py +179 -0
- bg3_authoring/skills/bg3-check-mod-conflicts/SKILL.md +45 -0
- bg3_authoring/skills/bg3-check-mod-conflicts/references/conflict_methodology.md +33 -0
- bg3_authoring/skills/bg3-check-mod-conflicts/resources/remediation_patch_template.md +43 -0
- bg3_authoring/skills/bg3-create-mod/SKILL.md +55 -0
- bg3_authoring/skills/bg3-create-mod/references/grill_me_interview_protocol.md +15 -0
- bg3_authoring/skills/bg3-create-mod/resources/subagent_templates.md +40 -0
- bg3_authoring/skills/bg3-diagnose-mod/SKILL.md +52 -0
- bg3_authoring/skills/bg3-diagnose-mod/references/runtime_log_triage.md +30 -0
- bg3_authoring/skills/bg3-post-release/SKILL.md +52 -0
- bg3_authoring/skills/bg3-post-release/references/storefront_metadata_guide.md +37 -0
- bg3_authoring/skills/bg3-release-prep/SKILL.md +52 -0
- bg3_authoring/skills/bg3-release-prep/references/release_pipeline_specs.md +17 -0
- bg3_authoring/skills/bg3-test-mod/SKILL.md +58 -0
- bg3_authoring/skills/bg3-test-mod/references/save_audit_runbook.md +21 -0
- bg3_authoring/skills/bg3-test-mod/references/smoke_testing_runbook.md +23 -0
- bg3_authoring/templates/custom_mod_readme.md +30 -0
- bg3_authoring-1.0.0b1.dist-info/METADATA +70 -0
- bg3_authoring-1.0.0b1.dist-info/RECORD +50 -0
- bg3_authoring-1.0.0b1.dist-info/WHEEL +4 -0
- bg3_authoring-1.0.0b1.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
packages/bg3-authoring/bg3_authoring/add_mod_dependency.py
|
|
4
|
+
-----------------------------------------------------------
|
|
5
|
+
Appends community or core mod dependencies and test dependencies to an existing custom mod in my_mods/.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Resolves dependency metadata (UUID, Version64, MD5, Folder, Name, PublishHandle)
|
|
9
|
+
via local mods/library.md before falling back to mod.io.
|
|
10
|
+
- Injects standard Larian v4.8 ModuleShortDesc nodes into Mods/*/meta.lsx.
|
|
11
|
+
- Updates mod README.md under ## Dependencies.
|
|
12
|
+
- Supports adding both permanent dependencies and test dependencies in a single invocation.
|
|
13
|
+
- Supports multiple comma-separated mod slugs or repeated CLI flags.
|
|
14
|
+
- Supports --test-only (with values or as a flag) and --test-dep flags to manage test dependencies in .test_deps.json.
|
|
15
|
+
- Supports target mod detection via --mod, positional argument, or current working directory.
|
|
16
|
+
- Supports --dry-run and structured --json output.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import xml.etree.ElementTree as ET
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
28
|
+
from bg3_core.env_paths import MY_MODS_DIR, resolve_mod_dir
|
|
29
|
+
from bg3_core.mod_resolver import resolve_dependency_metadata
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_mod_from_cwd() -> Optional[str]:
|
|
33
|
+
"""Detects mod name if the current working directory is inside a mod in MY_MODS_DIR."""
|
|
34
|
+
try:
|
|
35
|
+
cwd = Path.cwd().resolve()
|
|
36
|
+
my_mods = MY_MODS_DIR.resolve()
|
|
37
|
+
rel = cwd.relative_to(my_mods)
|
|
38
|
+
if rel.parts:
|
|
39
|
+
return rel.parts[0]
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_slug_list(values: Any) -> List[str]:
|
|
46
|
+
"""Parses a list of CLI values which may contain comma-separated mod slugs."""
|
|
47
|
+
if not values:
|
|
48
|
+
return []
|
|
49
|
+
if isinstance(values, str):
|
|
50
|
+
values = [values]
|
|
51
|
+
slugs: List[str] = []
|
|
52
|
+
for val in values:
|
|
53
|
+
if isinstance(val, str):
|
|
54
|
+
for part in val.split(","):
|
|
55
|
+
cleaned = part.strip()
|
|
56
|
+
if cleaned and cleaned not in slugs:
|
|
57
|
+
slugs.append(cleaned)
|
|
58
|
+
return slugs
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_test_dependencies(mod_dir: Path, dep_metas: List[Dict[str, Any]], dry_run: bool = False) -> List[Dict[str, Any]]:
|
|
62
|
+
"""Adds temporary test dependencies to .test_deps.json in the mod workspace."""
|
|
63
|
+
config_file = mod_dir / ".test_deps.json"
|
|
64
|
+
data: Dict[str, Any] = {"test_dependencies": []}
|
|
65
|
+
if config_file.is_file():
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
68
|
+
if "test_dependencies" not in data or not isinstance(data["test_dependencies"], list):
|
|
69
|
+
data["test_dependencies"] = []
|
|
70
|
+
except Exception:
|
|
71
|
+
data = {"test_dependencies": []}
|
|
72
|
+
|
|
73
|
+
existing = [str(d).lower() for d in data["test_dependencies"]]
|
|
74
|
+
results: List[Dict[str, Any]] = []
|
|
75
|
+
modified = False
|
|
76
|
+
|
|
77
|
+
for dep_meta in dep_metas:
|
|
78
|
+
dep_name = dep_meta.get("Name") or dep_meta.get("Folder") or dep_meta.get("UUID") or "Unknown"
|
|
79
|
+
dep_identifier = dep_meta.get("Folder") or dep_name
|
|
80
|
+
|
|
81
|
+
if dep_name.lower() in existing or dep_identifier.lower() in existing:
|
|
82
|
+
results.append({
|
|
83
|
+
"status": "already_present",
|
|
84
|
+
"mod": mod_dir.name,
|
|
85
|
+
"type": "test_dependency",
|
|
86
|
+
"dependency": dep_name,
|
|
87
|
+
"config_file": str(config_file),
|
|
88
|
+
})
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
data["test_dependencies"].append(dep_name)
|
|
92
|
+
existing.append(dep_name.lower())
|
|
93
|
+
existing.append(dep_identifier.lower())
|
|
94
|
+
modified = True
|
|
95
|
+
|
|
96
|
+
results.append({
|
|
97
|
+
"status": "added_test_dependency",
|
|
98
|
+
"mod": mod_dir.name,
|
|
99
|
+
"type": "test_dependency",
|
|
100
|
+
"dependency": dep_name,
|
|
101
|
+
"uuid": dep_meta.get("UUID"),
|
|
102
|
+
"config_file": str(config_file),
|
|
103
|
+
"dry_run": dry_run,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if modified and not dry_run:
|
|
107
|
+
config_file.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
return results
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def add_test_dependency(mod_dir: Path, dep_meta: Dict[str, Any], dry_run: bool = False) -> Dict[str, Any]:
|
|
113
|
+
"""Single-dependency wrapper for add_test_dependencies."""
|
|
114
|
+
res = add_test_dependencies(mod_dir, [dep_meta], dry_run=dry_run)
|
|
115
|
+
return res[0] if res else {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def add_permanent_dependencies(mod_dir: Path, dep_metas: List[Dict[str, Any]], dry_run: bool = False) -> List[Dict[str, Any]]:
|
|
119
|
+
"""Injects dependency ModuleShortDesc nodes into meta.lsx and updates README.md."""
|
|
120
|
+
meta_files = list(mod_dir.glob("Mods/*/meta.lsx"))
|
|
121
|
+
if not meta_files:
|
|
122
|
+
raise FileNotFoundError(f"Could not locate Mods/*/meta.lsx in '{mod_dir}'.")
|
|
123
|
+
|
|
124
|
+
target_meta = meta_files[0]
|
|
125
|
+
tree = ET.parse(target_meta)
|
|
126
|
+
root = tree.getroot()
|
|
127
|
+
|
|
128
|
+
# Find or create Dependencies node
|
|
129
|
+
deps_node = root.find(".//node[@id='Dependencies']")
|
|
130
|
+
if deps_node is None:
|
|
131
|
+
root_node = root.find(".//node[@id='root']/children")
|
|
132
|
+
if root_node is None:
|
|
133
|
+
raise ValueError(f"Malformed meta.lsx in '{target_meta}': missing root children.")
|
|
134
|
+
deps_node = ET.SubElement(root_node, "node", id="Dependencies")
|
|
135
|
+
|
|
136
|
+
children = deps_node.find("children")
|
|
137
|
+
if children is None:
|
|
138
|
+
children = ET.SubElement(deps_node, "children")
|
|
139
|
+
|
|
140
|
+
existing_uuids = set()
|
|
141
|
+
for short_desc in children.findall("node[@id='ModuleShortDesc']"):
|
|
142
|
+
u_attr = short_desc.find("attribute[@id='UUID']")
|
|
143
|
+
if u_attr is not None and u_attr.get("value"):
|
|
144
|
+
existing_uuids.add(u_attr.get("value", "").lower())
|
|
145
|
+
|
|
146
|
+
results: List[Dict[str, Any]] = []
|
|
147
|
+
meta_modified = False
|
|
148
|
+
new_readme_entries: List[Tuple[Dict[str, Any], str]] = []
|
|
149
|
+
|
|
150
|
+
for dep_meta in dep_metas:
|
|
151
|
+
dep_uuid = (dep_meta.get("UUID") or "").lower()
|
|
152
|
+
if dep_uuid and dep_uuid in existing_uuids:
|
|
153
|
+
results.append({
|
|
154
|
+
"status": "already_present",
|
|
155
|
+
"mod": mod_dir.name,
|
|
156
|
+
"type": "permanent_dependency",
|
|
157
|
+
"dependency": dep_meta.get("Name"),
|
|
158
|
+
"uuid": dep_meta.get("UUID"),
|
|
159
|
+
"file": str(target_meta),
|
|
160
|
+
})
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Construct new ModuleShortDesc node
|
|
164
|
+
new_desc = ET.Element("node", id="ModuleShortDesc")
|
|
165
|
+
ET.SubElement(new_desc, "attribute", id="Folder", type="LSString", value=dep_meta.get("Folder", dep_meta.get("Name", "")))
|
|
166
|
+
ET.SubElement(new_desc, "attribute", id="MD5", type="LSString", value=dep_meta.get("MD5", ""))
|
|
167
|
+
ET.SubElement(new_desc, "attribute", id="Name", type="LSString", value=dep_meta.get("Name", ""))
|
|
168
|
+
ET.SubElement(new_desc, "attribute", id="PublishHandle", type="uint64", value=str(dep_meta.get("PublishHandle", 0)))
|
|
169
|
+
ET.SubElement(new_desc, "attribute", id="UUID", type="guid", value=dep_meta.get("UUID", ""))
|
|
170
|
+
ET.SubElement(new_desc, "attribute", id="Version64", type="int64", value=str(dep_meta.get("Version64", 36028797018963968)))
|
|
171
|
+
|
|
172
|
+
children.append(new_desc)
|
|
173
|
+
if dep_uuid:
|
|
174
|
+
existing_uuids.add(dep_uuid)
|
|
175
|
+
meta_modified = True
|
|
176
|
+
|
|
177
|
+
dep_entry = f"- `{dep_meta.get('Name')}` (`{dep_meta.get('UUID')}`)"
|
|
178
|
+
new_readme_entries.append((dep_meta, dep_entry))
|
|
179
|
+
|
|
180
|
+
results.append({
|
|
181
|
+
"status": "added_dependency",
|
|
182
|
+
"mod": mod_dir.name,
|
|
183
|
+
"type": "permanent_dependency",
|
|
184
|
+
"dependency": dep_meta.get("Name"),
|
|
185
|
+
"uuid": dep_meta.get("UUID"),
|
|
186
|
+
"folder": dep_meta.get("Folder"),
|
|
187
|
+
"publish_handle": dep_meta.get("PublishHandle"),
|
|
188
|
+
"meta_file": str(target_meta),
|
|
189
|
+
"readme_updated": False,
|
|
190
|
+
"dry_run": dry_run,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if meta_modified and not dry_run:
|
|
194
|
+
ET.indent(tree, space=" ")
|
|
195
|
+
tree.write(target_meta, encoding="utf-8", xml_declaration=True)
|
|
196
|
+
|
|
197
|
+
# Update README.md if present
|
|
198
|
+
readme_path = mod_dir / "README.md"
|
|
199
|
+
readme_updated = False
|
|
200
|
+
if readme_path.is_file() and new_readme_entries:
|
|
201
|
+
readme_txt = readme_path.read_text(encoding="utf-8")
|
|
202
|
+
unlisted = [e for e in new_readme_entries if e[0].get("UUID") and e[0]["UUID"] not in readme_txt]
|
|
203
|
+
if unlisted and "## Dependencies" in readme_txt:
|
|
204
|
+
entries_block = "\n".join(e[1] for e in unlisted)
|
|
205
|
+
if re.search(r"## Dependencies\s*\n\s*- None\.", readme_txt):
|
|
206
|
+
readme_txt = re.sub(r"(## Dependencies\s*\n)\s*- None\.", r"\1" + entries_block, readme_txt)
|
|
207
|
+
else:
|
|
208
|
+
readme_txt = re.sub(r"(## Dependencies\s*\n)", r"\1" + entries_block + "\n", readme_txt)
|
|
209
|
+
readme_updated = True
|
|
210
|
+
if not dry_run:
|
|
211
|
+
readme_path.write_text(readme_txt, encoding="utf-8")
|
|
212
|
+
|
|
213
|
+
for r in results:
|
|
214
|
+
if r.get("status") == "added_dependency":
|
|
215
|
+
r["readme_updated"] = readme_updated
|
|
216
|
+
|
|
217
|
+
return results
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def add_permanent_dependency(mod_dir: Path, dep_meta: Dict[str, Any], dry_run: bool = False) -> Dict[str, Any]:
|
|
221
|
+
"""Single-dependency wrapper for add_permanent_dependencies."""
|
|
222
|
+
res = add_permanent_dependencies(mod_dir, [dep_meta], dry_run=dry_run)
|
|
223
|
+
return res[0] if res else {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def resolve_cli_targets(args: argparse.Namespace) -> Tuple[List[str], List[str]]:
|
|
227
|
+
"""
|
|
228
|
+
Categorizes CLI input into permanent dependencies and test dependencies.
|
|
229
|
+
Supports comma-separated slugs, repeated flags, --test-dep, and --test-only as flag or value.
|
|
230
|
+
"""
|
|
231
|
+
perm_slugs: List[str] = []
|
|
232
|
+
test_slugs: List[str] = []
|
|
233
|
+
|
|
234
|
+
# 1. Parse explicit --test-dep / --test-deps values
|
|
235
|
+
if getattr(args, "test_dep", None):
|
|
236
|
+
test_slugs.extend(parse_slug_list(args.test_dep))
|
|
237
|
+
|
|
238
|
+
# 2. Parse --test-only
|
|
239
|
+
raw_test_only = getattr(args, "test_only", [])
|
|
240
|
+
has_test_only_flag = any(v is True for v in raw_test_only)
|
|
241
|
+
test_only_values = parse_slug_list([v for v in raw_test_only if isinstance(v, str)])
|
|
242
|
+
if test_only_values:
|
|
243
|
+
test_slugs.extend(test_only_values)
|
|
244
|
+
|
|
245
|
+
# 3. Parse --dep / --dependency
|
|
246
|
+
raw_dep = getattr(args, "dep", [])
|
|
247
|
+
dep_slugs = parse_slug_list(raw_dep)
|
|
248
|
+
|
|
249
|
+
# If --test-only was passed as a bare flag without slug values:
|
|
250
|
+
# route --dep values to test dependencies (preserves backward compatibility)
|
|
251
|
+
if has_test_only_flag and not test_only_values:
|
|
252
|
+
test_slugs.extend(dep_slugs)
|
|
253
|
+
else:
|
|
254
|
+
perm_slugs.extend(dep_slugs)
|
|
255
|
+
|
|
256
|
+
# Remove duplicates preserving order
|
|
257
|
+
dedup_perm: List[str] = []
|
|
258
|
+
for s in perm_slugs:
|
|
259
|
+
if s not in dedup_perm:
|
|
260
|
+
dedup_perm.append(s)
|
|
261
|
+
|
|
262
|
+
dedup_test: List[str] = []
|
|
263
|
+
for s in test_slugs:
|
|
264
|
+
if s not in dedup_test:
|
|
265
|
+
dedup_test.append(s)
|
|
266
|
+
|
|
267
|
+
return dedup_perm, dedup_test
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main() -> None:
|
|
271
|
+
parser = argparse.ArgumentParser(
|
|
272
|
+
description="Add community or core mod dependencies and test dependencies to an existing custom mod in my_mods/."
|
|
273
|
+
)
|
|
274
|
+
parser.add_argument("pos_mod", nargs="?", default=None, help="Target mod name or slug (optional if --mod is specified)")
|
|
275
|
+
parser.add_argument("--mod", default=None, help="Custom mod name, slug, or folder in my_mods/")
|
|
276
|
+
parser.add_argument("--dep", "--dependency", dest="dep", action="append", default=[], help="Dependency mod name(s), slug(s), or UUID(s) (comma-separated or repeated)")
|
|
277
|
+
parser.add_argument("--test-dep", "--test-deps", dest="test_dep", action="append", default=[], help="Test dependency mod name(s), slug(s), or UUID(s) (comma-separated or repeated)")
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--test-only",
|
|
280
|
+
dest="test_only",
|
|
281
|
+
nargs="?",
|
|
282
|
+
const=True,
|
|
283
|
+
action="append",
|
|
284
|
+
default=[],
|
|
285
|
+
help="Add as test dependency in .test_deps.json. Can be a bare flag or take comma-separated mod slug(s)",
|
|
286
|
+
)
|
|
287
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview changes without modifying files")
|
|
288
|
+
parser.add_argument("--json", action="store_true", help="Output machine-readable JSON result")
|
|
289
|
+
|
|
290
|
+
args = parser.parse_args()
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
target_mod = args.mod or args.pos_mod or detect_mod_from_cwd()
|
|
294
|
+
if not target_mod:
|
|
295
|
+
raise ValueError("Target mod must be specified via --mod <name>, positional argument, or by running inside a mod folder in my_mods/.")
|
|
296
|
+
|
|
297
|
+
mod_dir = resolve_mod_dir(target_mod, prefer_my_mods=True)
|
|
298
|
+
if not mod_dir or not mod_dir.is_dir():
|
|
299
|
+
raise FileNotFoundError(f"Could not locate mod directory for '{target_mod}'.")
|
|
300
|
+
|
|
301
|
+
perm_slugs, test_slugs = resolve_cli_targets(args)
|
|
302
|
+
if not perm_slugs and not test_slugs:
|
|
303
|
+
raise ValueError("No dependencies specified. Provide dependency mod slugs via --dep, --test-dep, or --test-only.")
|
|
304
|
+
|
|
305
|
+
perm_results: List[Dict[str, Any]] = []
|
|
306
|
+
if perm_slugs:
|
|
307
|
+
perm_metas = [
|
|
308
|
+
resolve_dependency_metadata(slug, auto_document=True, strict=True)
|
|
309
|
+
for slug in perm_slugs
|
|
310
|
+
]
|
|
311
|
+
perm_results = add_permanent_dependencies(mod_dir, perm_metas, dry_run=args.dry_run)
|
|
312
|
+
|
|
313
|
+
test_results: List[Dict[str, Any]] = []
|
|
314
|
+
if test_slugs:
|
|
315
|
+
test_metas = [
|
|
316
|
+
resolve_dependency_metadata(slug, auto_document=True, strict=False)
|
|
317
|
+
for slug in test_slugs
|
|
318
|
+
]
|
|
319
|
+
test_results = add_test_dependencies(mod_dir, test_metas, dry_run=args.dry_run)
|
|
320
|
+
|
|
321
|
+
all_results = perm_results + test_results
|
|
322
|
+
success = True
|
|
323
|
+
|
|
324
|
+
if args.json:
|
|
325
|
+
response: Dict[str, Any] = {
|
|
326
|
+
"mod": mod_dir.name,
|
|
327
|
+
"status": "success",
|
|
328
|
+
"dependencies": perm_results,
|
|
329
|
+
"test_dependencies": test_results,
|
|
330
|
+
"results": all_results,
|
|
331
|
+
"success": success,
|
|
332
|
+
"dry_run": args.dry_run,
|
|
333
|
+
}
|
|
334
|
+
if len(all_results) == 1:
|
|
335
|
+
single = all_results[0]
|
|
336
|
+
for k, v in single.items():
|
|
337
|
+
if k not in response:
|
|
338
|
+
response[k] = v
|
|
339
|
+
response["status"] = single.get("status", "success")
|
|
340
|
+
print(json.dumps(response, indent=2))
|
|
341
|
+
else:
|
|
342
|
+
verb = "[DRY-RUN] Would add" if args.dry_run else "Successfully added"
|
|
343
|
+
for r in all_results:
|
|
344
|
+
dep_name = r.get("dependency") or r.get("uuid") or "Unknown"
|
|
345
|
+
status = r.get("status")
|
|
346
|
+
dep_kind = "test dependency" if r.get("type") == "test_dependency" else "dependency"
|
|
347
|
+
if status == "already_present":
|
|
348
|
+
print(f"• {dep_kind.capitalize()} '{dep_name}' is already present in '{r.get('mod')}'.")
|
|
349
|
+
else:
|
|
350
|
+
print(f"✓ {verb} {dep_kind} '{dep_name}' to '{r.get('mod')}'.")
|
|
351
|
+
if r.get("uuid"):
|
|
352
|
+
print(f" UUID: {r.get('uuid')}")
|
|
353
|
+
if r.get("meta_file"):
|
|
354
|
+
print(f" Updated: {r.get('meta_file')}")
|
|
355
|
+
elif r.get("config_file") and status != "already_present":
|
|
356
|
+
print(f" Updated: {r.get('config_file')}")
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
if args.json:
|
|
360
|
+
print(json.dumps({"error": str(e), "success": False}, indent=2))
|
|
361
|
+
else:
|
|
362
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
main()
|
bg3_authoring/cli.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""bg3-authoring CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from bg3_core.cli import build_parser, dispatch_command
|
|
6
|
+
from bg3_core.cli_common import ConsoleColor, ExitCode, OutputEmitter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
parser = build_parser()
|
|
11
|
+
if len(sys.argv) == 1:
|
|
12
|
+
parser.print_help(sys.stderr)
|
|
13
|
+
sys.exit(ExitCode.USAGE)
|
|
14
|
+
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
color = ConsoleColor(stream=sys.stderr, force_color=False)
|
|
17
|
+
emitter = OutputEmitter(json_mode=args.json, color=color)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
ret = dispatch_command(args, emitter)
|
|
21
|
+
sys.exit(ret)
|
|
22
|
+
except KeyboardInterrupt:
|
|
23
|
+
emitter.error(
|
|
24
|
+
message="Operation aborted by user.",
|
|
25
|
+
code="SIGINT",
|
|
26
|
+
exit_code=130,
|
|
27
|
+
exit_process=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|