diff --git a/.specstory/history/2025-11-10_16-59Z-change-compression-method-to-7z.md b/.specstory/history/2025-11-10_16-59Z-change-compression-method-to-7z.md index 134e5be..ec03bc3 100644 --- a/.specstory/history/2025-11-10_16-59Z-change-compression-method-to-7z.md +++ b/.specstory/history/2025-11-10_16-59Z-change-compression-method-to-7z.md @@ -42,7 +42,7 @@ Updating the code to use 7z with compression level 9 instead of Python's zipfile -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined @@ -64,7 +64,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -73,8 +73,8 @@ Lines added: 1, lines removed: 1 @@ -127,5 +127,5 @@ def archive_path_for(seq_dir: Path) -> Path: rel = seq_dir.relative_to(RENDER_ROOT) -- return (ARCHIVE_ROOT / rel).with_suffix(".zip") -+ return (ARCHIVE_ROOT / rel).with_suffix(".7z") +- return (ARCHIVE_ROOT / rel).with_suffix(".zip") ++ return (ARCHIVE_ROOT / rel).with_suffix(".7z") ``` @@ -87,7 +87,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 30, lines removed: 5 @@ -99,7 +99,7 @@ Lines added: 30, lines removed: 5 - from zipfile import ZIP_STORED, ZipFile - zip_path.parent.mkdir(parents=True, exist_ok=True) -- with ZipFile(zip_path, "w", compression=ZIP_STORED) as archive: +- with ZipFile(zip_path, "w", compression=ZIP_STORED) as archive: - for file_path in iter_sequence_files(seq_dir): - archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix()) + @@ -114,10 +114,10 @@ Lines added: 30, lines removed: 5 + # -mmt=on: multi-threading + # -y: assume yes to all prompts + cmd = [ -+ "7z", "a", -+ "-mx=9", -+ "-mmt=on", -+ "-y", ++ "7z", "a", ++ "-mx=9", ++ "-mmt=on", ++ "-y", + str(zip_path), + ] + @@ -131,7 +131,7 @@ Lines added: 30, lines removed: 5 + ) + + if result.returncode != 0: -+ raise RuntimeError(f"7z compression failed: {result.stderr}") ++ raise RuntimeError(f"7z compression failed: {result.stderr}") ``` @@ -144,7 +144,7 @@ Lines added: 30, lines removed: 5 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 19, lines removed: 4 @@ -160,15 +160,15 @@ Lines added: 19, lines removed: 4 shutil.rmtree(target_dir) target_dir.mkdir(parents=True, exist_ok=True) -- with ZipFile(zip_path, "r") as archive: +- with ZipFile(zip_path, "r") as archive: - archive.extractall(target_dir) + # Use 7z to extract archive + # -y: assume yes to all prompts + # -o: output directory + cmd = [ -+ "7z", "x", -+ "-y", -+ f"-o{target_dir}", ++ "7z", "x", ++ "-y", ++ f"-o{target_dir}", + str(zip_path), + ] + @@ -180,9 +180,9 @@ Lines added: 19, lines removed: 4 + ) + + if result.returncode != 0: -+ raise RuntimeError(f"7z extraction failed: {result.stderr}") ++ raise RuntimeError(f"7z extraction failed: {result.stderr}") - for entry in seq_state.get("files", []): + for entry in seq_state.get("files", []): ```
@@ -193,7 +193,7 @@ Lines added: 19, lines removed: 4 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -202,8 +202,8 @@ Lines added: 1, lines removed: 1 @@ -284,5 +284,5 @@ work_items: list[tuple[Path, dict]] = [] -- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): -+ for zip_path in ARCHIVE_ROOT.rglob("*.7z"): +- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): ++ for zip_path in ARCHIVE_ROOT.rglob("*.7z"): state_path = state_path_for(zip_path) seq_state = load_state(state_path) ``` @@ -215,8 +215,8 @@ Lines added: 1, lines removed: 1 @@ -327,5 +327,5 @@ removed: list[Path] = [] -- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): -+ for zip_path in ARCHIVE_ROOT.rglob("*.7z"): +- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): ++ for zip_path in ARCHIVE_ROOT.rglob("*.7z"): seq_dir = sequence_dir_for(zip_path) if seq_dir.exists(): ``` @@ -239,7 +239,7 @@ Updating the docstring to reflect 7z usage and checking for lint errors: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 5, lines removed: 5 @@ -247,8 +247,8 @@ Lines added: 5, lines removed: 5 ```diff @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -- """Maintain zipped render sequences for Git hooks. -+ """Maintain compressed render sequences for Git hooks using 7z. +- """Maintain zipped render sequences for Git hooks. ++ """Maintain compressed render sequences for Git hooks using 7z. - Default mode scans `Renders/`, produces ZIP archives under `Renders/_zipped/`, - and stages any updated archives so commits only track compact files. Switch to @@ -258,7 +258,7 @@ Lines added: 5, lines removed: 5 + `Renders/_zipped/`, and stages any updated archives so commits only track compact + files. Switch to `--mode expand` to inflate the tracked archives back into the + ignored working directories after checkouts or pulls. - """ + """ ``` @@ -362,7 +362,7 @@ The cleanup function only looks for `.7z` files, so old `.zip` files won't be re -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 11, lines removed: undefined @@ -372,9 +372,9 @@ Lines added: 11, lines removed: undefined removed: list[Path] = [] + # Remove obsolete .zip files (migrated to .7z) -+ for old_zip in ARCHIVE_ROOT.rglob("*.zip"): ++ for old_zip in ARCHIVE_ROOT.rglob("*.zip"): + rel = old_zip.relative_to(ARCHIVE_ROOT) -+ log("zip", f"Removing obsolete .zip archive {rel}", verbose_only=True, verbose=verbose) ++ log("zip", f"Removing obsolete .zip archive {rel}", verbose_only=True, verbose=verbose) + old_zip.unlink(missing_ok=True) + state_path = state_path_for(old_zip) + if state_path.exists(): @@ -382,7 +382,7 @@ Lines added: 11, lines removed: undefined + removed.append(old_zip) + + # Remove orphan .7z archives (sequence directory no longer exists) - for zip_path in ARCHIVE_ROOT.rglob("*.7z"): + for zip_path in ARCHIVE_ROOT.rglob("*.7z"): seq_dir = sequence_dir_for(zip_path) ``` @@ -504,19 +504,19 @@ Updating the code to use string values for both settings: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 ```diff @@ -34,6 +34,7 @@ - CONFIG_PATH = Path(__file__).resolve().with_name("config.json") + CONFIG_PATH = Path(__file__).resolve().with_name("config.json") DEFAULT_CONFIG = { -- "zipper": True, -+ "zipper": "7z", - "compression": 9, -+ "dailyFormat": "daily_YYMMDD", +- "zipper": True, ++ "zipper": "7z", + "compression": 9, ++ "dailyFormat": "daily_YYMMDD", } ``` @@ -529,7 +529,7 @@ Lines added: 2, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 @@ -538,10 +538,10 @@ Lines added: 2, lines removed: 1 @@ -62,5 +62,6 @@ CONFIG = load_config() -- USE_7Z = bool(CONFIG.get("zipper", True)) -+ ZIPPER_TYPE = str(CONFIG.get("zipper", "7z")).lower() -+ USE_7Z = ZIPPER_TYPE == "7z" - COMPRESSION_LEVEL = CONFIG.get("compression", 9) +- USE_7Z = bool(CONFIG.get("zipper", True)) ++ ZIPPER_TYPE = str(CONFIG.get("zipper", "7z")).lower() ++ USE_7Z = ZIPPER_TYPE == "7z" + COMPRESSION_LEVEL = CONFIG.get("compression", 9) if isinstance(COMPRESSION_LEVEL, str): ``` @@ -580,7 +580,7 @@ Adding backward compatibility for boolean values in the config: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: 1 @@ -589,15 +589,15 @@ Lines added: 6, lines removed: 1 @@ -62,5 +62,10 @@ CONFIG = load_config() -- ZIPPER_TYPE = str(CONFIG.get("zipper", "7z")).lower() -+ zipper_val = CONFIG.get("zipper", "7z") +- ZIPPER_TYPE = str(CONFIG.get("zipper", "7z")).lower() ++ zipper_val = CONFIG.get("zipper", "7z") + # Handle both old boolean format and new string format + if isinstance(zipper_val, bool): -+ ZIPPER_TYPE = "7z" if zipper_val else "zip" ++ ZIPPER_TYPE = "7z" if zipper_val else "zip" + else: + ZIPPER_TYPE = str(zipper_val).lower() - USE_7Z = ZIPPER_TYPE == "7z" - COMPRESSION_LEVEL = CONFIG.get("compression", 9) + USE_7Z = ZIPPER_TYPE == "7z" + COMPRESSION_LEVEL = CONFIG.get("compression", 9) ```
@@ -647,7 +647,7 @@ Updating `config.json` to use the new string format: -
Tool use: **code_edit** • Edit file: config.json +
Tool use: **search_replace** • Edit file: config.json **Chunk 1** Lines added: 2, lines removed: 2 @@ -655,12 +655,12 @@ Lines added: 2, lines removed: 2 ```diff @@ -1,6 +1,6 @@ { -- "dailyFormat": true, -+ "dailyFormat": "daily_YYMMDD", - "structDir": "D:\\ProjectStructure", -- "zipper": true, -+ "zipper": "7z", - "compression": 9 +- "dailyFormat": true, ++ "dailyFormat": "daily_YYMMDD", + "structDir": "D:\\ProjectStructure", +- "zipper": true, ++ "zipper": "7z", + "compression": 9 } ``` @@ -774,7 +774,7 @@ The condition checks `USE_7Z and SEVEN_Z_EXE`, but if 7z isn't found, `USE_7Z` i -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: 1 @@ -784,9 +784,9 @@ Lines added: 3, lines removed: 1 def zip_sequence(seq_dir: Path, zip_path: Path) -> None: - if USE_7Z and SEVEN_Z_EXE: -+ if ZIPPER_TYPE == "7z": ++ if ZIPPER_TYPE == "7z": + if SEVEN_Z_EXE is None: -+ raise RuntimeError("7z compression requested but 7z executable not found in PATH") ++ raise RuntimeError("7z compression requested but 7z executable not found in PATH") zip_path.parent.mkdir(parents=True, exist_ok=True) cmd = [ ``` @@ -798,7 +798,7 @@ Lines added: 1, lines removed: undefined @@ -207,4 +209,5 @@ return -+ # Use zipfile (ZIPPER_TYPE == "zip" or fallback) ++ # Use zipfile (ZIPPER_TYPE == "zip" or fallback) from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile ``` @@ -811,7 +811,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: 1 @@ -821,9 +821,9 @@ Lines added: 3, lines removed: 1 target_dir.mkdir(parents=True, exist_ok=True) - if USE_7Z and SEVEN_Z_EXE: -+ if ZIPPER_TYPE == "7z": ++ if ZIPPER_TYPE == "7z": + if SEVEN_Z_EXE is None: -+ raise RuntimeError("7z extraction requested but 7z executable not found in PATH") ++ raise RuntimeError("7z extraction requested but 7z executable not found in PATH") cmd = [ SEVEN_Z_EXE, ``` @@ -892,7 +892,7 @@ Removing the unused `USE_7Z` variable and updating the warning logic: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 @@ -901,8 +901,8 @@ Lines added: undefined, lines removed: 1 @@ -68,5 +68,4 @@ else: ZIPPER_TYPE = str(zipper_val).lower() -- USE_7Z = ZIPPER_TYPE == "7z" - COMPRESSION_LEVEL = CONFIG.get("compression", 9) +- USE_7Z = ZIPPER_TYPE == "7z" + COMPRESSION_LEVEL = CONFIG.get("compression", 9) if isinstance(COMPRESSION_LEVEL, str): ``` @@ -914,12 +914,12 @@ Lines added: 2, lines removed: 3 SEVEN_Z_EXE: str | None = None - if USE_7Z: -+ if ZIPPER_TYPE == "7z": - SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") ++ if ZIPPER_TYPE == "7z": + SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") if SEVEN_Z_EXE is None: -- print("[zip] Requested 7z compression but no 7z executable was found; falling back to zipfile.", file=sys.stderr) +- print("[zip] Requested 7z compression but no 7z executable was found; falling back to zipfile.", file=sys.stderr) - USE_7Z = False -+ print("[zip] Warning: 7z compression requested but no 7z executable was found in PATH.", file=sys.stderr) ++ print("[zip] Warning: 7z compression requested but no 7z executable was found in PATH.", file=sys.stderr) ``` @@ -1035,14 +1035,14 @@ Fixing the 7z command pattern. The `.\\*` may be causing issues. Using a simpler -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined ```diff @@ -195,4 +195,10 @@ - raise RuntimeError("7z compression requested but 7z executable not found in PATH") + raise RuntimeError("7z compression requested but 7z executable not found in PATH") zip_path.parent.mkdir(parents=True, exist_ok=True) + # Build list of files to archive with relative paths + file_list = [] @@ -1059,9 +1059,9 @@ Lines added: 1, lines removed: 2 ```diff @@ -202,6 +208,5 @@ - "-tzip", + "-tzip", str(zip_path), -- ".\\*", +- ".\\*", - ] + ] + file_list subprocess.run(cmd, cwd=seq_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -1221,7 +1221,7 @@ Updating the code to use a temporary list file for 7z to avoid Windows command-l -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 29, lines removed: 9 @@ -1232,29 +1232,29 @@ Lines added: 29, lines removed: 9 - cmd = [ - SEVEN_Z_EXE, -- "a", -- "-y", -- f"-mx={COMPRESSION_LEVEL}", -- "-tzip", +- "a", +- "-y", +- f"-mx={COMPRESSION_LEVEL}", +- "-tzip", - str(zip_path), - ] + file_list - subprocess.run(cmd, cwd=seq_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Use a temporary list file to avoid Windows command line length limits + import tempfile -+ with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: ++ with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: + list_file_path = Path(list_file.name) + for rel_path in file_list: -+ list_file.write(rel_path + "\n") ++ list_file.write(rel_path + "\n") + + try: + cmd = [ + SEVEN_Z_EXE, -+ "a", -+ "-y", -+ f"-mx={COMPRESSION_LEVEL}", -+ "-tzip", ++ "a", ++ "-y", ++ f"-mx={COMPRESSION_LEVEL}", ++ "-tzip", + str(zip_path), -+ f"@{list_file_path}", ++ f"@{list_file_path}", + ] + result = subprocess.run( + cmd, @@ -1265,7 +1265,7 @@ Lines added: 29, lines removed: 9 + text=True, + ) + if result.returncode != 0: -+ raise RuntimeError(f"7z compression failed: {result.stderr}") ++ raise RuntimeError(f"7z compression failed: {result.stderr}") + finally: + list_file_path.unlink(missing_ok=True) return @@ -1325,7 +1325,7 @@ Adding `tempfile` to the top-level imports: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 @@ -1352,7 +1352,7 @@ Lines added: 2, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 @@ -1362,7 +1362,7 @@ Lines added: undefined, lines removed: 1 # Use a temporary list file to avoid Windows command line length limits - import tempfile - with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: + with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: list_file_path = Path(list_file.name) ``` @@ -1534,7 +1534,7 @@ Adding a function to prompt for the project path: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 39, lines removed: undefined @@ -1546,18 +1546,18 @@ Lines added: 39, lines removed: undefined + + function Get-ProjectPathFromUser { + param( -+ [string]$Prompt = "Paste the project path to deploy in:" ++ [string]$Prompt = "Paste the project path to deploy in:" + ) + + do { + $inputPath = Read-Host -Prompt $Prompt + if ([string]::IsNullOrWhiteSpace($inputPath)) { -+ Write-Warning "Path cannot be empty. Please try again." ++ Write-Warning "Path cannot be empty. Please try again." + continue + } + + # Remove quotes if present -+ $inputPath = $inputPath.Trim('"', "'") ++ $inputPath = $inputPath.Trim('"', "'") + + # Try to resolve the path + try { @@ -1566,17 +1566,17 @@ Lines added: 39, lines removed: undefined + return $resolved.Path + } + else { -+ Write-Warning "Path does not exist or is not a directory: $inputPath" -+ $retry = Read-Host "Try again? (Y/N)" -+ if ($retry -notmatch '^[Yy]') { ++ Write-Warning "Path does not exist or is not a directory: $inputPath" ++ $retry = Read-Host "Try again? (Y/N)" ++ if ($retry -notmatch '^[Yy]') { + return $null + } + } + } + catch { -+ Write-Warning "Invalid path: $($_.Exception.Message)" -+ $retry = Read-Host "Try again? (Y/N)" -+ if ($retry -notmatch '^[Yy]') { ++ Write-Warning "Invalid path: $($_.Exception.Message)" ++ $retry = Read-Host "Try again? (Y/N)" ++ if ($retry -notmatch '^[Yy]') { + return $null + } + } @@ -1728,7 +1728,7 @@ Adding a main execution block so it runs when executed directly: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 12, lines removed: undefined @@ -1738,14 +1738,14 @@ Lines added: 12, lines removed: undefined } + # If script is run directly (not dot-sourced), prompt for project path -+ if ($MyInvocation.InvocationName -ne '.' -and $MyInvocation.InvocationName -ne $null) { ++ if ($MyInvocation.InvocationName -ne '.' -and $MyInvocation.InvocationName -ne $null) { + $projectPath = Get-ProjectPathFromUser + if ($null -ne $projectPath) { -+ Write-Host "Project path: $projectPath" -ForegroundColor Green ++ Write-Host "Project path: $projectPath" -ForegroundColor Green + return $projectPath + } + else { -+ Write-Host "No project path provided." -ForegroundColor Yellow ++ Write-Host "No project path provided." -ForegroundColor Yellow + exit 1 + } + } @@ -1793,7 +1793,7 @@ Refining the check to better detect when the script is run directly: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 2, lines removed: 1 @@ -1802,9 +1802,9 @@ Lines added: 2, lines removed: 1 @@ -152,5 +152,6 @@ # If script is run directly (not dot-sourced), prompt for project path -- if ($MyInvocation.InvocationName -ne '.' -and $MyInvocation.InvocationName -ne $null) { -+ # When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name -+ if ($MyInvocation.InvocationName -ne '.') { +- if ($MyInvocation.InvocationName -ne '.' -and $MyInvocation.InvocationName -ne $null) { ++ # When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name ++ if ($MyInvocation.InvocationName -ne '.') { $projectPath = Get-ProjectPathFromUser if ($null -ne $projectPath) { ``` @@ -1976,7 +1976,7 @@ Fixing the double colon and adding deployment logic to call `UpdateProjectBatche -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -1985,8 +1985,8 @@ Lines added: 1, lines removed: 1 @@ -88,5 +88,5 @@ function Get-ProjectPathFromUser { param( -- [string]$Prompt = "Paste the project path to deploy in:" -+ [string]$Prompt = "Paste the project path to deploy in" +- [string]$Prompt = "Paste the project path to deploy in:" ++ [string]$Prompt = "Paste the project path to deploy in" ) ``` @@ -1999,7 +1999,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 22, lines removed: 3 @@ -2010,31 +2010,31 @@ Lines added: 22, lines removed: 3 - # If script is run directly (not dot-sourced), prompt for project path + # If script is run directly (not dot-sourced), prompt for project path and deploy - # When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name - if ($MyInvocation.InvocationName -ne '.') { + # When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name + if ($MyInvocation.InvocationName -ne '.') { $projectPath = Get-ProjectPathFromUser if ($null -ne $projectPath) { -- Write-Host "Project path: $projectPath" -ForegroundColor Green +- Write-Host "Project path: $projectPath" -ForegroundColor Green - return $projectPath -+ Write-Host "`nDeploying to: $projectPath" -ForegroundColor Cyan ++ Write-Host "`nDeploying to: $projectPath" -ForegroundColor Cyan + + # Find UpdateProjectBatches.ps1 in the same directory as ConfigLoader.ps1 -+ $updateScript = Join-Path -Path $script:LoaderRoot -ChildPath 'UpdateProjectBatches.ps1' ++ $updateScript = Join-Path -Path $script:LoaderRoot -ChildPath 'UpdateProjectBatches.ps1' + if (Test-Path -LiteralPath $updateScript) { -+ Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow ++ Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow + & $updateScript -ProjectPath $projectPath + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { -+ Write-Host "`nDeployment completed successfully." -ForegroundColor Green ++ Write-Host "`nDeployment completed successfully." -ForegroundColor Green + } + else { -+ Write-Host "`nDeployment completed with errors (exit code: $exitCode)." -ForegroundColor Red ++ Write-Host "`nDeployment completed with errors (exit code: $exitCode)." -ForegroundColor Red + } + exit $exitCode + } + else { -+ Write-Warning "UpdateProjectBatches.ps1 not found in $script:LoaderRoot" -+ Write-Host "Project path: $projectPath" -ForegroundColor Green ++ Write-Warning "UpdateProjectBatches.ps1 not found in $script:LoaderRoot" ++ Write-Host "Project path: $projectPath" -ForegroundColor Green + return $projectPath + } } @@ -2217,37 +2217,37 @@ Parameters: ```json { - "plan": "# Fix PowerShell Integration Issues\n\n## Issues Identified\n\n1. **Unicode character encoding error** in `UpdateProjectBatches.ps1` - checkmark (✓) and cross (✗) characters cause PowerShell parsing errors\n2. **ConfigLoader.ps1 functions outdated** - `Use-IsoDailyFormat` and `Use-7Zip` still treat config as booleans but config now uses strings\n3. **Backward compatibility** - functions should handle both old boolean and new string values\n\n## Changes Required\n\n### 1. Fix Unicode Characters in UpdateProjectBatches.ps1\n\nReplace Unicode characters with ASCII-safe alternatives:\n- Line 104: `\"✓\"` → `\"[OK]\"` or `\"+\"`\n- Line 114: `\"✗\"` → `\"[FAIL]\"` or `\"-\"`\n- Line 121: `\"✗\"` → `\"[FAIL]\"` or `\"-\"`\n\n### 2. Update Use-IsoDailyFormat Function\n\nUpdate `ConfigLoader.ps1` line 127-130 to handle string values:\n- Check if value is boolean (backward compatibility) - return as-is\n- If string, check if it matches ISO format pattern (contains \"YYYY-MM-DD\" or similar)\n- Return boolean indicating whether ISO format is used\n\n### 3. Update Use-7Zip Function\n\nUpdate `ConfigLoader.ps1` line 132-135 to handle string values:\n- Check if value is boolean (backward compatibility) - convert to \"7z\" (true) or \"zip\" (false)\n- If string, check if it equals \"7z\" (case-insensitive)\n- Return boolean indicating whether 7z should be used\n\n## Files to Modify\n\n- `UpdateProjectBatches.ps1` - Replace Unicode characters (lines 104, 114, 121)\n- `ConfigLoader.ps1` - Update `Use-IsoDailyFormat` and `Use-7Zip` functions (lines 127-135)", - "name": "Fix PowerShell Integration Issues", - "todos": [ + "plan": "# Fix PowerShell Integration Issues\n\n## Issues Identified\n\n1. **Unicode character encoding error** in `UpdateProjectBatches.ps1` - checkmark (✓) and cross (✗) characters cause PowerShell parsing errors\n2. **ConfigLoader.ps1 functions outdated** - `Use-IsoDailyFormat` and `Use-7Zip` still treat config as booleans but config now uses strings\n3. **Backward compatibility** - functions should handle both old boolean and new string values\n\n## Changes Required\n\n### 1. Fix Unicode Characters in UpdateProjectBatches.ps1\n\nReplace Unicode characters with ASCII-safe alternatives:\n- Line 104: `\"✓\"` → `\"[OK]\"` or `\"+\"`\n- Line 114: `\"✗\"` → `\"[FAIL]\"` or `\"-\"`\n- Line 121: `\"✗\"` → `\"[FAIL]\"` or `\"-\"`\n\n### 2. Update Use-IsoDailyFormat Function\n\nUpdate `ConfigLoader.ps1` line 127-130 to handle string values:\n- Check if value is boolean (backward compatibility) - return as-is\n- If string, check if it matches ISO format pattern (contains \"YYYY-MM-DD\" or similar)\n- Return boolean indicating whether ISO format is used\n\n### 3. Update Use-7Zip Function\n\nUpdate `ConfigLoader.ps1` line 132-135 to handle string values:\n- Check if value is boolean (backward compatibility) - convert to \"7z\" (true) or \"zip\" (false)\n- If string, check if it equals \"7z\" (case-insensitive)\n- Return boolean indicating whether 7z should be used\n\n## Files to Modify\n\n- `UpdateProjectBatches.ps1` - Replace Unicode characters (lines 104, 114, 121)\n- `ConfigLoader.ps1` - Update `Use-IsoDailyFormat` and `Use-7Zip` functions (lines 127-135)", + "name": "Fix PowerShell Integration Issues", + "todos": [ { - "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", - "status": "pending", - "id": "fix-unicode-chars" + "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", + "status": "pending", + "id": "fix-unicode-chars" }, { - "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", - "status": "pending", - "id": "update-daily-format" + "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", + "status": "pending", + "id": "update-daily-format" }, { - "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", - "status": "pending", - "id": "update-7zip-function" + "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", + "status": "pending", + "id": "update-7zip-function" } ], - "overview": "Fix Unicode character encoding errors in UpdateProjectBatches.ps1 and update ConfigLoader.ps1 functions to handle new string-based config values while maintaining backward compatibility." + "overview": "Fix Unicode character encoding errors in UpdateProjectBatches.ps1 and update ConfigLoader.ps1 functions to handle new string-based config values while maintaining backward compatibility." } ``` Additional data: ```json { - "status": "accepted", - "reviewData": { - "status": "Done", - "selectedOption": "approve", - "isShowingInput": false + "status": "accepted", + "reviewData": { + "status": "Done", + "selectedOption": "approve", + "isShowingInput": false } } ``` @@ -2255,22 +2255,22 @@ Result: ```json { - "accepted": { - "finalTodos": [ + "accepted": { + "finalTodos": [ { - "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", - "status": "pending", - "id": "fix-unicode-chars" + "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", + "status": "pending", + "id": "fix-unicode-chars" }, { - "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", - "status": "pending", - "id": "update-daily-format" + "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", + "status": "pending", + "id": "update-daily-format" }, { - "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", - "status": "pending", - "id": "update-7zip-function" + "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", + "status": "pending", + "id": "update-7zip-function" } ] } @@ -2336,7 +2336,7 @@ Replacing Unicode characters with ASCII-safe alternatives: -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -2345,8 +2345,8 @@ Lines added: 1, lines removed: 1 @@ -102,5 +102,5 @@ try { Copy-Item -Path $spec.Source -Destination $target.FullName -Force -- Write-Host "✓ $($target.FullName)" -ForegroundColor Green -+ Write-Host "[OK] $($target.FullName)" -ForegroundColor Green +- Write-Host "✓ $($target.FullName)" -ForegroundColor Green ++ Write-Host "[OK] $($target.FullName)" -ForegroundColor Green $updated++ ``` @@ -2359,7 +2359,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -2368,8 +2368,8 @@ Lines added: 1, lines removed: 1 @@ -112,5 +112,5 @@ } catch { -- Write-Host " ✗ Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red -+ Write-Host " [FAIL] Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red +- Write-Host " ✗ Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red ++ Write-Host " [FAIL] Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red } } ``` @@ -2382,7 +2382,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -2391,9 +2391,9 @@ Lines added: 1, lines removed: 1 @@ -119,5 +119,5 @@ } catch { -- Write-Host "✗ $($target.FullName)" -ForegroundColor Red -+ Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red - Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed +- Write-Host "✗ $($target.FullName)" -ForegroundColor Red ++ Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red + Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed $failed++ ``` @@ -2445,7 +2445,7 @@ Updating `Use-IsoDailyFormat` to handle string values: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 19, lines removed: 1 @@ -2453,7 +2453,7 @@ Lines added: 19, lines removed: 1 ```diff @@ -127,5 +127,23 @@ function Use-IsoDailyFormat { - $dailyFormat = Get-ConfigValue -Name 'dailyFormat' -Default $true + $dailyFormat = Get-ConfigValue -Name 'dailyFormat' -Default $true - return [bool]$dailyFormat + + # Handle backward compatibility with boolean values @@ -2465,7 +2465,7 @@ Lines added: 19, lines removed: 1 + if ($dailyFormat -is [string]) { + $formatStr = $dailyFormat.Trim() + # ISO format contains YYYY-MM-DD pattern -+ if ($formatStr -match 'YYYY-MM-DD' -or $formatStr -match '\d{4}-\d{2}-\d{2}') { ++ if ($formatStr -match 'YYYY-MM-DD' -or $formatStr -match '\d{4}-\d{2}-\d{2}') { + return $true + } + # daily_YYMMDD or other formats are not ISO @@ -2508,7 +2508,7 @@ Updating the `Use-7Zip` function: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 14, lines removed: 1 @@ -2516,7 +2516,7 @@ Lines added: 14, lines removed: 1 ```diff @@ -150,5 +150,18 @@ function Use-7Zip { - $zipper = Get-ConfigValue -Name 'zipper' -Default $true + $zipper = Get-ConfigValue -Name 'zipper' -Default $true - return [bool]$zipper + + # Handle backward compatibility with boolean values @@ -2527,7 +2527,7 @@ Lines added: 14, lines removed: 1 + # Handle string values + if ($zipper -is [string]) { + $zipperStr = $zipper.Trim().ToLower() -+ return ($zipperStr -eq '7z') ++ return ($zipperStr -eq '7z') + } + + # Default to false (use zip) for unknown types @@ -2627,7 +2627,7 @@ Fixing the variable reference by using `${targetDir}` to delimit the variable na -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -2636,8 +2636,8 @@ Lines added: 1, lines removed: 1 @@ -112,5 +112,5 @@ } catch { -- Write-Host " [FAIL] Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red -+ Write-Host " [FAIL] Failed to copy $($asset.Name) into ${targetDir}: $($_.Exception.Message)" -ForegroundColor Red +- Write-Host " [FAIL] Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red ++ Write-Host " [FAIL] Failed to copy $($asset.Name) into ${targetDir}: $($_.Exception.Message)" -ForegroundColor Red } } ``` @@ -2730,7 +2730,7 @@ The script isn't being executed correctly. Updating the call to explicitly run i -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -2738,7 +2738,7 @@ Lines added: 1, lines removed: 1 ```diff @@ -193,5 +193,5 @@ if (Test-Path -LiteralPath $updateScript) { - Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow + Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow - & $updateScript -ProjectPath $projectPath + & powershell -NoProfile -ExecutionPolicy Bypass -File $updateScript -ProjectPath $projectPath $exitCode = $LASTEXITCODE @@ -2837,7 +2837,7 @@ I'm focused on fixing the problem where the param block in UpdateProjectBatches. -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **edit_file** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 3, lines removed: 3 @@ -2847,14 +2847,14 @@ Lines added: 3, lines removed: 3 # Refresh helper batch scripts within a specific project directory. - - Set-StrictMode -Version Latest -- $ErrorActionPreference = 'Stop' +- $ErrorActionPreference = 'Stop' param( [string]$ProjectPath ) + + Set-StrictMode -Version Latest -+ $ErrorActionPreference = 'Stop' ++ $ErrorActionPreference = 'Stop' if (-not $PSScriptRoot) { ``` @@ -2943,7 +2943,7 @@ When `Where-Object` returns a single item, it's not an array, so `.Count` fails. -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 1, lines removed: 1 @@ -3151,7 +3151,7 @@ Fixing the list file handling: ensuring it's closed before 7z reads it and using -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 10, lines removed: 6 @@ -3160,19 +3160,19 @@ Lines added: 10, lines removed: 6 @@ -203,10 +203,14 @@ # Use a temporary list file to avoid Windows command line length limits -- with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: +- with tempfile.NamedTemporaryFile(mode="w", suffix=".lst", delete=False, encoding="utf-8") as list_file: - list_file_path = Path(list_file.name) - for rel_path in file_list: -- list_file.write(rel_path + "\n") +- list_file.write(rel_path + "\n") - - try: + list_file_path = None + try: + # Create list file with absolute path -+ list_file_path = Path(tempfile.mkstemp(suffix=".lst", text=True)[1]) -+ with open(list_file_path, "w", encoding="utf-8") as list_file: ++ list_file_path = Path(tempfile.mkstemp(suffix=".lst", text=True)[1]) ++ with open(list_file_path, "w", encoding="utf-8") as list_file: + for rel_path in file_list: -+ list_file.write(rel_path + "\n") ++ list_file.write(rel_path + "\n") + + # Use absolute path for list file in 7z command + list_file_abs = list_file_path.resolve() @@ -3185,10 +3185,10 @@ Lines added: 1, lines removed: 1 ```diff @@ -216,5 +220,5 @@ - "-tzip", + "-tzip", str(zip_path), -- f"@{list_file_path}", -+ f"@{list_file_abs}", +- f"@{list_file_path}", ++ f"@{list_file_abs}", ] result = subprocess.run( ``` @@ -3198,7 +3198,7 @@ Lines added: 2, lines removed: 1 ```diff @@ -229,5 +233,6 @@ - raise RuntimeError(f"7z compression failed: {result.stderr}") + raise RuntimeError(f"7z compression failed: {result.stderr}") finally: - list_file_path.unlink(missing_ok=True) + if list_file_path and list_file_path.exists(): @@ -3242,14 +3242,14 @@ Deleting any existing zip file before creating a new one, in case it's corrupted -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: undefined ```diff @@ -196,4 +196,7 @@ - raise RuntimeError("7z compression requested but 7z executable not found in PATH") + raise RuntimeError("7z compression requested but 7z executable not found in PATH") zip_path.parent.mkdir(parents=True, exist_ok=True) + # Remove existing zip file if it exists to avoid corruption issues + if zip_path.exists(): @@ -3421,14 +3421,14 @@ Adding a retry with a delay to handle the file lock, and catching the error if d -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 13, lines removed: 1 ```diff @@ -236,6 +236,18 @@ - raise RuntimeError(f"7z compression failed: {result.stderr}") + raise RuntimeError(f"7z compression failed: {result.stderr}") finally: + # Clean up list file, with retry in case 7z still has it open if list_file_path and list_file_path.exists(): @@ -3492,7 +3492,7 @@ Moving the `time` import to the top and ensuring the file is flushed: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined @@ -3514,7 +3514,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: undefined @@ -3522,7 +3522,7 @@ Lines added: 2, lines removed: undefined ```diff @@ -214,4 +214,6 @@ for rel_path in file_list: - list_file.write(rel_path + "\n") + list_file.write(rel_path + "\n") + list_file.flush() + os.fsync(list_file.fileno()) # Ensure data is written to disk @@ -3537,7 +3537,7 @@ Lines added: 2, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 @@ -3743,51 +3743,51 @@ Parameters: ```json { - "plan": "# Refactor Config Deployment to Avoid Self-Reference\n\n## Current Issues\n- ConfigLoader.ps1 deploys itself to each project (self-referential)\n- Config files are in Renders/ alongside batch files\n- Batch files depend on ConfigLoader.ps1 being deployed\n\n## New Structure\n- ConfigLoader.ps1 stays in ProjectStructure repo (for deployment only)\n- config.json deploys to `projectroot\\.config\\config.json`\n- Simple helper script (e.g., `GetStructDir.ps1`) in `.config\\` for batch files\n- Batch files read from `%PROJ_ROOT%\\.config\\config.json`\n\n## Changes Required\n\n### 1. Create Simple Helper Script\nCreate `GetStructDir.ps1` in ProjectStructure that:\n- Reads `config.json` from project root `.config` folder\n- Returns the `structDir` value\n- No other dependencies or functions\n\n### 2. Update UpdateProjectBatches.ps1\n- Remove `ConfigLoader.ps1` from `$sharedAssets` (line 59)\n- Change config.json deployment target to `projectroot\\.config\\config.json`\n- Deploy `GetStructDir.ps1` to `projectroot\\.config\\GetStructDir.ps1`\n- Update deployment logic to create `.config` directory if needed\n\n### 3. Update All Batch Files\nUpdate these files to use new structure:\n- `ZipSeqArchv.bat`\n- `UnzipSeqArchv.bat`\n- `UpdateSequences.bat`\n- `UpdateAllSequences.bat`\n- Any other batch files that use ConfigLoader\n\nChanges for each:\n- Remove ConfigLoader.ps1 checks\n- Point to `%PROJ_ROOT%\\.config\\config.json`\n- Use `GetStructDir.ps1` or direct JSON parsing to get structDir\n- Update error messages\n\n### 4. Files to Modify\n- `GetStructDir.ps1` (new file)\n- `UpdateProjectBatches.ps1`\n- `ZipSeqArchv.bat`\n- `UnzipSeqArchv.bat`\n- `UpdateSequences.bat`\n- `UpdateAllSequences.bat`", - "name": "Fix PowerShell Integration Issues", - "todos": [ + "plan": "# Refactor Config Deployment to Avoid Self-Reference\n\n## Current Issues\n- ConfigLoader.ps1 deploys itself to each project (self-referential)\n- Config files are in Renders/ alongside batch files\n- Batch files depend on ConfigLoader.ps1 being deployed\n\n## New Structure\n- ConfigLoader.ps1 stays in ProjectStructure repo (for deployment only)\n- config.json deploys to `projectroot\\.config\\config.json`\n- Simple helper script (e.g., `GetStructDir.ps1`) in `.config\\` for batch files\n- Batch files read from `%PROJ_ROOT%\\.config\\config.json`\n\n## Changes Required\n\n### 1. Create Simple Helper Script\nCreate `GetStructDir.ps1` in ProjectStructure that:\n- Reads `config.json` from project root `.config` folder\n- Returns the `structDir` value\n- No other dependencies or functions\n\n### 2. Update UpdateProjectBatches.ps1\n- Remove `ConfigLoader.ps1` from `$sharedAssets` (line 59)\n- Change config.json deployment target to `projectroot\\.config\\config.json`\n- Deploy `GetStructDir.ps1` to `projectroot\\.config\\GetStructDir.ps1`\n- Update deployment logic to create `.config` directory if needed\n\n### 3. Update All Batch Files\nUpdate these files to use new structure:\n- `ZipSeqArchv.bat`\n- `UnzipSeqArchv.bat`\n- `UpdateSequences.bat`\n- `UpdateAllSequences.bat`\n- Any other batch files that use ConfigLoader\n\nChanges for each:\n- Remove ConfigLoader.ps1 checks\n- Point to `%PROJ_ROOT%\\.config\\config.json`\n- Use `GetStructDir.ps1` or direct JSON parsing to get structDir\n- Update error messages\n\n### 4. Files to Modify\n- `GetStructDir.ps1` (new file)\n- `UpdateProjectBatches.ps1`\n- `ZipSeqArchv.bat`\n- `UnzipSeqArchv.bat`\n- `UpdateSequences.bat`\n- `UpdateAllSequences.bat`", + "name": "Fix PowerShell Integration Issues", + "todos": [ { - "content": "Create GetStructDir.ps1 helper script that reads config.json from .config folder and returns structDir", - "status": "pending", - "id": "create-helper-script" + "content": "Create GetStructDir.ps1 helper script that reads config.json from .config folder and returns structDir", + "status": "pending", + "id": "create-helper-script" }, { - "content": "Update UpdateProjectBatches.ps1 to deploy config.json and GetStructDir.ps1 to projectroot\\.config\\ instead of deploying ConfigLoader.ps1", - "status": "pending", - "id": "update-deployment-script", - "dependencies": [ - "create-helper-script" + "content": "Update UpdateProjectBatches.ps1 to deploy config.json and GetStructDir.ps1 to projectroot\\.config\\ instead of deploying ConfigLoader.ps1", + "status": "pending", + "id": "update-deployment-script", + "dependencies": [ + "create-helper-script" ] }, { - "content": "Update ZipSeqArchv.bat and UnzipSeqArchv.bat to use .config\\config.json and GetStructDir.ps1", - "status": "pending", - "id": "update-zip-batches", - "dependencies": [ - "create-helper-script" + "content": "Update ZipSeqArchv.bat and UnzipSeqArchv.bat to use .config\\config.json and GetStructDir.ps1", + "status": "pending", + "id": "update-zip-batches", + "dependencies": [ + "create-helper-script" ] }, { - "content": "Update UpdateSequences.bat and UpdateAllSequences.bat to use .config\\config.json and GetStructDir.ps1", - "status": "pending", - "id": "update-sequence-batches", - "dependencies": [ - "create-helper-script" + "content": "Update UpdateSequences.bat and UpdateAllSequences.bat to use .config\\config.json and GetStructDir.ps1", + "status": "pending", + "id": "update-sequence-batches", + "dependencies": [ + "create-helper-script" ] } ], - "overview": "Remove self-referential ConfigLoader deployment. Deploy config.json to projectroot\\.config\\ instead of Renders\\. Create a simple helper script with a different name for batch files to read the config. Update all batch files to use the new structure." + "overview": "Remove self-referential ConfigLoader deployment. Deploy config.json to projectroot\\.config\\ instead of Renders\\. Create a simple helper script with a different name for batch files to read the config. Update all batch files to use the new structure." } ``` Additional data: ```json { - "status": "accepted", - "reviewData": { - "status": "Done", - "selectedOption": "edit", - "isShowingInput": false + "status": "accepted", + "reviewData": { + "status": "Done", + "selectedOption": "edit", + "isShowingInput": false } } ``` @@ -3795,42 +3795,42 @@ Result: ```json { - "accepted": { - "finalTodos": [ + "accepted": { + "finalTodos": [ { - "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", - "status": "completed", - "id": "fix-unicode-chars" + "content": "Replace Unicode checkmark (✓) and cross (✗) characters in UpdateProjectBatches.ps1 with ASCII-safe alternatives", + "status": "completed", + "id": "fix-unicode-chars" }, { - "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", - "status": "completed", - "id": "update-daily-format" + "content": "Update Use-IsoDailyFormat function in ConfigLoader.ps1 to handle string values like daily_YYMMDD or YYYY-MM-DD", + "status": "completed", + "id": "update-daily-format" }, { - "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", - "status": "completed", - "id": "update-7zip-function" + "content": "Update Use-7Zip function in ConfigLoader.ps1 to handle string values like 7z or zip", + "status": "completed", + "id": "update-7zip-function" }, { - "content": "Create GetStructDir.ps1 helper script that reads config.json from .config folder and returns structDir", - "status": "pending", - "id": "todo-1762797185974-d69psh338" + "content": "Create GetStructDir.ps1 helper script that reads config.json from .config folder and returns structDir", + "status": "pending", + "id": "todo-1762797185974-d69psh338" }, { - "content": "Update UpdateProjectBatches.ps1 to deploy config.json and GetStructDir.ps1 to projectroot\\.config\\ instead of deploying ConfigLoader.ps1", - "status": "pending", - "id": "todo-1762797185974-xjceev24l" + "content": "Update UpdateProjectBatches.ps1 to deploy config.json and GetStructDir.ps1 to projectroot\\.config\\ instead of deploying ConfigLoader.ps1", + "status": "pending", + "id": "todo-1762797185974-xjceev24l" }, { - "content": "Update ZipSeqArchv.bat and UnzipSeqArchv.bat to use .config\\config.json and GetStructDir.ps1", - "status": "pending", - "id": "todo-1762797185974-qf5021121" + "content": "Update ZipSeqArchv.bat and UnzipSeqArchv.bat to use .config\\config.json and GetStructDir.ps1", + "status": "pending", + "id": "todo-1762797185974-qf5021121" }, { - "content": "Update UpdateSequences.bat and UpdateAllSequences.bat to use .config\\config.json and GetStructDir.ps1", - "status": "pending", - "id": "todo-1762797185974-27cbuii9j" + "content": "Update UpdateSequences.bat and UpdateAllSequences.bat to use .config\\config.json and GetStructDir.ps1", + "status": "pending", + "id": "todo-1762797185974-27cbuii9j" } ] } @@ -3883,7 +3883,7 @@ Starting with the first task: creating the GetStructDir.ps1 helper script. -
Tool use: **code_edit** • Edit file: GetStructDir.ps1 +
Tool use: **write** • Edit file: GetStructDir.ps1 **Chunk 1** Lines added: 68, lines removed: 1 @@ -3899,7 +3899,7 @@ Lines added: 68, lines removed: 1 + ) + + Set-StrictMode -Version Latest -+ $ErrorActionPreference = 'Stop' ++ $ErrorActionPreference = 'Stop' + + if ([string]::IsNullOrWhiteSpace($ProjectRoot)) { + # Try to determine project root from script location @@ -3910,25 +3910,25 @@ Lines added: 68, lines removed: 1 + $ProjectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) + } + else { -+ Write-Error "Unable to determine project root. Please provide -ProjectRoot parameter." ++ Write-Error "Unable to determine project root. Please provide -ProjectRoot parameter." + exit 1 + } + } + -+ $configPath = Join-Path -Path $ProjectRoot -ChildPath '.config\config.json' ++ $configPath = Join-Path -Path $ProjectRoot -ChildPath '.config\config.json' + + if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { -+ Write-Error "config.json not found at: $configPath" ++ Write-Error "config.json not found at: $configPath" + exit 1 + } + + try { + $config = Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json + -+ if ($config.PSObject.Properties.Name -contains 'structDir') { ++ if ($config.PSObject.Properties.Name -contains 'structDir') { + $structDir = $config.structDir + if ($null -ne $structDir -and ($structDir -isnot [string] -or $structDir.Trim().Length -gt 0)) { -+ # If it's an absolute path, resolve it ++ # If it's an absolute path, resolve it + if ([System.IO.Path]::IsPathRooted($structDir)) { + $resolved = Resolve-Path -LiteralPath $structDir -ErrorAction SilentlyContinue + if ($null -ne $resolved) { @@ -3955,7 +3955,7 @@ Lines added: 68, lines removed: 1 + exit 0 + } + catch { -+ Write-Error "Failed to read or parse config.json: $($_.Exception.Message)" ++ Write-Error "Failed to read or parse config.json: $($_.Exception.Message)" + exit 1 + } + @@ -4009,7 +4009,7 @@ Updating UpdateProjectBatches.ps1: -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 4, lines removed: 3 @@ -4019,12 +4019,12 @@ Lines added: 4, lines removed: 3 ) - $sharedAssets = @( -- @{ Name = 'ConfigLoader.ps1'; Source = Join-Path -Path $structDir -ChildPath 'ConfigLoader.ps1' }, -- @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' } +- @{ Name = 'ConfigLoader.ps1'; Source = Join-Path -Path $structDir -ChildPath 'ConfigLoader.ps1' }, +- @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' } + # Config files to deploy to projectroot\.config\ + $configAssets = @( -+ @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' }, -+ @{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' } ++ @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' }, ++ @{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' } ) ``` @@ -4039,17 +4039,17 @@ Lines added: 9, lines removed: 2 - foreach ($asset in $sharedAssets) { + foreach ($asset in $configAssets) { if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) { -- Write-Error "Shared asset not found: $($asset.Source)" -+ Write-Error "Config asset not found: $($asset.Source)" +- Write-Error "Shared asset not found: $($asset.Source)" ++ Write-Error "Config asset not found: $($asset.Source)" exit 1 } } + + # Ensure .config directory exists in project root -+ $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' ++ $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' + if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) { + New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null -+ Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan ++ Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan + } $touchedDirs = @{} @@ -4069,7 +4069,7 @@ Lines added: 9, lines removed: 2 -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: undefined, lines removed: 8 @@ -4083,7 +4083,7 @@ Lines added: undefined, lines removed: 8 - Copy-Item -Path $asset.Source -Destination (Join-Path -Path $targetDir -ChildPath $asset.Name) -Force - } - catch { -- Write-Host " [FAIL] Failed to copy $($asset.Name) into ${targetDir}: $($_.Exception.Message)" -ForegroundColor Red +- Write-Host " [FAIL] Failed to copy $($asset.Name) into ${targetDir}: $($_.Exception.Message)" -ForegroundColor Red - } - } $touchedDirs[$targetDir] = $true @@ -4116,7 +4116,7 @@ Adding deployment of config assets to the .config directory: -
Tool use: **code_edit** • Edit file: UpdateProjectBatches.ps1 +
Tool use: **search_replace** • Edit file: UpdateProjectBatches.ps1 **Chunk 1** Lines added: 17, lines removed: undefined @@ -4124,24 +4124,24 @@ Lines added: 17, lines removed: undefined ```diff @@ -145,4 +145,21 @@ - Write-Host "All batch files refreshed successfully." -ForegroundColor Green + Write-Host "All batch files refreshed successfully." -ForegroundColor Green + + # Deploy config files to projectroot\.config\ -+ Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta ++ Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta + foreach ($asset in $configAssets) { + $targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name + try { + Copy-Item -Path $asset.Source -Destination $targetPath -Force -+ Write-Host "[OK] $targetPath" -ForegroundColor Green ++ Write-Host "[OK] $targetPath" -ForegroundColor Green + } + catch { -+ Write-Host "[FAIL] $targetPath" -ForegroundColor Red -+ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed ++ Write-Host "[FAIL] $targetPath" -ForegroundColor Red ++ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed + exit 1 + } + } + -+ Write-Host "Config files deployed successfully." -ForegroundColor Green ++ Write-Host "Config files deployed successfully." -ForegroundColor Green ``` @@ -4183,40 +4183,40 @@ Updating ZipSeqArchv.bat and UnzipSeqArchv.bat: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 8, lines removed: 7 ```diff @@ -5,21 +5,22 @@ - for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" + for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" -- set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" -- set "CONFIG_PATH=%REN_DIR%config.json" -+ set "CONFIG_DIR=%PROJ_ROOT%\.config" -+ set "CONFIG_PATH=%CONFIG_DIR%\config.json" -+ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" +- set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" +- set "CONFIG_PATH=%REN_DIR%config.json" ++ set "CONFIG_DIR=%PROJ_ROOT%\.config" ++ set "CONFIG_PATH=%CONFIG_DIR%\config.json" ++ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" -- if not exist "%CONFIG_LOADER%" ( +- if not exist "%CONFIG_LOADER%" ( - echo [ERROR] ConfigLoader.ps1 not found next to ZipSeqArchv.bat. -+ if not exist "%CONFIG_PATH%" ( ++ if not exist "%CONFIG_PATH%" ( + echo [ERROR] config.json not found at %CONFIG_PATH% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) -- if not exist "%CONFIG_PATH%" ( +- if not exist "%CONFIG_PATH%" ( - echo [ERROR] config.json not found next to ZipSeqArchv.bat. -+ if not exist "%GET_STRUCT_DIR%" ( ++ if not exist "%GET_STRUCT_DIR%" ( + echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) - for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ -- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" -+ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" + for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ +- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" ++ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" if not defined PY_SCRIPT ( ``` @@ -4229,40 +4229,40 @@ Lines added: 8, lines removed: 7 -
Tool use: **code_edit** • Edit file: UnzipSeqArchv.bat +
Tool use: **search_replace** • Edit file: UnzipSeqArchv.bat **Chunk 1** Lines added: 8, lines removed: 7 ```diff @@ -5,21 +5,22 @@ - for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" + for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" -- set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" -- set "CONFIG_PATH=%REN_DIR%config.json" -+ set "CONFIG_DIR=%PROJ_ROOT%\.config" -+ set "CONFIG_PATH=%CONFIG_DIR%\config.json" -+ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" +- set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" +- set "CONFIG_PATH=%REN_DIR%config.json" ++ set "CONFIG_DIR=%PROJ_ROOT%\.config" ++ set "CONFIG_PATH=%CONFIG_DIR%\config.json" ++ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" -- if not exist "%CONFIG_LOADER%" ( +- if not exist "%CONFIG_LOADER%" ( - echo [ERROR] ConfigLoader.ps1 not found next to UnzipSeqArchv.bat. -+ if not exist "%CONFIG_PATH%" ( ++ if not exist "%CONFIG_PATH%" ( + echo [ERROR] config.json not found at %CONFIG_PATH% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) -- if not exist "%CONFIG_PATH%" ( +- if not exist "%CONFIG_PATH%" ( - echo [ERROR] config.json not found next to UnzipSeqArchv.bat. -+ if not exist "%GET_STRUCT_DIR%" ( ++ if not exist "%GET_STRUCT_DIR%" ( + echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) - for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ -- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" -+ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" + for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ +- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" ++ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" if not defined PY_SCRIPT ( ``` @@ -4331,7 +4331,7 @@ Updating UpdateSequences.bat. UpdateAllSequences.bat doesn't use ConfigLoader, s -
Tool use: **code_edit** • Edit file: UpdateSequences.bat +
Tool use: **search_replace** • Edit file: UpdateSequences.bat **Chunk 1** Lines added: 10, lines removed: 7 @@ -4339,34 +4339,34 @@ Lines added: 10, lines removed: 7 ```diff @@ -3,21 +3,24 @@ - set "script_dir=%~dp0" -- set "config_loader=%script_dir%ConfigLoader.ps1" -- set "config_path=%script_dir%config.json" -+ for %%I in ("%script_dir%..") do set "PROJ_ROOT=%%~fI" + set "script_dir=%~dp0" +- set "config_loader=%script_dir%ConfigLoader.ps1" +- set "config_path=%script_dir%config.json" ++ for %%I in ("%script_dir%..") do set "PROJ_ROOT=%%~fI" + -+ set "CONFIG_DIR=%PROJ_ROOT%\.config" -+ set "CONFIG_PATH=%CONFIG_DIR%\config.json" -+ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" ++ set "CONFIG_DIR=%PROJ_ROOT%\.config" ++ set "CONFIG_PATH=%CONFIG_DIR%\config.json" ++ set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1" -- if not exist "%config_loader%" ( +- if not exist "%config_loader%" ( - echo [ERROR] ConfigLoader.ps1 not found next to UpdateSequences.bat. -+ if not exist "%CONFIG_PATH%" ( ++ if not exist "%CONFIG_PATH%" ( + echo [ERROR] config.json not found at %CONFIG_PATH% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) -- if not exist "%config_path%" ( +- if not exist "%config_path%" ( - echo [ERROR] config.json not found next to UpdateSequences.bat. -+ if not exist "%GET_STRUCT_DIR%" ( ++ if not exist "%GET_STRUCT_DIR%" ( + echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% echo Please run UpdateProjectBatches.ps1 to refresh helper files. exit /b 1 ) - for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ -- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%config_loader%' -ErrorAction Stop; . $loader.Path; $ps1Path = Join-Path (Get-StructDirectory) 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I" -+ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $ps1Path = Join-Path $structDir 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I" + for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ +- "Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%config_loader%' -ErrorAction Stop; . $loader.Path; $ps1Path = Join-Path (Get-StructDirectory) 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I" ++ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $ps1Path = Join-Path $structDir 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I" if not defined ps1 ( ``` @@ -4490,7 +4490,7 @@ Merging UpdateProjectBatches.ps1 into ConfigLoader.ps1: -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 146, lines removed: 28 @@ -4499,30 +4499,30 @@ Lines added: 146, lines removed: 28 @@ -187,30 +187,148 @@ $projectPath = Get-ProjectPathFromUser if ($null -ne $projectPath) { -- Write-Host "`nDeploying to: $projectPath" -ForegroundColor Cyan +- Write-Host "`nDeploying to: $projectPath" -ForegroundColor Cyan - - # Find UpdateProjectBatches.ps1 in the same directory as ConfigLoader.ps1 -- $updateScript = Join-Path -Path $script:LoaderRoot -ChildPath 'UpdateProjectBatches.ps1' +- $updateScript = Join-Path -Path $script:LoaderRoot -ChildPath 'UpdateProjectBatches.ps1' - if (Test-Path -LiteralPath $updateScript) { -- Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow +- Write-Host "Running UpdateProjectBatches.ps1...`n" -ForegroundColor Yellow - & powershell -NoProfile -ExecutionPolicy Bypass -File $updateScript -ProjectPath $projectPath - $exitCode = $LASTEXITCODE - if ($exitCode -eq 0) { -- Write-Host "`nDeployment completed successfully." -ForegroundColor Green +- Write-Host "`nDeployment completed successfully." -ForegroundColor Green - } - else { -- Write-Host "`nDeployment completed with errors (exit code: $exitCode)." -ForegroundColor Red +- Write-Host "`nDeployment completed with errors (exit code: $exitCode)." -ForegroundColor Red - } - exit $exitCode - } - else { -- Write-Warning "UpdateProjectBatches.ps1 not found in $script:LoaderRoot" -- Write-Host "Project path: $projectPath" -ForegroundColor Green +- Write-Warning "UpdateProjectBatches.ps1 not found in $script:LoaderRoot" +- Write-Host "Project path: $projectPath" -ForegroundColor Green - return $projectPath - } - } - else { -- Write-Host "No project path provided." -ForegroundColor Yellow +- Write-Host "No project path provided." -ForegroundColor Yellow - exit 1 - } - } @@ -4530,7 +4530,7 @@ Lines added: 146, lines removed: 28 + # Deploy batch files and config to the project + $structDir = Get-StructDirectory + if (-not (Test-Path -LiteralPath $structDir -PathType Container)) { -+ Write-Error "Configured structDir not found: $structDir" ++ Write-Error "Configured structDir not found: $structDir" + exit 1 + } + @@ -4538,64 +4538,64 @@ Lines added: 146, lines removed: 28 + $resolvedProject = (Resolve-Path -LiteralPath $projectPath -ErrorAction Stop).Path + } + catch { -+ Write-Error "Unable to resolve project directory: $($_.Exception.Message)" ++ Write-Error "Unable to resolve project directory: $($_.Exception.Message)" + exit 1 + } + + if (-not (Test-Path -LiteralPath $resolvedProject -PathType Container)) { -+ Write-Error "Project path is not a directory: $resolvedProject" ++ Write-Error "Project path is not a directory: $resolvedProject" + exit 1 + } + -+ Write-Host "`nDeploying to: $resolvedProject" -ForegroundColor Cyan -+ Write-Host "Struct directory: $structDir" -ForegroundColor Cyan ++ Write-Host "`nDeploying to: $resolvedProject" -ForegroundColor Cyan ++ Write-Host "Struct directory: $structDir" -ForegroundColor Cyan + + $specs = @( -+ @{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' }, -+ @{ Name = 'UpdateAllSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateAllSequences.bat' }, -+ @{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' }, -+ @{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' }, -+ @{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' } ++ @{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' }, ++ @{ Name = 'UpdateAllSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateAllSequences.bat' }, ++ @{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' }, ++ @{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' }, ++ @{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' } + ) + + # Config files to deploy to projectroot\.config\ + $configAssets = @( -+ @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' }, -+ @{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' } ++ @{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' }, ++ @{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' } + ) + + foreach ($spec in $specs) { + if (-not (Test-Path -LiteralPath $spec.Source -PathType Leaf)) { -+ Write-Error "Source file not found: $($spec.Source)" ++ Write-Error "Source file not found: $($spec.Source)" + exit 1 + } + } + + foreach ($asset in $configAssets) { + if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) { -+ Write-Error "Config asset not found: $($asset.Source)" ++ Write-Error "Config asset not found: $($asset.Source)" + exit 1 + } + } + + # Ensure .config directory exists in project root -+ $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' ++ $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' + if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) { + New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null -+ Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan ++ Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan + } + + $touchedDirs = @{} + $summary = @() + + foreach ($spec in $specs) { -+ Write-Host "`n=== Updating $($spec.Name) ===" -ForegroundColor Magenta ++ Write-Host "`n=== Updating $($spec.Name) ===" -ForegroundColor Magenta + + $targets = Get-ChildItem -LiteralPath $resolvedProject -Recurse -Filter $spec.Name -File -ErrorAction SilentlyContinue + $targets = $targets | Where-Object { $_.FullName -ne $spec.Source } + + if (-not $targets) { -+ Write-Host "No targets found." -ForegroundColor Yellow ++ Write-Host "No targets found." -ForegroundColor Yellow + $summary += [pscustomobject]@{ + Name = $spec.Name + Updated = 0 @@ -4612,7 +4612,7 @@ Lines added: 146, lines removed: 28 + foreach ($target in $targets) { + try { + Copy-Item -Path $spec.Source -Destination $target.FullName -Force -+ Write-Host "[OK] $($target.FullName)" -ForegroundColor Green ++ Write-Host "[OK] $($target.FullName)" -ForegroundColor Green + $updated++ + + $targetDir = $target.Directory.FullName @@ -4621,8 +4621,8 @@ Lines added: 146, lines removed: 28 + } + } + catch { -+ Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red -+ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed ++ Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red ++ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed + $failed++ + } + } @@ -4636,39 +4636,39 @@ Lines added: 146, lines removed: 28 + } + } + -+ Write-Host "`n=== Summary ===" -ForegroundColor Cyan ++ Write-Host "`n=== Summary ===" -ForegroundColor Cyan + foreach ($item in $summary) { -+ Write-Host ("{0,-22} Updated: {1,3} Failed: {2,3} Total: {3,3}" -f $item.Name, $item.Updated, $item.Failed, $item.Total) ++ Write-Host ("{0,-22} Updated: {1,3} Failed: {2,3} Total: {3,3}" -f $item.Name, $item.Updated, $item.Failed, $item.Total) + } + + if (($summary | Measure-Object -Property Failed -Sum).Sum -gt 0) { -+ Write-Host "Completed with errors." -ForegroundColor Yellow ++ Write-Host "Completed with errors." -ForegroundColor Yellow + exit 1 + } + -+ Write-Host "All batch files refreshed successfully." -ForegroundColor Green ++ Write-Host "All batch files refreshed successfully." -ForegroundColor Green + + # Deploy config files to projectroot\.config\ -+ Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta ++ Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta + foreach ($asset in $configAssets) { + $targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name + try { + Copy-Item -Path $asset.Source -Destination $targetPath -Force -+ Write-Host "[OK] $targetPath" -ForegroundColor Green ++ Write-Host "[OK] $targetPath" -ForegroundColor Green + } + catch { -+ Write-Host "[FAIL] $targetPath" -ForegroundColor Red -+ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed ++ Write-Host "[FAIL] $targetPath" -ForegroundColor Red ++ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed + exit 1 + } + } + -+ Write-Host "`nConfig files deployed successfully." -ForegroundColor Green -+ Write-Host "Deployment completed successfully." -ForegroundColor Green ++ Write-Host "`nConfig files deployed successfully." -ForegroundColor Green ++ Write-Host "Deployment completed successfully." -ForegroundColor Green + exit 0 + } + else { -+ Write-Host "No project path provided." -ForegroundColor Yellow ++ Write-Host "No project path provided." -ForegroundColor Yellow + exit 1 + } + } @@ -4852,7 +4852,7 @@ Fixing the 7z file locking issue. 7z may be reading the list file before it's fu -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 17, lines removed: 6 @@ -4861,18 +4861,18 @@ Lines added: 17, lines removed: 6 @@ -210,10 +210,21 @@ try: # Create list file with absolute path -- list_file_path = Path(tempfile.mkstemp(suffix=".lst", text=True)[1]) -- with open(list_file_path, "w", encoding="utf-8") as list_file: +- list_file_path = Path(tempfile.mkstemp(suffix=".lst", text=True)[1]) +- with open(list_file_path, "w", encoding="utf-8") as list_file: - for rel_path in file_list: -- list_file.write(rel_path + "\n") +- list_file.write(rel_path + "\n") - list_file.flush() - os.fsync(list_file.fileno()) # Ensure data is written to disk -+ fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) ++ fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) + list_file_path = Path(temp_path) + try: -+ with os.fdopen(fd, "w", encoding="utf-8") as list_file: ++ with os.fdopen(fd, "w", encoding="utf-8") as list_file: + for rel_path in file_list: -+ list_file.write(rel_path + "\n") ++ list_file.write(rel_path + "\n") + list_file.flush() + os.fsync(fd) # Ensure data is written to disk + finally: @@ -4923,14 +4923,14 @@ Fixing the file handle management: `os.fdopen` already closes the file descripto -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 9 ```diff @@ -217,13 +217,6 @@ - list_file.write(rel_path + "\n") + list_file.write(rel_path + "\n") list_file.flush() - os.fsync(fd) # Ensure data is written to disk - finally: @@ -5020,7 +5020,7 @@ Updating ConfigLoader.ps1 to set the `.config` folder as hidden when it's create -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 4, lines removed: 1 @@ -5031,10 +5031,10 @@ Lines added: 4, lines removed: 1 - # Ensure .config directory exists in project root + # Ensure .config directory exists in project root and is hidden - $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' + $projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config' if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) { New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null - Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan + Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan } + # Set hidden attribute on .config directory + $folder = Get-Item -LiteralPath $projectConfigDir -Force @@ -5136,24 +5136,24 @@ Removing the unnecessary nested try block: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 5, lines removed: 6 ```diff @@ -212,10 +212,9 @@ - fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) + fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) list_file_path = Path(temp_path) - try: -- with os.fdopen(fd, "w", encoding="utf-8") as list_file: +- with os.fdopen(fd, "w", encoding="utf-8") as list_file: - for rel_path in file_list: -- list_file.write(rel_path + "\n") +- list_file.write(rel_path + "\n") - list_file.flush() - os.fsync(list_file.fileno()) # Ensure data is written to disk -+ with os.fdopen(fd, "w", encoding="utf-8") as list_file: ++ with os.fdopen(fd, "w", encoding="utf-8") as list_file: + for rel_path in file_list: -+ list_file.write(rel_path + "\n") ++ list_file.write(rel_path + "\n") + list_file.flush() + os.fsync(list_file.fileno()) # Ensure data is written to disk # File is closed here by context manager, small delay to ensure OS releases handle @@ -5322,7 +5322,7 @@ Improving error handling and ensuring the zip file is deleted before 7z runs. Ad -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: 1 @@ -5350,7 +5350,7 @@ Lines added: 3, lines removed: undefined file_list.append(rel_path) + if not file_list: -+ raise RuntimeError(f"No files found to archive in {seq_dir}") ++ raise RuntimeError(f"No files found to archive in {seq_dir}") + # Use a temporary list file to avoid Windows command line length limits list_file_path = None @@ -5371,12 +5371,12 @@ Lines added: 4, lines removed: 1 + # Use -aoa to overwrite all files, -bb0 to suppress progress output cmd = [ SEVEN_Z_EXE, - "a", - "-y", -+ "-aoa", # Overwrite all existing files -+ "-bb0", # Suppress progress output - f"-mx={COMPRESSION_LEVEL}", - "-tzip", + "a", + "-y", ++ "-aoa", # Overwrite all existing files ++ "-bb0", # Suppress progress output + f"-mx={COMPRESSION_LEVEL}", + "-tzip", ``` **Chunk 4** @@ -5386,11 +5386,11 @@ Lines added: 4, lines removed: 1 @@ -240,5 +251,8 @@ ) if result.returncode != 0: -- raise RuntimeError(f"7z compression failed: {result.stderr}") -+ error_msg = result.stderr.strip() if result.stderr else "Unknown error" +- raise RuntimeError(f"7z compression failed: {result.stderr}") ++ error_msg = result.stderr.strip() if result.stderr else "Unknown error" + if result.stdout: -+ error_msg += f"\nstdout: {result.stdout.strip()}" -+ raise RuntimeError(f"7z compression failed: {error_msg}") ++ error_msg += f"\nstdout: {result.stdout.strip()}" ++ raise RuntimeError(f"7z compression failed: {error_msg}") finally: # Clean up list file, with retry in case 7z still has it open ``` @@ -5618,14 +5618,14 @@ Creating the zip in a temporary location first, then moving it to avoid opening -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 7 ```diff @@ -197,11 +197,4 @@ - raise RuntimeError("7z compression requested but 7z executable not found in PATH") + raise RuntimeError("7z compression requested but 7z executable not found in PATH") zip_path.parent.mkdir(parents=True, exist_ok=True) - # Remove existing zip file if it exists to avoid corruption issues - if zip_path.exists(): @@ -5643,7 +5643,7 @@ Lines added: 7, lines removed: 1 ```diff @@ -214,7 +207,13 @@ - raise RuntimeError(f"No files found to archive in {seq_dir}") + raise RuntimeError(f"No files found to archive in {seq_dir}") - # Use a temporary list file to avoid Windows command line length limits + # Create zip in temporary location first to avoid issues with corrupted existing files @@ -5651,12 +5651,12 @@ Lines added: 7, lines removed: 1 list_file_path = None try: + # Create temporary zip file -+ fd_zip, temp_zip_path = tempfile.mkstemp(suffix=".zip", dir=zip_path.parent) -+ os.close(fd_zip) # Close the file handle, we'll let 7z create it ++ fd_zip, temp_zip_path = tempfile.mkstemp(suffix=".zip", dir=zip_path.parent) ++ os.close(fd_zip) # Close the file handle, we'll let 7z create it + temp_zip = Path(temp_zip_path) + # Create list file with absolute path - fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) + fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True) ``` **Chunk 3** @@ -5670,15 +5670,15 @@ Lines added: 2, lines removed: 3 + # Create archive in temp location first cmd = [ SEVEN_Z_EXE, - "a", - "-y", -- "-aoa", # Overwrite all existing files - "-bb0", # Suppress progress output - f"-mx={COMPRESSION_LEVEL}", - "-tzip", + "a", + "-y", +- "-aoa", # Overwrite all existing files + "-bb0", # Suppress progress output + f"-mx={COMPRESSION_LEVEL}", + "-tzip", - str(zip_path), + str(temp_zip), - f"@{list_file_abs}", + f"@{list_file_abs}", ] ``` @@ -5687,14 +5687,14 @@ Lines added: 6, lines removed: undefined ```diff @@ -255,4 +253,10 @@ - error_msg += f"\nstdout: {result.stdout.strip()}" - raise RuntimeError(f"7z compression failed: {error_msg}") + error_msg += f"\nstdout: {result.stdout.strip()}" + raise RuntimeError(f"7z compression failed: {error_msg}") + + # Move temp zip to final location, replacing any existing file + if zip_path.exists(): + zip_path.unlink() + temp_zip.replace(zip_path) -+ temp_zip = None # Mark as moved so we don't delete it ++ temp_zip = None # Mark as moved so we don't delete it finally: # Clean up list file, with retry in case 7z still has it open ``` @@ -5707,16 +5707,16 @@ Lines added: 6, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined ```diff @@ -260,4 +260,10 @@ - temp_zip = None # Mark as moved so we don't delete it + temp_zip = None # Mark as moved so we don't delete it finally: -+ # Clean up temp zip if it wasn't moved ++ # Clean up temp zip if it wasn't moved + if temp_zip and temp_zip.exists(): + try: + temp_zip.unlink(missing_ok=True) @@ -5804,7 +5804,7 @@ The temp zip created by `mkstemp` is empty, and 7z treats it as corrupted when t -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 3 @@ -5814,10 +5814,10 @@ Lines added: 2, lines removed: 3 list_file_path = None try: - # Create temporary zip file -- fd_zip, temp_zip_path = tempfile.mkstemp(suffix=".zip", dir=zip_path.parent) -- os.close(fd_zip) # Close the file handle, we'll let 7z create it -+ # Create temporary zip file path (but don't create the file - let 7z create it) -+ temp_zip_path = tempfile.mktemp(suffix=".zip", dir=zip_path.parent) +- fd_zip, temp_zip_path = tempfile.mkstemp(suffix=".zip", dir=zip_path.parent) +- os.close(fd_zip) # Close the file handle, we'll let 7z create it ++ # Create temporary zip file path (but don't create the file - let 7z create it) ++ temp_zip_path = tempfile.mktemp(suffix=".zip", dir=zip_path.parent) temp_zip = Path(temp_zip_path) ``` @@ -5857,7 +5857,7 @@ Using `mktemp` instead of `mkstemp` so 7z creates a fresh zip. Ensuring we use a -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: 2 @@ -5881,11 +5881,11 @@ Lines added: 1, lines removed: 1 ```diff @@ -236,5 +237,5 @@ - f"-mx={COMPRESSION_LEVEL}", - "-tzip", + f"-mx={COMPRESSION_LEVEL}", + "-tzip", - str(temp_zip), + str(temp_zip_abs), - f"@{list_file_abs}", + f"@{list_file_abs}", ] ``` @@ -6060,7 +6060,7 @@ The script reads `config.json` from the ProjectStructure directory instead of th -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 @@ -6068,10 +6068,10 @@ Lines added: undefined, lines removed: 1 ```diff @@ -34,5 +34,4 @@ } - STATE_SUFFIX = ".meta.json" -- CONFIG_PATH = Path(__file__).resolve().with_name("config.json") + STATE_SUFFIX = ".meta.json" +- CONFIG_PATH = Path(__file__).resolve().with_name("config.json") DEFAULT_CONFIG = { - "zipper": "7z", + "zipper": "7z", ``` **Chunk 2** @@ -6082,7 +6082,7 @@ Lines added: 24, lines removed: 18 def load_config() -> dict: - try: -- text = CONFIG_PATH.read_text(encoding="utf-8") +- text = CONFIG_PATH.read_text(encoding="utf-8") - except FileNotFoundError: - return DEFAULT_CONFIG.copy() - except OSError: @@ -6099,17 +6099,17 @@ Lines added: 24, lines removed: 18 - merged = DEFAULT_CONFIG.copy() - merged.update(data) - return merged -+ # First try to load from project's .config folder (current working directory) ++ # First try to load from project's .config folder (current working directory) + # Then fall back to ProjectStructure repo config (next to zip_sequences.py) -+ project_config = Path(".config/config.json") -+ repo_config = Path(__file__).resolve().with_name("config.json") ++ project_config = Path(".config/config.json") ++ repo_config = Path(__file__).resolve().with_name("config.json") + + config_paths = [project_config, repo_config] + + for config_path in config_paths: + try: + if config_path.exists(): -+ text = config_path.read_text(encoding="utf-8") ++ text = config_path.read_text(encoding="utf-8") + try: + data = json.loads(text) + if isinstance(data, dict): @@ -6335,18 +6335,18 @@ Adding debug output to see what's happening with the config and 7z detection: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 ```diff @@ -89,5 +89,6 @@ - SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") + SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") if SEVEN_Z_EXE is None: -- print("[zip] Warning: 7z compression requested but no 7z executable was found in PATH.", file=sys.stderr) -+ print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) -+ print(f"[zip] Config zipper value: {zipper_val!r}, resolved ZIPPER_TYPE: {ZIPPER_TYPE!r}", file=sys.stderr) +- print("[zip] Warning: 7z compression requested but no 7z executable was found in PATH.", file=sys.stderr) ++ print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) ++ print(f"[zip] Config zipper value: {zipper_val!r}, resolved ZIPPER_TYPE: {ZIPPER_TYPE!r}", file=sys.stderr) ``` @@ -6399,7 +6399,7 @@ If `ZIPPER_TYPE != "7z"`, it falls through to zipfile. Adding debug output to ve -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 4, lines removed: undefined @@ -6410,9 +6410,9 @@ Lines added: 4, lines removed: undefined ZIPPER_TYPE = str(zipper_val).lower() + + # Debug: Print config info if not using 7z when expected -+ if ZIPPER_TYPE != "7z" and str(zipper_val).lower() in ("7z", "true", "1"): -+ print(f"[zip] Debug: zipper_val={zipper_val!r}, ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG={CONFIG}", file=sys.stderr) - COMPRESSION_LEVEL = CONFIG.get("compression", 9) ++ if ZIPPER_TYPE != "7z" and str(zipper_val).lower() in ("7z", "true", "1"): ++ print(f"[zip] Debug: zipper_val={zipper_val!r}, ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG={CONFIG}", file=sys.stderr) + COMPRESSION_LEVEL = CONFIG.get("compression", 9) if isinstance(COMPRESSION_LEVEL, str): ``` @@ -6451,24 +6451,24 @@ The config loads at module import time, and the working directory may not be set -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 7, lines removed: 3 ```diff @@ -44,10 +44,14 @@ - # First try to load from project's .config folder (current working directory) + # First try to load from project's .config folder (current working directory) # Then fall back to ProjectStructure repo config (next to zip_sequences.py) -- project_config = Path(".config/config.json") +- project_config = Path(".config/config.json") + cwd = Path.cwd() -+ project_config = cwd / ".config" / "config.json" - repo_config = Path(__file__).resolve().with_name("config.json") ++ project_config = cwd / ".config" / "config.json" + repo_config = Path(__file__).resolve().with_name("config.json") - config_paths = [project_config, repo_config] + config_paths = [ -+ ("project", project_config), -+ ("repo", repo_config), ++ ("project", project_config), ++ ("repo", repo_config), + ] - for config_path in config_paths: @@ -6582,7 +6582,7 @@ Debug isn't showing, so `ZIPPER_TYPE` may not be "7z". Adding debug output that -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 3 @@ -6592,11 +6592,11 @@ Lines added: 2, lines removed: 3 ZIPPER_TYPE = str(zipper_val).lower() - # Debug: Print config info if not using 7z when expected -- if ZIPPER_TYPE != "7z" and str(zipper_val).lower() in ("7z", "true", "1"): -- print(f"[zip] Debug: zipper_val={zipper_val!r}, ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG={CONFIG}", file=sys.stderr) +- if ZIPPER_TYPE != "7z" and str(zipper_val).lower() in ("7z", "true", "1"): +- print(f"[zip] Debug: zipper_val={zipper_val!r}, ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG={CONFIG}", file=sys.stderr) + # Debug: Always print config info to diagnose issues -+ print(f"[zip] Debug: zipper_val={zipper_val!r} (type: {type(zipper_val).__name__}), ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG keys: {list(CONFIG.keys())}", file=sys.stderr) - COMPRESSION_LEVEL = CONFIG.get("compression", 9) ++ print(f"[zip] Debug: zipper_val={zipper_val!r} (type: {type(zipper_val).__name__}), ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG keys: {list(CONFIG.keys())}", file=sys.stderr) + COMPRESSION_LEVEL = CONFIG.get("compression", 9) if isinstance(COMPRESSION_LEVEL, str): ``` @@ -6655,18 +6655,18 @@ Config looks correct: `ZIPPER_TYPE='7z'`. Checking if 7z is found and why it mig -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined ```diff @@ -95,4 +95,5 @@ - if ZIPPER_TYPE == "7z": - SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") -+ print(f"[zip] Debug: ZIPPER_TYPE='{ZIPPER_TYPE}', SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) + if ZIPPER_TYPE == "7z": + SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") ++ print(f"[zip] Debug: ZIPPER_TYPE='{ZIPPER_TYPE}', SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) if SEVEN_Z_EXE is None: - print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) + print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) ```
@@ -6695,7 +6695,7 @@ Adding debug output in `zip_sequence` to see which path it takes: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: undefined @@ -6704,11 +6704,11 @@ Lines added: 2, lines removed: undefined @@ -207,7 +207,9 @@ def zip_sequence(seq_dir: Path, zip_path: Path) -> None: -+ print(f"[zip] Debug zip_sequence: ZIPPER_TYPE={ZIPPER_TYPE!r}, SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) - if ZIPPER_TYPE == "7z": ++ print(f"[zip] Debug zip_sequence: ZIPPER_TYPE={ZIPPER_TYPE!r}, SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: - raise RuntimeError("7z compression requested but 7z executable not found in PATH") -+ print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) + raise RuntimeError("7z compression requested but 7z executable not found in PATH") ++ print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) zip_path.parent.mkdir(parents=True, exist_ok=True) ``` @@ -6721,7 +6721,7 @@ Lines added: 2, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined @@ -6729,8 +6729,8 @@ Lines added: 1, lines removed: undefined ```diff @@ -298,4 +298,5 @@ - # Use zipfile (ZIPPER_TYPE == "zip" or fallback) -+ print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) + # Use zipfile (ZIPPER_TYPE == "zip" or fallback) ++ print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile ``` @@ -6839,7 +6839,7 @@ Removing the zipfile fallback. If 7z is requested but unavailable, fail with a c -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 22, lines removed: 13 @@ -6848,12 +6848,12 @@ Lines added: 22, lines removed: 13 @@ -297,19 +297,28 @@ return -- # Use zipfile (ZIPPER_TYPE == "zip" or fallback) -- print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) +- # Use zipfile (ZIPPER_TYPE == "zip" or fallback) +- print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) - from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile -+ # Use zipfile (only if ZIPPER_TYPE == "zip") -+ if ZIPPER_TYPE == "zip": -+ print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) ++ # Use zipfile (only if ZIPPER_TYPE == "zip") ++ if ZIPPER_TYPE == "zip": ++ print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) + from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile - zip_path.parent.mkdir(parents=True, exist_ok=True) @@ -6862,28 +6862,28 @@ Lines added: 22, lines removed: 13 - zip_kwargs = {} - else: - compression = ZIP_DEFLATED -- zip_kwargs = {"compresslevel": COMPRESSION_LEVEL} +- zip_kwargs = {"compresslevel": COMPRESSION_LEVEL} + zip_path.parent.mkdir(parents=True, exist_ok=True) + if COMPRESSION_LEVEL <= 0: + compression = ZIP_STORED + zip_kwargs = {} + else: + compression = ZIP_DEFLATED -+ zip_kwargs = {"compresslevel": COMPRESSION_LEVEL} ++ zip_kwargs = {"compresslevel": COMPRESSION_LEVEL} -- with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive: +- with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive: - for file_path in iter_sequence_files(seq_dir): - archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix()) -+ with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive: ++ with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive: + for file_path in iter_sequence_files(seq_dir): + archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix()) + return + + # Unknown ZIPPER_TYPE - fail with clear error + raise RuntimeError( -+ f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. " -+ f"Expected '7z' or 'zip'. " -+ f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}" ++ f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. " ++ f"Expected '7z' or 'zip'. " ++ f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}" + ) @@ -6897,19 +6897,19 @@ Lines added: 22, lines removed: 13 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 4, lines removed: 1 ```diff @@ -331,5 +331,8 @@ - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: -- raise RuntimeError("7z extraction requested but 7z executable not found in PATH") +- raise RuntimeError("7z extraction requested but 7z executable not found in PATH") + raise RuntimeError( -+ "7z extraction requested but 7z executable not found in PATH. " -+ "Please install 7z or set zipper to 'zip' in config.json" ++ "7z extraction requested but 7z executable not found in PATH. " ++ "Please install 7z or set zipper to 'zip' in config.json" + ) cmd = [ SEVEN_Z_EXE, @@ -6920,7 +6920,7 @@ Lines added: 19, lines removed: 2 ```diff @@ -339,10 +342,27 @@ - f"-o{target_dir}", + f"-o{target_dir}", ] - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - else: @@ -6932,23 +6932,23 @@ Lines added: 19, lines removed: 2 + text=True, + ) + if result.returncode != 0: -+ error_msg = result.stderr.strip() if result.stderr else "Unknown error" ++ error_msg = result.stderr.strip() if result.stderr else "Unknown error" + if result.stdout: -+ error_msg += f"\nstdout: {result.stdout.strip()}" -+ raise RuntimeError(f"7z extraction failed: {error_msg}") -+ elif ZIPPER_TYPE == "zip": ++ error_msg += f"\nstdout: {result.stdout.strip()}" ++ raise RuntimeError(f"7z extraction failed: {error_msg}") ++ elif ZIPPER_TYPE == "zip": from zipfile import ZipFile - with ZipFile(zip_path, "r") as archive: + with ZipFile(zip_path, "r") as archive: archive.extractall(target_dir) + else: + raise RuntimeError( -+ f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. " -+ f"Expected '7z' or 'zip'. " -+ f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}" ++ f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. " ++ f"Expected '7z' or 'zip'. " ++ f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}" + ) - for entry in seq_state.get("files", []): + for entry in seq_state.get("files", []): ```
@@ -6959,22 +6959,22 @@ Lines added: 19, lines removed: 2 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 5, lines removed: 1 ```diff @@ -210,5 +210,9 @@ - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: -- raise RuntimeError("7z compression requested but 7z executable not found in PATH") +- raise RuntimeError("7z compression requested but 7z executable not found in PATH") + raise RuntimeError( -+ "7z compression requested but 7z executable not found in PATH. " -+ "Please install 7z (e.g., via Chocolatey: choco install 7zip) " -+ "or set zipper to 'zip' in config.json" ++ "7z compression requested but 7z executable not found in PATH. " ++ "Please install 7z (e.g., via Chocolatey: choco install 7zip) " ++ "or set zipper to 'zip' in config.json" + ) - print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) + print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) zip_path.parent.mkdir(parents=True, exist_ok=True) ``` @@ -7056,7 +7056,7 @@ Confirmed: config loads correctly, 7z is found, and the script uses 7z compressi -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 2 @@ -7066,8 +7066,8 @@ Lines added: undefined, lines removed: 2 ZIPPER_TYPE = str(zipper_val).lower() - # Debug: Always print config info to diagnose issues -- print(f"[zip] Debug: zipper_val={zipper_val!r} (type: {type(zipper_val).__name__}), ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG keys: {list(CONFIG.keys())}", file=sys.stderr) - COMPRESSION_LEVEL = CONFIG.get("compression", 9) +- print(f"[zip] Debug: zipper_val={zipper_val!r} (type: {type(zipper_val).__name__}), ZIPPER_TYPE={ZIPPER_TYPE!r}, CONFIG keys: {list(CONFIG.keys())}", file=sys.stderr) + COMPRESSION_LEVEL = CONFIG.get("compression", 9) if isinstance(COMPRESSION_LEVEL, str): ``` @@ -7079,18 +7079,18 @@ Lines added: undefined, lines removed: 2 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 ```diff @@ -93,5 +93,4 @@ - if ZIPPER_TYPE == "7z": - SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") -- print(f"[zip] Debug: ZIPPER_TYPE='{ZIPPER_TYPE}', SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) + if ZIPPER_TYPE == "7z": + SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") +- print(f"[zip] Debug: ZIPPER_TYPE='{ZIPPER_TYPE}', SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) if SEVEN_Z_EXE is None: - print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) + print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) ```
@@ -7101,7 +7101,7 @@ Lines added: undefined, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 @@ -7110,8 +7110,8 @@ Lines added: undefined, lines removed: 1 @@ -204,5 +204,4 @@ def zip_sequence(seq_dir: Path, zip_path: Path) -> None: -- print(f"[zip] Debug zip_sequence: ZIPPER_TYPE={ZIPPER_TYPE!r}, SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) - if ZIPPER_TYPE == "7z": +- print(f"[zip] Debug zip_sequence: ZIPPER_TYPE={ZIPPER_TYPE!r}, SEVEN_Z_EXE={SEVEN_Z_EXE!r}", file=sys.stderr) + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: ``` @@ -7120,9 +7120,9 @@ Lines added: undefined, lines removed: 1 ```diff @@ -212,5 +211,4 @@ - "or set zipper to 'zip' in config.json" + "or set zipper to 'zip' in config.json" ) -- print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) +- print(f"[zip] Debug: Using 7z compression with {SEVEN_Z_EXE}", file=sys.stderr) zip_path.parent.mkdir(parents=True, exist_ok=True) ``` @@ -7135,16 +7135,16 @@ Lines added: undefined, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 1 ```diff @@ -298,5 +298,4 @@ - # Use zipfile (only if ZIPPER_TYPE == "zip") - if ZIPPER_TYPE == "zip": -- print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) + # Use zipfile (only if ZIPPER_TYPE == "zip") + if ZIPPER_TYPE == "zip": +- print(f"[zip] Debug: Using zipfile compression (ZIPPER_TYPE={ZIPPER_TYPE!r})", file=sys.stderr) from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile ``` @@ -7184,18 +7184,18 @@ Removing the old warning print statement since we now raise an error instead: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 3 ```diff @@ -93,7 +93,4 @@ - if ZIPPER_TYPE == "7z": - SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") + if ZIPPER_TYPE == "7z": + SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") - if SEVEN_Z_EXE is None: -- print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) -- print(f"[zip] Config zipper value: {zipper_val!r}, resolved ZIPPER_TYPE: {ZIPPER_TYPE!r}", file=sys.stderr) +- print(f"[zip] Warning: 7z compression requested (ZIPPER_TYPE='{ZIPPER_TYPE}') but no 7z executable was found in PATH.", file=sys.stderr) +- print(f"[zip] Config zipper value: {zipper_val!r}, resolved ZIPPER_TYPE: {ZIPPER_TYPE!r}", file=sys.stderr) ``` @@ -7365,7 +7365,7 @@ Updating the code to use `.7z` extension and 7z format: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 @@ -7374,8 +7374,8 @@ Lines added: 2, lines removed: 1 @@ -188,5 +188,6 @@ def archive_path_for(seq_dir: Path) -> Path: rel = seq_dir.relative_to(RENDER_ROOT) -- return (ARCHIVE_ROOT / rel).with_suffix(".zip") -+ suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip" +- return (ARCHIVE_ROOT / rel).with_suffix(".zip") ++ suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip" + return (ARCHIVE_ROOT / rel).with_suffix(suffix) @@ -7389,7 +7389,7 @@ Lines added: 2, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 2 @@ -7398,10 +7398,10 @@ Lines added: 2, lines removed: 2 @@ -224,6 +224,6 @@ list_file_path = None try: -- # Create temporary zip file path (but don't create the file - let 7z create it) -- temp_zip_path = tempfile.mktemp(suffix=".zip", dir=zip_path.parent) -+ # Create temporary archive file path (but don't create the file - let 7z create it) -+ temp_zip_path = tempfile.mktemp(suffix=".7z", dir=zip_path.parent) +- # Create temporary zip file path (but don't create the file - let 7z create it) +- temp_zip_path = tempfile.mktemp(suffix=".zip", dir=zip_path.parent) ++ # Create temporary archive file path (but don't create the file - let 7z create it) ++ temp_zip_path = tempfile.mktemp(suffix=".7z", dir=zip_path.parent) temp_zip = Path(temp_zip_path) ``` @@ -7414,19 +7414,19 @@ Lines added: 2, lines removed: 2 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 ```diff @@ -249,5 +249,5 @@ - "-bb0", # Suppress progress output - f"-mx={COMPRESSION_LEVEL}", -- "-tzip", -+ "-t7z", # Use 7z format, not zip + "-bb0", # Suppress progress output + f"-mx={COMPRESSION_LEVEL}", +- "-tzip", ++ "-t7z", # Use 7z format, not zip str(temp_zip_abs), - f"@{list_file_abs}", + f"@{list_file_abs}", ```
@@ -7463,7 +7463,7 @@ Updating `run_expand` and `cleanup_orphan_archives` to handle both `.zip` and `. -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 9, lines removed: 6 @@ -7472,20 +7472,20 @@ Lines added: 9, lines removed: 6 @@ -447,10 +447,13 @@ work_items: list[tuple[Path, dict]] = [] -- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): +- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): - state_path = state_path_for(zip_path) - seq_state = load_state(state_path) - if seq_state is None: -- log("expand", f"Skipping {zip_path} (missing metadata)") +- log("expand", f"Skipping {zip_path} (missing metadata)") - continue + # Look for both .zip and .7z archives -+ archive_patterns = ["*.zip", "*.7z"] ++ archive_patterns = ["*.zip", "*.7z"] + for pattern in archive_patterns: + for zip_path in ARCHIVE_ROOT.rglob(pattern): + state_path = state_path_for(zip_path) + seq_state = load_state(state_path) + if seq_state is None: -+ log("expand", f"Skipping {zip_path} (missing metadata)") ++ log("expand", f"Skipping {zip_path} (missing metadata)") + continue target_dir = sequence_dir_for(zip_path) @@ -7517,7 +7517,7 @@ Fixing indentation: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 4, lines removed: 4 @@ -7547,7 +7547,7 @@ Lines added: 4, lines removed: 4 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: 3 @@ -7556,11 +7556,11 @@ Lines added: 6, lines removed: 3 @@ -493,7 +493,10 @@ removed: list[Path] = [] -- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): +- for zip_path in ARCHIVE_ROOT.rglob("*.zip"): - seq_dir = sequence_dir_for(zip_path) - if seq_dir.exists(): + # Look for both .zip and .7z archives -+ archive_patterns = ["*.zip", "*.7z"] ++ archive_patterns = ["*.zip", "*.7z"] + for pattern in archive_patterns: + for zip_path in ARCHIVE_ROOT.rglob(pattern): + seq_dir = sequence_dir_for(zip_path) @@ -7607,7 +7607,7 @@ Fixing indentation in `cleanup_orphan_archives`: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 7, lines removed: 7 @@ -7620,9 +7620,9 @@ Lines added: 7, lines removed: 7 + continue - rel = zip_path.relative_to(ARCHIVE_ROOT) -- log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) +- log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) + rel = zip_path.relative_to(ARCHIVE_ROOT) -+ log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) ++ log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) - zip_path.unlink(missing_ok=True) - state_path = state_path_for(zip_path) @@ -7695,7 +7695,7 @@ Fixing the `removed.append` placement: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -7806,7 +7806,7 @@ Updating the logic to check for existing `.zip` files and only convert them when -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 24, lines removed: 1 @@ -7815,17 +7815,17 @@ Lines added: 24, lines removed: 1 @@ -389,9 +389,32 @@ continue -+ # Get the target archive path (will be .7z if ZIPPER_TYPE is "7z") ++ # Get the target archive path (will be .7z if ZIPPER_TYPE is "7z") zip_path = archive_path_for(seq_dir) state_path = state_path_for(zip_path) + + # Check if we need to upgrade from .zip to .7z + old_zip_path = None -+ if ZIPPER_TYPE == "7z": ++ if ZIPPER_TYPE == "7z": + # Check if an old .zip file exists -+ old_zip_path = zip_path.with_suffix(".zip") ++ old_zip_path = zip_path.with_suffix(".zip") + if old_zip_path.exists(): -+ # Check if the old .zip's metadata matches current state ++ # Check if the old .zip's metadata matches current state + old_state_path = state_path_for(old_zip_path) + old_stored_state = load_state(old_state_path) + if not state_changed(seq_state, old_stored_state): @@ -7882,7 +7882,7 @@ Updating `zip_sequence` to delete any existing `.zip` file when creating a new ` -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 9, lines removed: undefined @@ -7892,8 +7892,8 @@ Lines added: 9, lines removed: undefined zip_path.parent.mkdir(parents=True, exist_ok=True) + # If creating a .7z file, remove any existing .zip file for the same sequence -+ if zip_path.suffix == ".7z": -+ old_zip_path = zip_path.with_suffix(".zip") ++ if zip_path.suffix == ".7z": ++ old_zip_path = zip_path.with_suffix(".zip") + if old_zip_path.exists(): + old_zip_path.unlink(missing_ok=True) + old_state_path = state_path_for(old_zip_path) @@ -8005,7 +8005,7 @@ The error "`. was unexpected at this time.`" is a batch syntax issue. Checking i -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -8015,7 +8015,7 @@ Lines added: 1, lines removed: 1 popd >nul 2>&1 - exit /b %ERR% -+ exit /b "%ERR%" ++ exit /b "%ERR%" ``` @@ -8028,7 +8028,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: UnzipSeqArchv.bat +
Tool use: **search_replace** • Edit file: UnzipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -8038,7 +8038,7 @@ Lines added: 1, lines removed: 1 popd >nul 2>&1 - exit /b %ERR% -+ exit /b "%ERR%" ++ exit /b "%ERR%" ``` @@ -8061,7 +8061,7 @@ Quoting the exit code may not work. Using delayed expansion instead: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -8072,7 +8072,7 @@ Lines added: 1, lines removed: 1 - setlocal EnableExtensions + setlocal EnableExtensions EnableDelayedExpansion - set "REN_DIR=%~dp0" + set "REN_DIR=%~dp0" ```
@@ -8083,7 +8083,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 4, lines removed: 4 @@ -8091,18 +8091,18 @@ Lines added: 4, lines removed: 4 ```diff @@ -32,12 +32,12 @@ - python "%PY_SCRIPT%" --verbose %* -- set "ERR=%ERRORLEVEL%" -+ set "ERR=!ERRORLEVEL!" + python "%PY_SCRIPT%" --verbose %* +- set "ERR=%ERRORLEVEL%" ++ set "ERR=!ERRORLEVEL!" -- if not "%ERR%"=="0" ( +- if not "%ERR%"=="0" ( - echo Failed to update render sequence archives (exit code %ERR%). -+ if not "!ERR!"=="0" ( ++ if not "!ERR!"=="0" ( + echo Failed to update render sequence archives (exit code !ERR!). ) popd >nul 2>&1 -- exit /b "%ERR%" +- exit /b "%ERR%" + exit /b !ERR! @@ -8116,7 +8116,7 @@ Lines added: 4, lines removed: 4 -
Tool use: **code_edit** • Edit file: UnzipSeqArchv.bat +
Tool use: **search_replace** • Edit file: UnzipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -8127,7 +8127,7 @@ Lines added: 1, lines removed: 1 - setlocal EnableExtensions + setlocal EnableExtensions EnableDelayedExpansion - set "REN_DIR=%~dp0" + set "REN_DIR=%~dp0" ```
@@ -8138,7 +8138,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: UnzipSeqArchv.bat +
Tool use: **search_replace** • Edit file: UnzipSeqArchv.bat **Chunk 1** Lines added: 4, lines removed: 4 @@ -8146,18 +8146,18 @@ Lines added: 4, lines removed: 4 ```diff @@ -32,12 +32,12 @@ - python "%PY_SCRIPT%" --mode expand --verbose %* -- set "ERR=%ERRORLEVEL%" -+ set "ERR=!ERRORLEVEL!" + python "%PY_SCRIPT%" --mode expand --verbose %* +- set "ERR=%ERRORLEVEL%" ++ set "ERR=!ERRORLEVEL!" -- if not "%ERR%"=="0" ( +- if not "%ERR%"=="0" ( - echo Failed to expand render sequence archives (exit code %ERR%). -+ if not "!ERR!"=="0" ( ++ if not "!ERR!"=="0" ( + echo Failed to expand render sequence archives (exit code !ERR!). ) popd >nul 2>&1 -- exit /b "%ERR%" +- exit /b "%ERR%" + exit /b !ERR! @@ -8204,7 +8204,7 @@ Use 7z to view archive properties. Options: --- -
Tool use: **command** • Run command: 7z l -slt +
Tool use: **run_terminal_cmd** • Run command: 7z l -slt ```bash 7z l -slt @@ -16818,7 +16818,7 @@ _**Agent (model default, mode Agent)**_ --- -
Tool use: **command** • Run command: 7z l -slt "T:\250827_FestivalTurf\Renders\_zipped\01 Develop Your Turf Plan\01_Blueprint.7z" +
Tool use: **run_terminal_cmd** • Run command: 7z l -slt "T:\250827_FestivalTurf\Renders\_zipped\01 Develop Your Turf Plan\01_Blueprint.7z" ```bash 7z l -slt "T:\250827_FestivalTurf\Renders\_zipped\01 Develop Your Turf Plan\01_Blueprint.7z" @@ -18478,7 +18478,7 @@ Enabling multi-threading in 7z. LZMA2 is single-threaded by default, which expla -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: undefined @@ -18498,10 +18498,10 @@ Lines added: 1, lines removed: undefined ```diff @@ -258,4 +260,5 @@ - "-bb0", # Suppress progress output - f"-mx={COMPRESSION_LEVEL}", -+ "-mmt=on", # Enable multi-threading (uses all available CPU cores) - "-t7z", # Use 7z format, not zip + "-bb0", # Suppress progress output + f"-mx={COMPRESSION_LEVEL}", ++ "-mmt=on", # Enable multi-threading (uses all available CPU cores) + "-t7z", # Use 7z format, not zip str(temp_zip_abs), ``` @@ -18540,7 +18540,7 @@ Removing the unused variable: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 2 @@ -18675,17 +18675,17 @@ Adding support for compression methods with better multi-threading. PPMd typical -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined ```diff @@ -37,4 +37,5 @@ - "zipper": "7z", - "compression": 9, -+ "compressionMethod": "LZMA2", # Options: LZMA2, PPMd, BZip2, Deflate - "dailyFormat": "daily_YYMMDD", + "zipper": "7z", + "compression": 9, ++ "compressionMethod": "LZMA2", # Options: LZMA2, PPMd, BZip2, Deflate + "dailyFormat": "daily_YYMMDD", } ``` @@ -18697,7 +18697,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined @@ -18707,13 +18707,13 @@ Lines added: 6, lines removed: undefined COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL)) + # Compression method: LZMA2 (default, best ratio but slower), PPMd (faster, better multi-threading), BZip2, Deflate -+ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "LZMA2").upper() ++ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "LZMA2").upper() + # Validate method -+ if COMPRESSION_METHOD not in ("LZMA2", "PPMD", "BZIP2", "DEFLATE"): -+ COMPRESSION_METHOD = "LZMA2" ++ if COMPRESSION_METHOD not in ("LZMA2", "PPMD", "BZIP2", "DEFLATE"): ++ COMPRESSION_METHOD = "LZMA2" + SEVEN_Z_EXE: str | None = None - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": ```
@@ -18724,7 +18724,7 @@ Lines added: 6, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 12, lines removed: undefined @@ -18734,16 +18734,16 @@ Lines added: 12, lines removed: undefined temp_zip_abs = temp_zip.resolve() # Create archive in temp location first (7z will create it fresh) + # Build compression method parameter -+ if COMPRESSION_METHOD == "LZMA2": -+ method_param = f"-mm=LZMA2" -+ elif COMPRESSION_METHOD == "PPMD": -+ method_param = f"-mm=PPMd" -+ elif COMPRESSION_METHOD == "BZIP2": -+ method_param = f"-mm=BZip2" -+ elif COMPRESSION_METHOD == "DEFLATE": -+ method_param = f"-mm=Deflate" ++ if COMPRESSION_METHOD == "LZMA2": ++ method_param = f"-mm=LZMA2" ++ elif COMPRESSION_METHOD == "PPMD": ++ method_param = f"-mm=PPMd" ++ elif COMPRESSION_METHOD == "BZIP2": ++ method_param = f"-mm=BZip2" ++ elif COMPRESSION_METHOD == "DEFLATE": ++ method_param = f"-mm=Deflate" + else: -+ method_param = f"-mm=LZMA2" ++ method_param = f"-mm=LZMA2" + cmd = [ SEVEN_Z_EXE, @@ -18754,11 +18754,11 @@ Lines added: 2, lines removed: undefined ```diff @@ -265,4 +277,6 @@ - "-bb0", # Suppress progress output - f"-mx={COMPRESSION_LEVEL}", + "-bb0", # Suppress progress output + f"-mx={COMPRESSION_LEVEL}", + method_param, # Compression method -+ "-mmt=on", # Enable multi-threading (works better with PPMd/BZip2) - "-t7z", # Use 7z format, not zip ++ "-mmt=on", # Enable multi-threading (works better with PPMd/BZip2) + "-t7z", # Use 7z format, not zip str(temp_zip_abs), ``` @@ -18854,18 +18854,18 @@ For maximum compression with high CPU utilization, use PPMd. Updating the defaul -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 ```diff @@ -37,5 +37,5 @@ - "zipper": "7z", - "compression": 9, -- "compressionMethod": "LZMA2", # Options: LZMA2, PPMd, BZip2, Deflate -+ "compressionMethod": "PPMd", # Options: LZMA2 (best ratio, limited threading), PPMd (high ratio, good threading), BZip2, Deflate - "dailyFormat": "daily_YYMMDD", + "zipper": "7z", + "compression": 9, +- "compressionMethod": "LZMA2", # Options: LZMA2, PPMd, BZip2, Deflate ++ "compressionMethod": "PPMd", # Options: LZMA2 (best ratio, limited threading), PPMd (high ratio, good threading), BZip2, Deflate + "dailyFormat": "daily_YYMMDD", } ``` @@ -19010,21 +19010,21 @@ The error message references the old script name. Deployment is now handled by ` -
Tool use: **code_edit** • Edit file: UpdateSequences.bat +
Tool use: **search_replace** • Edit file: UpdateSequences.bat **Chunk 1** Lines added: 2, lines removed: 2 ```diff @@ -11,11 +11,11 @@ - if not exist "%CONFIG_PATH%" ( + if not exist "%CONFIG_PATH%" ( echo [ERROR] config.json not found at %CONFIG_PATH% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. exit /b 1 ) - if not exist "%GET_STRUCT_DIR%" ( + if not exist "%GET_STRUCT_DIR%" ( echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. @@ -19040,21 +19040,21 @@ Lines added: 2, lines removed: 2 -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 2, lines removed: 2 ```diff @@ -11,11 +11,11 @@ - if not exist "%CONFIG_PATH%" ( + if not exist "%CONFIG_PATH%" ( echo [ERROR] config.json not found at %CONFIG_PATH% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. exit /b 1 ) - if not exist "%GET_STRUCT_DIR%" ( + if not exist "%GET_STRUCT_DIR%" ( echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. @@ -19070,21 +19070,21 @@ Lines added: 2, lines removed: 2 -
Tool use: **code_edit** • Edit file: UnzipSeqArchv.bat +
Tool use: **search_replace** • Edit file: UnzipSeqArchv.bat **Chunk 1** Lines added: 2, lines removed: 2 ```diff @@ -11,11 +11,11 @@ - if not exist "%CONFIG_PATH%" ( + if not exist "%CONFIG_PATH%" ( echo [ERROR] config.json not found at %CONFIG_PATH% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. exit /b 1 ) - if not exist "%GET_STRUCT_DIR%" ( + if not exist "%GET_STRUCT_DIR%" ( echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR% - echo Please run UpdateProjectBatches.ps1 to refresh helper files. + echo Please run ConfigLoader.ps1 to deploy helper files. @@ -19166,7 +19166,7 @@ The issue: `UpdateSequences.bat` is in `Renders\04 Securing Your Seam\`, so goin -
Tool use: **code_edit** • Edit file: UpdateSequences.bat +
Tool use: **search_replace** • Edit file: UpdateSequences.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -19174,11 +19174,11 @@ Lines added: 1, lines removed: 1 ```diff @@ -3,5 +3,5 @@ - set "script_dir=%~dp0" -- for %%I in ("%script_dir%..") do set "PROJ_ROOT=%%~fI" -+ for %%I in ("%script_dir%..\..") do set "PROJ_ROOT=%%~fI" + set "script_dir=%~dp0" +- for %%I in ("%script_dir%..") do set "PROJ_ROOT=%%~fI" ++ for %%I in ("%script_dir%..\..") do set "PROJ_ROOT=%%~fI" - set "CONFIG_DIR=%PROJ_ROOT%\.config" + set "CONFIG_DIR=%PROJ_ROOT%\.config" ```
@@ -19277,7 +19277,7 @@ Fixing the logic: when a `.zip` exists, `.7z` doesn't, and metadata matches, kee -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: 2 @@ -19287,13 +19287,13 @@ Lines added: 3, lines removed: 2 # Check if we need to upgrade from .zip to .7z old_zip_path = None + old_stored_state = None - if ZIPPER_TYPE == "7z": - old_zip_path = zip_path.with_suffix(".zip") + if ZIPPER_TYPE == "7z": + old_zip_path = zip_path.with_suffix(".zip") if old_zip_path.exists(): old_state_path = state_path_for(old_zip_path) old_stored_state = load_state(old_state_path) -- # If old .zip exists and archive doesn't, we'll check state later -+ # If old .zip exists and .7z doesn't, use old .zip's state for comparison +- # If old .zip exists and archive doesn't, we'll check state later ++ # If old .zip exists and .7z doesn't, use old .zip's state for comparison if not zip_path.exists() and old_stored_state is not None: stored_state = old_stored_state @@ -19308,7 +19308,7 @@ Lines added: 1, lines removed: 1 ```diff @@ -667,5 +668,5 @@ - # If directory wasn't modified since archive was created, skip state computation + # If directory wasn't modified since archive was created, skip state computation if dir_mtime <= archive_mtime: - # Still need to check for old .zip cleanup + # Still need to check for old .zip cleanup (we have .7z, so .zip is obsolete) @@ -19339,11 +19339,11 @@ Lines added: 15, lines removed: 7 + if old_state_path.exists(): + old_state_path.unlink(missing_ok=True) + elif old_zip_path and old_zip_path.exists() and old_stored_state is not None: -+ # .7z doesn't exist, but .zip exists and metadata matches -+ # Keep the .zip file, don't create .7z ++ # .7z doesn't exist, but .zip exists and metadata matches ++ # Keep the .zip file, don't create .7z + continue + else: -+ # No archive exists, but state matches (shouldn't happen, but be safe) ++ # No archive exists, but state matches (shouldn't happen, but be safe) + continue work_items.append((seq_dir, zip_path, state_path, seq_state)) @@ -19455,7 +19455,7 @@ Updating the auto-calculation to be more aggressive when `Max7zInst=0`: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: 5 @@ -19464,10 +19464,10 @@ Lines added: 6, lines removed: 5 @@ -282,12 +282,13 @@ # Auto-calculate based on RAM if Max7zInst not configured -+ # "Balls-to-the-walls" mode: use maximum resources ++ # "Balls-to-the-walls" mode: use maximum resources if available_ram is not None: - # 7z uses ~2-3x dictionary size in RAM, but with overhead use 8x for safety -- # This accounts for 7z's internal buffers, OS overhead, and other processes +- # This accounts for 7z's internal buffers, OS overhead, and other processes - FIXED_RAM_PER_JOB = FIXED_DICT_SIZE_MB * 8 * 1024 * 1024 # 8GB per job + # 7z uses ~2-3x dictionary size in RAM, use 3x for aggressive mode + # This is more realistic and allows more concurrent workers @@ -19488,10 +19488,10 @@ Lines added: 1, lines removed: 1 ```diff @@ -305,5 +306,5 @@ log( - "zip", -- f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (40%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " -+ f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " - f"RAM limit: {ram_limit}, requested: {requested}, final: {final_limit}", + "zip", +- f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (40%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " ++ f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " + f"RAM limit: {ram_limit}, requested: {requested}, final: {final_limit}", verbose_only=True, ``` @@ -19644,7 +19644,7 @@ Capping workers at the number of work items: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 5, lines removed: undefined @@ -19653,7 +19653,7 @@ Lines added: 5, lines removed: undefined @@ -295,4 +295,9 @@ ram_limit = max(1, compression_ram // FIXED_RAM_PER_JOB) -+ # Cap at number of actual work items (can't have more workers than jobs) ++ # Cap at number of actual work items (can't have more workers than jobs) + num_work_items = len(work_items) if work_items else 0 + if num_work_items > 0: + ram_limit = min(ram_limit, num_work_items) @@ -19667,10 +19667,10 @@ Lines added: 1, lines removed: 1 ```diff @@ -307,5 +312,5 @@ - "zip", - f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " -- f"RAM limit: {ram_limit}, requested: {requested}, final: {final_limit}", -+ f"RAM limit: {ram_limit}, work items: {num_work_items}, requested: {requested}, final: {final_limit}", + "zip", + f"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%), {ram_per_job_gb:.1f}GB per job (dict: {FIXED_DICT_SIZE_MB}MB) → " +- f"RAM limit: {ram_limit}, requested: {requested}, final: {final_limit}", ++ f"RAM limit: {ram_limit}, work items: {num_work_items}, requested: {requested}, final: {final_limit}", verbose_only=True, verbose=verbose ``` @@ -19692,7 +19692,7 @@ Updating the configured `MAX_7Z_INSTANCES` case to also cap at the number of wor -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 4, lines removed: 1 @@ -19701,8 +19701,8 @@ Lines added: 4, lines removed: 1 @@ -265,6 +265,9 @@ # Check if Max7zInst is configured if MAX_7Z_INSTANCES is not None: -- # Use configured maximum instances, but still respect user's --jobs -+ # Use configured maximum instances, but still respect user's --jobs and work items +- # Use configured maximum instances, but still respect user's --jobs ++ # Use configured maximum instances, but still respect user's --jobs and work items final_limit = MAX_7Z_INSTANCES + num_work_items = len(work_items) if work_items else 0 + if num_work_items > 0: @@ -19716,10 +19716,10 @@ Lines added: 1, lines removed: 1 ```diff @@ -274,5 +277,5 @@ - "zip", - f"Using Max7zInst={MAX_7Z_INSTANCES} from config → " -- f"requested: {requested}, final: {final_limit}", -+ f"work items: {num_work_items}, requested: {requested}, final: {final_limit}", + "zip", + f"Using Max7zInst={MAX_7Z_INSTANCES} from config → " +- f"requested: {requested}, final: {final_limit}", ++ f"work items: {num_work_items}, requested: {requested}, final: {final_limit}", verbose_only=True, verbose=verbose ``` @@ -19741,7 +19741,7 @@ Updating the default case when RAM detection fails: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 12, lines removed: 3 @@ -19751,7 +19751,7 @@ Lines added: 12, lines removed: 3 # RAM detection failed, use a safe default (no CPU limit) - if verbose: -- log("zip", "RAM detection failed and Max7zInst not set, using default worker limit of 4", verbose_only=True, verbose=verbose) +- log("zip", "RAM detection failed and Max7zInst not set, using default worker limit of 4", verbose_only=True, verbose=verbose) default_limit = 4 + num_work_items = len(work_items) if work_items else 0 + if num_work_items > 0: @@ -19761,9 +19761,9 @@ Lines added: 12, lines removed: 3 + default_limit = min(default_limit, requested) + if verbose: + log( -+ "zip", -+ f"RAM detection failed and Max7zInst not set, using default worker limit → " -+ f"work items: {num_work_items}, requested: {requested}, final: {default_limit}", ++ "zip", ++ f"RAM detection failed and Max7zInst not set, using default worker limit → " ++ f"work items: {num_work_items}, requested: {requested}, final: {default_limit}", + verbose_only=True, + verbose=verbose + ) @@ -19970,7 +19970,7 @@ Updating the RAM estimate to match actual usage (~7–10GB per 7z process). The -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 12, lines removed: 17 @@ -20190,14 +20190,14 @@ Tightening the RAM estimate to match actual usage (~2GB per process): -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 10, lines removed: 19 ```diff @@ -211,24 +211,15 @@ - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": # Fixed dictionary size: 1GB (1024MB) - FIXED_DICT_SIZE_MB = 1024 - FIXED_DICT_SIZE_BYTES = FIXED_DICT_SIZE_MB * 1024 * 1024 @@ -20408,7 +20408,7 @@ The bin-packing minimizes bins (workers), so it packs all sequences into one bin -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 2 @@ -20461,7 +20461,7 @@ Lines added: 36, lines removed: 27 - placed = True - break - -- # If doesn't fit, create new bin +- # If doesn't fit, create new bin - if not placed: - bins.append([(seq_dir, estimated_ram, work_item)]) - bin_remaining.append(compression_ram - estimated_ram) @@ -20495,7 +20495,7 @@ Lines added: 36, lines removed: 27 + placed = True + break + -+ # If doesn't fit, create new bin ++ # If doesn't fit, create new bin + if not placed: + bins.append([(seq_dir, estimated_ram, work_item)]) + bin_remaining.append(compression_ram - estimated_ram) @@ -20503,7 +20503,7 @@ Lines added: 36, lines removed: 27 + # Worker count is number of bins + worker_count = len(bins) - # Cap at number of actual work items (can't have more workers than jobs) + # Cap at number of actual work items (can't have more workers than jobs) - num_work_items = len(work_items) if work_items else 0 if num_work_items > 0: worker_count = min(worker_count, num_work_items) @@ -20519,7 +20519,7 @@ Lines added: 1, lines removed: 1 - total_estimated_gb = sum(ram for _, ram, _ in work_items_with_ram) / (1024 ** 3) + total_estimated_gb = total_estimated_ram / (1024 ** 3) log( - "zip", + "zip", ``` **Chunk 4** @@ -20533,21 +20533,21 @@ Lines added: 9, lines removed: 3 - bin_sizes = [sum(ram for _, ram, _ in bin_items) / (1024 ** 3) for bin_items in bins] + if total_estimated_ram <= compression_ram: log( - "zip", -- f"Bin-packing: {worker_count} workers, bin sizes: {[f'{s:.1f}GB' for s in bin_sizes[:5]]}{'...' if len(bin_sizes) > 5 else ''}", -+ f"All sequences fit in RAM → using {worker_count} workers (one per sequence)", + "zip", +- f"Bin-packing: {worker_count} workers, bin sizes: {[f'{s:.1f}GB' for s in bin_sizes[:5]]}{'...' if len(bin_sizes) > 5 else ''}", ++ f"All sequences fit in RAM → using {worker_count} workers (one per sequence)", verbose_only=True, verbose=verbose ) + else: + log( -+ "zip", -+ f"Using bin-packing: {worker_count} workers needed", ++ "zip", ++ f"Using bin-packing: {worker_count} workers needed", + verbose_only=True, + verbose=verbose + ) log( - "zip", + "zip", ```
@@ -20822,7 +20822,7 @@ Updating the code to pass the worker count and allocate more CPU threads when th -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -20833,7 +20833,7 @@ Lines added: 1, lines removed: 1 - def zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None) -> None: + def zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None, worker_count: int = 1) -> None: - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: ``` @@ -20845,24 +20845,24 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 10, lines removed: undefined ```diff @@ -610,4 +610,14 @@ - cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") + cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") -+ # CPU thread allocation: when there's only 1 worker, use all CPU cores ++ # CPU thread allocation: when there's only 1 worker, use all CPU cores + # When there are multiple workers, use auto mode to let 7z decide + cpu_cores = os.cpu_count() or 1 + if worker_count == 1: + # Single worker: use all CPU cores for maximum speed -+ cmd.append(f"-mmt={cpu_cores}") ++ cmd.append(f"-mmt={cpu_cores}") + else: + # Multiple workers: use auto mode (7z will manage threads) -+ cmd.append("-mmt=on") ++ cmd.append("-mmt=on") + cmd.extend([ str(temp_zip_abs), @@ -20876,7 +20876,7 @@ Lines added: 10, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 2 @@ -20887,7 +20887,7 @@ Lines added: 2, lines removed: 2 - def process_zip(seq_dir: Path, zip_path: Path, state_path: Path, seq_state: dict, per_job_memory_limit: int | None, *, verbose: bool) -> Sequence[Path]: + def process_zip(seq_dir: Path, zip_path: Path, state_path: Path, seq_state: dict, per_job_memory_limit: int | None, worker_count: int, *, verbose: bool) -> Sequence[Path]: - log("zip", f"{seq_dir} -> {zip_path}", verbose_only=True, verbose=verbose) + log("zip", f"{seq_dir} -> {zip_path}", verbose_only=True, verbose=verbose) - zip_sequence(seq_dir, zip_path, per_job_memory_limit) + zip_sequence(seq_dir, zip_path, per_job_memory_limit, worker_count) state_path.write_text(json.dumps(seq_state, indent=2)) @@ -20902,7 +20902,7 @@ Lines added: 2, lines removed: 2 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -21089,18 +21089,18 @@ Adding compression method support. LZMA2 (default) is largely single-threaded, s -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined ```diff @@ -54,4 +54,5 @@ - "zipper": "7z", - "compression": 9, -+ "compressionMethod": "PPMd", # Compression method: LZMA2, PPMd, BZip2, Deflate - "dailyFormat": "daily_YYMMDD", - "Max7zInst": 0, # Maximum concurrent 7z instances (0 = auto-calculate) + "zipper": "7z", + "compression": 9, ++ "compressionMethod": "PPMd", # Compression method: LZMA2, PPMd, BZip2, Deflate + "dailyFormat": "daily_YYMMDD", + "Max7zInst": 0, # Maximum concurrent 7z instances (0 = auto-calculate) ```
@@ -21111,7 +21111,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined @@ -21120,13 +21120,13 @@ Lines added: 6, lines removed: undefined @@ -121,4 +121,10 @@ COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL)) -+ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "PPMd") ++ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "PPMd") + # Validate compression method -+ valid_methods = {"LZMA2", "PPMd", "BZip2", "Deflate"} ++ valid_methods = {"LZMA2", "PPMd", "BZip2", "Deflate"} + if COMPRESSION_METHOD not in valid_methods: -+ COMPRESSION_METHOD = "PPMd" # Default to PPMd for better multi-threading ++ COMPRESSION_METHOD = "PPMd" # Default to PPMd for better multi-threading + - MAX_7Z_INSTANCES = CONFIG.get("Max7zInst", 0) + MAX_7Z_INSTANCES = CONFIG.get("Max7zInst", 0) if MAX_7Z_INSTANCES is not None: ``` @@ -21138,18 +21138,18 @@ Lines added: 6, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 3, lines removed: undefined ```diff @@ -610,4 +610,7 @@ - "-t7z", # Use 7z format, not zip + "-t7z", # Use 7z format, not zip ] + + # Set compression method (PPMd is better for multi-threading than default LZMA2) -+ cmd.append(f"-m0={COMPRESSION_METHOD}") ++ cmd.append(f"-m0={COMPRESSION_METHOD}") # Always use fixed dictionary size: 1GB (1024MB) ``` @@ -21227,19 +21227,19 @@ Lint results -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 ```diff @@ -1004,5 +1004,5 @@ - exe = SEVEN_Z_EXE or "not found" - max_inst = MAX_7Z_INSTANCES if MAX_7Z_INSTANCES is not None else "auto" -- log("init", f"7z executable: {exe}, Max7zInst: {max_inst}") -+ log("init", f"7z executable: {exe}, Max7zInst: {max_inst}, method: {COMPRESSION_METHOD}, level: {COMPRESSION_LEVEL}") + exe = SEVEN_Z_EXE or "not found" + max_inst = MAX_7Z_INSTANCES if MAX_7Z_INSTANCES is not None else "auto" +- log("init", f"7z executable: {exe}, Max7zInst: {max_inst}") ++ log("init", f"7z executable: {exe}, Max7zInst: {max_inst}, method: {COMPRESSION_METHOD}, level: {COMPRESSION_LEVEL}") - if args.mode == "expand": + if args.mode == "expand": ```
@@ -21489,7 +21489,7 @@ Updating the code to set memory/dictionary parameters based on the compression m -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 21, lines removed: 7 @@ -21499,7 +21499,7 @@ Lines added: 21, lines removed: 7 ] - # Set compression method (PPMd is better for multi-threading than default LZMA2) -- cmd.append(f"-m0={COMPRESSION_METHOD}") +- cmd.append(f"-m0={COMPRESSION_METHOD}") - - # Always use fixed dictionary size: 1GB (1024MB) - # The per_job_memory_limit parameter is the estimated RAM usage (for logging/info only) @@ -21507,28 +21507,28 @@ Lines added: 21, lines removed: 7 + # Set compression method and memory/dictionary size based on method + # PPMd uses memory parameter in method string, LZMA2 uses -md flag FIXED_DICT_SIZE_MB = 1024 -- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") -+ if COMPRESSION_METHOD == "PPMd": +- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") ++ if COMPRESSION_METHOD == "PPMd": + # PPMd: specify memory as part of method string -+ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") -+ elif COMPRESSION_METHOD == "LZMA2": ++ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") ++ elif COMPRESSION_METHOD == "LZMA2": + # LZMA2: use -md for dictionary size -+ cmd.append("-m0=LZMA2") -+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") -+ elif COMPRESSION_METHOD == "BZip2": ++ cmd.append("-m0=LZMA2") ++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") ++ elif COMPRESSION_METHOD == "BZip2": + # BZip2: use -md for dictionary size (smaller max: 900KB) + max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB -+ cmd.append("-m0=BZip2") -+ cmd.append(f"-md={max_bzip2_dict}k") -+ elif COMPRESSION_METHOD == "Deflate": -+ # Deflate: doesn't use dictionary size parameter -+ cmd.append("-m0=Deflate") ++ cmd.append("-m0=BZip2") ++ cmd.append(f"-md={max_bzip2_dict}k") ++ elif COMPRESSION_METHOD == "Deflate": ++ # Deflate: doesn't use dictionary size parameter ++ cmd.append("-m0=Deflate") + else: + # Fallback: use LZMA2 -+ cmd.append("-m0=LZMA2") -+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") ++ cmd.append("-m0=LZMA2") ++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") - # CPU thread allocation: when there's only 1 worker, use all CPU cores + # CPU thread allocation: when there's only 1 worker, use all CPU cores ```
@@ -21701,19 +21701,19 @@ PPMd is single-threaded, so it can't use multiple cores even with `-mmt`. Switch -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 ```diff @@ -54,5 +54,5 @@ - "zipper": "7z", - "compression": 9, -- "compressionMethod": "PPMd", # Compression method: LZMA2, PPMd, BZip2, Deflate -+ "compressionMethod": "LZMA2", # Compression method: LZMA2 (multi-threaded), PPMd (single-threaded), BZip2, Deflate - "dailyFormat": "daily_YYMMDD", - "Max7zInst": 0, # Maximum concurrent 7z instances (0 = auto-calculate) + "zipper": "7z", + "compression": 9, +- "compressionMethod": "PPMd", # Compression method: LZMA2, PPMd, BZip2, Deflate ++ "compressionMethod": "LZMA2", # Compression method: LZMA2 (multi-threaded), PPMd (single-threaded), BZip2, Deflate + "dailyFormat": "daily_YYMMDD", + "Max7zInst": 0, # Maximum concurrent 7z instances (0 = auto-calculate) ```
@@ -21724,7 +21724,7 @@ Lines added: 1, lines removed: 1 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 2 @@ -21733,15 +21733,15 @@ Lines added: 2, lines removed: 2 @@ -121,9 +121,9 @@ COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL)) -- COMPRESSION_METHOD = CONFIG.get("compressionMethod", "PPMd") -+ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "LZMA2") +- COMPRESSION_METHOD = CONFIG.get("compressionMethod", "PPMd") ++ COMPRESSION_METHOD = CONFIG.get("compressionMethod", "LZMA2") # Validate compression method - valid_methods = {"LZMA2", "PPMd", "BZip2", "Deflate"} + valid_methods = {"LZMA2", "PPMd", "BZip2", "Deflate"} if COMPRESSION_METHOD not in valid_methods: -- COMPRESSION_METHOD = "PPMd" # Default to PPMd for better multi-threading -+ COMPRESSION_METHOD = "LZMA2" # Default to LZMA2 for multi-threading support +- COMPRESSION_METHOD = "PPMd" # Default to PPMd for better multi-threading ++ COMPRESSION_METHOD = "LZMA2" # Default to LZMA2 for multi-threading support - MAX_7Z_INSTANCES = CONFIG.get("Max7zInst", 0) + MAX_7Z_INSTANCES = CONFIG.get("Max7zInst", 0) ```
@@ -21752,26 +21752,26 @@ Lines added: 2, lines removed: 2 -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 7, lines removed: 2 ```diff @@ -636,7 +636,12 @@ - # CPU thread allocation: when there's only 1 worker, use all CPU cores + # CPU thread allocation: when there's only 1 worker, use all CPU cores # When there are multiple workers, use auto mode to let 7z decide -+ # Note: PPMd is single-threaded and won't benefit from -mmt ++ # Note: PPMd is single-threaded and won't benefit from -mmt cpu_cores = os.cpu_count() or 1 - if worker_count == 1: - # Single worker: use all CPU cores for maximum speed -+ if COMPRESSION_METHOD == "PPMd": -+ # PPMd is single-threaded, so -mmt won't help ++ if COMPRESSION_METHOD == "PPMd": ++ # PPMd is single-threaded, so -mmt won't help + # But we can still set it for consistency -+ cmd.append("-mmt=on") ++ cmd.append("-mmt=on") + elif worker_count == 1: + # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this) - cmd.append(f"-mmt={cpu_cores}") + cmd.append(f"-mmt={cpu_cores}") else: ``` @@ -21801,20 +21801,20 @@ Adding verbose logging to show the exact 7z command being executed: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined ```diff @@ -653,4 +653,10 @@ - f"@{list_file_abs}", + f"@{list_file_abs}", ]) + + # Log the command in verbose mode for debugging + if verbose: -+ cmd_str = " ".join(cmd) -+ log("zip", f"7z command: {cmd_str}", verbose_only=True, verbose=verbose) ++ cmd_str = " ".join(cmd) ++ log("zip", f"7z command: {cmd_str}", verbose_only=True, verbose=verbose) + result = subprocess.run( cmd, @@ -21840,7 +21840,7 @@ Model supplied an ambiguous edit -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -21848,7 +21848,7 @@ Lines added: 1, lines removed: 1 ```diff @@ -776,5 +776,5 @@ def process_zip(seq_dir: Path, zip_path: Path, state_path: Path, seq_state: dict, per_job_memory_limit: int | None, worker_count: int, *, verbose: bool) -> Sequence[Path]: - log("zip", f"{seq_dir} -> {zip_path}", verbose_only=True, verbose=verbose) + log("zip", f"{seq_dir} -> {zip_path}", verbose_only=True, verbose=verbose) - zip_sequence(seq_dir, zip_path, per_job_memory_limit, worker_count) + zip_sequence(seq_dir, zip_path, per_job_memory_limit, worker_count, verbose=verbose) state_path.write_text(json.dumps(seq_state, indent=2)) @@ -21919,7 +21919,7 @@ Output mode: content -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -21930,7 +21930,7 @@ Lines added: 1, lines removed: 1 - def zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None, worker_count: int = 1) -> None: + def zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None, worker_count: int = 1, *, verbose: bool = False) -> None: - if ZIPPER_TYPE == "7z": + if ZIPPER_TYPE == "7z": if SEVEN_Z_EXE is None: ``` @@ -22135,24 +22135,24 @@ Trying BZip2, which may parallelize better, and adding diagnostic info: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 6, lines removed: undefined ```diff @@ -619,6 +619,12 @@ - elif COMPRESSION_METHOD == "LZMA2": + elif COMPRESSION_METHOD == "LZMA2": # LZMA2: use -md for dictionary size + # For better multi-threading, we can also specify block size + # Smaller blocks = more parallelism, but slightly worse compression - cmd.append("-m0=LZMA2") - cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") + cmd.append("-m0=LZMA2") + cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") + # Use smaller solid block size for better parallelization + # -ms=on enables solid archive (default), -ms=off disables it for better threading + # Actually, for LZMA2, we want solid=off for maximum threading -+ cmd.append("-ms=off") # Disable solid archive for better multi-threading - elif COMPRESSION_METHOD == "BZip2": ++ cmd.append("-ms=off") # Disable solid archive for better multi-threading + elif COMPRESSION_METHOD == "BZip2": # BZip2: use -md for dictionary size (smaller max: 900KB) ``` @@ -22202,24 +22202,24 @@ Removing the `-ms=off` change and reverting to the previous LZMA2 configuration: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 6 ```diff @@ -619,12 +619,6 @@ - elif COMPRESSION_METHOD == "LZMA2": + elif COMPRESSION_METHOD == "LZMA2": # LZMA2: use -md for dictionary size - # For better multi-threading, we can also specify block size - # Smaller blocks = more parallelism, but slightly worse compression - cmd.append("-m0=LZMA2") - cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") + cmd.append("-m0=LZMA2") + cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") - # Use smaller solid block size for better parallelization - # -ms=on enables solid archive (default), -ms=off disables it for better threading - # Actually, for LZMA2, we want solid=off for maximum threading -- cmd.append("-ms=off") # Disable solid archive for better multi-threading - elif COMPRESSION_METHOD == "BZip2": +- cmd.append("-ms=off") # Disable solid archive for better multi-threading + elif COMPRESSION_METHOD == "BZip2": # BZip2: use -md for dictionary size (smaller max: 900KB) ``` diff --git a/.specstory/history/2025-11-17_19-23Z-sort-and-comment-gitattributes-file.md b/.specstory/history/2025-11-17_19-23Z-sort-and-comment-gitattributes-file.md index c6a7983..bf02147 100644 --- a/.specstory/history/2025-11-17_19-23Z-sort-and-comment-gitattributes-file.md +++ b/.specstory/history/2025-11-17_19-23Z-sort-and-comment-gitattributes-file.md @@ -16,7 +16,6 @@ _**Agent (model default, mode Agent)**_ --- - Organizing the gitattributes file into sections with comments and removing duplicates. Checking for duplicates first: @@ -34,7 +33,6 @@ Organizing the gitattributes file into sections with comments and removing dupli --- - Organizing into sections with comments, removing duplicates, and sorting each section alphabetically: @@ -42,7 +40,7 @@ Organizing into sections with comments, removing duplicates, and sorting each se -
Tool use: **code_edit** • Edit file: components/gitattributes +
Tool use: **write** • Edit file: components/gitattributes **Chunk 1** Lines added: 63, lines removed: 42 @@ -183,7 +181,6 @@ Lines added: 63, lines removed: 42 --- - Checking for linting errors: @@ -210,7 +207,6 @@ Lint results --- - Reorganized the gitattributes file: - **Organized into sections** with comments: diff --git a/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md b/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md index aefff28..685a4a6 100644 --- a/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md +++ b/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md @@ -130,14 +130,14 @@ Improving `cleanup_orphan_archives()` to reliably detect and remove orphaned arc -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 39, lines removed: 14 ```diff @@ -994,27 +994,52 @@ - archive_patterns = ["*.zip", "*.7z"] + archive_patterns = ["*.zip", "*.7z"] for pattern in archive_patterns: - for zip_path in ARCHIVE_ROOT.rglob(pattern): - seq_dir = sequence_dir_for(zip_path) @@ -155,10 +155,10 @@ Lines added: 39, lines removed: 14 + continue - rel = zip_path.relative_to(ARCHIVE_ROOT) -- log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) -+ # Sequence directory doesn't exist - this is an orphan archive +- log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) ++ # Sequence directory doesn't exist - this is an orphan archive + rel = zip_path.relative_to(ARCHIVE_ROOT) -+ log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) ++ log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) - zip_path.unlink(missing_ok=True) - state_path = state_path_for(zip_path) @@ -172,16 +172,16 @@ Lines added: 39, lines removed: 14 + # Remove state file if it exists + if state_path.exists(): + state_path.unlink() -+ log("zip", f"Removed orphan metadata {state_path.relative_to(ARCHIVE_ROOT)}", verbose_only=False, verbose=verbose) ++ log("zip", f"Removed orphan metadata {state_path.relative_to(ARCHIVE_ROOT)}", verbose_only=False, verbose=verbose) + + removed.append(zip_path) + except Exception as e: + # Log error but continue processing other archives + rel = zip_path.relative_to(ARCHIVE_ROOT) if zip_path.is_relative_to(ARCHIVE_ROOT) else zip_path -+ log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) ++ log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) + continue + except Exception as e: -+ log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) ++ log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) + continue if not removed: @@ -395,7 +395,7 @@ The error ". was unexpected at this time" is a batch syntax issue, likely from p -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -403,7 +403,7 @@ Lines added: 1, lines removed: 1 ```diff @@ -35,5 +35,5 @@ - if not "!ERR!"=="0" ( + if not "!ERR!"=="0" ( - echo Failed to update render sequence archives (exit code !ERR!). + echo Failed to update render sequence archives (exit code !ERR!^). ) @@ -428,7 +428,7 @@ Trying a more robust fix. The issue may be with `%*` expansion or error handling -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 6, lines removed: 2 @@ -436,7 +436,7 @@ Lines added: 6, lines removed: 2 ```diff @@ -35,9 +35,13 @@ - if not "!ERR!"=="0" ( + if not "!ERR!"=="0" ( - echo Failed to update render sequence archives (exit code !ERR!^). + echo Failed to update render sequence archives ^(exit code !ERR!^). ) @@ -488,19 +488,19 @@ Rewriting the error handling section to be more robust: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 4, lines removed: 6 ```diff @@ -33,15 +33,13 @@ - python "%PY_SCRIPT%" --verbose %* - set "ERR=!ERRORLEVEL!" + python "%PY_SCRIPT%" --verbose %* + set "ERR=!ERRORLEVEL!" + + popd >nul 2>&1 - if not "!ERR!"=="0" ( + if not "!ERR!"=="0" ( echo Failed to update render sequence archives ^(exit code !ERR!^). - ) - @@ -534,23 +534,23 @@ Trying a more explicit error handling approach: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 2, lines removed: 2 ```diff @@ -31,10 +31,10 @@ - pushd "%PROJ_ROOT%" >nul 2>&1 + pushd "%PROJ_ROOT%" >nul 2>&1 -- python "%PY_SCRIPT%" --verbose %* -+ call python "%PY_SCRIPT%" --verbose %* - set "ERR=!ERRORLEVEL!" +- python "%PY_SCRIPT%" --verbose %* ++ call python "%PY_SCRIPT%" --verbose %* + set "ERR=!ERRORLEVEL!" popd >nul 2>&1 -- if not "!ERR!"=="0" ( -+ if "!ERR!" NEQ "0" ( +- if not "!ERR!"=="0" ( ++ if "!ERR!" NEQ "0" ( echo Failed to update render sequence archives ^(exit code !ERR!^). exit /b !ERR! ``` @@ -573,7 +573,7 @@ Adding error handling and ensuring the Python script path is properly trimmed: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 1, lines removed: 1 @@ -581,9 +581,9 @@ Lines added: 1, lines removed: 1 ```diff @@ -22,5 +22,5 @@ - for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ -- "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" -+ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%~I" + for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ +- "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I" ++ "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%~I" if not defined PY_SCRIPT ( ``` @@ -606,22 +606,22 @@ Making argument passing conditional to handle empty `%*`: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 5, lines removed: 1 ```diff @@ -31,5 +31,9 @@ - pushd "%PROJ_ROOT%" >nul 2>&1 + pushd "%PROJ_ROOT%" >nul 2>&1 -- call python "%PY_SCRIPT%" --verbose %* -+ if "%~1"=="" ( -+ call python "%PY_SCRIPT%" --verbose +- call python "%PY_SCRIPT%" --verbose %* ++ if "%~1"=="" ( ++ call python "%PY_SCRIPT%" --verbose + ) else ( -+ call python "%PY_SCRIPT%" --verbose %* ++ call python "%PY_SCRIPT%" --verbose %* + ) - set "ERR=!ERRORLEVEL!" + set "ERR=!ERRORLEVEL!" ``` @@ -643,26 +643,26 @@ Simplifying to a more standard approach: -
Tool use: **code_edit** • Edit file: ZipSeqArchv.bat +
Tool use: **search_replace** • Edit file: ZipSeqArchv.bat **Chunk 1** Lines added: 2, lines removed: 6 ```diff @@ -31,14 +31,10 @@ - pushd "%PROJ_ROOT%" >nul 2>&1 + pushd "%PROJ_ROOT%" >nul 2>&1 -- if "%~1"=="" ( -- call python "%PY_SCRIPT%" --verbose +- if "%~1"=="" ( +- call python "%PY_SCRIPT%" --verbose - ) else ( -- call python "%PY_SCRIPT%" --verbose %* +- call python "%PY_SCRIPT%" --verbose %* - ) -+ python "%PY_SCRIPT%" --verbose %* - set "ERR=!ERRORLEVEL!" ++ python "%PY_SCRIPT%" --verbose %* + set "ERR=!ERRORLEVEL!" popd >nul 2>&1 -- if "!ERR!" NEQ "0" ( +- if "!ERR!" NEQ "0" ( + if !ERR! NEQ 0 ( echo Failed to update render sequence archives ^(exit code !ERR!^). exit /b !ERR! @@ -852,7 +852,7 @@ Improving the cleanup function to resolve paths absolutely and add better loggin -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 45, lines removed: 18 @@ -861,7 +861,7 @@ Lines added: 45, lines removed: 18 @@ -987,56 +987,83 @@ def cleanup_orphan_archives(*, verbose: bool) -> int: if not ARCHIVE_ROOT.exists(): -+ log("zip", "Archive root does not exist; nothing to clean up.", verbose_only=True, verbose=verbose) ++ log("zip", "Archive root does not exist; nothing to clean up.", verbose_only=True, verbose=verbose) return 0 removed: list[Path] = [] @@ -871,10 +871,10 @@ Lines added: 45, lines removed: 18 + archive_root_abs = ARCHIVE_ROOT.resolve() + render_root_abs = RENDER_ROOT.resolve() + -+ log("zip", f"Scanning for orphan archives in {archive_root_abs}", verbose_only=True, verbose=verbose) ++ log("zip", f"Scanning for orphan archives in {archive_root_abs}", verbose_only=True, verbose=verbose) + # Look for both .zip and .7z archives - archive_patterns = ["*.zip", "*.7z"] + archive_patterns = ["*.zip", "*.7z"] for pattern in archive_patterns: try: - for zip_path in ARCHIVE_ROOT.rglob(pattern): @@ -895,30 +895,30 @@ Lines added: 45, lines removed: 18 # Check if sequence directory exists and is actually a directory - if seq_dir.exists() and seq_dir.is_dir(): + if seq_dir_abs.exists() and seq_dir_abs.is_dir(): -+ log("zip", f"Archive {zip_path_abs.relative_to(archive_root_abs)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) ++ log("zip", f"Archive {zip_path_abs.relative_to(archive_root_abs)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) continue - # Sequence directory doesn't exist - this is an orphan archive + # Sequence directory doesn't exist - this is an orphan archive - rel = zip_path.relative_to(ARCHIVE_ROOT) + rel = zip_path_abs.relative_to(archive_root_abs) - log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) + log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) # Remove archive file - if zip_path.exists(): - zip_path.unlink() + if zip_path_abs.exists(): + zip_path_abs.unlink() -+ log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) ++ log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) # Remove state file if it exists - if state_path.exists(): - state_path.unlink() -- log("zip", f"Removed orphan metadata {state_path.relative_to(ARCHIVE_ROOT)}", verbose_only=False, verbose=verbose) +- log("zip", f"Removed orphan metadata {state_path.relative_to(ARCHIVE_ROOT)}", verbose_only=False, verbose=verbose) + state_path_abs = state_path.resolve() + if state_path_abs.exists(): + state_path_abs.unlink() + state_rel = state_path_abs.relative_to(archive_root_abs) -+ log("zip", f"Removed orphan metadata {state_rel}", verbose_only=False, verbose=verbose) ++ log("zip", f"Removed orphan metadata {state_rel}", verbose_only=False, verbose=verbose) - removed.append(zip_path) + removed.append(zip_path_abs) @@ -929,18 +929,18 @@ Lines added: 45, lines removed: 18 + rel = zip_path_abs.relative_to(archive_root_abs) if zip_path_abs.is_relative_to(archive_root_abs) else zip_path_abs + except: + rel = zip_path_abs - log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) + log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) + import traceback -+ log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) ++ log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) continue except Exception as e: - log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) + log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) + import traceback -+ log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) ++ log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) continue if not removed: -+ log("zip", "No orphan archives found.", verbose_only=True, verbose=verbose) ++ log("zip", "No orphan archives found.", verbose_only=True, verbose=verbose) return 0 # Clean up empty parent directories @@ -988,7 +988,7 @@ Fixing a bug: `sequence_dir_for()` expects a path relative to `ARCHIVE_ROOT`, bu -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 16, lines removed: 1 @@ -1015,7 +1015,7 @@ Lines added: 16, lines removed: 1 + else: + # Fallback: construct path manually + rel = zip_path_abs.relative_to(archive_root_abs) -+ seq_dir = (render_root_abs / rel).with_suffix("") ++ seq_dir = (render_root_abs / rel).with_suffix("") # Resolve sequence directory to absolute for consistent checking ``` @@ -1027,20 +1027,20 @@ Lines added: 3, lines removed: 4 @@ -1016,15 +1031,14 @@ # Check if sequence directory exists and is actually a directory if seq_dir_abs.exists() and seq_dir_abs.is_dir(): -- log("zip", f"Archive {zip_path_abs.relative_to(archive_root_abs)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) -+ log("zip", f"Archive {zip_path_rel} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) +- log("zip", f"Archive {zip_path_abs.relative_to(archive_root_abs)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) ++ log("zip", f"Archive {zip_path_rel} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) continue - # Sequence directory doesn't exist - this is an orphan archive + # Sequence directory doesn't exist - this is an orphan archive - rel = zip_path_abs.relative_to(archive_root_abs) -- log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) -+ log("zip", f"Removing orphan archive {zip_path_rel}", verbose_only=False, verbose=verbose) +- log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) ++ log("zip", f"Removing orphan archive {zip_path_rel}", verbose_only=False, verbose=verbose) # Remove archive file if zip_path_abs.exists(): zip_path_abs.unlink() -- log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) -+ log("zip", f"Deleted archive: {zip_path_rel}", verbose_only=True, verbose=verbose) +- log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) ++ log("zip", f"Deleted archive: {zip_path_rel}", verbose_only=True, verbose=verbose) # Remove state file if it exists ``` @@ -1063,7 +1063,7 @@ Simplifying: `sequence_dir_for()` uses the relative `ARCHIVE_ROOT`. Using the re -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 14, lines removed: 31 @@ -1076,11 +1076,11 @@ Lines added: 14, lines removed: 31 - archive_root_abs = ARCHIVE_ROOT.resolve() - render_root_abs = RENDER_ROOT.resolve() - -- log("zip", f"Scanning for orphan archives in {archive_root_abs}", verbose_only=True, verbose=verbose) -+ log("zip", f"Scanning for orphan archives in {ARCHIVE_ROOT.resolve()}", verbose_only=True, verbose=verbose) +- log("zip", f"Scanning for orphan archives in {archive_root_abs}", verbose_only=True, verbose=verbose) ++ log("zip", f"Scanning for orphan archives in {ARCHIVE_ROOT.resolve()}", verbose_only=True, verbose=verbose) # Look for both .zip and .7z archives - archive_patterns = ["*.zip", "*.7z"] + archive_patterns = ["*.zip", "*.7z"] for pattern in archive_patterns: try: - for zip_path in archive_root_abs.rglob(pattern): @@ -1107,7 +1107,7 @@ Lines added: 14, lines removed: 31 - else: - # Fallback: construct path manually - rel = zip_path_abs.relative_to(archive_root_abs) -- seq_dir = (render_root_abs / rel).with_suffix("") +- seq_dir = (render_root_abs / rel).with_suffix("") - - # Resolve sequence directory to absolute for consistent checking + state_path = state_path_for(zip_path) @@ -1120,20 +1120,20 @@ Lines added: 14, lines removed: 31 # Check if sequence directory exists and is actually a directory if seq_dir_abs.exists() and seq_dir_abs.is_dir(): -- log("zip", f"Archive {zip_path_rel} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) -+ log("zip", f"Archive {zip_path.relative_to(ARCHIVE_ROOT)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) +- log("zip", f"Archive {zip_path_rel} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) ++ log("zip", f"Archive {zip_path.relative_to(ARCHIVE_ROOT)} has matching sequence directory; keeping", verbose_only=True, verbose=verbose) continue - # Sequence directory doesn't exist - this is an orphan archive -- log("zip", f"Removing orphan archive {zip_path_rel}", verbose_only=False, verbose=verbose) + # Sequence directory doesn't exist - this is an orphan archive +- log("zip", f"Removing orphan archive {zip_path_rel}", verbose_only=False, verbose=verbose) + rel = zip_path.relative_to(ARCHIVE_ROOT) -+ log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) ++ log("zip", f"Removing orphan archive {rel}", verbose_only=False, verbose=verbose) # Remove archive file if zip_path_abs.exists(): zip_path_abs.unlink() -- log("zip", f"Deleted archive: {zip_path_rel}", verbose_only=True, verbose=verbose) -+ log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) +- log("zip", f"Deleted archive: {zip_path_rel}", verbose_only=True, verbose=verbose) ++ log("zip", f"Deleted archive: {rel}", verbose_only=True, verbose=verbose) # Remove state file if it exists - state_path_abs = state_path.resolve() @@ -1141,7 +1141,7 @@ Lines added: 14, lines removed: 31 state_path_abs.unlink() - state_rel = state_path_abs.relative_to(archive_root_abs) + state_rel = state_path.relative_to(ARCHIVE_ROOT) - log("zip", f"Removed orphan metadata {state_rel}", verbose_only=False, verbose=verbose) + log("zip", f"Removed orphan metadata {state_rel}", verbose_only=False, verbose=verbose) ``` @@ -1157,7 +1157,7 @@ Lines added: 2, lines removed: 2 except: - rel = zip_path_abs + rel = zip_path - log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) + log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) import traceback ``` @@ -1234,7 +1234,7 @@ Adding traceback to the imports: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined @@ -1256,7 +1256,7 @@ Lines added: 1, lines removed: undefined -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: undefined, lines removed: 2 @@ -1264,14 +1264,14 @@ Lines added: undefined, lines removed: 2 ```diff @@ -1041,10 +1041,8 @@ rel = zip_path - log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) + log("zip", f"Error processing archive {rel}: {e}", verbose_only=True, verbose=verbose) - import traceback - log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) + log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) continue except Exception as e: - log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) + log("zip", f"Error scanning for {pattern} archives: {e}", verbose_only=True, verbose=verbose) - import traceback - log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) + log("zip", f"Traceback: {traceback.format_exc()}", verbose_only=True, verbose=verbose) continue ``` @@ -1396,7 +1396,7 @@ _**Agent (model gpt-5.1-codex, mode Agent)**_ --- -
Tool use: **command** • Run command: cd "A:/1 Amazon_Active_Projects/3 ProjectStructure" && dir +
Tool use: **run_terminal_cmd** • Run command: cd "A:/1 Amazon_Active_Projects/3 ProjectStructure" && dir ```bash cd "A:/1 Amazon_Active_Projects/3 ProjectStructure" && dir @@ -1451,36 +1451,36 @@ Parameters: ```json { - "relativeWorkspacePath": "zip_sequences.py" + "relativeWorkspacePath": "zip_sequences.py" } ``` Additional data: ```json { - "codeblockId": "1974fc26-4703-42ed-9c78-5f5dbe1f53a1" + "codeblockId": "1974fc26-4703-42ed-9c78-5f5dbe1f53a1" } ``` Result: ```json { - "diff": { - "chunks": [ + "diff": { + "chunks": [ { - "diffString": " f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n+ removed = cleanup_orphan_archives(verbose=verbose)\r\n+ if removed:\r\n+ log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n return 0\r\n \r", - "oldStart": 890, - "newStart": 890, - "oldLines": 4, - "newLines": 7, - "linesAdded": 3 + "diffString": " f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n+ removed = cleanup_orphan_archives(verbose=verbose)\r\n+ if removed:\r\n+ log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n return 0\r\n \r", + "oldStart": 890, + "newStart": 890, + "oldLines": 4, + "newLines": 7, + "linesAdded": 3 } ], - "editor": "EDITOR_AI" + "editor": "EDITOR_AI" }, - "shouldAutoFixLints": true, - "resultForModel": "Success. Updated the following files:\nM zip_sequences.py", - "contentsAfterEdit": "#!/usr/bin/env python3\r\n\"\"\"Maintain zipped render sequences for Git hooks.\r\n\r\nDefault mode scans `Renders/`, produces ZIP archives under `Renders/_zipped/`,\r\nand stages any updated archives so commits only track compact files. Switch to\r\n`--mode expand` to inflate the tracked archives back into the ignored working\r\ndirectories after checkouts or pulls.\r\n\"\"\"\r\n\r\nfrom __future__ import annotations\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport platform\r\nimport shutil\r\nimport subprocess\r\nimport sys\r\nimport tempfile\r\nimport time\r\nimport traceback\r\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\r\nfrom pathlib import Path\r\nfrom typing import Iterator, Sequence\r\n\r\n# Try to import psutil for cross-platform RAM detection\r\ntry:\r\n import psutil\r\n HAS_PSUTIL = True\r\nexcept ImportError:\r\n HAS_PSUTIL = False\r\n # For Windows fallback\r\n if platform.system() == \"Windows\":\r\n try:\r\n import ctypes\r\n HAS_CTYPES = True\r\n except ImportError:\r\n HAS_CTYPES = False\r\n else:\r\n HAS_CTYPES = False\r\n\r\n\r\nRENDER_ROOT = Path(\"Renders\")\r\nARCHIVE_ROOT = RENDER_ROOT / \"_zipped\"\r\nSEQUENCE_EXTENSIONS = {\r\n \".png\",\r\n \".jpg\",\r\n \".jpeg\",\r\n \".tif\",\r\n \".tiff\",\r\n \".exr\",\r\n}\r\nSTATE_SUFFIX = \".meta.json\"\r\nDEFAULT_CONFIG = {\r\n \"zipper\": \"7z\",\r\n \"compression\": 9,\r\n \"compressionMethod\": \"LZMA2\", # Compression method: LZMA2 (multi-threaded), PPMd (single-threaded), BZip2, Deflate\r\n \"dailyFormat\": \"daily_YYMMDD\",\r\n \"Max7zInst\": 0, # Maximum concurrent 7z instances (0 = auto-calculate)\r\n}\r\n\r\n\r\ndef log(mode: str, message: str, *, verbose_only: bool = False, verbose: bool = False) -> None:\r\n if verbose_only and not verbose:\r\n return\r\n print(f\"[{mode}] {message}\", flush=True)\r\n\r\n\r\ndef load_config() -> dict:\r\n # First try to load from project's .config folder (current working directory)\r\n # Then fall back to ProjectStructure repo config (next to zip_sequences.py)\r\n cwd = Path.cwd()\r\n project_config = cwd / \".config\" / \"config.json\"\r\n repo_config = Path(__file__).resolve().with_name(\"config.json\")\r\n \r\n config_paths = [\r\n (\"project\", project_config),\r\n (\"repo\", repo_config),\r\n ]\r\n \r\n log(\"init\", \"Loading configuration sources...\")\r\n for source, config_path in config_paths:\r\n try:\r\n if config_path.exists():\r\n log(\"init\", f\"Reading {source} config at {config_path}\")\r\n text = config_path.read_text(encoding=\"utf-8\")\r\n try:\r\n data = json.loads(text)\r\n if isinstance(data, dict):\r\n merged = DEFAULT_CONFIG.copy()\r\n merged.update(data)\r\n log(\"init\", f\"Configuration loaded from {source}\")\r\n return merged\r\n except json.JSONDecodeError:\r\n log(\"init\", f\"Config file at {config_path} is invalid JSON; skipping\")\r\n continue\r\n except OSError:\r\n log(\"init\", f\"Unable to read config at {config_path}; skipping\")\r\n continue\r\n \r\n # If no config found, return defaults\r\n log(\"init\", \"No config files found; using default settings\")\r\n return DEFAULT_CONFIG.copy()\r\n\r\n\r\nCONFIG = load_config()\r\nzipper_val = CONFIG.get(\"zipper\", \"7z\")\r\n# Handle both old boolean format and new string format\r\nif isinstance(zipper_val, bool):\r\n ZIPPER_TYPE = \"7z\" if zipper_val else \"zip\"\r\nelse:\r\n ZIPPER_TYPE = str(zipper_val).lower()\r\n\r\nCOMPRESSION_LEVEL = CONFIG.get(\"compression\", 9)\r\nif isinstance(COMPRESSION_LEVEL, str):\r\n try:\r\n COMPRESSION_LEVEL = int(COMPRESSION_LEVEL)\r\n except ValueError:\r\n COMPRESSION_LEVEL = 9\r\nif not isinstance(COMPRESSION_LEVEL, int):\r\n COMPRESSION_LEVEL = 9\r\nCOMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL))\r\n\r\nCOMPRESSION_METHOD = CONFIG.get(\"compressionMethod\", \"LZMA2\")\r\n# Validate compression method\r\nvalid_methods = {\"LZMA2\", \"PPMd\", \"BZip2\", \"Deflate\"}\r\nif COMPRESSION_METHOD not in valid_methods:\r\n COMPRESSION_METHOD = \"LZMA2\" # Default to LZMA2 for multi-threading support\r\n\r\nMAX_7Z_INSTANCES = CONFIG.get(\"Max7zInst\", 0)\r\nif MAX_7Z_INSTANCES is not None:\r\n if isinstance(MAX_7Z_INSTANCES, str):\r\n try:\r\n MAX_7Z_INSTANCES = int(MAX_7Z_INSTANCES)\r\n except ValueError:\r\n MAX_7Z_INSTANCES = 0\r\n if not isinstance(MAX_7Z_INSTANCES, int) or MAX_7Z_INSTANCES < 1:\r\n MAX_7Z_INSTANCES = 0\r\n# Treat 0 as None (auto-calculate)\r\nif MAX_7Z_INSTANCES == 0:\r\n MAX_7Z_INSTANCES = None\r\n\r\nSEVEN_Z_EXE: str | None = None\r\nif ZIPPER_TYPE == \"7z\":\r\n SEVEN_Z_EXE = shutil.which(\"7z\") or shutil.which(\"7za\")\r\n\r\n\r\ndef parse_args() -> argparse.Namespace:\r\n parser = argparse.ArgumentParser(description=\"Sync render sequences with zipped archives.\")\r\n parser.add_argument(\r\n \"--mode\",\r\n choices=(\"zip\", \"expand\"),\r\n default=\"zip\",\r\n help=\"zip sequences for commit (default) or expand tracked archives\",\r\n )\r\n parser.add_argument(\"--jobs\", type=int, help=\"max parallel workers\")\r\n parser.add_argument(\"--verbose\", action=\"store_true\", help=\"print extra progress details\")\r\n return parser.parse_args()\r\n\r\n\r\ndef get_available_ram() -> int | None:\r\n \"\"\"Get available RAM in bytes, reserving 20% for system.\r\n \r\n Returns:\r\n Available RAM in bytes, or None if detection fails.\r\n \"\"\"\r\n try:\r\n if HAS_PSUTIL:\r\n # Use psutil for cross-platform RAM detection\r\n mem = psutil.virtual_memory()\r\n # Reserve 20% for system, use 80% for compression jobs\r\n available = int(mem.total * 0.8)\r\n return available\r\n elif HAS_CTYPES and platform.system() == \"Windows\":\r\n # Windows fallback using ctypes\r\n class MEMORYSTATUSEX(ctypes.Structure):\r\n _fields_ = [\r\n (\"dwLength\", ctypes.c_ulong),\r\n (\"dwMemoryLoad\", ctypes.c_ulong),\r\n (\"ullTotalPhys\", ctypes.c_ulonglong),\r\n (\"ullAvailPhys\", ctypes.c_ulonglong),\r\n (\"ullTotalPageFile\", ctypes.c_ulonglong),\r\n (\"ullAvailPageFile\", ctypes.c_ulonglong),\r\n (\"ullTotalVirtual\", ctypes.c_ulonglong),\r\n (\"ullAvailVirtual\", ctypes.c_ulonglong),\r\n (\"ullAvailExtendedVirtual\", ctypes.c_ulonglong),\r\n ]\r\n \r\n kernel32 = ctypes.windll.kernel32\r\n kernel32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(MEMORYSTATUSEX)]\r\n kernel32.GlobalMemoryStatusEx.restype = ctypes.c_bool\r\n \r\n mem_status = MEMORYSTATUSEX()\r\n mem_status.dwLength = ctypes.sizeof(MEMORYSTATUSEX)\r\n \r\n if kernel32.GlobalMemoryStatusEx(ctypes.byref(mem_status)):\r\n # Reserve 20% for system, use 80% for compression jobs\r\n available = int(mem_status.ullTotalPhys * 0.8)\r\n return available\r\n except Exception:\r\n pass\r\n \r\n return None\r\n\r\n\r\ndef estimate_ram_per_job(seq_dir: Path, seq_state: dict) -> int:\r\n \"\"\"Estimate RAM usage per compression job based on folder size.\r\n \r\n Args:\r\n seq_dir: Path to the sequence directory\r\n seq_state: State dictionary containing file information\r\n \r\n Returns:\r\n Estimated RAM usage in bytes\r\n \"\"\"\r\n # Calculate total folder size from seq_state\r\n total_bytes = sum(entry.get(\"size\", 0) for entry in seq_state.get(\"files\", []))\r\n \r\n if ZIPPER_TYPE == \"7z\":\r\n # Fixed dictionary size: 1GB (1024MB)\r\n # Actual observed usage: ~2GB per process regardless of sequence size\r\n # Use a tight estimate to allow maximum concurrency\r\n # Estimate 3GB per job (50% safety margin over observed ~2GB)\r\n base_ram = 3 * 1024 * 1024 * 1024 # 3GB base estimate\r\n \r\n # For very large sequences (>50GB), add small buffer\r\n if total_bytes > 50 * 1024 * 1024 * 1024: # >50GB\r\n # Add 1GB for very large sequences\r\n estimated_ram = base_ram + (1 * 1024 * 1024 * 1024)\r\n else:\r\n estimated_ram = base_ram\r\n \r\n return estimated_ram\r\n else:\r\n # zip compression is more memory-efficient\r\n # Conservative estimate: 1GB per job\r\n return 1024 * 1024 * 1024 # 1 GB\r\n\r\n\r\ndef max_workers(\r\n requested: int | None,\r\n work_items: list[tuple[Path, Path, Path, dict]] | None = None,\r\n *,\r\n verbose: bool = False\r\n) -> tuple[int, int | None]:\r\n \"\"\"Calculate maximum worker count based on CPU and RAM constraints.\r\n \r\n Args:\r\n requested: User-requested worker count (from --jobs)\r\n work_items: List of work items (seq_dir, zip_path, state_path, seq_state)\r\n verbose: Whether to log RAM-based calculations\r\n \r\n Returns:\r\n Tuple of (worker_count, per_job_memory_limit_bytes)\r\n per_job_memory_limit_bytes is None if not using 7z or RAM detection failed\r\n \"\"\"\r\n cpu = os.cpu_count() or 1\r\n cpu_limit = max(1, min(8, cpu))\r\n if requested and requested > 0:\r\n cpu_limit = min(requested, max(1, cpu))\r\n \r\n # If no work items provided, return CPU-based limit\r\n if work_items is None or len(work_items) == 0:\r\n return (cpu_limit, {})\r\n \r\n # Try to calculate RAM-based limit\r\n available_ram = get_available_ram()\r\n \r\n if available_ram is None:\r\n # RAM detection failed, fall back to CPU limit\r\n if verbose:\r\n log(\"zip\", \"RAM detection failed, using CPU-based worker limit\", verbose_only=True, verbose=verbose)\r\n return (cpu_limit, {})\r\n \r\n # For 7z: use fixed dictionary size and calculate workers\r\n if ZIPPER_TYPE == \"7z\":\r\n # Fixed dictionary size: 1GB (1024MB)\r\n FIXED_DICT_SIZE_MB = 1024\r\n fixed_dict_size_bytes = FIXED_DICT_SIZE_MB * 1024 * 1024\r\n \r\n # Check if Max7zInst is configured\r\n if MAX_7Z_INSTANCES is not None:\r\n # Use configured maximum instances, but still respect user's --jobs and work items\r\n final_limit = MAX_7Z_INSTANCES\r\n num_work_items = len(work_items) if work_items else 0\r\n if num_work_items > 0:\r\n final_limit = min(final_limit, num_work_items)\r\n if requested and requested > 0:\r\n final_limit = min(final_limit, requested)\r\n \r\n # Create RAM limits dict (all use fixed dict size, but return as dict for consistency)\r\n ram_limits_dict: dict[Path, int] = {}\r\n if work_items:\r\n for seq_dir, _, _, _ in work_items:\r\n ram_limits_dict[seq_dir] = fixed_dict_size_bytes\r\n \r\n if verbose:\r\n log(\r\n \"zip\",\r\n f\"Using Max7zInst={MAX_7Z_INSTANCES} from config → \"\r\n f\"work items: {num_work_items}, requested: {requested}, final: {final_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n return (final_limit, ram_limits_dict)\r\n \r\n # Auto-calculate based on RAM if Max7zInst not configured\r\n # Prioritize maximum concurrency: one worker per sequence when possible\r\n if available_ram is not None:\r\n # available_ram is already 80% of total (20% reserved for system)\r\n # Use 95% of available RAM for compression jobs\r\n compression_ram = int(available_ram * 0.95)\r\n \r\n # Estimate RAM for each work item\r\n work_items_with_ram: list[tuple[Path, int, tuple[Path, Path, Path, dict]]] = []\r\n ram_limits_dict: dict[Path, int] = {}\r\n \r\n for seq_dir, zip_path, state_path, seq_state in work_items:\r\n try:\r\n estimated_ram = estimate_ram_per_job(seq_dir, seq_state)\r\n work_items_with_ram.append((seq_dir, estimated_ram, (seq_dir, zip_path, state_path, seq_state)))\r\n ram_limits_dict[seq_dir] = estimated_ram\r\n except Exception:\r\n # If estimation fails, use a safe default (3GB)\r\n default_ram = 3 * 1024 * 1024 * 1024 # 3GB\r\n work_items_with_ram.append((seq_dir, default_ram, (seq_dir, zip_path, state_path, seq_state)))\r\n ram_limits_dict[seq_dir] = default_ram\r\n \r\n num_work_items = len(work_items) if work_items else 0\r\n \r\n # Calculate total estimated RAM\r\n total_estimated_ram = sum(ram for _, ram, _ in work_items_with_ram)\r\n \r\n # If all sequences fit in available RAM, use one worker per sequence (maximum concurrency)\r\n if total_estimated_ram <= compression_ram:\r\n worker_count = num_work_items\r\n else:\r\n # Not all fit - use bin-packing to minimize workers\r\n # Sort by estimated RAM (largest first) for bin-packing\r\n work_items_with_ram.sort(key=lambda x: x[1], reverse=True)\r\n \r\n # Bin-packing algorithm: pack largest items first\r\n bins: list[list[tuple[Path, int, tuple[Path, Path, Path, dict]]]] = []\r\n bin_remaining: list[int] = []\r\n \r\n for seq_dir, estimated_ram, work_item in work_items_with_ram:\r\n # Try to fit in existing bin\r\n placed = False\r\n for i, remaining in enumerate(bin_remaining):\r\n if remaining >= estimated_ram:\r\n bins[i].append((seq_dir, estimated_ram, work_item))\r\n bin_remaining[i] -= estimated_ram\r\n placed = True\r\n break\r\n \r\n # If doesn't fit, create new bin\r\n if not placed:\r\n bins.append([(seq_dir, estimated_ram, work_item)])\r\n bin_remaining.append(compression_ram - estimated_ram)\r\n \r\n # Worker count is number of bins\r\n worker_count = len(bins)\r\n \r\n # Cap at number of actual work items (can't have more workers than jobs)\r\n if num_work_items > 0:\r\n worker_count = min(worker_count, num_work_items)\r\n \r\n # Respect user's --jobs if provided\r\n if requested and requested > 0:\r\n worker_count = min(worker_count, requested)\r\n \r\n if verbose:\r\n ram_gb = available_ram / (1024 ** 3)\r\n compression_ram_gb = compression_ram / (1024 ** 3)\r\n total_estimated_gb = total_estimated_ram / (1024 ** 3)\r\n log(\r\n \"zip\",\r\n f\"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%)\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n log(\r\n \"zip\",\r\n f\"Estimated RAM per sequence: {total_estimated_gb:.1f}GB total across {num_work_items} sequences\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n if total_estimated_ram <= compression_ram:\r\n log(\r\n \"zip\",\r\n f\"All sequences fit in RAM → using {worker_count} workers (one per sequence)\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n else:\r\n log(\r\n \"zip\",\r\n f\"Using bin-packing: {worker_count} workers needed\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n log(\r\n \"zip\",\r\n f\"Final worker count: {worker_count} (requested: {requested})\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n # Return worker count and RAM limits dict\r\n return (worker_count, ram_limits_dict)\r\n \r\n # RAM detection failed, use a safe default (no CPU limit)\r\n default_limit = 4\r\n num_work_items = len(work_items) if work_items else 0\r\n if num_work_items > 0:\r\n default_limit = min(default_limit, num_work_items)\r\n if requested and requested > 0:\r\n default_limit = min(default_limit, requested)\r\n \r\n # Create RAM limits dict with safe defaults (12GB per job for 1GB dict)\r\n ram_limits_dict: dict[Path, int] = {}\r\n if work_items:\r\n default_ram = 12 * 1024 * 1024 * 1024 # 12GB default\r\n for seq_dir, _, _, _ in work_items:\r\n ram_limits_dict[seq_dir] = default_ram\r\n \r\n if verbose:\r\n log(\r\n \"zip\",\r\n f\"RAM detection failed and Max7zInst not set, using default worker limit → \"\r\n f\"work items: {num_work_items}, requested: {requested}, final: {default_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n return (default_limit, ram_limits_dict)\r\n \r\n # For zip compression, use existing estimation-based approach\r\n # Estimate RAM per job for each work item\r\n ram_estimates = []\r\n ram_limits_dict: dict[Path, int] = {}\r\n for seq_dir, zip_path, state_path, seq_state in work_items:\r\n try:\r\n estimated_ram = estimate_ram_per_job(seq_dir, seq_state)\r\n ram_estimates.append(estimated_ram)\r\n ram_limits_dict[seq_dir] = estimated_ram\r\n except Exception:\r\n # If estimation fails, use fallback estimate\r\n fallback_ram = 1024 * 1024 * 1024 # 1GB fallback for zip\r\n ram_estimates.append(fallback_ram)\r\n ram_limits_dict[seq_dir] = fallback_ram\r\n \r\n if not ram_estimates:\r\n return (cpu_limit, {})\r\n \r\n max_ram_per_job = max(ram_estimates)\r\n ram_limit = max(1, available_ram // max_ram_per_job)\r\n ram_limit = min(ram_limit, 6) # Conservative limit for zip\r\n final_limit = min(cpu_limit, ram_limit)\r\n \r\n if verbose:\r\n ram_gb = available_ram / (1024 ** 3)\r\n max_ram_gb = max_ram_per_job / (1024 ** 3)\r\n log(\r\n \"zip\",\r\n f\"RAM: {ram_gb:.1f}GB available, ~{max_ram_gb:.1f}GB per job → \"\r\n f\"RAM limit: {ram_limit}, CPU limit: {cpu_limit}, final: {final_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n return (final_limit, ram_limits_dict)\r\n\r\n\r\ndef is_archive_path(path: Path) -> bool:\r\n return any(part in (\"_archive\", \"_CURRENT\") for part in path.parts)\r\n\r\n\r\ndef find_sequence_dirs(root: Path) -> Iterator[Path]:\r\n for dirpath, dirnames, filenames in os.walk(root):\r\n path = Path(dirpath)\r\n dirnames[:] = [d for d in dirnames if d not in (\"_archive\", \"_CURRENT\")]\r\n if is_archive_path(path):\r\n continue\r\n has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames)\r\n if has_frames:\r\n yield path\r\n\r\n\r\ndef iter_sequence_files(seq_dir: Path) -> Iterator[Path]:\r\n for dirpath, dirnames, filenames in os.walk(seq_dir):\r\n path = Path(dirpath)\r\n dirnames[:] = [d for d in dirnames if d not in (\"_archive\", \"_CURRENT\")]\r\n if is_archive_path(path):\r\n continue\r\n for filename in filenames:\r\n yield path / filename\r\n\r\n\r\ndef compute_state(seq_dir: Path) -> dict:\r\n entries = []\r\n files = sorted(\r\n iter_sequence_files(seq_dir),\r\n key=lambda p: p.relative_to(seq_dir).as_posix(),\r\n )\r\n for file_path in files:\r\n stat = file_path.stat()\r\n entries.append(\r\n {\r\n \"path\": file_path.relative_to(seq_dir).as_posix(),\r\n \"size\": stat.st_size,\r\n \"mtime_ns\": stat.st_mtime_ns,\r\n }\r\n )\r\n return {\"files\": entries}\r\n\r\n\r\ndef current_state(seq_dir: Path) -> dict:\r\n if not seq_dir.exists() or not seq_dir.is_dir():\r\n return {\"files\": []}\r\n return compute_state(seq_dir)\r\n\r\n\r\ndef load_state(state_path: Path) -> dict | None:\r\n if not state_path.exists():\r\n return None\r\n try:\r\n return json.loads(state_path.read_text())\r\n except json.JSONDecodeError:\r\n return None\r\n\r\n\r\ndef state_changed(seq_state: dict, stored_state: dict | None) -> bool:\r\n if stored_state is None:\r\n return True\r\n return seq_state != stored_state\r\n\r\n\r\ndef archive_path_for(seq_dir: Path) -> Path:\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n suffix = \".7z\" if ZIPPER_TYPE == \"7z\" else \".zip\"\r\n return (ARCHIVE_ROOT / rel).with_suffix(suffix)\r\n\r\n\r\ndef sequence_dir_for(zip_path: Path) -> Path:\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n return (RENDER_ROOT / rel).with_suffix(\"\")\r\n\r\n\r\ndef state_path_for(zip_path: Path) -> Path:\r\n return zip_path.with_suffix(zip_path.suffix + STATE_SUFFIX)\r\n\r\n\r\ndef zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None, worker_count: int = 1, *, verbose: bool = False) -> None:\r\n if ZIPPER_TYPE == \"7z\":\r\n if SEVEN_Z_EXE is None:\r\n raise RuntimeError(\r\n \"7z compression requested but 7z executable not found in PATH. \"\r\n \"Please install 7z (e.g., via Chocolatey: choco install 7zip) \"\r\n \"or set zipper to 'zip' in config.json\"\r\n )\r\n zip_path.parent.mkdir(parents=True, exist_ok=True)\r\n \r\n # If creating a .7z file, remove any existing .zip file for the same sequence\r\n if zip_path.suffix == \".7z\":\r\n old_zip_path = zip_path.with_suffix(\".zip\")\r\n if old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n \r\n # Build list of files to archive with relative paths\r\n file_list = []\r\n for file_path in iter_sequence_files(seq_dir):\r\n rel_path = file_path.relative_to(seq_dir).as_posix()\r\n file_list.append(rel_path)\r\n \r\n if not file_list:\r\n raise RuntimeError(f\"No files found to archive in {seq_dir}\")\r\n \r\n # Create zip in temporary location first to avoid issues with corrupted existing files\r\n temp_zip = None\r\n list_file_path = None\r\n try:\r\n # Create temporary archive file path (but don't create the file - let 7z create it)\r\n temp_zip_path = tempfile.mktemp(suffix=\".7z\", dir=zip_path.parent)\r\n temp_zip = Path(temp_zip_path)\r\n \r\n # Create list file with absolute path\r\n fd, temp_path = tempfile.mkstemp(suffix=\".lst\", text=True)\r\n list_file_path = Path(temp_path)\r\n with os.fdopen(fd, \"w\", encoding=\"utf-8\") as list_file:\r\n for rel_path in file_list:\r\n list_file.write(rel_path + \"\\n\")\r\n list_file.flush()\r\n os.fsync(list_file.fileno()) # Ensure data is written to disk\r\n # File is closed here by context manager, small delay to ensure OS releases handle\r\n time.sleep(0.1)\r\n \r\n # Use absolute paths for both list file and temp zip\r\n list_file_abs = list_file_path.resolve()\r\n temp_zip_abs = temp_zip.resolve()\r\n # Create archive in temp location first (7z will create it fresh)\r\n cmd = [\r\n SEVEN_Z_EXE,\r\n \"a\",\r\n \"-y\",\r\n \"-bb0\", # Suppress progress output\r\n f\"-mx={COMPRESSION_LEVEL}\",\r\n \"-t7z\", # Use 7z format, not zip\r\n ]\r\n \r\n # Set compression method and memory/dictionary size based on method\r\n # PPMd uses memory parameter in method string, LZMA2 uses -md flag\r\n FIXED_DICT_SIZE_MB = 1024\r\n if COMPRESSION_METHOD == \"PPMd\":\r\n # PPMd: specify memory as part of method string\r\n cmd.append(f\"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m\")\r\n elif COMPRESSION_METHOD == \"LZMA2\":\r\n # LZMA2: use -md for dictionary size\r\n cmd.append(\"-m0=LZMA2\")\r\n cmd.append(f\"-md={FIXED_DICT_SIZE_MB}m\")\r\n elif COMPRESSION_METHOD == \"BZip2\":\r\n # BZip2: use -md for dictionary size (smaller max: 900KB)\r\n max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB\r\n cmd.append(\"-m0=BZip2\")\r\n cmd.append(f\"-md={max_bzip2_dict}k\")\r\n elif COMPRESSION_METHOD == \"Deflate\":\r\n # Deflate: doesn't use dictionary size parameter\r\n cmd.append(\"-m0=Deflate\")\r\n else:\r\n # Fallback: use LZMA2\r\n cmd.append(\"-m0=LZMA2\")\r\n cmd.append(f\"-md={FIXED_DICT_SIZE_MB}m\")\r\n \r\n # CPU thread allocation: when there's only 1 worker, use all CPU cores\r\n # When there are multiple workers, use auto mode to let 7z decide\r\n # Note: PPMd is single-threaded and won't benefit from -mmt\r\n cpu_cores = os.cpu_count() or 1\r\n if COMPRESSION_METHOD == \"PPMd\":\r\n # PPMd is single-threaded, so -mmt won't help\r\n # But we can still set it for consistency\r\n cmd.append(\"-mmt=on\")\r\n elif worker_count == 1:\r\n # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)\r\n cmd.append(f\"-mmt={cpu_cores}\")\r\n else:\r\n # Multiple workers: use auto mode (7z will manage threads)\r\n cmd.append(\"-mmt=on\")\r\n \r\n cmd.extend([\r\n str(temp_zip_abs),\r\n f\"@{list_file_abs}\",\r\n ])\r\n \r\n # Log the command in verbose mode for debugging\r\n if verbose:\r\n cmd_str = \" \".join(cmd)\r\n log(\"zip\", f\"7z command: {cmd_str}\", verbose_only=True, verbose=verbose)\r\n \r\n result = subprocess.run(\r\n cmd,\r\n cwd=seq_dir,\r\n check=False,\r\n stdout=subprocess.PIPE,\r\n stderr=subprocess.PIPE,\r\n text=True,\r\n )\r\n if result.returncode != 0:\r\n error_msg = result.stderr.strip() if result.stderr else \"Unknown error\"\r\n if result.stdout:\r\n error_msg += f\"\\nstdout: {result.stdout.strip()}\"\r\n raise RuntimeError(f\"7z compression failed: {error_msg}\")\r\n \r\n # Move temp zip to final location, replacing any existing file\r\n if zip_path.exists():\r\n zip_path.unlink()\r\n temp_zip.replace(zip_path)\r\n temp_zip = None # Mark as moved so we don't delete it\r\n finally:\r\n # Clean up temp zip if it wasn't moved\r\n if temp_zip and temp_zip.exists():\r\n try:\r\n temp_zip.unlink(missing_ok=True)\r\n except OSError:\r\n pass\r\n # Clean up list file, with retry in case 7z still has it open\r\n if list_file_path and list_file_path.exists():\r\n for attempt in range(3):\r\n try:\r\n list_file_path.unlink(missing_ok=True)\r\n break\r\n except PermissionError:\r\n if attempt < 2:\r\n time.sleep(0.1) # Wait 100ms before retry\r\n else:\r\n # Last attempt failed, just log and continue\r\n # The temp file will be cleaned up by the OS eventually\r\n pass\r\n return\r\n\r\n # Use zipfile (only if ZIPPER_TYPE == \"zip\")\r\n if ZIPPER_TYPE == \"zip\":\r\n from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile\r\n\r\n zip_path.parent.mkdir(parents=True, exist_ok=True)\r\n if COMPRESSION_LEVEL <= 0:\r\n compression = ZIP_STORED\r\n zip_kwargs = {}\r\n else:\r\n compression = ZIP_DEFLATED\r\n zip_kwargs = {\"compresslevel\": COMPRESSION_LEVEL}\r\n\r\n with ZipFile(zip_path, \"w\", compression=compression, **zip_kwargs) as archive:\r\n for file_path in iter_sequence_files(seq_dir):\r\n archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix())\r\n return\r\n\r\n # Unknown ZIPPER_TYPE - fail with clear error\r\n raise RuntimeError(\r\n f\"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. \"\r\n f\"Expected '7z' or 'zip'. \"\r\n f\"Config zipper value: {CONFIG.get('zipper', 'not set')!r}\"\r\n )\r\n\r\n\r\ndef expand_sequence(zip_path: Path, seq_state: dict) -> None:\r\n target_dir = sequence_dir_for(zip_path)\r\n if target_dir.exists():\r\n shutil.rmtree(target_dir)\r\n target_dir.mkdir(parents=True, exist_ok=True)\r\n\r\n if ZIPPER_TYPE == \"7z\":\r\n if SEVEN_Z_EXE is None:\r\n raise RuntimeError(\r\n \"7z extraction requested but 7z executable not found in PATH. \"\r\n \"Please install 7z or set zipper to 'zip' in config.json\"\r\n )\r\n cmd = [\r\n SEVEN_Z_EXE,\r\n \"x\",\r\n \"-y\",\r\n str(zip_path),\r\n f\"-o{target_dir}\",\r\n ]\r\n result = subprocess.run(\r\n cmd,\r\n check=False,\r\n stdout=subprocess.PIPE,\r\n stderr=subprocess.PIPE,\r\n text=True,\r\n )\r\n if result.returncode != 0:\r\n error_msg = result.stderr.strip() if result.stderr else \"Unknown error\"\r\n if result.stdout:\r\n error_msg += f\"\\nstdout: {result.stdout.strip()}\"\r\n raise RuntimeError(f\"7z extraction failed: {error_msg}\")\r\n elif ZIPPER_TYPE == \"zip\":\r\n from zipfile import ZipFile\r\n\r\n with ZipFile(zip_path, \"r\") as archive:\r\n archive.extractall(target_dir)\r\n else:\r\n raise RuntimeError(\r\n f\"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. \"\r\n f\"Expected '7z' or 'zip'. \"\r\n f\"Config zipper value: {CONFIG.get('zipper', 'not set')!r}\"\r\n )\r\n\r\n for entry in seq_state.get(\"files\", []):\r\n file_path = target_dir / entry[\"path\"]\r\n if file_path.exists():\r\n os.utime(file_path, ns=(entry[\"mtime_ns\"], entry[\"mtime_ns\"]))\r\n\r\n\r\ndef process_zip(seq_dir: Path, zip_path: Path, state_path: Path, seq_state: dict, per_job_memory_limit: int | None, worker_count: int, *, verbose: bool) -> Sequence[Path]:\r\n log(\"zip\", f\"{seq_dir} -> {zip_path}\", verbose_only=True, verbose=verbose)\r\n zip_sequence(seq_dir, zip_path, per_job_memory_limit, worker_count, verbose=verbose)\r\n state_path.write_text(json.dumps(seq_state, indent=2))\r\n return (zip_path, state_path)\r\n\r\n\r\ndef process_expand(zip_path: Path, state: dict, *, verbose: bool) -> None:\r\n log(\"expand\", f\"{zip_path} -> {sequence_dir_for(zip_path)}\", verbose_only=True, verbose=verbose)\r\n expand_sequence(zip_path, state)\r\n\r\n\r\ndef run_zip(requested_workers: int | None, *, verbose: bool) -> int:\r\n work_items: list[tuple[Path, Path, Path, dict]] = []\r\n log(\"init\", f\"Scanning sequences under {RENDER_ROOT.resolve()}\")\r\n total_scanned = 0\r\n quick_skipped = 0\r\n state_skipped = 0\r\n empty_dirs = 0\r\n queued = 0\r\n\r\n if RENDER_ROOT.exists():\r\n for seq_dir in find_sequence_dirs(RENDER_ROOT):\r\n total_scanned += 1\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n if total_scanned <= 5 or total_scanned % 10 == 0:\r\n log(\"scan\", f\"[{total_scanned}] Inspecting {rel}\")\r\n\r\n # Get the target archive path (will be .7z if ZIPPER_TYPE is \"7z\")\r\n zip_path = archive_path_for(seq_dir)\r\n state_path = state_path_for(zip_path)\r\n \r\n # Quick check: if archive exists, load stored state first (fast)\r\n stored_state = load_state(state_path)\r\n \r\n # Check if we need to upgrade from .zip to .7z\r\n old_zip_path = None\r\n old_stored_state = None\r\n if ZIPPER_TYPE == \"7z\":\r\n old_zip_path = zip_path.with_suffix(\".zip\")\r\n if old_zip_path.exists():\r\n old_state_path = state_path_for(old_zip_path)\r\n old_stored_state = load_state(old_state_path)\r\n # If old .zip exists and .7z doesn't, use old .zip's state for comparison\r\n if not zip_path.exists() and old_stored_state is not None:\r\n stored_state = old_stored_state\r\n \r\n # If .7z archive exists and we have stored state, do quick check before computing full state\r\n if zip_path.exists() and stored_state is not None:\r\n # Quick check: if directory mtime is older than archive, likely unchanged\r\n try:\r\n dir_mtime = seq_dir.stat().st_mtime_ns\r\n archive_mtime = zip_path.stat().st_mtime_ns\r\n # If directory wasn't modified since archive was created, skip state computation\r\n if dir_mtime <= archive_mtime:\r\n quick_skipped += 1\r\n if quick_skipped <= 5:\r\n log(\"scan\", f\"Skipping {rel} (unchanged since archive)\")\r\n # Still need to check for old .zip cleanup (we have .7z, so .zip is obsolete)\r\n if old_zip_path and old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n continue\r\n except OSError:\r\n # If stat fails, fall through to full state computation\r\n pass\r\n \r\n # Compute current state only if we need to\r\n seq_state = compute_state(seq_dir)\r\n if not seq_state[\"files\"]:\r\n empty_dirs += 1\r\n if empty_dirs <= 5:\r\n log(\"scan\", f\"{rel} has no files; skipping\")\r\n continue\r\n \r\n # Check if state changed\r\n if stored_state is not None and not state_changed(seq_state, stored_state):\r\n # Metadata matches stored state\r\n state_skipped += 1\r\n if state_skipped <= 5:\r\n log(\"scan\", f\"{rel} metadata unchanged; archive up to date\")\r\n if zip_path.exists():\r\n # .7z exists and is up to date, clean up old .zip if it exists\r\n if old_zip_path and old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n elif old_zip_path and old_zip_path.exists() and old_stored_state is not None:\r\n # .7z doesn't exist, but .zip exists and metadata matches\r\n # Keep the .zip file, don't create .7z\r\n continue\r\n else:\r\n # No archive exists, but state matches (shouldn't happen, but be safe)\r\n continue\r\n\r\n work_items.append((seq_dir, zip_path, state_path, seq_state))\r\n queued += 1\r\n if queued <= 5 or queued % 5 == 0:\r\n total_bytes = sum(entry.get(\"size\", 0) for entry in seq_state.get(\"files\", []))\r\n size_gb = total_bytes / (1024 ** 3)\r\n log(\"scan\", f\"Queued {rel} for compression (~{size_gb:.2f}GB) [{queued} total]\")\r\n\r\n if not work_items:\r\n if not RENDER_ROOT.exists():\r\n log(\"zip\", \"Render root 'Renders' not found; nothing to zip.\")\r\n else:\r\n log(\"zip\", \"Archives already up to date; no sequences needed zipping.\")\r\n log(\r\n \"scan\",\r\n f\"Summary: scanned {total_scanned}, quick-skipped {quick_skipped}, \"\r\n f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n removed = cleanup_orphan_archives(verbose=verbose)\r\n if removed:\r\n log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n return 0\r\n\r\n # Calculate RAM-aware worker count based on work items\r\n worker_count, ram_limits_dict = max_workers(requested_workers, work_items, verbose=verbose)\r\n log(\r\n \"init\",\r\n f\"Preparing to compress {len(work_items)} sequence(s) with {worker_count} worker(s)\",\r\n )\r\n\r\n updated_paths: list[Path] = []\r\n\r\n total = len(work_items)\r\n completed = 0\r\n\r\n with ThreadPoolExecutor(max_workers=worker_count) as executor:\r\n future_map = {\r\n executor.submit(process_zip, seq_dir, zip_path, state_path, seq_state, ram_limits_dict.get(seq_dir), worker_count, verbose=verbose): seq_dir\r\n for seq_dir, zip_path, state_path, seq_state in work_items\r\n }\r\n\r\n for future in as_completed(future_map):\r\n updated_paths.extend(future.result())\r\n completed += 1\r\n seq_dir = future_map[future]\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n log(\"zip\", f\"{completed}/{total} {rel}\")\r\n\r\n updated_count = len(updated_paths) // 2\r\n log(\"zip\", f\"Updated {updated_count} sequence archive(s).\", verbose=verbose)\r\n if updated_paths:\r\n log(\r\n \"zip\",\r\n \"Archives updated. Stage manually with `git add Renders/_zipped`, if desired.\",\r\n verbose_only=True,\r\n verbose=verbose,\r\n )\r\n log(\r\n \"scan\",\r\n f\"Summary: scanned {total_scanned}, quick-skipped {quick_skipped}, \"\r\n f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n\r\n removed = cleanup_orphan_archives(verbose=verbose)\r\n if removed:\r\n log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n\r\n return updated_count\r\n\r\n\r\ndef run_expand(worker_count: int, *, verbose: bool) -> int:\r\n if not ARCHIVE_ROOT.exists():\r\n log(\"expand\", \"No archives to expand (missing 'Renders/_zipped').\")\r\n return 0\r\n\r\n work_items: list[tuple[Path, dict]] = []\r\n\r\n # Look for both .zip and .7z archives\r\n archive_patterns = [\"*.zip\", \"*.7z\"]\r\n for pattern in archive_patterns:\r\n for zip_path in ARCHIVE_ROOT.rglob(pattern):\r\n state_path = state_path_for(zip_path)\r\n seq_state = load_state(state_path)\r\n if seq_state is None:\r\n log(\"expand\", f\"Skipping {zip_path} (missing metadata)\")\r\n continue\r\n\r\n target_dir = sequence_dir_for(zip_path)\r\n if current_state(target_dir) == seq_state:\r\n continue\r\n\r\n work_items.append((zip_path, seq_state))\r\n\r\n if not work_items:\r\n log(\"expand\", \"Working folders already match archives; nothing to expand.\")\r\n return 0\r\n\r\n total = len(work_items)\r\n completed = 0\r\n\r\n with ThreadPoolExecutor(max_workers=worker_count) as executor:\r\n future_map = {\r\n executor.submit(process_expand, zip_path, seq_state, verbose=verbose): zip_path\r\n for zip_path, seq_state in work_items\r\n }\r\n\r\n for future in as_completed(future_map):\r\n future.result()\r\n completed += 1\r\n zip_path = future_map[future]\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n log(\"expand\", f\"{completed}/{total} {rel}\")\r\n\r\n log(\"expand\", f\"Refreshed {len(work_items)} sequence folder(s).\", verbose=verbose)\r\n return len(work_items)\r\n\r\n\r\ndef cleanup_orphan_archives(*, verbose: bool) -> int:\r\n if not ARCHIVE_ROOT.exists():\r\n log(\"zip\", \"Archive root does not exist; nothing to clean up.\", verbose_only=True, verbose=verbose)\r\n return 0\r\n\r\n removed: list[Path] = []\r\n \r\n log(\"zip\", f\"Scanning for orphan archives in {ARCHIVE_ROOT.resolve()}\", verbose_only=True, verbose=verbose)\r\n\r\n # Look for both .zip and .7z archives\r\n archive_patterns = [\"*.zip\", \"*.7z\"]\r\n for pattern in archive_patterns:\r\n try:\r\n for zip_path in ARCHIVE_ROOT.rglob(pattern):\r\n try:\r\n # Resolve to absolute paths for consistent checking\r\n zip_path_abs = zip_path.resolve()\r\n \r\n # Calculate state path BEFORE checking/removing archive\r\n state_path = state_path_for(zip_path)\r\n state_path_abs = state_path.resolve()\r\n \r\n # Calculate sequence directory using sequence_dir_for\r\n # This function works with paths relative to ARCHIVE_ROOT\r\n seq_dir = sequence_dir_for(zip_path)\r\n seq_dir_abs = seq_dir.resolve()\r\n \r\n # Check if sequence directory exists and is actually a directory\r\n if seq_dir_abs.exists() and seq_dir_abs.is_dir():\r\n log(\"zip\", f\"Archive {zip_path.relative_to(ARCHIVE_ROOT)} has matching sequence directory; keeping\", verbose_only=True, verbose=verbose)\r\n continue\r\n\r\n # Sequence directory doesn't exist - this is an orphan archive\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n log(\"zip\", f\"Removing orphan archive {rel}\", verbose_only=False, verbose=verbose)\r\n\r\n # Remove archive file\r\n if zip_path_abs.exists():\r\n zip_path_abs.unlink()\r\n log(\"zip\", f\"Deleted archive: {rel}\", verbose_only=True, verbose=verbose)\r\n \r\n # Remove state file if it exists\r\n if state_path_abs.exists():\r\n state_path_abs.unlink()\r\n state_rel = state_path.relative_to(ARCHIVE_ROOT)\r\n log(\"zip\", f\"Removed orphan metadata {state_rel}\", verbose_only=False, verbose=verbose)\r\n \r\n removed.append(zip_path_abs)\r\n except Exception as e:\r\n # Log error but continue processing other archives\r\n try:\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n except:\r\n rel = zip_path\r\n log(\"zip\", f\"Error processing archive {rel}: {e}\", verbose_only=True, verbose=verbose)\r\n log(\"zip\", f\"Traceback: {traceback.format_exc()}\", verbose_only=True, verbose=verbose)\r\n continue\r\n except Exception as e:\r\n log(\"zip\", f\"Error scanning for {pattern} archives: {e}\", verbose_only=True, verbose=verbose)\r\n log(\"zip\", f\"Traceback: {traceback.format_exc()}\", verbose_only=True, verbose=verbose)\r\n continue\r\n\r\n if not removed:\r\n log(\"zip\", \"No orphan archives found.\", verbose_only=True, verbose=verbose)\r\n return 0\r\n\r\n # Clean up empty parent directories\r\n archive_root_abs = ARCHIVE_ROOT.resolve()\r\n for parent in sorted({p.resolve().parent for p in removed}, key=lambda p: len(p.parts), reverse=True):\r\n parent_resolved = parent.resolve()\r\n if not parent_resolved.exists():\r\n continue\r\n try:\r\n while parent_resolved != archive_root_abs and not any(parent_resolved.iterdir()):\r\n parent_resolved.rmdir()\r\n parent_resolved = parent_resolved.parent.resolve()\r\n except OSError:\r\n # Ignore errors when removing directories (might be in use)\r\n pass\r\n\r\n return len(removed)\r\n\r\n\r\ndef main() -> int:\r\n args = parse_args()\r\n log(\"init\", \"zip_sequences starting up...\")\r\n log(\"init\", f\"Working directory: {Path.cwd()}\")\r\n log(\"init\", f\"Mode: {args.mode}, zipper: {ZIPPER_TYPE}, jobs arg: {args.jobs or 'auto'}\")\r\n if ZIPPER_TYPE == \"7z\":\r\n exe = SEVEN_Z_EXE or \"not found\"\r\n max_inst = MAX_7Z_INSTANCES if MAX_7Z_INSTANCES is not None else \"auto\"\r\n log(\"init\", f\"7z executable: {exe}, Max7zInst: {max_inst}, method: {COMPRESSION_METHOD}, level: {COMPRESSION_LEVEL}\")\r\n\r\n if args.mode == \"expand\":\r\n # For expand mode, use simple CPU-based worker calculation\r\n workers, _ = max_workers(args.jobs, work_items=None, verbose=args.verbose)\r\n run_expand(workers, verbose=args.verbose)\r\n return 0\r\n\r\n # For zip mode, work items will be calculated in run_zip\r\n updated = run_zip(args.jobs, verbose=args.verbose)\r\n return 0 if updated >= 0 else 1\r\n\r\n\r\nif __name__ == \"__main__\":\r\n try:\r\n raise SystemExit(main())\r\n except Exception as exc: # broad to surface unexpected errors cleanly\r\n print(f\"Sequence sync failed: {exc}\", file=sys.stderr)\r\n raise\r\n\r\n" + "shouldAutoFixLints": true, + "resultForModel": "Success. Updated the following files:\nM zip_sequences.py", + "contentsAfterEdit": "#!/usr/bin/env python3\r\n\"\"\"Maintain zipped render sequences for Git hooks.\r\n\r\nDefault mode scans `Renders/`, produces ZIP archives under `Renders/_zipped/`,\r\nand stages any updated archives so commits only track compact files. Switch to\r\n`--mode expand` to inflate the tracked archives back into the ignored working\r\ndirectories after checkouts or pulls.\r\n\"\"\"\r\n\r\nfrom __future__ import annotations\r\n\r\nimport argparse\r\nimport json\r\nimport os\r\nimport platform\r\nimport shutil\r\nimport subprocess\r\nimport sys\r\nimport tempfile\r\nimport time\r\nimport traceback\r\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\r\nfrom pathlib import Path\r\nfrom typing import Iterator, Sequence\r\n\r\n# Try to import psutil for cross-platform RAM detection\r\ntry:\r\n import psutil\r\n HAS_PSUTIL = True\r\nexcept ImportError:\r\n HAS_PSUTIL = False\r\n # For Windows fallback\r\n if platform.system() == \"Windows\":\r\n try:\r\n import ctypes\r\n HAS_CTYPES = True\r\n except ImportError:\r\n HAS_CTYPES = False\r\n else:\r\n HAS_CTYPES = False\r\n\r\n\r\nRENDER_ROOT = Path(\"Renders\")\r\nARCHIVE_ROOT = RENDER_ROOT / \"_zipped\"\r\nSEQUENCE_EXTENSIONS = {\r\n \".png\",\r\n \".jpg\",\r\n \".jpeg\",\r\n \".tif\",\r\n \".tiff\",\r\n \".exr\",\r\n}\r\nSTATE_SUFFIX = \".meta.json\"\r\nDEFAULT_CONFIG = {\r\n \"zipper\": \"7z\",\r\n \"compression\": 9,\r\n \"compressionMethod\": \"LZMA2\", # Compression method: LZMA2 (multi-threaded), PPMd (single-threaded), BZip2, Deflate\r\n \"dailyFormat\": \"daily_YYMMDD\",\r\n \"Max7zInst\": 0, # Maximum concurrent 7z instances (0 = auto-calculate)\r\n}\r\n\r\n\r\ndef log(mode: str, message: str, *, verbose_only: bool = False, verbose: bool = False) -> None:\r\n if verbose_only and not verbose:\r\n return\r\n print(f\"[{mode}] {message}\", flush=True)\r\n\r\n\r\ndef load_config() -> dict:\r\n # First try to load from project's .config folder (current working directory)\r\n # Then fall back to ProjectStructure repo config (next to zip_sequences.py)\r\n cwd = Path.cwd()\r\n project_config = cwd / \".config\" / \"config.json\"\r\n repo_config = Path(__file__).resolve().with_name(\"config.json\")\r\n \r\n config_paths = [\r\n (\"project\", project_config),\r\n (\"repo\", repo_config),\r\n ]\r\n \r\n log(\"init\", \"Loading configuration sources...\")\r\n for source, config_path in config_paths:\r\n try:\r\n if config_path.exists():\r\n log(\"init\", f\"Reading {source} config at {config_path}\")\r\n text = config_path.read_text(encoding=\"utf-8\")\r\n try:\r\n data = json.loads(text)\r\n if isinstance(data, dict):\r\n merged = DEFAULT_CONFIG.copy()\r\n merged.update(data)\r\n log(\"init\", f\"Configuration loaded from {source}\")\r\n return merged\r\n except json.JSONDecodeError:\r\n log(\"init\", f\"Config file at {config_path} is invalid JSON; skipping\")\r\n continue\r\n except OSError:\r\n log(\"init\", f\"Unable to read config at {config_path}; skipping\")\r\n continue\r\n \r\n # If no config found, return defaults\r\n log(\"init\", \"No config files found; using default settings\")\r\n return DEFAULT_CONFIG.copy()\r\n\r\n\r\nCONFIG = load_config()\r\nzipper_val = CONFIG.get(\"zipper\", \"7z\")\r\n# Handle both old boolean format and new string format\r\nif isinstance(zipper_val, bool):\r\n ZIPPER_TYPE = \"7z\" if zipper_val else \"zip\"\r\nelse:\r\n ZIPPER_TYPE = str(zipper_val).lower()\r\n\r\nCOMPRESSION_LEVEL = CONFIG.get(\"compression\", 9)\r\nif isinstance(COMPRESSION_LEVEL, str):\r\n try:\r\n COMPRESSION_LEVEL = int(COMPRESSION_LEVEL)\r\n except ValueError:\r\n COMPRESSION_LEVEL = 9\r\nif not isinstance(COMPRESSION_LEVEL, int):\r\n COMPRESSION_LEVEL = 9\r\nCOMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL))\r\n\r\nCOMPRESSION_METHOD = CONFIG.get(\"compressionMethod\", \"LZMA2\")\r\n# Validate compression method\r\nvalid_methods = {\"LZMA2\", \"PPMd\", \"BZip2\", \"Deflate\"}\r\nif COMPRESSION_METHOD not in valid_methods:\r\n COMPRESSION_METHOD = \"LZMA2\" # Default to LZMA2 for multi-threading support\r\n\r\nMAX_7Z_INSTANCES = CONFIG.get(\"Max7zInst\", 0)\r\nif MAX_7Z_INSTANCES is not None:\r\n if isinstance(MAX_7Z_INSTANCES, str):\r\n try:\r\n MAX_7Z_INSTANCES = int(MAX_7Z_INSTANCES)\r\n except ValueError:\r\n MAX_7Z_INSTANCES = 0\r\n if not isinstance(MAX_7Z_INSTANCES, int) or MAX_7Z_INSTANCES < 1:\r\n MAX_7Z_INSTANCES = 0\r\n# Treat 0 as None (auto-calculate)\r\nif MAX_7Z_INSTANCES == 0:\r\n MAX_7Z_INSTANCES = None\r\n\r\nSEVEN_Z_EXE: str | None = None\r\nif ZIPPER_TYPE == \"7z\":\r\n SEVEN_Z_EXE = shutil.which(\"7z\") or shutil.which(\"7za\")\r\n\r\n\r\ndef parse_args() -> argparse.Namespace:\r\n parser = argparse.ArgumentParser(description=\"Sync render sequences with zipped archives.\")\r\n parser.add_argument(\r\n \"--mode\",\r\n choices=(\"zip\", \"expand\"),\r\n default=\"zip\",\r\n help=\"zip sequences for commit (default) or expand tracked archives\",\r\n )\r\n parser.add_argument(\"--jobs\", type=int, help=\"max parallel workers\")\r\n parser.add_argument(\"--verbose\", action=\"store_true\", help=\"print extra progress details\")\r\n return parser.parse_args()\r\n\r\n\r\ndef get_available_ram() -> int | None:\r\n \"\"\"Get available RAM in bytes, reserving 20% for system.\r\n \r\n Returns:\r\n Available RAM in bytes, or None if detection fails.\r\n \"\"\"\r\n try:\r\n if HAS_PSUTIL:\r\n # Use psutil for cross-platform RAM detection\r\n mem = psutil.virtual_memory()\r\n # Reserve 20% for system, use 80% for compression jobs\r\n available = int(mem.total * 0.8)\r\n return available\r\n elif HAS_CTYPES and platform.system() == \"Windows\":\r\n # Windows fallback using ctypes\r\n class MEMORYSTATUSEX(ctypes.Structure):\r\n _fields_ = [\r\n (\"dwLength\", ctypes.c_ulong),\r\n (\"dwMemoryLoad\", ctypes.c_ulong),\r\n (\"ullTotalPhys\", ctypes.c_ulonglong),\r\n (\"ullAvailPhys\", ctypes.c_ulonglong),\r\n (\"ullTotalPageFile\", ctypes.c_ulonglong),\r\n (\"ullAvailPageFile\", ctypes.c_ulonglong),\r\n (\"ullTotalVirtual\", ctypes.c_ulonglong),\r\n (\"ullAvailVirtual\", ctypes.c_ulonglong),\r\n (\"ullAvailExtendedVirtual\", ctypes.c_ulonglong),\r\n ]\r\n \r\n kernel32 = ctypes.windll.kernel32\r\n kernel32.GlobalMemoryStatusEx.argtypes = [ctypes.POINTER(MEMORYSTATUSEX)]\r\n kernel32.GlobalMemoryStatusEx.restype = ctypes.c_bool\r\n \r\n mem_status = MEMORYSTATUSEX()\r\n mem_status.dwLength = ctypes.sizeof(MEMORYSTATUSEX)\r\n \r\n if kernel32.GlobalMemoryStatusEx(ctypes.byref(mem_status)):\r\n # Reserve 20% for system, use 80% for compression jobs\r\n available = int(mem_status.ullTotalPhys * 0.8)\r\n return available\r\n except Exception:\r\n pass\r\n \r\n return None\r\n\r\n\r\ndef estimate_ram_per_job(seq_dir: Path, seq_state: dict) -> int:\r\n \"\"\"Estimate RAM usage per compression job based on folder size.\r\n \r\n Args:\r\n seq_dir: Path to the sequence directory\r\n seq_state: State dictionary containing file information\r\n \r\n Returns:\r\n Estimated RAM usage in bytes\r\n \"\"\"\r\n # Calculate total folder size from seq_state\r\n total_bytes = sum(entry.get(\"size\", 0) for entry in seq_state.get(\"files\", []))\r\n \r\n if ZIPPER_TYPE == \"7z\":\r\n # Fixed dictionary size: 1GB (1024MB)\r\n # Actual observed usage: ~2GB per process regardless of sequence size\r\n # Use a tight estimate to allow maximum concurrency\r\n # Estimate 3GB per job (50% safety margin over observed ~2GB)\r\n base_ram = 3 * 1024 * 1024 * 1024 # 3GB base estimate\r\n \r\n # For very large sequences (>50GB), add small buffer\r\n if total_bytes > 50 * 1024 * 1024 * 1024: # >50GB\r\n # Add 1GB for very large sequences\r\n estimated_ram = base_ram + (1 * 1024 * 1024 * 1024)\r\n else:\r\n estimated_ram = base_ram\r\n \r\n return estimated_ram\r\n else:\r\n # zip compression is more memory-efficient\r\n # Conservative estimate: 1GB per job\r\n return 1024 * 1024 * 1024 # 1 GB\r\n\r\n\r\ndef max_workers(\r\n requested: int | None,\r\n work_items: list[tuple[Path, Path, Path, dict]] | None = None,\r\n *,\r\n verbose: bool = False\r\n) -> tuple[int, int | None]:\r\n \"\"\"Calculate maximum worker count based on CPU and RAM constraints.\r\n \r\n Args:\r\n requested: User-requested worker count (from --jobs)\r\n work_items: List of work items (seq_dir, zip_path, state_path, seq_state)\r\n verbose: Whether to log RAM-based calculations\r\n \r\n Returns:\r\n Tuple of (worker_count, per_job_memory_limit_bytes)\r\n per_job_memory_limit_bytes is None if not using 7z or RAM detection failed\r\n \"\"\"\r\n cpu = os.cpu_count() or 1\r\n cpu_limit = max(1, min(8, cpu))\r\n if requested and requested > 0:\r\n cpu_limit = min(requested, max(1, cpu))\r\n \r\n # If no work items provided, return CPU-based limit\r\n if work_items is None or len(work_items) == 0:\r\n return (cpu_limit, {})\r\n \r\n # Try to calculate RAM-based limit\r\n available_ram = get_available_ram()\r\n \r\n if available_ram is None:\r\n # RAM detection failed, fall back to CPU limit\r\n if verbose:\r\n log(\"zip\", \"RAM detection failed, using CPU-based worker limit\", verbose_only=True, verbose=verbose)\r\n return (cpu_limit, {})\r\n \r\n # For 7z: use fixed dictionary size and calculate workers\r\n if ZIPPER_TYPE == \"7z\":\r\n # Fixed dictionary size: 1GB (1024MB)\r\n FIXED_DICT_SIZE_MB = 1024\r\n fixed_dict_size_bytes = FIXED_DICT_SIZE_MB * 1024 * 1024\r\n \r\n # Check if Max7zInst is configured\r\n if MAX_7Z_INSTANCES is not None:\r\n # Use configured maximum instances, but still respect user's --jobs and work items\r\n final_limit = MAX_7Z_INSTANCES\r\n num_work_items = len(work_items) if work_items else 0\r\n if num_work_items > 0:\r\n final_limit = min(final_limit, num_work_items)\r\n if requested and requested > 0:\r\n final_limit = min(final_limit, requested)\r\n \r\n # Create RAM limits dict (all use fixed dict size, but return as dict for consistency)\r\n ram_limits_dict: dict[Path, int] = {}\r\n if work_items:\r\n for seq_dir, _, _, _ in work_items:\r\n ram_limits_dict[seq_dir] = fixed_dict_size_bytes\r\n \r\n if verbose:\r\n log(\r\n \"zip\",\r\n f\"Using Max7zInst={MAX_7Z_INSTANCES} from config → \"\r\n f\"work items: {num_work_items}, requested: {requested}, final: {final_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n return (final_limit, ram_limits_dict)\r\n \r\n # Auto-calculate based on RAM if Max7zInst not configured\r\n # Prioritize maximum concurrency: one worker per sequence when possible\r\n if available_ram is not None:\r\n # available_ram is already 80% of total (20% reserved for system)\r\n # Use 95% of available RAM for compression jobs\r\n compression_ram = int(available_ram * 0.95)\r\n \r\n # Estimate RAM for each work item\r\n work_items_with_ram: list[tuple[Path, int, tuple[Path, Path, Path, dict]]] = []\r\n ram_limits_dict: dict[Path, int] = {}\r\n \r\n for seq_dir, zip_path, state_path, seq_state in work_items:\r\n try:\r\n estimated_ram = estimate_ram_per_job(seq_dir, seq_state)\r\n work_items_with_ram.append((seq_dir, estimated_ram, (seq_dir, zip_path, state_path, seq_state)))\r\n ram_limits_dict[seq_dir] = estimated_ram\r\n except Exception:\r\n # If estimation fails, use a safe default (3GB)\r\n default_ram = 3 * 1024 * 1024 * 1024 # 3GB\r\n work_items_with_ram.append((seq_dir, default_ram, (seq_dir, zip_path, state_path, seq_state)))\r\n ram_limits_dict[seq_dir] = default_ram\r\n \r\n num_work_items = len(work_items) if work_items else 0\r\n \r\n # Calculate total estimated RAM\r\n total_estimated_ram = sum(ram for _, ram, _ in work_items_with_ram)\r\n \r\n # If all sequences fit in available RAM, use one worker per sequence (maximum concurrency)\r\n if total_estimated_ram <= compression_ram:\r\n worker_count = num_work_items\r\n else:\r\n # Not all fit - use bin-packing to minimize workers\r\n # Sort by estimated RAM (largest first) for bin-packing\r\n work_items_with_ram.sort(key=lambda x: x[1], reverse=True)\r\n \r\n # Bin-packing algorithm: pack largest items first\r\n bins: list[list[tuple[Path, int, tuple[Path, Path, Path, dict]]]] = []\r\n bin_remaining: list[int] = []\r\n \r\n for seq_dir, estimated_ram, work_item in work_items_with_ram:\r\n # Try to fit in existing bin\r\n placed = False\r\n for i, remaining in enumerate(bin_remaining):\r\n if remaining >= estimated_ram:\r\n bins[i].append((seq_dir, estimated_ram, work_item))\r\n bin_remaining[i] -= estimated_ram\r\n placed = True\r\n break\r\n \r\n # If doesn't fit, create new bin\r\n if not placed:\r\n bins.append([(seq_dir, estimated_ram, work_item)])\r\n bin_remaining.append(compression_ram - estimated_ram)\r\n \r\n # Worker count is number of bins\r\n worker_count = len(bins)\r\n \r\n # Cap at number of actual work items (can't have more workers than jobs)\r\n if num_work_items > 0:\r\n worker_count = min(worker_count, num_work_items)\r\n \r\n # Respect user's --jobs if provided\r\n if requested and requested > 0:\r\n worker_count = min(worker_count, requested)\r\n \r\n if verbose:\r\n ram_gb = available_ram / (1024 ** 3)\r\n compression_ram_gb = compression_ram / (1024 ** 3)\r\n total_estimated_gb = total_estimated_ram / (1024 ** 3)\r\n log(\r\n \"zip\",\r\n f\"RAM: {ram_gb:.1f}GB available (80% of total), {compression_ram_gb:.1f}GB for compression (95%)\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n log(\r\n \"zip\",\r\n f\"Estimated RAM per sequence: {total_estimated_gb:.1f}GB total across {num_work_items} sequences\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n if total_estimated_ram <= compression_ram:\r\n log(\r\n \"zip\",\r\n f\"All sequences fit in RAM → using {worker_count} workers (one per sequence)\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n else:\r\n log(\r\n \"zip\",\r\n f\"Using bin-packing: {worker_count} workers needed\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n log(\r\n \"zip\",\r\n f\"Final worker count: {worker_count} (requested: {requested})\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n # Return worker count and RAM limits dict\r\n return (worker_count, ram_limits_dict)\r\n \r\n # RAM detection failed, use a safe default (no CPU limit)\r\n default_limit = 4\r\n num_work_items = len(work_items) if work_items else 0\r\n if num_work_items > 0:\r\n default_limit = min(default_limit, num_work_items)\r\n if requested and requested > 0:\r\n default_limit = min(default_limit, requested)\r\n \r\n # Create RAM limits dict with safe defaults (12GB per job for 1GB dict)\r\n ram_limits_dict: dict[Path, int] = {}\r\n if work_items:\r\n default_ram = 12 * 1024 * 1024 * 1024 # 12GB default\r\n for seq_dir, _, _, _ in work_items:\r\n ram_limits_dict[seq_dir] = default_ram\r\n \r\n if verbose:\r\n log(\r\n \"zip\",\r\n f\"RAM detection failed and Max7zInst not set, using default worker limit → \"\r\n f\"work items: {num_work_items}, requested: {requested}, final: {default_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n return (default_limit, ram_limits_dict)\r\n \r\n # For zip compression, use existing estimation-based approach\r\n # Estimate RAM per job for each work item\r\n ram_estimates = []\r\n ram_limits_dict: dict[Path, int] = {}\r\n for seq_dir, zip_path, state_path, seq_state in work_items:\r\n try:\r\n estimated_ram = estimate_ram_per_job(seq_dir, seq_state)\r\n ram_estimates.append(estimated_ram)\r\n ram_limits_dict[seq_dir] = estimated_ram\r\n except Exception:\r\n # If estimation fails, use fallback estimate\r\n fallback_ram = 1024 * 1024 * 1024 # 1GB fallback for zip\r\n ram_estimates.append(fallback_ram)\r\n ram_limits_dict[seq_dir] = fallback_ram\r\n \r\n if not ram_estimates:\r\n return (cpu_limit, {})\r\n \r\n max_ram_per_job = max(ram_estimates)\r\n ram_limit = max(1, available_ram // max_ram_per_job)\r\n ram_limit = min(ram_limit, 6) # Conservative limit for zip\r\n final_limit = min(cpu_limit, ram_limit)\r\n \r\n if verbose:\r\n ram_gb = available_ram / (1024 ** 3)\r\n max_ram_gb = max_ram_per_job / (1024 ** 3)\r\n log(\r\n \"zip\",\r\n f\"RAM: {ram_gb:.1f}GB available, ~{max_ram_gb:.1f}GB per job → \"\r\n f\"RAM limit: {ram_limit}, CPU limit: {cpu_limit}, final: {final_limit}\",\r\n verbose_only=True,\r\n verbose=verbose\r\n )\r\n \r\n return (final_limit, ram_limits_dict)\r\n\r\n\r\ndef is_archive_path(path: Path) -> bool:\r\n return any(part in (\"_archive\", \"_CURRENT\") for part in path.parts)\r\n\r\n\r\ndef find_sequence_dirs(root: Path) -> Iterator[Path]:\r\n for dirpath, dirnames, filenames in os.walk(root):\r\n path = Path(dirpath)\r\n dirnames[:] = [d for d in dirnames if d not in (\"_archive\", \"_CURRENT\")]\r\n if is_archive_path(path):\r\n continue\r\n has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames)\r\n if has_frames:\r\n yield path\r\n\r\n\r\ndef iter_sequence_files(seq_dir: Path) -> Iterator[Path]:\r\n for dirpath, dirnames, filenames in os.walk(seq_dir):\r\n path = Path(dirpath)\r\n dirnames[:] = [d for d in dirnames if d not in (\"_archive\", \"_CURRENT\")]\r\n if is_archive_path(path):\r\n continue\r\n for filename in filenames:\r\n yield path / filename\r\n\r\n\r\ndef compute_state(seq_dir: Path) -> dict:\r\n entries = []\r\n files = sorted(\r\n iter_sequence_files(seq_dir),\r\n key=lambda p: p.relative_to(seq_dir).as_posix(),\r\n )\r\n for file_path in files:\r\n stat = file_path.stat()\r\n entries.append(\r\n {\r\n \"path\": file_path.relative_to(seq_dir).as_posix(),\r\n \"size\": stat.st_size,\r\n \"mtime_ns\": stat.st_mtime_ns,\r\n }\r\n )\r\n return {\"files\": entries}\r\n\r\n\r\ndef current_state(seq_dir: Path) -> dict:\r\n if not seq_dir.exists() or not seq_dir.is_dir():\r\n return {\"files\": []}\r\n return compute_state(seq_dir)\r\n\r\n\r\ndef load_state(state_path: Path) -> dict | None:\r\n if not state_path.exists():\r\n return None\r\n try:\r\n return json.loads(state_path.read_text())\r\n except json.JSONDecodeError:\r\n return None\r\n\r\n\r\ndef state_changed(seq_state: dict, stored_state: dict | None) -> bool:\r\n if stored_state is None:\r\n return True\r\n return seq_state != stored_state\r\n\r\n\r\ndef archive_path_for(seq_dir: Path) -> Path:\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n suffix = \".7z\" if ZIPPER_TYPE == \"7z\" else \".zip\"\r\n return (ARCHIVE_ROOT / rel).with_suffix(suffix)\r\n\r\n\r\ndef sequence_dir_for(zip_path: Path) -> Path:\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n return (RENDER_ROOT / rel).with_suffix(\"\")\r\n\r\n\r\ndef state_path_for(zip_path: Path) -> Path:\r\n return zip_path.with_suffix(zip_path.suffix + STATE_SUFFIX)\r\n\r\n\r\ndef zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None = None, worker_count: int = 1, *, verbose: bool = False) -> None:\r\n if ZIPPER_TYPE == \"7z\":\r\n if SEVEN_Z_EXE is None:\r\n raise RuntimeError(\r\n \"7z compression requested but 7z executable not found in PATH. \"\r\n \"Please install 7z (e.g., via Chocolatey: choco install 7zip) \"\r\n \"or set zipper to 'zip' in config.json\"\r\n )\r\n zip_path.parent.mkdir(parents=True, exist_ok=True)\r\n \r\n # If creating a .7z file, remove any existing .zip file for the same sequence\r\n if zip_path.suffix == \".7z\":\r\n old_zip_path = zip_path.with_suffix(\".zip\")\r\n if old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n \r\n # Build list of files to archive with relative paths\r\n file_list = []\r\n for file_path in iter_sequence_files(seq_dir):\r\n rel_path = file_path.relative_to(seq_dir).as_posix()\r\n file_list.append(rel_path)\r\n \r\n if not file_list:\r\n raise RuntimeError(f\"No files found to archive in {seq_dir}\")\r\n \r\n # Create zip in temporary location first to avoid issues with corrupted existing files\r\n temp_zip = None\r\n list_file_path = None\r\n try:\r\n # Create temporary archive file path (but don't create the file - let 7z create it)\r\n temp_zip_path = tempfile.mktemp(suffix=\".7z\", dir=zip_path.parent)\r\n temp_zip = Path(temp_zip_path)\r\n \r\n # Create list file with absolute path\r\n fd, temp_path = tempfile.mkstemp(suffix=\".lst\", text=True)\r\n list_file_path = Path(temp_path)\r\n with os.fdopen(fd, \"w\", encoding=\"utf-8\") as list_file:\r\n for rel_path in file_list:\r\n list_file.write(rel_path + \"\\n\")\r\n list_file.flush()\r\n os.fsync(list_file.fileno()) # Ensure data is written to disk\r\n # File is closed here by context manager, small delay to ensure OS releases handle\r\n time.sleep(0.1)\r\n \r\n # Use absolute paths for both list file and temp zip\r\n list_file_abs = list_file_path.resolve()\r\n temp_zip_abs = temp_zip.resolve()\r\n # Create archive in temp location first (7z will create it fresh)\r\n cmd = [\r\n SEVEN_Z_EXE,\r\n \"a\",\r\n \"-y\",\r\n \"-bb0\", # Suppress progress output\r\n f\"-mx={COMPRESSION_LEVEL}\",\r\n \"-t7z\", # Use 7z format, not zip\r\n ]\r\n \r\n # Set compression method and memory/dictionary size based on method\r\n # PPMd uses memory parameter in method string, LZMA2 uses -md flag\r\n FIXED_DICT_SIZE_MB = 1024\r\n if COMPRESSION_METHOD == \"PPMd\":\r\n # PPMd: specify memory as part of method string\r\n cmd.append(f\"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m\")\r\n elif COMPRESSION_METHOD == \"LZMA2\":\r\n # LZMA2: use -md for dictionary size\r\n cmd.append(\"-m0=LZMA2\")\r\n cmd.append(f\"-md={FIXED_DICT_SIZE_MB}m\")\r\n elif COMPRESSION_METHOD == \"BZip2\":\r\n # BZip2: use -md for dictionary size (smaller max: 900KB)\r\n max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB\r\n cmd.append(\"-m0=BZip2\")\r\n cmd.append(f\"-md={max_bzip2_dict}k\")\r\n elif COMPRESSION_METHOD == \"Deflate\":\r\n # Deflate: doesn't use dictionary size parameter\r\n cmd.append(\"-m0=Deflate\")\r\n else:\r\n # Fallback: use LZMA2\r\n cmd.append(\"-m0=LZMA2\")\r\n cmd.append(f\"-md={FIXED_DICT_SIZE_MB}m\")\r\n \r\n # CPU thread allocation: when there's only 1 worker, use all CPU cores\r\n # When there are multiple workers, use auto mode to let 7z decide\r\n # Note: PPMd is single-threaded and won't benefit from -mmt\r\n cpu_cores = os.cpu_count() or 1\r\n if COMPRESSION_METHOD == \"PPMd\":\r\n # PPMd is single-threaded, so -mmt won't help\r\n # But we can still set it for consistency\r\n cmd.append(\"-mmt=on\")\r\n elif worker_count == 1:\r\n # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)\r\n cmd.append(f\"-mmt={cpu_cores}\")\r\n else:\r\n # Multiple workers: use auto mode (7z will manage threads)\r\n cmd.append(\"-mmt=on\")\r\n \r\n cmd.extend([\r\n str(temp_zip_abs),\r\n f\"@{list_file_abs}\",\r\n ])\r\n \r\n # Log the command in verbose mode for debugging\r\n if verbose:\r\n cmd_str = \" \".join(cmd)\r\n log(\"zip\", f\"7z command: {cmd_str}\", verbose_only=True, verbose=verbose)\r\n \r\n result = subprocess.run(\r\n cmd,\r\n cwd=seq_dir,\r\n check=False,\r\n stdout=subprocess.PIPE,\r\n stderr=subprocess.PIPE,\r\n text=True,\r\n )\r\n if result.returncode != 0:\r\n error_msg = result.stderr.strip() if result.stderr else \"Unknown error\"\r\n if result.stdout:\r\n error_msg += f\"\\nstdout: {result.stdout.strip()}\"\r\n raise RuntimeError(f\"7z compression failed: {error_msg}\")\r\n \r\n # Move temp zip to final location, replacing any existing file\r\n if zip_path.exists():\r\n zip_path.unlink()\r\n temp_zip.replace(zip_path)\r\n temp_zip = None # Mark as moved so we don't delete it\r\n finally:\r\n # Clean up temp zip if it wasn't moved\r\n if temp_zip and temp_zip.exists():\r\n try:\r\n temp_zip.unlink(missing_ok=True)\r\n except OSError:\r\n pass\r\n # Clean up list file, with retry in case 7z still has it open\r\n if list_file_path and list_file_path.exists():\r\n for attempt in range(3):\r\n try:\r\n list_file_path.unlink(missing_ok=True)\r\n break\r\n except PermissionError:\r\n if attempt < 2:\r\n time.sleep(0.1) # Wait 100ms before retry\r\n else:\r\n # Last attempt failed, just log and continue\r\n # The temp file will be cleaned up by the OS eventually\r\n pass\r\n return\r\n\r\n # Use zipfile (only if ZIPPER_TYPE == \"zip\")\r\n if ZIPPER_TYPE == \"zip\":\r\n from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile\r\n\r\n zip_path.parent.mkdir(parents=True, exist_ok=True)\r\n if COMPRESSION_LEVEL <= 0:\r\n compression = ZIP_STORED\r\n zip_kwargs = {}\r\n else:\r\n compression = ZIP_DEFLATED\r\n zip_kwargs = {\"compresslevel\": COMPRESSION_LEVEL}\r\n\r\n with ZipFile(zip_path, \"w\", compression=compression, **zip_kwargs) as archive:\r\n for file_path in iter_sequence_files(seq_dir):\r\n archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix())\r\n return\r\n\r\n # Unknown ZIPPER_TYPE - fail with clear error\r\n raise RuntimeError(\r\n f\"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. \"\r\n f\"Expected '7z' or 'zip'. \"\r\n f\"Config zipper value: {CONFIG.get('zipper', 'not set')!r}\"\r\n )\r\n\r\n\r\ndef expand_sequence(zip_path: Path, seq_state: dict) -> None:\r\n target_dir = sequence_dir_for(zip_path)\r\n if target_dir.exists():\r\n shutil.rmtree(target_dir)\r\n target_dir.mkdir(parents=True, exist_ok=True)\r\n\r\n if ZIPPER_TYPE == \"7z\":\r\n if SEVEN_Z_EXE is None:\r\n raise RuntimeError(\r\n \"7z extraction requested but 7z executable not found in PATH. \"\r\n \"Please install 7z or set zipper to 'zip' in config.json\"\r\n )\r\n cmd = [\r\n SEVEN_Z_EXE,\r\n \"x\",\r\n \"-y\",\r\n str(zip_path),\r\n f\"-o{target_dir}\",\r\n ]\r\n result = subprocess.run(\r\n cmd,\r\n check=False,\r\n stdout=subprocess.PIPE,\r\n stderr=subprocess.PIPE,\r\n text=True,\r\n )\r\n if result.returncode != 0:\r\n error_msg = result.stderr.strip() if result.stderr else \"Unknown error\"\r\n if result.stdout:\r\n error_msg += f\"\\nstdout: {result.stdout.strip()}\"\r\n raise RuntimeError(f\"7z extraction failed: {error_msg}\")\r\n elif ZIPPER_TYPE == \"zip\":\r\n from zipfile import ZipFile\r\n\r\n with ZipFile(zip_path, \"r\") as archive:\r\n archive.extractall(target_dir)\r\n else:\r\n raise RuntimeError(\r\n f\"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. \"\r\n f\"Expected '7z' or 'zip'. \"\r\n f\"Config zipper value: {CONFIG.get('zipper', 'not set')!r}\"\r\n )\r\n\r\n for entry in seq_state.get(\"files\", []):\r\n file_path = target_dir / entry[\"path\"]\r\n if file_path.exists():\r\n os.utime(file_path, ns=(entry[\"mtime_ns\"], entry[\"mtime_ns\"]))\r\n\r\n\r\ndef process_zip(seq_dir: Path, zip_path: Path, state_path: Path, seq_state: dict, per_job_memory_limit: int | None, worker_count: int, *, verbose: bool) -> Sequence[Path]:\r\n log(\"zip\", f\"{seq_dir} -> {zip_path}\", verbose_only=True, verbose=verbose)\r\n zip_sequence(seq_dir, zip_path, per_job_memory_limit, worker_count, verbose=verbose)\r\n state_path.write_text(json.dumps(seq_state, indent=2))\r\n return (zip_path, state_path)\r\n\r\n\r\ndef process_expand(zip_path: Path, state: dict, *, verbose: bool) -> None:\r\n log(\"expand\", f\"{zip_path} -> {sequence_dir_for(zip_path)}\", verbose_only=True, verbose=verbose)\r\n expand_sequence(zip_path, state)\r\n\r\n\r\ndef run_zip(requested_workers: int | None, *, verbose: bool) -> int:\r\n work_items: list[tuple[Path, Path, Path, dict]] = []\r\n log(\"init\", f\"Scanning sequences under {RENDER_ROOT.resolve()}\")\r\n total_scanned = 0\r\n quick_skipped = 0\r\n state_skipped = 0\r\n empty_dirs = 0\r\n queued = 0\r\n\r\n if RENDER_ROOT.exists():\r\n for seq_dir in find_sequence_dirs(RENDER_ROOT):\r\n total_scanned += 1\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n if total_scanned <= 5 or total_scanned % 10 == 0:\r\n log(\"scan\", f\"[{total_scanned}] Inspecting {rel}\")\r\n\r\n # Get the target archive path (will be .7z if ZIPPER_TYPE is \"7z\")\r\n zip_path = archive_path_for(seq_dir)\r\n state_path = state_path_for(zip_path)\r\n \r\n # Quick check: if archive exists, load stored state first (fast)\r\n stored_state = load_state(state_path)\r\n \r\n # Check if we need to upgrade from .zip to .7z\r\n old_zip_path = None\r\n old_stored_state = None\r\n if ZIPPER_TYPE == \"7z\":\r\n old_zip_path = zip_path.with_suffix(\".zip\")\r\n if old_zip_path.exists():\r\n old_state_path = state_path_for(old_zip_path)\r\n old_stored_state = load_state(old_state_path)\r\n # If old .zip exists and .7z doesn't, use old .zip's state for comparison\r\n if not zip_path.exists() and old_stored_state is not None:\r\n stored_state = old_stored_state\r\n \r\n # If .7z archive exists and we have stored state, do quick check before computing full state\r\n if zip_path.exists() and stored_state is not None:\r\n # Quick check: if directory mtime is older than archive, likely unchanged\r\n try:\r\n dir_mtime = seq_dir.stat().st_mtime_ns\r\n archive_mtime = zip_path.stat().st_mtime_ns\r\n # If directory wasn't modified since archive was created, skip state computation\r\n if dir_mtime <= archive_mtime:\r\n quick_skipped += 1\r\n if quick_skipped <= 5:\r\n log(\"scan\", f\"Skipping {rel} (unchanged since archive)\")\r\n # Still need to check for old .zip cleanup (we have .7z, so .zip is obsolete)\r\n if old_zip_path and old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n continue\r\n except OSError:\r\n # If stat fails, fall through to full state computation\r\n pass\r\n \r\n # Compute current state only if we need to\r\n seq_state = compute_state(seq_dir)\r\n if not seq_state[\"files\"]:\r\n empty_dirs += 1\r\n if empty_dirs <= 5:\r\n log(\"scan\", f\"{rel} has no files; skipping\")\r\n continue\r\n \r\n # Check if state changed\r\n if stored_state is not None and not state_changed(seq_state, stored_state):\r\n # Metadata matches stored state\r\n state_skipped += 1\r\n if state_skipped <= 5:\r\n log(\"scan\", f\"{rel} metadata unchanged; archive up to date\")\r\n if zip_path.exists():\r\n # .7z exists and is up to date, clean up old .zip if it exists\r\n if old_zip_path and old_zip_path.exists():\r\n old_zip_path.unlink(missing_ok=True)\r\n old_state_path = state_path_for(old_zip_path)\r\n if old_state_path.exists():\r\n old_state_path.unlink(missing_ok=True)\r\n elif old_zip_path and old_zip_path.exists() and old_stored_state is not None:\r\n # .7z doesn't exist, but .zip exists and metadata matches\r\n # Keep the .zip file, don't create .7z\r\n continue\r\n else:\r\n # No archive exists, but state matches (shouldn't happen, but be safe)\r\n continue\r\n\r\n work_items.append((seq_dir, zip_path, state_path, seq_state))\r\n queued += 1\r\n if queued <= 5 or queued % 5 == 0:\r\n total_bytes = sum(entry.get(\"size\", 0) for entry in seq_state.get(\"files\", []))\r\n size_gb = total_bytes / (1024 ** 3)\r\n log(\"scan\", f\"Queued {rel} for compression (~{size_gb:.2f}GB) [{queued} total]\")\r\n\r\n if not work_items:\r\n if not RENDER_ROOT.exists():\r\n log(\"zip\", \"Render root 'Renders' not found; nothing to zip.\")\r\n else:\r\n log(\"zip\", \"Archives already up to date; no sequences needed zipping.\")\r\n log(\r\n \"scan\",\r\n f\"Summary: scanned {total_scanned}, quick-skipped {quick_skipped}, \"\r\n f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n removed = cleanup_orphan_archives(verbose=verbose)\r\n if removed:\r\n log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n return 0\r\n\r\n # Calculate RAM-aware worker count based on work items\r\n worker_count, ram_limits_dict = max_workers(requested_workers, work_items, verbose=verbose)\r\n log(\r\n \"init\",\r\n f\"Preparing to compress {len(work_items)} sequence(s) with {worker_count} worker(s)\",\r\n )\r\n\r\n updated_paths: list[Path] = []\r\n\r\n total = len(work_items)\r\n completed = 0\r\n\r\n with ThreadPoolExecutor(max_workers=worker_count) as executor:\r\n future_map = {\r\n executor.submit(process_zip, seq_dir, zip_path, state_path, seq_state, ram_limits_dict.get(seq_dir), worker_count, verbose=verbose): seq_dir\r\n for seq_dir, zip_path, state_path, seq_state in work_items\r\n }\r\n\r\n for future in as_completed(future_map):\r\n updated_paths.extend(future.result())\r\n completed += 1\r\n seq_dir = future_map[future]\r\n rel = seq_dir.relative_to(RENDER_ROOT)\r\n log(\"zip\", f\"{completed}/{total} {rel}\")\r\n\r\n updated_count = len(updated_paths) // 2\r\n log(\"zip\", f\"Updated {updated_count} sequence archive(s).\", verbose=verbose)\r\n if updated_paths:\r\n log(\r\n \"zip\",\r\n \"Archives updated. Stage manually with `git add Renders/_zipped`, if desired.\",\r\n verbose_only=True,\r\n verbose=verbose,\r\n )\r\n log(\r\n \"scan\",\r\n f\"Summary: scanned {total_scanned}, quick-skipped {quick_skipped}, \"\r\n f\"state-skipped {state_skipped}, empty {empty_dirs}, queued {queued}\",\r\n )\r\n\r\n removed = cleanup_orphan_archives(verbose=verbose)\r\n if removed:\r\n log(\"zip\", f\"Removed {removed} orphan archive(s).\", verbose=verbose)\r\n\r\n return updated_count\r\n\r\n\r\ndef run_expand(worker_count: int, *, verbose: bool) -> int:\r\n if not ARCHIVE_ROOT.exists():\r\n log(\"expand\", \"No archives to expand (missing 'Renders/_zipped').\")\r\n return 0\r\n\r\n work_items: list[tuple[Path, dict]] = []\r\n\r\n # Look for both .zip and .7z archives\r\n archive_patterns = [\"*.zip\", \"*.7z\"]\r\n for pattern in archive_patterns:\r\n for zip_path in ARCHIVE_ROOT.rglob(pattern):\r\n state_path = state_path_for(zip_path)\r\n seq_state = load_state(state_path)\r\n if seq_state is None:\r\n log(\"expand\", f\"Skipping {zip_path} (missing metadata)\")\r\n continue\r\n\r\n target_dir = sequence_dir_for(zip_path)\r\n if current_state(target_dir) == seq_state:\r\n continue\r\n\r\n work_items.append((zip_path, seq_state))\r\n\r\n if not work_items:\r\n log(\"expand\", \"Working folders already match archives; nothing to expand.\")\r\n return 0\r\n\r\n total = len(work_items)\r\n completed = 0\r\n\r\n with ThreadPoolExecutor(max_workers=worker_count) as executor:\r\n future_map = {\r\n executor.submit(process_expand, zip_path, seq_state, verbose=verbose): zip_path\r\n for zip_path, seq_state in work_items\r\n }\r\n\r\n for future in as_completed(future_map):\r\n future.result()\r\n completed += 1\r\n zip_path = future_map[future]\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n log(\"expand\", f\"{completed}/{total} {rel}\")\r\n\r\n log(\"expand\", f\"Refreshed {len(work_items)} sequence folder(s).\", verbose=verbose)\r\n return len(work_items)\r\n\r\n\r\ndef cleanup_orphan_archives(*, verbose: bool) -> int:\r\n if not ARCHIVE_ROOT.exists():\r\n log(\"zip\", \"Archive root does not exist; nothing to clean up.\", verbose_only=True, verbose=verbose)\r\n return 0\r\n\r\n removed: list[Path] = []\r\n \r\n log(\"zip\", f\"Scanning for orphan archives in {ARCHIVE_ROOT.resolve()}\", verbose_only=True, verbose=verbose)\r\n\r\n # Look for both .zip and .7z archives\r\n archive_patterns = [\"*.zip\", \"*.7z\"]\r\n for pattern in archive_patterns:\r\n try:\r\n for zip_path in ARCHIVE_ROOT.rglob(pattern):\r\n try:\r\n # Resolve to absolute paths for consistent checking\r\n zip_path_abs = zip_path.resolve()\r\n \r\n # Calculate state path BEFORE checking/removing archive\r\n state_path = state_path_for(zip_path)\r\n state_path_abs = state_path.resolve()\r\n \r\n # Calculate sequence directory using sequence_dir_for\r\n # This function works with paths relative to ARCHIVE_ROOT\r\n seq_dir = sequence_dir_for(zip_path)\r\n seq_dir_abs = seq_dir.resolve()\r\n \r\n # Check if sequence directory exists and is actually a directory\r\n if seq_dir_abs.exists() and seq_dir_abs.is_dir():\r\n log(\"zip\", f\"Archive {zip_path.relative_to(ARCHIVE_ROOT)} has matching sequence directory; keeping\", verbose_only=True, verbose=verbose)\r\n continue\r\n\r\n # Sequence directory doesn't exist - this is an orphan archive\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n log(\"zip\", f\"Removing orphan archive {rel}\", verbose_only=False, verbose=verbose)\r\n\r\n # Remove archive file\r\n if zip_path_abs.exists():\r\n zip_path_abs.unlink()\r\n log(\"zip\", f\"Deleted archive: {rel}\", verbose_only=True, verbose=verbose)\r\n \r\n # Remove state file if it exists\r\n if state_path_abs.exists():\r\n state_path_abs.unlink()\r\n state_rel = state_path.relative_to(ARCHIVE_ROOT)\r\n log(\"zip\", f\"Removed orphan metadata {state_rel}\", verbose_only=False, verbose=verbose)\r\n \r\n removed.append(zip_path_abs)\r\n except Exception as e:\r\n # Log error but continue processing other archives\r\n try:\r\n rel = zip_path.relative_to(ARCHIVE_ROOT)\r\n except:\r\n rel = zip_path\r\n log(\"zip\", f\"Error processing archive {rel}: {e}\", verbose_only=True, verbose=verbose)\r\n log(\"zip\", f\"Traceback: {traceback.format_exc()}\", verbose_only=True, verbose=verbose)\r\n continue\r\n except Exception as e:\r\n log(\"zip\", f\"Error scanning for {pattern} archives: {e}\", verbose_only=True, verbose=verbose)\r\n log(\"zip\", f\"Traceback: {traceback.format_exc()}\", verbose_only=True, verbose=verbose)\r\n continue\r\n\r\n if not removed:\r\n log(\"zip\", \"No orphan archives found.\", verbose_only=True, verbose=verbose)\r\n return 0\r\n\r\n # Clean up empty parent directories\r\n archive_root_abs = ARCHIVE_ROOT.resolve()\r\n for parent in sorted({p.resolve().parent for p in removed}, key=lambda p: len(p.parts), reverse=True):\r\n parent_resolved = parent.resolve()\r\n if not parent_resolved.exists():\r\n continue\r\n try:\r\n while parent_resolved != archive_root_abs and not any(parent_resolved.iterdir()):\r\n parent_resolved.rmdir()\r\n parent_resolved = parent_resolved.parent.resolve()\r\n except OSError:\r\n # Ignore errors when removing directories (might be in use)\r\n pass\r\n\r\n return len(removed)\r\n\r\n\r\ndef main() -> int:\r\n args = parse_args()\r\n log(\"init\", \"zip_sequences starting up...\")\r\n log(\"init\", f\"Working directory: {Path.cwd()}\")\r\n log(\"init\", f\"Mode: {args.mode}, zipper: {ZIPPER_TYPE}, jobs arg: {args.jobs or 'auto'}\")\r\n if ZIPPER_TYPE == \"7z\":\r\n exe = SEVEN_Z_EXE or \"not found\"\r\n max_inst = MAX_7Z_INSTANCES if MAX_7Z_INSTANCES is not None else \"auto\"\r\n log(\"init\", f\"7z executable: {exe}, Max7zInst: {max_inst}, method: {COMPRESSION_METHOD}, level: {COMPRESSION_LEVEL}\")\r\n\r\n if args.mode == \"expand\":\r\n # For expand mode, use simple CPU-based worker calculation\r\n workers, _ = max_workers(args.jobs, work_items=None, verbose=args.verbose)\r\n run_expand(workers, verbose=args.verbose)\r\n return 0\r\n\r\n # For zip mode, work items will be calculated in run_zip\r\n updated = run_zip(args.jobs, verbose=args.verbose)\r\n return 0 if updated >= 0 else 1\r\n\r\n\r\nif __name__ == \"__main__\":\r\n try:\r\n raise SystemExit(main())\r\n except Exception as exc: # broad to surface unexpected errors cleanly\r\n print(f\"Sequence sync failed: {exc}\", file=sys.stderr)\r\n raise\r\n\r\n" } ``` User decision: **accepted** @@ -1907,7 +1907,7 @@ Adding debug logging to see why `scab_v2.1` is being skipped. Improving the dete -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 17, lines removed: undefined @@ -1918,7 +1918,7 @@ Lines added: 17, lines removed: undefined yield path + # Also check subdirectories for sequence files (in case the directory itself has no files) + # but only if the directory is not too deep (to avoid scanning archive directories) -+ elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts): ++ elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts): + # Check if any immediate subdirectory has sequence files + for subdir in dirnames: + subdir_path = path / subdir @@ -1931,7 +1931,7 @@ Lines added: 17, lines removed: undefined + yield path + break + except (OSError, PermissionError): -+ # Skip if we can't access the subdirectory ++ # Skip if we can't access the subdirectory + continue @@ -1955,7 +1955,7 @@ Reverting that change and adding debug logging to see why it's being skipped: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 11, lines removed: 18 @@ -1968,18 +1968,18 @@ Lines added: 11, lines removed: 18 + def find_sequence_dirs(root: Path, *, verbose: bool = False) -> Iterator[Path]: for dirpath, dirnames, filenames in os.walk(root): path = Path(dirpath) - dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT")] + dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT")] if is_archive_path(path): + if verbose: + rel = path.relative_to(root) if path.is_relative_to(root) else path -+ log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose) ++ log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose) continue has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames) if has_frames: yield path - # Also check subdirectories for sequence files (in case the directory itself has no files) - # but only if the directory is not too deep (to avoid scanning archive directories) -- elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts): +- elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts): - # Check if any immediate subdirectory has sequence files - for subdir in dirnames: - subdir_path = path / subdir @@ -1992,15 +1992,15 @@ Lines added: 11, lines removed: 18 - yield path - break - except (OSError, PermissionError): -- # Skip if we can't access the subdirectory +- # Skip if we can't access the subdirectory - continue + elif verbose: -+ # Log directories that don't have sequence files for debugging ++ # Log directories that don't have sequence files for debugging + rel = path.relative_to(root) if path.is_relative_to(root) else path -+ if "scab" in path.name.lower() or "v2" in path.name.lower(): ++ if "scab" in path.name.lower() or "v2" in path.name.lower(): + # Special logging for directories that might be sequences + frame_extensions = [f for f in filenames if Path(f).suffix.lower() in SEQUENCE_EXTENSIONS] -+ log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose) ++ log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose) ``` @@ -2031,7 +2031,7 @@ Updating the call to `find_sequence_dirs` to pass the verbose flag: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: 1 @@ -2089,7 +2089,7 @@ Improving detection to handle sequences where images are in subdirectories. Upda -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 1, lines removed: undefined @@ -2098,7 +2098,7 @@ Lines added: 1, lines removed: undefined @@ -478,4 +478,5 @@ def find_sequence_dirs(root: Path, *, verbose: bool = False) -> Iterator[Path]: -+ seen_dirs = set() # Track directories we've already yielded to avoid duplicates ++ seen_dirs = set() # Track directories we've already yielded to avoid duplicates for dirpath, dirnames, filenames in os.walk(root): path = Path(dirpath) ``` @@ -2108,7 +2108,7 @@ Lines added: 19, lines removed: 3 ```diff @@ -486,14 +487,30 @@ - log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose) + log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose) continue + + # Check if this directory has sequence files directly @@ -2120,15 +2120,15 @@ Lines added: 19, lines removed: 3 + seen_dirs.add(path_resolved) + yield path elif verbose: - # Log directories that don't have sequence files for debugging + # Log directories that don't have sequence files for debugging rel = path.relative_to(root) if path.is_relative_to(root) else path -- if "scab" in path.name.lower() or "v2" in path.name.lower(): -+ if "scab" in path.name.lower(): +- if "scab" in path.name.lower() or "v2" in path.name.lower(): ++ if "scab" in path.name.lower(): # Special logging for directories that might be sequences frame_extensions = [f for f in filenames if Path(f).suffix.lower() in SEQUENCE_EXTENSIONS] -- log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose) +- log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose) + all_files = list(path.iterdir()) if path.exists() else [] -+ log("scan", f"Directory {rel}: {len(filenames)} files in dir, {len(frame_extensions)} with sequence extensions, {len(all_files)} total items", verbose_only=True, verbose=verbose) ++ log("scan", f"Directory {rel}: {len(filenames)} files in dir, {len(frame_extensions)} with sequence extensions, {len(all_files)} total items", verbose_only=True, verbose=verbose) + # Check subdirectories + for subdir in dirnames[:5]: # Check first 5 subdirs + subdir_path = path / subdir @@ -2136,7 +2136,7 @@ Lines added: 19, lines removed: 3 + subdir_files = list(subdir_path.iterdir()) if subdir_path.exists() else [] + subdir_frame_files = [f for f in subdir_files if f.is_file() and f.suffix.lower() in SEQUENCE_EXTENSIONS] + if subdir_frame_files: -+ log("scan", f" Subdirectory {subdir} has {len(subdir_frame_files)} sequence files", verbose_only=True, verbose=verbose) ++ log("scan", f" Subdirectory {subdir} has {len(subdir_frame_files)} sequence files", verbose_only=True, verbose=verbose) + except (OSError, PermissionError): + pass @@ -2340,7 +2340,7 @@ The issue is that `archive_path_for` uses `.with_suffix()`, which replaces the s -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 2, lines removed: 1 @@ -2348,10 +2348,10 @@ Lines added: 2, lines removed: 1 ```diff @@ -567,5 +567,6 @@ rel = seq_dir.relative_to(RENDER_ROOT) - suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip" + suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip" - return (ARCHIVE_ROOT / rel).with_suffix(suffix) -+ # Append suffix instead of replacing, since directory names might have dots (e.g., "scab_v2.1") -+ return ARCHIVE_ROOT / f"{rel}{suffix}" ++ # Append suffix instead of replacing, since directory names might have dots (e.g., "scab_v2.1") ++ return ARCHIVE_ROOT / f"{rel}{suffix}" ``` @@ -2390,7 +2390,7 @@ Fixing `sequence_dir_for` to handle this correctly: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 8, lines removed: 1 @@ -2399,13 +2399,13 @@ Lines added: 8, lines removed: 1 @@ -573,5 +573,12 @@ def sequence_dir_for(zip_path: Path) -> Path: rel = zip_path.relative_to(ARCHIVE_ROOT) -- return (RENDER_ROOT / rel).with_suffix("") +- return (RENDER_ROOT / rel).with_suffix("") + # Remove the archive suffix (.7z or .zip) from the end + # Handle both .7z and .zip extensions + rel_str = str(rel) -+ if rel_str.endswith(".7z"): ++ if rel_str.endswith(".7z"): + rel_str = rel_str[:-3] -+ elif rel_str.endswith(".zip"): ++ elif rel_str.endswith(".zip"): + rel_str = rel_str[:-4] + return RENDER_ROOT / rel_str @@ -2586,7 +2586,7 @@ Updating the code to use Copy (store) when compression level is 0: -
Tool use: **code_edit** • Edit file: zip_sequences.py +
Tool use: **search_replace** • Edit file: zip_sequences.py **Chunk 1** Lines added: 42, lines removed: 35 @@ -2598,81 +2598,81 @@ Lines added: 42, lines removed: 35 - # PPMd uses memory parameter in method string, LZMA2 uses -md flag + # At compression level 0, use Copy (store) method for maximum speed FIXED_DICT_SIZE_MB = 1024 -- if COMPRESSION_METHOD == "PPMd": +- if COMPRESSION_METHOD == "PPMd": - # PPMd: specify memory as part of method string -- cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") -- elif COMPRESSION_METHOD == "LZMA2": +- cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") +- elif COMPRESSION_METHOD == "LZMA2": - # LZMA2: use -md for dictionary size -- cmd.append("-m0=LZMA2") -- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") -- elif COMPRESSION_METHOD == "BZip2": +- cmd.append("-m0=LZMA2") +- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") +- elif COMPRESSION_METHOD == "BZip2": - # BZip2: use -md for dictionary size (smaller max: 900KB) - max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB -- cmd.append("-m0=BZip2") -- cmd.append(f"-md={max_bzip2_dict}k") -- elif COMPRESSION_METHOD == "Deflate": -- # Deflate: doesn't use dictionary size parameter -- cmd.append("-m0=Deflate") +- cmd.append("-m0=BZip2") +- cmd.append(f"-md={max_bzip2_dict}k") +- elif COMPRESSION_METHOD == "Deflate": +- # Deflate: doesn't use dictionary size parameter +- cmd.append("-m0=Deflate") - else: - # Fallback: use LZMA2 -- cmd.append("-m0=LZMA2") -- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") +- cmd.append("-m0=LZMA2") +- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") - -- # CPU thread allocation: when there's only 1 worker, use all CPU cores +- # CPU thread allocation: when there's only 1 worker, use all CPU cores - # When there are multiple workers, use auto mode to let 7z decide -- # Note: PPMd is single-threaded and won't benefit from -mmt +- # Note: PPMd is single-threaded and won't benefit from -mmt - cpu_cores = os.cpu_count() or 1 -- if COMPRESSION_METHOD == "PPMd": -- # PPMd is single-threaded, so -mmt won't help +- if COMPRESSION_METHOD == "PPMd": +- # PPMd is single-threaded, so -mmt won't help - # But we can still set it for consistency -- cmd.append("-mmt=on") +- cmd.append("-mmt=on") - elif worker_count == 1: - # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this) -- cmd.append(f"-mmt={cpu_cores}") +- cmd.append(f"-mmt={cpu_cores}") - else: - # Multiple workers: use auto mode (7z will manage threads) -- cmd.append("-mmt=on") +- cmd.append("-mmt=on") + if COMPRESSION_LEVEL == 0: + # Level 0 = no compression, just store files (fastest) -+ cmd.append("-m0=Copy") -+ # Copy method doesn't need threading, but enable it anyway for consistency -+ cmd.append("-mmt=on") ++ cmd.append("-m0=Copy") ++ # Copy method doesn't need threading, but enable it anyway for consistency ++ cmd.append("-mmt=on") + else: + # Compression levels 1-9: use configured compression method -+ if COMPRESSION_METHOD == "PPMd": ++ if COMPRESSION_METHOD == "PPMd": + # PPMd: specify memory as part of method string -+ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") -+ elif COMPRESSION_METHOD == "LZMA2": ++ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m") ++ elif COMPRESSION_METHOD == "LZMA2": + # LZMA2: use -md for dictionary size -+ cmd.append("-m0=LZMA2") -+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") -+ elif COMPRESSION_METHOD == "BZip2": ++ cmd.append("-m0=LZMA2") ++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") ++ elif COMPRESSION_METHOD == "BZip2": + # BZip2: use -md for dictionary size (smaller max: 900KB) + max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB -+ cmd.append("-m0=BZip2") -+ cmd.append(f"-md={max_bzip2_dict}k") -+ elif COMPRESSION_METHOD == "Deflate": -+ # Deflate: doesn't use dictionary size parameter -+ cmd.append("-m0=Deflate") ++ cmd.append("-m0=BZip2") ++ cmd.append(f"-md={max_bzip2_dict}k") ++ elif COMPRESSION_METHOD == "Deflate": ++ # Deflate: doesn't use dictionary size parameter ++ cmd.append("-m0=Deflate") + else: + # Fallback: use LZMA2 -+ cmd.append("-m0=LZMA2") -+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") ++ cmd.append("-m0=LZMA2") ++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m") + -+ # CPU thread allocation: when there's only 1 worker, use all CPU cores ++ # CPU thread allocation: when there's only 1 worker, use all CPU cores + # When there are multiple workers, use auto mode to let 7z decide -+ # Note: PPMd is single-threaded and won't benefit from -mmt ++ # Note: PPMd is single-threaded and won't benefit from -mmt + cpu_cores = os.cpu_count() or 1 -+ if COMPRESSION_METHOD == "PPMd": -+ # PPMd is single-threaded, so -mmt won't help ++ if COMPRESSION_METHOD == "PPMd": ++ # PPMd is single-threaded, so -mmt won't help + # But we can still set it for consistency -+ cmd.append("-mmt=on") ++ cmd.append("-mmt=on") + elif worker_count == 1: + # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this) -+ cmd.append(f"-mmt={cpu_cores}") ++ cmd.append(f"-mmt={cpu_cores}") + else: + # Multiple workers: use auto mode (7z will manage threads) -+ cmd.append("-mmt=on") ++ cmd.append("-mmt=on") cmd.extend([ ``` @@ -2812,7 +2812,6 @@ _**Agent (model default, mode Agent)**_ --- - Yes, it's working correctly. 1. Compression level 0 is working: the 7z command shows `-m0=Copy` instead of `-m0=LZMA2`, so it's using Copy (store) with no compression, which should be faster. diff --git a/.specstory/history/2025-11-26_20-08Z-configure-configloader-for-zipseq-and-unzipseq.md b/.specstory/history/2025-11-26_20-08Z-configure-configloader-for-zipseq-and-unzipseq.md index eb3c1d4..ea179d9 100644 --- a/.specstory/history/2025-11-26_20-08Z-configure-configloader-for-zipseq-and-unzipseq.md +++ b/.specstory/history/2025-11-26_20-08Z-configure-configloader-for-zipseq-and-unzipseq.md @@ -109,7 +109,7 @@ Adding logic to deploy `ZipSeqArchv.bat` and `UnzipSeqArchv.bat` to the project -
Tool use: **code_edit** • Edit file: ConfigLoader.ps1 +
Tool use: **search_replace** • Edit file: ConfigLoader.ps1 **Chunk 1** Lines added: 24, lines removed: undefined @@ -117,33 +117,33 @@ Lines added: 24, lines removed: undefined ```diff @@ -317,4 +317,28 @@ - Write-Host "All batch files refreshed successfully." -ForegroundColor Green + Write-Host "All batch files refreshed successfully." -ForegroundColor Green + + # Deploy ZipSeqArchv.bat and UnzipSeqArchv.bat to \Renders -+ Write-Host "`n=== Deploying ZipSeqArchv.bat and UnzipSeqArchv.bat to \Renders ===" -ForegroundColor Magenta -+ $rendersDir = Join-Path -Path $resolvedProject -ChildPath 'Renders' ++ Write-Host "`n=== Deploying ZipSeqArchv.bat and UnzipSeqArchv.bat to \Renders ===" -ForegroundColor Magenta ++ $rendersDir = Join-Path -Path $resolvedProject -ChildPath 'Renders' + if (-not (Test-Path -LiteralPath $rendersDir -PathType Container)) { + New-Item -Path $rendersDir -ItemType Directory -Force | Out-Null -+ Write-Host "Created Renders directory: $rendersDir" -ForegroundColor Cyan ++ Write-Host "Created Renders directory: $rendersDir" -ForegroundColor Cyan + } + $zipFiles = @( -+ @{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' }, -+ @{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' } ++ @{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' }, ++ @{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' } + ) + foreach ($zipFile in $zipFiles) { + $targetPath = Join-Path -Path $rendersDir -ChildPath $zipFile.Name + try { + Copy-Item -Path $zipFile.Source -Destination $targetPath -Force -+ Write-Host "[OK] $targetPath" -ForegroundColor Green ++ Write-Host "[OK] $targetPath" -ForegroundColor Green + } + catch { -+ Write-Host "[FAIL] $targetPath" -ForegroundColor Red -+ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed ++ Write-Host "[FAIL] $targetPath" -ForegroundColor Red ++ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed + exit 1 + } + } - # Deploy .gitattributes and .gitignore to project root if they don't exist + # Deploy .gitattributes and .gitignore to project root if they don't exist ```
diff --git a/components/gitattributes b/components/gitattributes index 626c87f..d8f35c7 100644 --- a/components/gitattributes +++ b/components/gitattributes @@ -50,6 +50,7 @@ # Flip Fluids cache files *.data filter=lfs diff=lfs merge=lfs -text *.sqlite3 filter=lfs diff=lfs merge=lfs -text +*.ffp3 filter=lfs diff=lfs merge=lfs -text # Substance files *.sbs filter=lfs diff=lfs merge=lfs -text