Initial release: full Arma 3 mod management toolchain
Pipeline: parse HTML presets, compare modlists, download from Caddy file server, create junctions/symlinks to Arma 3 Server directory. Includes update/sync flows, missing-mod reporting, OS compat layer, shared config, dep checker, comprehensive test suite (71 tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
183
link_mods.py
Normal file
183
link_mods.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI entry point: manage Arma 3 Server junction/symlink links for downloaded mods.
|
||||
|
||||
Usage:
|
||||
python link_mods.py status --group shared
|
||||
python link_mods.py link --group shared
|
||||
python link_mods.py unlink --group shared
|
||||
|
||||
Omit --group to list available groups.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from arma_modlist_tools.compat import fix_console_encoding
|
||||
from arma_modlist_tools.config import load_config
|
||||
from arma_modlist_tools.linker import get_link_status, link_group, unlink_group, create_junction, remove_junction
|
||||
|
||||
fix_console_encoding()
|
||||
|
||||
|
||||
def _available_groups(cfg) -> list[str]:
|
||||
if not cfg.downloads.is_dir():
|
||||
return []
|
||||
return sorted(p.name for p in cfg.downloads.iterdir() if p.is_dir())
|
||||
|
||||
|
||||
def _print_separator(width: int = 60) -> None:
|
||||
print(" " + "-" * width)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_status(cfg, group: str) -> None:
|
||||
group_dir = cfg.downloads / group
|
||||
if not group_dir.is_dir():
|
||||
print(f"ERROR: group folder not found: {group_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
status = get_link_status(group_dir, cfg.arma_dir)
|
||||
|
||||
print()
|
||||
print(f" Group : {group}")
|
||||
print(f" Path : {group_dir}")
|
||||
print(f" Arma : {cfg.arma_dir}")
|
||||
print()
|
||||
print(f" {'Mod':<50} Status")
|
||||
_print_separator()
|
||||
|
||||
for s in status:
|
||||
icon = "[LINKED]" if s["is_linked"] else "[------]"
|
||||
print(f" {s['name']:<50} {icon}")
|
||||
|
||||
linked = sum(1 for s in status if s["is_linked"])
|
||||
print()
|
||||
print(f" {linked} / {len(status)} linked")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_link(cfg, group: str) -> None:
|
||||
group_dir = cfg.downloads / group
|
||||
if not group_dir.is_dir():
|
||||
print(f"ERROR: group folder not found: {group_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
status = get_link_status(group_dir, cfg.arma_dir)
|
||||
if not status:
|
||||
print(f"No @Mod folders found in {group_dir}")
|
||||
sys.exit(0)
|
||||
|
||||
print()
|
||||
print(f" Linking group: {group} -> {cfg.arma_dir}")
|
||||
print()
|
||||
|
||||
linked = already_linked = failed = 0
|
||||
|
||||
for s in status:
|
||||
if s["is_linked"]:
|
||||
print(f" [=] {s['name']:<48} already linked")
|
||||
already_linked += 1
|
||||
continue
|
||||
if s["link_path"].exists():
|
||||
print(f" [!] {s['name']:<48} SKIP — path exists, not a junction")
|
||||
failed += 1
|
||||
continue
|
||||
ok = create_junction(s["link_path"], s["source_path"])
|
||||
if ok:
|
||||
print(f" [+] {s['name']:<48} linked")
|
||||
linked += 1
|
||||
else:
|
||||
print(f" [X] {s['name']:<48} FAILED")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print(f" Done: {linked} linked, {already_linked} already linked, {failed} failed")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_unlink(cfg, group: str) -> None:
|
||||
group_dir = cfg.downloads / group
|
||||
if not group_dir.is_dir():
|
||||
print(f"ERROR: group folder not found: {group_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
status = get_link_status(group_dir, cfg.arma_dir)
|
||||
linked_count = sum(1 for s in status if s["is_linked"])
|
||||
|
||||
if linked_count == 0:
|
||||
print(f"\n Nothing to unlink — 0 active links in group '{group}'.\n")
|
||||
sys.exit(0)
|
||||
|
||||
print()
|
||||
print(f" WARNING: This will remove {linked_count} link(s) from Arma 3 Server:")
|
||||
print(f" {cfg.arma_dir}")
|
||||
print(f" Group: {group} ({linked_count} linked mod(s))")
|
||||
print()
|
||||
|
||||
choice = input(" Continue? [y/N]: ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
print(" Aborted.\n")
|
||||
sys.exit(0)
|
||||
|
||||
print()
|
||||
|
||||
unlinked = not_linked = failed = 0
|
||||
|
||||
for s in status:
|
||||
if not s["is_linked"]:
|
||||
print(f" [=] {s['name']:<48} not linked")
|
||||
not_linked += 1
|
||||
continue
|
||||
ok, err = remove_junction(s["link_path"])
|
||||
if ok:
|
||||
print(f" [-] {s['name']:<48} unlinked")
|
||||
unlinked += 1
|
||||
else:
|
||||
print(f" [X] {s['name']:<48} FAILED: {err}")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print(f" Done: {unlinked} removed, {not_linked} not linked"
|
||||
+ (f", {failed} failed" if failed else ""))
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
cfg = load_config()
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Manage Arma 3 Server junction/symlink links for downloaded mods."
|
||||
)
|
||||
parser.add_argument("command", choices=["status", "link", "unlink"])
|
||||
parser.add_argument("--group", "-g", metavar="GROUP")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.group:
|
||||
groups = _available_groups(cfg)
|
||||
if groups:
|
||||
print(f"\nAvailable groups in '{cfg.downloads}':")
|
||||
for g in groups:
|
||||
print(f" {g}")
|
||||
print(f"\nUsage: python link_mods.py {args.command} --group <GROUP>\n")
|
||||
else:
|
||||
print(f"No groups found in '{cfg.downloads}'. Run fetch_mods.py first.")
|
||||
sys.exit(0)
|
||||
|
||||
if args.command == "status":
|
||||
cmd_status(cfg, args.group)
|
||||
elif args.command == "link":
|
||||
cmd_link(cfg, args.group)
|
||||
elif args.command == "unlink":
|
||||
cmd_unlink(cfg, args.group)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user