mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 13:34:42 +00:00
Cleanup part one
This commit is contained in:
@@ -1,234 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Patchuje:
|
||||
1) wg-voidship-hulls.db -> base.power = 0 (dla wszystkich kadłubów)
|
||||
2) wg-voidship-items.db -> reaktory: use.power=0, mods.power wg subtype;
|
||||
aux-plasma: use.power=0;
|
||||
dopisuje 4 reaktory jeśli brak.
|
||||
Uruchom w katalogu z plikami .db
|
||||
"""
|
||||
|
||||
import json, shutil
|
||||
from pathlib import Path
|
||||
|
||||
HULLS_FN = Path("wg-voidship-hulls.db")
|
||||
ITEMS_FN = Path("wg-voidship-items.db")
|
||||
|
||||
REACTOR_POWER_BY_SUBTYPE = {
|
||||
"escort": 40,
|
||||
"frigate": 45,
|
||||
"light-cruiser": 55,
|
||||
"cruiser": 60,
|
||||
}
|
||||
|
||||
# 4 reaktory „wzorcowe” do dopisania, jeśli brak:
|
||||
REACTOR_TEMPLATES = [
|
||||
{
|
||||
"_id": "ITEMREACTCRU0001",
|
||||
"name": "Plasma Reactor (Cruiser) — Mars-Pattern",
|
||||
"type": "gear",
|
||||
"img": "icons/svg/energy.svg",
|
||||
"flags": {
|
||||
"wg-voidships-builder": {
|
||||
"voidOnly": True,
|
||||
"component": {"kind": "reactor", "subtype": "cruiser", "tags": ["reactor", "plasma"]}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"description": "Standardowy reaktor plazmowy klasy krążownik (Mars-Pattern). Generuje moc dla wszystkich systemów okrętu.",
|
||||
"voidship": {
|
||||
"slot": "Internal",
|
||||
"use": {"space": 20, "power": 0},
|
||||
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 60}
|
||||
}
|
||||
},
|
||||
"effects": []
|
||||
},
|
||||
{
|
||||
"_id": "ITEMREACTLCR0001",
|
||||
"name": "Plasma Reactor (Light Cruiser) — Mars-Pattern",
|
||||
"type": "gear",
|
||||
"img": "icons/svg/energy.svg",
|
||||
"flags": {
|
||||
"wg-voidships-builder": {
|
||||
"voidOnly": True,
|
||||
"component": {"kind": "reactor", "subtype": "light-cruiser", "tags": ["reactor", "plasma"]}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"description": "Reaktor plazmowy dla lekkich krążowników (Mars-Pattern).",
|
||||
"voidship": {
|
||||
"slot": "Internal",
|
||||
"use": {"space": 18, "power": 0},
|
||||
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 55}
|
||||
}
|
||||
},
|
||||
"effects": []
|
||||
},
|
||||
{
|
||||
"_id": "ITEMREACTFRI0001",
|
||||
"name": "Plasma Reactor (Frigate) — Jovian-Pattern",
|
||||
"type": "gear",
|
||||
"img": "icons/svg/energy.svg",
|
||||
"flags": {
|
||||
"wg-voidships-builder": {
|
||||
"voidOnly": True,
|
||||
"component": {"kind": "reactor", "subtype": "frigate", "tags": ["reactor", "plasma"]}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"description": "Reaktor plazmowy okrętów klasy fregata (Jovian-Pattern).",
|
||||
"voidship": {
|
||||
"slot": "Internal",
|
||||
"use": {"space": 15, "power": 0},
|
||||
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 45}
|
||||
}
|
||||
},
|
||||
"effects": []
|
||||
},
|
||||
{
|
||||
"_id": "ITEMREACTESC0001",
|
||||
"name": "Plasma Reactor (Escort) — Jovian-Pattern",
|
||||
"type": "gear",
|
||||
"img": "icons/svg/energy.svg",
|
||||
"flags": {
|
||||
"wg-voidships-builder": {
|
||||
"voidOnly": True,
|
||||
"component": {"kind": "reactor", "subtype": "escort", "tags": ["reactor", "plasma"]}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"description": "Lżejszy reaktor plazmowy dla eskort.",
|
||||
"voidship": {
|
||||
"slot": "Internal",
|
||||
"use": {"space": 12, "power": 0},
|
||||
"mods": {"armour": 0, "integrity": 0, "manoeuvre": 0, "detection": 0, "speed": 0, "space": 0, "power": 40}
|
||||
}
|
||||
},
|
||||
"effects": []
|
||||
},
|
||||
]
|
||||
|
||||
def read_ndjson(path: Path):
|
||||
if not path.exists():
|
||||
print(f"[WARN] Brak pliku: {path.name}")
|
||||
return []
|
||||
rows = []
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except Exception as e:
|
||||
print(f"[WARN] {path.name}:{i}: nie mogę zdekodować JSON: {e}")
|
||||
return rows
|
||||
|
||||
def write_ndjson(path: Path, rows):
|
||||
tmp = path.with_suffix(path.suffix + ".patched")
|
||||
with tmp.open("w", encoding="utf-8") as f:
|
||||
for obj in rows:
|
||||
f.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
tmp.replace(path)
|
||||
|
||||
def ensure_backup(path: Path):
|
||||
bak = path.with_suffix(path.suffix + ".bak")
|
||||
if path.exists() and not bak.exists():
|
||||
shutil.copy2(path, bak)
|
||||
print(f"[OK] Backup -> {bak.name}")
|
||||
|
||||
def patch_hulls():
|
||||
if not HULLS_FN.exists():
|
||||
print(f"[INFO] Pomijam hulls (brak {HULLS_FN.name})")
|
||||
return
|
||||
ensure_backup(HULLS_FN)
|
||||
rows = read_ndjson(HULLS_FN)
|
||||
patched = 0
|
||||
for o in rows:
|
||||
flags = o.setdefault("flags", {}).setdefault("wg-voidships-builder", {})
|
||||
base = flags.setdefault("base", {})
|
||||
if base.get("power", None) != 0:
|
||||
base["power"] = 0
|
||||
patched += 1
|
||||
write_ndjson(HULLS_FN, rows)
|
||||
print(f"[OK] Hulls: ustawiono base.power=0 w {patched} rekordach ({len(rows)} linii)")
|
||||
|
||||
def has_tag(flags_obj, tag: str) -> bool:
|
||||
try:
|
||||
tags = flags_obj["wg-voidships-builder"]["component"]["tags"]
|
||||
return isinstance(tags, list) and tag in tags
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def patch_items():
|
||||
if not ITEMS_FN.exists():
|
||||
print(f"[INFO] Pomijam items (brak {ITEMS_FN.name})")
|
||||
return
|
||||
ensure_backup(ITEMS_FN)
|
||||
rows = read_ndjson(ITEMS_FN)
|
||||
|
||||
existing_ids = set()
|
||||
patched_reactors = 0
|
||||
patched_aux = 0
|
||||
|
||||
for o in rows:
|
||||
existing_ids.add(o.get("_id", ""))
|
||||
|
||||
flags = o.get("flags", {})
|
||||
comp = flags.get("wg-voidships-builder", {}).get("component", {})
|
||||
kind = comp.get("kind")
|
||||
subtype = comp.get("subtype")
|
||||
|
||||
# Upewnij się, że ścieżka system.voidship.use/mods istnieje
|
||||
sys = o.setdefault("system", {})
|
||||
vsh = sys.setdefault("voidship", {})
|
||||
use = vsh.setdefault("use", {})
|
||||
mods = vsh.setdefault("mods", {})
|
||||
|
||||
# Auxiliary plasma (po tagu)
|
||||
if has_tag(flags, "aux-plasma"):
|
||||
if use.get("power") != 0:
|
||||
use["power"] = 0
|
||||
patched_aux += 1
|
||||
|
||||
# Reaktory
|
||||
if kind == "reactor":
|
||||
if use.get("power") != 0:
|
||||
use["power"] = 0
|
||||
if subtype in REACTOR_POWER_BY_SUBTYPE:
|
||||
pow_val = REACTOR_POWER_BY_SUBTYPE[subtype]
|
||||
if mods.get("power") != pow_val:
|
||||
mods["power"] = pow_val
|
||||
patched_reactors += 1
|
||||
|
||||
# zadbaj o wypełnienie brakujących kluczy modów
|
||||
for k in ["armour","integrity","manoeuvre","detection","speed","space"]:
|
||||
mods.setdefault(k, 0)
|
||||
# i sensowne sloty/space
|
||||
vsh.setdefault("slot", "Internal")
|
||||
use.setdefault("space", use.get("space", 0))
|
||||
|
||||
# Zapisz zmodyfikowane wiersze
|
||||
write_ndjson(ITEMS_FN, rows)
|
||||
print(f"[OK] Items: zaktualizowano reaktory: {patched_reactors}, aux-plasma: {patched_aux}")
|
||||
|
||||
# Dopisz brakujące 4 reaktory
|
||||
to_append = [r for r in REACTOR_TEMPLATES if r["_id"] not in existing_ids]
|
||||
if to_append:
|
||||
with ITEMS_FN.open("a", encoding="utf-8") as f:
|
||||
for obj in to_append:
|
||||
f.write(json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
print(f"[OK] Items: dopisano brakujące reaktory: {[o['_id'] for o in to_append]}")
|
||||
else:
|
||||
print(f"[OK] Items: wszystkie reaktory już są — nic do dopisania.")
|
||||
|
||||
def main():
|
||||
print("== WG Voidships Patch ==")
|
||||
patch_hulls()
|
||||
patch_items()
|
||||
print("Gotowe. Zrestartuj/reloadnij Foundry i reimportuj kompendia, jeśli potrzeba.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,36 +0,0 @@
|
||||
import json, sys
|
||||
BLURB = {
|
||||
"Jericho-class Pilgrim Vessel": "Immense conversions for pilgrim passages and reliquary cargo. Cavernous holds and chapels; range and capacity over armour and speed.",
|
||||
"Vagabond-class Merchant Trader": "Modular cargobays and adaptable fittings; a chartist workhorse. Simple to keep running in frontier yards; heavily customised.",
|
||||
"Hazeroth-class Privateer": "Knife-fast raider with reinforced internals and stripped armour. Favoured for ambushes and commerce raiding.",
|
||||
"Havoc-class Merchant Raider": "Hybrid carrier-raider balancing battery weight with cargo. Less protected than Navy hulls but brutal on traffic.",
|
||||
"Sword-class Frigate": "Reliable escort with accurate dorsal lasers and stout drives. Squadrons screen convoys and prosecute pirates.",
|
||||
"Tempest-class Strike Frigate": "Close-action brawler for boardings and short-range gunnery. Reinforced prow and over-tuned drives.",
|
||||
"Dauntless-class Light Cruiser": "Versatile patrol cruiser—good acceleration and flexible refits. Often leads Passage Watch patrols.",
|
||||
"Lunar-class Cruiser": "Archetypal line cruiser—balanced, dependable, easily refitted. Backbone of many squadrons.",
|
||||
"Cobra-class Destroyer": "Torpedo raider hunting in packs; strikes, then disengages to reload.",
|
||||
"Falchion-class Frigate": "Modernised frigate with improved drives for frontier scouting.",
|
||||
"Turbulent-class Heavy Frigate": "Strengthened frame and broader arcs; trades agility for staying power.",
|
||||
"Defiant-class Light Cruiser (Carrier)": "Light carrier with significant attack craft; excellent for recon and strikes.",
|
||||
"Universe-class Mass Conveyor": "Super-massive hauler for colony loads and crusade materiel; logistics linchpin.",
|
||||
"Goliath-class Factory Ship": "Roving manufactorum and refinery; long autonomous deployments.",
|
||||
"Conquest-class Star Galleon": "Heavily built merchant galleon with strong batteries and hull.",
|
||||
"Orion-class Star Clipper": "Fast passenger/light-cargo clipper for courier missions and risky runs.",
|
||||
"Secutor-class Monitor-Cruiser": "Mechanicus monitor with powerful dorsals and redundant shielding; anchors actions.",
|
||||
"Lathe-class Monitor-Cruiser": "Lathes-forged variant emphasising resilient sanctified systems.",
|
||||
"Tyrant-class Cruiser": "Macrobattery-focused cruiser optimised for crushing broadsides.",
|
||||
"Cobra-class Destroyer (Renegade)": "Ex-Navy Cobra in privateer hands with non-standard prow refits."
|
||||
}
|
||||
path = "packs/wg-voidship-hulls.db"
|
||||
out = []
|
||||
with open(path,"r",encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if not line.strip(): continue
|
||||
d = json.loads(line)
|
||||
if d.get("name") in BLURB:
|
||||
d.setdefault("system", {})
|
||||
d["system"]["notes"] = BLURB[d["name"]]
|
||||
out.append(json.dumps(d, ensure_ascii=False))
|
||||
with open(path,"w",encoding="utf-8") as f:
|
||||
f.write("\n".join(out) + "\n")
|
||||
print("Patched:", path, "entries:", len(out))
|
||||
@@ -1,424 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sync_foundry_module.py (z dry-run, argumentami CLI i preflightem git/module.json)
|
||||
|
||||
Kroki:
|
||||
[PRE] W katalogu źródłowym:
|
||||
- Wczytaj module.json, pobierz 'id' i 'version'
|
||||
- Zwiększ ostatnią liczbę w 'version' o 1, zapisz module.json
|
||||
- git add -A; git commit -m "sync: <id> v<version>"; git push
|
||||
(w dry-run wyłącznie plan, bez modyfikacji i bez gita)
|
||||
|
||||
[2] Wyczyść zawartość docelowego modułu (z potwierdzeniem lub bez; w dry-run tylko lista)
|
||||
[3] Skopiuj zawartość źródła do celu (w dry-run: plan)
|
||||
|
||||
Wymagania: Python 3.8+, git w PATH, uprawnienia do zapisu.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
# ==========================
|
||||
# KONFIGURACJA (DOMYŚLNA)
|
||||
# ==========================
|
||||
|
||||
TARGET_USER: str = "mtusz" # nazwa folderu w C:\Users\...
|
||||
APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming"
|
||||
TARGET_MODULES_DIR_OVERRIDE: Optional[str] = None # pełna ścieżka do ...\FoundryVTT\Data\modules
|
||||
MODULE_SUBDIR_NAME: str = "wg-voidships-builder"
|
||||
CONFIRM_DELETE: bool = True
|
||||
SOURCE_BASE_DIR: str = r"C:\vtt_work"
|
||||
RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules"
|
||||
DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
|
||||
|
||||
# ==========================
|
||||
# KONIEC KONFIGURACJI
|
||||
# ==========================
|
||||
|
||||
APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming")
|
||||
|
||||
|
||||
class AmbiguityError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def norm(p: Path) -> Path:
|
||||
return Path(os.path.normpath(str(p)))
|
||||
|
||||
|
||||
def find_modules_dir_for_user(user: str,
|
||||
relative_pattern: str,
|
||||
appdata_kind: Optional[str]) -> Path:
|
||||
base = Path(f"C:/Users/{user}/AppData")
|
||||
if not base.exists():
|
||||
raise FileNotFoundError(f"Nie znaleziono katalogu użytkownika: {norm(base)}")
|
||||
|
||||
candidates: List[Path] = []
|
||||
|
||||
if appdata_kind:
|
||||
if appdata_kind not in APPDATA_VARIANTS:
|
||||
raise ValueError(f"APPDATA_KIND musi być jednym z: {APPDATA_VARIANTS}, a nie '{appdata_kind}'")
|
||||
p = base / appdata_kind / Path(relative_pattern)
|
||||
if p.is_dir():
|
||||
return norm(p)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Nie znaleziono katalogu: {norm(p)} (dla APPDATA_KIND={appdata_kind})."
|
||||
)
|
||||
|
||||
for kind in APPDATA_VARIANTS:
|
||||
p = base / kind / Path(relative_pattern)
|
||||
if p.is_dir():
|
||||
candidates.append(p)
|
||||
|
||||
if not candidates:
|
||||
raise FileNotFoundError(
|
||||
"Nie znaleziono żadnego katalogu zgodnego z wzorcem.\n"
|
||||
f"Sprawdź, czy istnieje któryś z poniższych:\n"
|
||||
+ "\n".join(f" - {norm(base / kind / Path(relative_pattern))}" for kind in APPDATA_VARIANTS)
|
||||
)
|
||||
|
||||
if len(candidates) == 1:
|
||||
return norm(candidates[0])
|
||||
|
||||
suggestions = []
|
||||
for p in candidates:
|
||||
try:
|
||||
kind = p.parts[p.parts.index("AppData") + 1]
|
||||
except Exception:
|
||||
kind = "??"
|
||||
suggestions.append(f'* Ustaw APPDATA_KIND="{kind}" # trafi w: {norm(p)}')
|
||||
suggestions.append(f'* Albo ustaw TARGET_MODULES_DIR_OVERRIDE="{norm(candidates[0])}" # pełny override')
|
||||
|
||||
raise AmbiguityError(
|
||||
"Znaleziono wiele pasujących lokalizacji dla wzorca 'FoundryVTT\\Data\\modules':\n"
|
||||
+ "\n".join(f" - {norm(c)}" for c in candidates)
|
||||
+ "\n\nPropozycje ujednolicenia:\n" + "\n".join(suggestions)
|
||||
)
|
||||
|
||||
|
||||
def ensure_dir(path: Path) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def iter_dir_contents(dir_path: Path) -> List[Path]:
|
||||
if not dir_path.exists():
|
||||
return []
|
||||
return list(dir_path.iterdir())
|
||||
|
||||
|
||||
def list_recursive(dir_path: Path) -> List[Path]:
|
||||
items: List[Path] = []
|
||||
if not dir_path.exists():
|
||||
return items
|
||||
for root, dirs, files in os.walk(dir_path):
|
||||
root_p = Path(root)
|
||||
for d in dirs:
|
||||
items.append(root_p / d)
|
||||
for f in files:
|
||||
items.append(root_p / f)
|
||||
return items
|
||||
|
||||
|
||||
def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
ensure_dir(dir_path)
|
||||
files = 0
|
||||
dirs = 0
|
||||
to_delete = iter_dir_contents(dir_path)
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Zawartość do skasowania:")
|
||||
if not to_delete:
|
||||
print(" (pusto)")
|
||||
for child in to_delete:
|
||||
tag = "[plik]" if child.is_file() else "[katalog]" if child.is_dir() else "[inne]"
|
||||
print(f" {tag} {norm(child)}")
|
||||
for child in to_delete:
|
||||
if child.is_file() or child.is_symlink():
|
||||
files += 1
|
||||
elif child.is_dir():
|
||||
dirs += 1
|
||||
return files, dirs
|
||||
|
||||
for child in to_delete:
|
||||
if child.is_file() or child.is_symlink():
|
||||
files += 1
|
||||
child.unlink(missing_ok=True)
|
||||
elif child.is_dir():
|
||||
dirs += 1
|
||||
shutil.rmtree(child, ignore_errors=True)
|
||||
return files, dirs
|
||||
|
||||
|
||||
def plan_copy(src: Path, dst: Path) -> List[Tuple[Path, Path, str]]:
|
||||
plan: List[Tuple[Path, Path, str]] = []
|
||||
if not src.is_dir():
|
||||
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(src)}")
|
||||
|
||||
for root, dirs, files in os.walk(src):
|
||||
root_p = Path(root)
|
||||
rel_root = root_p.relative_to(src)
|
||||
for d in dirs:
|
||||
s = root_p / d
|
||||
t = dst / rel_root / d
|
||||
status = "overwrite" if t.exists() else "create"
|
||||
plan.append((s, t, status))
|
||||
for f in files:
|
||||
s = root_p / f
|
||||
t = dst / rel_root / f
|
||||
status = "overwrite" if t.exists() else "create"
|
||||
plan.append((s, t, status))
|
||||
return plan
|
||||
|
||||
|
||||
def copy_dir_contents(src: Path, dst: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
ensure_dir(dst)
|
||||
plan = plan_copy(src, dst)
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Plan kopiowania:")
|
||||
if not plan:
|
||||
print(" (brak plików do skopiowania)")
|
||||
for s, t, status in plan:
|
||||
kind = "[katalog]" if s.is_dir() else "[plik]"
|
||||
print(f" {kind} {norm(s)} -> {norm(t)} ({status})")
|
||||
files = sum(1 for s, _, _ in plan if s.is_file())
|
||||
dirs = sum(1 for s, _, _ in plan if s.is_dir())
|
||||
return files, dirs
|
||||
|
||||
files = 0
|
||||
dirs = 0
|
||||
for s, t, _ in plan:
|
||||
if s.is_dir():
|
||||
shutil.copytree(s, t, dirs_exist_ok=True)
|
||||
dirs += 1
|
||||
elif s.is_file():
|
||||
ensure_dir(t.parent)
|
||||
shutil.copy2(s, t)
|
||||
files += 1
|
||||
elif s.is_symlink():
|
||||
target = s.resolve()
|
||||
ensure_dir(t.parent)
|
||||
if target.is_dir():
|
||||
shutil.copytree(target, t, dirs_exist_ok=True)
|
||||
dirs += 1
|
||||
else:
|
||||
shutil.copy2(target, t)
|
||||
files += 1
|
||||
else:
|
||||
print(f"[OSTRZEŻENIE] Pominięto nieobsługiwany typ pliku: {norm(s)}")
|
||||
return files, dirs
|
||||
|
||||
|
||||
# ============ PRE-FLIGHT: module.json + git ============
|
||||
|
||||
def read_manifest(manifest_path: Path) -> dict:
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(f"Brak pliku manifestu: {norm(manifest_path)}")
|
||||
try:
|
||||
with manifest_path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Niepoprawny JSON w {norm(manifest_path)}: {e}") from e
|
||||
|
||||
|
||||
def bump_version(last_version: str) -> str:
|
||||
"""
|
||||
Zwiększa o 1 ostatnią *liczbę* w wersji rozdzielanej kropkami.
|
||||
Przykłady: 1 -> 2, 1.2 -> 1.3, 1.2.9 -> 1.2.10
|
||||
"""
|
||||
parts = last_version.strip().split(".")
|
||||
if not parts:
|
||||
raise ValueError(f"Niepoprawna wersja: '{last_version}'")
|
||||
try:
|
||||
n = int(parts[-1])
|
||||
except ValueError:
|
||||
raise ValueError(f"Ostatni segment wersji nie jest liczbą: '{last_version}'")
|
||||
parts[-1] = str(n + 1)
|
||||
return ".".join(parts)
|
||||
|
||||
|
||||
def write_manifest(manifest_path: Path, data: dict) -> None:
|
||||
tmp = manifest_path.with_suffix(".json.tmp")
|
||||
with tmp.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
tmp.replace(manifest_path) # atomowa podmiana na tym samym FS
|
||||
|
||||
|
||||
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
|
||||
"""
|
||||
Wykonuje: git add -A; git commit -m "<msg>"; git push
|
||||
W dry_run — tylko wyświetla plan.
|
||||
"""
|
||||
if dry_run:
|
||||
print("[DRY-RUN] git add -A")
|
||||
print(f"[DRY-RUN] git commit -m {commit_msg!r}")
|
||||
print("[DRY-RUN] git push")
|
||||
return
|
||||
|
||||
def run(cmd: List[str]) -> None:
|
||||
r = subprocess.run(cmd, cwd=str(repo_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{r.stdout}")
|
||||
else:
|
||||
if r.stdout.strip():
|
||||
print(r.stdout)
|
||||
|
||||
# add, commit (może nie utworzyć commita jeśli brak zmian), push
|
||||
run(["git", "add", "-A"])
|
||||
# commit powinien się powieść — jeśli nie ma zmian, chcemy FAIL
|
||||
r = subprocess.run(["git", "commit", "-m", commit_msg], cwd=str(repo_dir),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if r.returncode != 0:
|
||||
# typowy komunikat: "nothing to commit, working tree clean" -> traktujemy jako błąd wg wymagań
|
||||
raise RuntimeError(f"Błąd podczas git commit:\n{r.stdout}")
|
||||
else:
|
||||
if r.stdout.strip():
|
||||
print(r.stdout)
|
||||
run(["git", "push"])
|
||||
|
||||
|
||||
def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
|
||||
"""
|
||||
Wczytuje module.json, bumpuje wersję, zapisuje (jeśli nie dry_run),
|
||||
i wykonuje git add/commit/push. Zwraca (module_id, new_version).
|
||||
"""
|
||||
manifest_path = src_dir / "module.json"
|
||||
manifest = read_manifest(manifest_path)
|
||||
|
||||
if "id" not in manifest or "version" not in manifest:
|
||||
raise KeyError(f"W {norm(manifest_path)} brakuje pola 'id' lub 'version'.")
|
||||
|
||||
module_id = str(manifest["id"])
|
||||
old_version = str(manifest["version"])
|
||||
new_version = bump_version(old_version)
|
||||
|
||||
print(f"[PRE] module.json: id={module_id}, version={old_version} -> {new_version}")
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.")
|
||||
else:
|
||||
manifest["version"] = new_version
|
||||
write_manifest(manifest_path, manifest)
|
||||
print(f"[PRE] Zapisano zaktualizowany {norm(manifest_path)}")
|
||||
|
||||
commit_msg = f"sync: {module_id} v{new_version}"
|
||||
run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
|
||||
return module_id, new_version
|
||||
|
||||
|
||||
# ================= CLI / MAIN =================
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (preflight: module.json + git).")
|
||||
p.add_argument("--user", dest="user", default=TARGET_USER,
|
||||
help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})")
|
||||
p.add_argument("--appdata-kind", dest="appdata_kind", choices=APPDATA_VARIANTS + (None,),
|
||||
default=APPDATA_KIND,
|
||||
help=f"Wymuś AppData wariant: {APPDATA_VARIANTS} lub brak (None). Domyślnie: {APPDATA_KIND}")
|
||||
p.add_argument("--modules-dir-override", dest="modules_dir_override",
|
||||
default=TARGET_MODULES_DIR_OVERRIDE,
|
||||
help="Pełna ścieżka do ...\\FoundryVTT\\Data\\modules (pomija wyszukiwanie).")
|
||||
p.add_argument("--module-subdir-name", dest="module_subdir_name",
|
||||
default=MODULE_SUBDIR_NAME,
|
||||
help=f"Nazwa podkatalogu modułu (domyślnie: {MODULE_SUBDIR_NAME})")
|
||||
|
||||
cd_group = p.add_mutually_exclusive_group()
|
||||
cd_group.add_argument("--confirm-delete", dest="confirm_delete", action="store_true",
|
||||
help="Włącz potwierdzenie kasowania (jeśli było wyłączone).")
|
||||
cd_group.add_argument("--no-confirm-delete", dest="confirm_delete", action="store_false",
|
||||
help="Wyłącz potwierdzenie kasowania (tryb bez pytania).")
|
||||
p.set_defaults(confirm_delete=CONFIRM_DELETE)
|
||||
|
||||
p.add_argument("--source-base-dir", dest="source_base_dir",
|
||||
default=SOURCE_BASE_DIR,
|
||||
help=f"Katalog źródłowy (domyślnie: {SOURCE_BASE_DIR})")
|
||||
p.add_argument("--relative-pattern", dest="relative_pattern",
|
||||
default=RELATIVE_PATTERN,
|
||||
help=f"Wzorzec relatywny od AppData (domyślnie: {RELATIVE_PATTERN})")
|
||||
|
||||
dr_group = p.add_mutually_exclusive_group()
|
||||
dr_group.add_argument("--dry-run", dest="dry_run", action="store_true",
|
||||
help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).")
|
||||
dr_group.add_argument("--no-dry-run", dest="dry_run", action="store_false",
|
||||
help="Tryb wykonawczy: wprowadza zmiany i robi git.")
|
||||
p.set_defaults(dry_run=DRY_RUN)
|
||||
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
user = args.user
|
||||
appdata_kind = args.appdata_kind if args.appdata_kind != "None" else None
|
||||
modules_override = args.modules_dir_override
|
||||
module_subdir = args.module_subdir_name
|
||||
confirm_delete = args.confirm_delete
|
||||
source_base = args.source_base_dir
|
||||
relative_pattern = args.relative_pattern
|
||||
dry_run = args.dry_run
|
||||
|
||||
# Ścieżki
|
||||
src_dir = Path(source_base) / module_subdir
|
||||
|
||||
# [PRE] module.json + git w katalogu źródłowym
|
||||
print(f"[PRE] Katalog źródłowy: {norm(src_dir)}")
|
||||
preflight_source(src_dir, dry_run=dry_run)
|
||||
|
||||
# 1) Ustal katalog modules (cel)
|
||||
if modules_override:
|
||||
modules_dir = Path(modules_override)
|
||||
if not modules_dir.is_dir():
|
||||
raise FileNotFoundError(f"TARGET_MODULES_DIR_OVERRIDE wskazuje na nieistniejący katalog: {norm(modules_dir)}")
|
||||
else:
|
||||
modules_dir = find_modules_dir_for_user(user, relative_pattern, appdata_kind)
|
||||
|
||||
print(f"[INFO] Katalog modules: {norm(modules_dir)}")
|
||||
|
||||
target_module_dir = modules_dir / module_subdir
|
||||
print(f"[INFO] Docelowy katalog modułu: {norm(target_module_dir)}")
|
||||
|
||||
# 2) Kasowanie zawartości (albo plan w dry-run)
|
||||
if confirm_delete and not dry_run:
|
||||
ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_module_dir)}'? [y/N]: ").strip().lower()
|
||||
if ans not in ("y", "yes", "t", "tak"):
|
||||
print("[INFO] Przerwano na życzenie użytkownika.")
|
||||
return 0
|
||||
elif confirm_delete and dry_run:
|
||||
print("[INFO] (dry-run) Pytanie o potwierdzenie pomijane — tryb symulacji.")
|
||||
|
||||
print("[AKCJA] Czyszczenie zawartości katalogu modułu..." if not dry_run else "[SYMULACJA] Plan czyszczenia katalogu modułu...")
|
||||
del_files, del_dirs = clear_dir_contents(target_module_dir, dry_run=dry_run)
|
||||
print(f"[INFO] {'Do usunięcia' if dry_run else 'Usunięto'}: plików={del_files}, katalogów={del_dirs}")
|
||||
|
||||
# 3) Kopiowanie (albo plan kopiowania)
|
||||
print("[AKCJA] Kopiowanie zawartości..." if not dry_run else "[SYMULACJA] Plan kopiowania...")
|
||||
cp_files, cp_dirs = copy_dir_contents(src_dir, target_module_dir, dry_run=dry_run)
|
||||
print(f"[INFO] {'Do skopiowania' if dry_run else 'Skopiowano'}: plików={cp_files}, katalogów={cp_dirs}")
|
||||
|
||||
print("[SUKCES] " + ("Symulacja zakończona." if dry_run else "Zakończono synchronizację."))
|
||||
print(f" Źródło: {norm(src_dir)}")
|
||||
print(f" Cel: {norm(target_module_dir)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except AmbiguityError as e:
|
||||
print("\n[NIERÓWNOZNACZNOŚĆ]\n" + str(e), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
except Exception as e:
|
||||
print("\n[BŁĄD]\n" + str(e), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Binary file not shown.
Reference in New Issue
Block a user