sync: help_scripts v0.1

This commit is contained in:
2025-09-19 21:38:42 +02:00
parent bf6e1050d9
commit ee632ee088
2 changed files with 338 additions and 109 deletions
+337 -109
View File
@@ -1,19 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
sync_foundry_module.py (z dry-run, argumentami CLI i preflightem git/module.json)
sync_foundry_module.py
Rozszerzony:
- Tryb AUTO: wykrywanie katalogów ze zmianami (git), obsługa modułów i niemodułów.
- Każdy katalog -> osobny commit i push.
- Root version.nfo: opcjonalny bump globalnej wersji (0.x) jeśli są zmiany i HEAD ma tag z obecną wersją.
- Zipowanie po commit/push, przed podmianą plików (moduły) lub tuż po commicie (niemoduły):
ZIP_MODE = "ALL" | "MODULE_ONLY" | "OFF"; CLI: --zip-mode; --zip-output-dir.
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)
Dwa tryby:
- AUTO (domyślny): wykryj wszystkie ZMIENIONE podkatalogi w SOURCE_BASE_DIR:
* jeśli folder zawiera module.json -> procedura "moduł"
* jeśli nie -> procedura "niemoduł" (version.nfo 0.x, inkrementacja)
- SINGLE: przetwórz wyłącznie katalog wskazany w MODULE_SUBDIR_NAME (jak wcześniej)
[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)
Dry-run:
- nic NIE modyfikuje (ani plików, ani repo), nie czyści i nie kopiuje tylko plan.
Wymagania: Python 3.8+, git w PATH, uprawnienia do zapisu.
Wymagania:
- Python 3.8+
- git w PATH (dla commit/push i wykrywania zmian)
- uprawnienia do zapisu w repo i w katalogu docelowym modułów
"""
from __future__ import annotations
@@ -24,27 +32,38 @@ import argparse
import json
import subprocess
from pathlib import Path
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Set
# ==========================
# KONFIGURACJA (DOMYŚLNA)
# ==========================
# Tryb: "auto" (wykryj zmienione katalogi) lub "single" (tylko MODULE_SUBDIR_NAME)
MODE: str = "auto" # "auto" | "single"
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"
MODULE_SUBDIR_NAME: str = "wg-voidships" # używane w trybie SINGLE
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)
# --- ZIP settings ---
# ALL -> zipuj WSZYSTKIE katalogi (moduły i niemoduły)
# MODULE_ONLY -> zipuj TYLKO katalogi z module.json
# OFF -> nie twórz zipów
ZIP_MODE: str = "MODULE_ONLY" # "ALL" | "MODULE_ONLY" | "OFF"
# Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips
ZIP_OUTPUT_DIR: Optional[str] = None
# ==========================
# KONIEC KONFIGURACJI
# ==========================
APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming")
ARGS = None # wypełniane w main()
class AmbiguityError(RuntimeError):
pass
@@ -54,6 +73,56 @@ def norm(p: Path) -> Path:
return Path(os.path.normpath(str(p)))
def sh(cmd: List[str], cwd: Path) -> Tuple[int, str]:
"""Uruchom polecenie i zwróć (rc, out)."""
r = subprocess.run(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
return r.returncode, r.stdout
def git_head_tag(repo_dir: Path) -> Optional[str]:
"""Zwraca tag dokładnie na HEAD (jeśli istnieje), inaczej None."""
rc, out = sh(["git", "describe", "--tags", "--exact-match", "HEAD"], repo_dir)
if rc != 0:
return None
return out.strip() if out.strip() else None
def ensure_git_repo(path: Path) -> None:
rc, out = sh(["git", "rev-parse", "--is-inside-work-tree"], path)
if rc != 0 or "true" not in out.strip().lower():
raise RuntimeError(f"Katalog {norm(path)} nie wygląda na repozytorium git.\n{out}")
def git_changed_top_level_dirs(repo_dir: Path) -> List[str]:
"""
Zwraca posortowaną listę unikalnych NAZW PODKATALOGÓW (1. poziom) w repo,
które mają zmiany wg `git status --porcelain` (staged/unstaged/untracked).
Zmiany w plikach w root repo są ignorowane (brak katalogu).
"""
rc, out = sh(["git", "status", "--porcelain"], repo_dir)
if rc != 0:
raise RuntimeError(f"Błąd podczas `git status --porcelain` w {norm(repo_dir)}:\n{out}")
changed: Set[str] = set()
for line in out.splitlines():
if not line.strip():
continue
# format: XY <path> lub "?? <path>"
try:
path = line[3:].strip()
except Exception:
continue
# ignoruj rename "R a -> b": wówczas path ma "a -> b"
if "->" in path:
path = path.split("->", 1)[1].strip()
parts = Path(path).parts
if len(parts) >= 1:
top = parts[0]
# bierz tylko katalogi (jeśli top to plik w root, pomiń)
if (repo_dir / top).is_dir():
changed.add(top)
return sorted(changed)
def find_modules_dir_for_user(user: str,
relative_pattern: str,
appdata_kind: Optional[str]) -> Path:
@@ -115,19 +184,6 @@ def iter_dir_contents(dir_path: Path) -> List[Path]:
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
@@ -231,10 +287,6 @@ def read_manifest(manifest_path: Path) -> dict:
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}'")
@@ -246,19 +298,15 @@ def bump_version(last_version: str) -> str:
return ".".join(parts)
def write_manifest(manifest_path: Path, data: dict) -> None:
tmp = manifest_path.with_suffix(".json.tmp")
def write_json_atomic(path: Path, data: dict) -> None:
tmp = path.with_suffix(path.suffix + ".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
tmp.replace(path)
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}")
@@ -266,31 +314,25 @@ def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
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)
rc, out = sh(cmd, repo_dir)
if rc != 0:
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{out}")
if out.strip():
print(out)
# 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)
rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir)
if rc != 0:
raise RuntimeError(f"Błąd podczas git commit:\n{out}")
if out.strip():
print(out)
run(["git", "push"])
def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
def preflight_module(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).
Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push.
Zwraca (module_id, new_version).
"""
manifest_path = src_dir / "module.json"
manifest = read_manifest(manifest_path)
@@ -302,25 +344,140 @@ def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
old_version = str(manifest["version"])
new_version = bump_version(old_version)
print(f"[PRE] module.json: id={module_id}, version={old_version} -> {new_version}")
print(f"[PRE][MODULE] {src_dir.name}: 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)}")
write_json_atomic(manifest_path, manifest)
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 preflight_non_module(src_dir: Path, dry_run: bool) -> str:
"""
Niemoduł: version.nfo w formacie '0.x' (x int).
- jeśli brak -> '0.1'
- jeśli jest -> inkrementuj x
Zapis (nie dry-run) oraz osobny commit/push.
Zwraca new_version (np. '0.4').
"""
nfo = src_dir / "version.nfo"
if not nfo.exists():
new_version = "0.1"
print(f"[PRE][FOLDER] {src_dir.name}: version.nfo brak -> {new_version}")
if not dry_run:
with nfo.open("w", encoding="utf-8") as f:
f.write(new_version + "\n")
commit_msg = f"sync: {src_dir.name} v{new_version}"
run_git_commands(src_dir, commit_msg, dry_run=False)
return new_version
# istnieje -> wczytaj i bumpnij
content = nfo.read_text(encoding="utf-8").strip()
if not content.startswith("0."):
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0.x', otrzymano '{content}'")
tail = content.split(".", 1)[1]
try:
x = int(tail)
except ValueError:
raise ValueError(f"{norm(nfo)}: część po '0.' nie jest liczbą: '{tail}'")
new_version = f"0.{x+1}"
print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}")
if not dry_run:
with nfo.open("w", encoding="utf-8") as f:
f.write(new_version + "\n")
commit_msg = f"sync: {src_dir.name} v{new_version}"
run_git_commands(src_dir, commit_msg, dry_run=False)
return new_version
# ====== ROOT version.nfo (globalna wersja repo) ======
def maybe_bump_root_version(repo_dir: Path, changed_any: bool, dry_run: bool) -> Optional[str]:
"""
Jeśli:
- istnieją zmiany do przetworzenia (changed_any == True)
- HEAD ma tag zawierający aktualną wersję globalną z version.nfo (0.x)
to bumpnij version.nfo w roocie (0.x -> 0.(x+1)), zrób osobny commit i push.
Zwraca nową wersję (str) lub None, jeśli pominięto.
"""
if not changed_any:
print("[ROOT] Pomijam bump wersji globalnej: brak zmian do przetworzenia.")
return None
nfo = repo_dir / "version.nfo"
if not nfo.exists():
print("[ROOT] Brak version.nfo w roocie; pomijam bump (wymagany istniejący plik).")
return None
current = nfo.read_text(encoding="utf-8").strip()
if not current.startswith("0."):
raise ValueError(f"{nfo}: oczekiwano formatu '0.x', otrzymano '{current}'")
# HEAD musi mieć tag zawierający 'current'
tag = git_head_tag(repo_dir)
if not tag or current not in tag:
print(f"[ROOT] Pomijam bump: {'brak taga na HEAD' if not tag else f'tag \"{tag}\" nie zawiera wersji {current}'}")
return None
# Bump
try:
tail = int(current.split(".", 1)[1])
except ValueError:
raise ValueError(f"{nfo}: część po '0.' nie jest liczbą: '{current}'")
new_version = f"0.{tail+1}"
print(f"[ROOT] Globalna wersja: {current} -> {new_version}")
if dry_run:
print("[DRY-RUN][ROOT] Zapis version.nfo i commit/push zostaną pominięte.")
return new_version
nfo.write_text(new_version + "\n", encoding="utf-8")
run_git_commands(repo_dir, f"sync: root v{new_version}", dry_run=False)
return new_version
# ====== ZIP utilities ======
def should_zip(is_module: bool, zip_mode: str) -> bool:
if zip_mode == "OFF":
return False
if zip_mode == "ALL":
return True
# MODULE_ONLY
return is_module
def ensure_zip_output_dir(source_base: Path, override: Optional[str]) -> Path:
out = Path(override) if override else (source_base / "_zips")
out.mkdir(parents=True, exist_ok=True)
return out
def make_zip_for_folder(src_dir: Path, version: str, out_dir: Path, dry_run: bool) -> Path:
"""
Tworzy .zip z zawartością src_dir (bez nadkatalogów) o nazwie NAME-vVERSION.zip
Zwraca ścieżkę do archiwum.
"""
base_name = f"{src_dir.name}-v{version}"
target = out_dir / (base_name + ".zip")
if dry_run:
print(f"[DRY-RUN][ZIP] {src_dir} -> {target}")
return target
# shutil.make_archive potrzebuje base_name BEZ .zip i root_dir+base_dir
base_name_path = out_dir / base_name
shutil.make_archive(str(base_name_path), "zip", root_dir=str(src_dir), base_dir=".")
print(f"[ZIP] Utworzono {target}")
return target
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (preflight: module.json + git).")
p = argparse.ArgumentParser(description="Synchronizacja FoundryVTT: AUTO wykrywanie zmian + obsługa modułów/niemodułów.")
p.add_argument("--mode", choices=["auto", "single"], default=MODE,
help=f"Tryb działania (domyślnie: {MODE}).")
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,),
@@ -331,85 +488,156 @@ def parse_args() -> argparse.Namespace:
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})")
help=f"Nazwa podkatalogu modułu (tylko w --mode single). 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).")
help="Włącz potwierdzenie kasowania.")
cd_group.add_argument("--no-confirm-delete", dest="confirm_delete", action="store_false",
help="Wyłącz potwierdzenie kasowania (tryb bez pytania).")
help="Wyłącz potwierdzenie kasowania.")
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})")
help=f"Katalog źródłowy repo (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})")
help=f"Wzorzec relatywny od AppData dla katalogu modules (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.")
help="Tryb wykonawczy: modyfikacje + git.")
p.set_defaults(dry_run=DRY_RUN)
# ZIP options
p.add_argument("--zip-mode", dest="zip_mode",
choices=["ALL", "MODULE_ONLY", "OFF"],
default=ZIP_MODE,
help=f"Zipowanie: ALL | MODULE_ONLY | OFF (domyślnie: {ZIP_MODE})")
p.add_argument("--zip-output-dir", dest="zip_output_dir",
default=ZIP_OUTPUT_DIR,
help="Katalog wyjściowy na zipy (domyślnie: SOURCE_BASE_DIR/_zips)")
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)
def resolve_modules_base(user: str, relative_pattern: str, appdata_kind: Optional[str],
override: Optional[str]) -> Path:
if override:
modules_dir = Path(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)
return modules_dir
return 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)}")
def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_run: bool) -> None:
module_id, new_ver = preflight_module(src_dir, dry_run=dry_run)
target_dir = modules_base / src_dir.name
print(f"[INFO][MODULE] Cel modułu: {norm(target_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()
ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_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.")
raise RuntimeError("Przerwano na życzenie użytkownika.")
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)
if confirm_delete and dry_run:
print("[INFO] (dry-run) Pytanie o potwierdzenie pominięte.")
# ZIP po commicie, przed podmianą
if should_zip(is_module=True, zip_mode=ARGS.zip_mode):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e:
raise RuntimeError(f"Błąd zipowania modułu {src_dir.name}: {e}") from e
print("[AKCJA] Czyszczenie..." if not dry_run else "[SYMULACJA] Plan czyszczenia...")
del_files, del_dirs = clear_dir_contents(target_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("[AKCJA] Kopiowanie..." if not dry_run else "[SYMULACJA] Plan kopiowania...")
cp_files, cp_dirs = copy_dir_contents(src_dir, target_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)}")
print(f"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run
else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.")
def process_non_module(src_dir: Path, dry_run: bool) -> None:
new_ver = preflight_non_module(src_dir, dry_run=dry_run)
# ZIP po commicie (jeśli ustawienie pozwala)
if should_zip(is_module=False, zip_mode=ARGS.zip_mode):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e:
raise RuntimeError(f"Błąd zipowania folderu {src_dir.name}: {e}") from e
print(f"[DONE][FOLDER] {src_dir.name} -> version.nfo = {new_ver}; commit wykonany." if not dry_run
else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.")
def main() -> int:
global ARGS
ARGS = parse_args()
mode = ARGS.mode
user = ARGS.user
appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None
modules_override = ARGS.modules_dir_override
module_subdir_single = ARGS.module_subdir_name
confirm_delete = ARGS.confirm_delete
source_base = Path(ARGS.source_base_dir)
relative_pattern = ARGS.relative_pattern
dry_run = ARGS.dry_run
if not source_base.is_dir():
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(source_base)}")
ensure_git_repo(source_base)
# Ustal bazę modules (tylko raz)
modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override)
print(f"[INFO] Katalog modules base: {norm(modules_base)}")
if mode == "single":
src_dir = source_base / module_subdir_single
if not src_dir.is_dir():
raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}")
# Root bump (globalna wersja) tylko jeśli "są zmiany do przetworzenia" (single -> True)
maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
if (src_dir / "module.json").is_file():
print(f"[MODE=SINGLE][MODULE] {src_dir.name}")
process_module(src_dir, modules_base, confirm_delete, dry_run)
else:
print(f"[MODE=SINGLE][FOLDER] {src_dir.name}")
process_non_module(src_dir, dry_run)
print("[SUKCES] Tryb SINGLE zakończony.")
return 0
# AUTO: wykryj zmienione katalogi
changed = git_changed_top_level_dirs(source_base)
print(f"[AUTO] Zmienione katalogi w {norm(source_base)}: {changed if changed else '(brak)'}")
# Root bump (globalna wersja) tylko jeśli są jakiekolwiek zmiany do przetworzenia
maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
if not changed:
print("[INFO] Brak zmian do przetworzenia.")
return 0
# Przetwarzaj w stałej kolejności
for name in changed:
src_dir = source_base / name
if (src_dir / "module.json").is_file():
print(f"\n[AUTO][MODULE] {name}")
process_module(src_dir, modules_base, confirm_delete, dry_run)
else:
print(f"\n[AUTO][FOLDER] {name}")
process_non_module(src_dir, dry_run)
print("\n[SUKCES] Tryb AUTO zakończony.")
return 0