6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Common Commands
# Run all tests (no network required)
python test_suite.py
# Check Python version and dependencies
python check_deps.py
# Full pipeline (parse → compare → fetch → link)
python run.py
# Parse + compare only (no download, no linking)
python run.py --skip-fetch --skip-link
# Diagnose mod folder name / steam_id issues
python check_names.py
python check_names.py --fix --fix-ids
# Launch the GUI
python gui.py
There is no build step, linter config, or package install beyond pip install -r requirements.txt.
Architecture
Package vs CLI layer
arma_modlist_tools/ is a pure library — no I/O side effects, no sys.exit, no print. All CLI scripts (run.py, fetch_mods.py, link_mods.py, etc.) sit at the project root and call into the package. New functionality goes in the package first, then a CLI script wraps it.
Data flow
modlist_html/*.html
└─ parser.parse_modlist_dir()
└─ compare.compare_presets()
└─ comparison.json ←─ source of truth for groups + mod identity
├─ fetcher.build_server_index() ←─ Caddy JSON API
│ └─ fetcher.find_mod_folder() (steam_id first, name fallback)
│ └─ downloads/{group}/@ModName/
│ └─ linker.link_group()
│ └─ arma_dir/@ModName (junction/symlink)
└─ reporter.build_missing_report() → missing_report.json
Group naming convention
"shared"— mods present in all compared presets"<preset_name>"— mods unique to one preset (key fromcomparison["unique"])
This group label is stored in missing_report.json per-mod so sync_missing.py knows where to place newly available mods without re-reading comparison.json.
Server index structure
build_server_index() returns:
{
"by_steam_id": {"450814997": "https://server/@cba_a3/"}, # primary lookup
"by_name": {"cbaa3": "https://server/@cba_a3/"}, # normalized fallback
"folders": [...] # raw Caddy listing
}
_normalize_name strips @, lowercases, removes all non-alphanumeric: "@CBA_A3" → "cbaa3". Used in both the index builder and every lookup.
Junction / symlink critical rules
Detection: os.path.islink() returns False for Windows junctions. Always use _is_junction() from linker.py, which checks st_file_attributes & 0x400 (FILE_ATTRIBUTE_REPARSE_POINT) on Windows.
Removal: Use os.rmdir() on Windows and os.unlink() on Linux. Never shutil.rmtree() — it follows the junction and deletes the target mod files.
Creation: cmd /c mklink /J <link> <target> on Windows, os.symlink() on Linux.
check_names.py classification (two-pass)
Pass 1 collects raw (server_name, local_steam_id) for every disk folder.
Pass 2 builds ok_disk_names — the set of disk names that already match the server exactly. Any MISMATCH whose proposed server name is in ok_disk_names is reclassified as ID_COLLISION (the local meta.cpp has a wrong publishedid that belongs to a different mod). This prevents false rename suggestions caused by shared/duplicate steam IDs on the server.
--fix-ids corrects meta.cpp using steam IDs from comparison.json (sourced from Steam Workshop URLs in the HTML presets) as the authoritative source.
GUI package
gui/ is a CustomTkinter desktop application wrapping the CLI toolchain. Entry point is gui.py at the project root, which calls gui.run_app().
Key files:
gui/__init__.py— sets dark theme + blue color scheme; exportsrun_app()gui/app.py—ArmaModManagerAppmain window; manages view routing, config loading, thread-safe log queue, and background pipeline executiongui/wizard.py—SetupWizarddialog shown on first launch when noconfig.jsonexistsgui/_constants.py— window dimensions, status color constants, file pathsgui/_io.py—_QueueWriterredirects stdout/stderr to a thread-safe queue so pipeline output streams into the Logs view
Views (gui/views/): each inherits BaseView; build() runs once on creation, refresh() runs on each navigation:
dashboard.py— overview, status, quick statsmods.py— browse and manage downloaded mods by grouptools.py— link/unlink, rename folders, sync missing mods, check serverlogs.py— real-time log viewer fed from the stdout/stderr queuesettings.py— in-app editor forconfig.json(server URL, paths, credentials)
_find_folder (mods.py) — three-level name matching: The mods view resolves a mod's local folder by mod name from comparison.json, which may differ from the server-canonical folder name used by the fetcher. Lookup order:
- Exact:
@{mod_name} - Case-insensitive:
@CBA_A3matchesCBA_A3 - Normalized (
_normalize_name): strips all non-alphanumeric — handles punctuation/spacing differences, e.g.@US GEAr- Units (IFA3)matchesUS GEAr: Units (IFA3)(both →usgearunitsifa3)
selection.json — GUI selection state file, tracked in git. Persists which mods/groups are selected between GUI sessions. Written by the GUI; safe to delete (GUI recreates it on next save).
run_tool subprocess streaming: Tool scripts are launched via subprocess.Popen (not subprocess.run) with stdout=PIPE, stderr=STDOUT, read line-by-line via iter(proc.stdout.readline, ""), and posted to the log queue immediately. Python's own output buffering is disabled with the -u flag and PYTHONUNBUFFERED=1 in the environment — without these, output would batch inside the pipe and only appear when the script exits.
Python Version Compatibility
Minimum is Python 3.9. All files that use X | Y union type annotations must have from __future__ import annotations as the first import. Without it, the | syntax raises TypeError at runtime on Python < 3.10. Every module in arma_modlist_tools/ already has it; any new CLI script you add must include it too.
Test Suite
test_suite.py uses a custom harness (no pytest/unittest dependency). Structure:
group("section name") # prints header
test("description", callable) # runs fn, catches exceptions, tracks pass/fail
skip("description", "reason") # marks skipped
Tests that exercise the linker use tempfile.TemporaryDirectory() — never the real arma_dir. Tests that would require network calls mock list_mod_files with unittest.mock.patch.
Key Files Not in Git
config.json— credentials + paths (copy fromconfig.template.json)downloads/— downloaded mod files, can be several GBmodlist_json/— generated JSON output
The .html preset files in modlist_html/ are tracked as example inputs.