mirror of
https://github.com/hanzoui/workflow-templates-broken-zap-2026-06-08.git
synced 2026-10-30 20:44:35 +00:00
370 lines
10 KiB
Python
370 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Restore archived templates back into the active templates set.
|
|
|
|
This script:
|
|
1. Finds all templates with "status": "active" in archived/index.json
|
|
2. Moves template JSON files and media files back to templates/ folder
|
|
3. Moves i18n translations from archived/archived_i18n.json back to scripts/i18n.json
|
|
4. Moves localized index entries from archived/index.[locale].json back to templates/index.[locale].json
|
|
5. Moves main index entries from archived/index.json back to templates/index.json
|
|
|
|
Notes:
|
|
- This script does NOT modify bundles.json because bundle membership is not encoded in the index metadata.
|
|
- The restore trigger is purely metadata-driven: set a template's status to "active" in archived/index.json.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
def load_json(file_path: Path) -> Any:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
|
|
def save_json(file_path: Path, data: Any, indent: int = 2) -> None:
|
|
json_str = json.dumps(
|
|
data, ensure_ascii=False, indent=indent, separators=(',', ': ')
|
|
)
|
|
|
|
import re
|
|
|
|
def compact_array(match):
|
|
content = match.group(1)
|
|
try:
|
|
array_content = json.loads(f'[{content}]')
|
|
if all(
|
|
isinstance(item, (str, int, float, bool)) or item is None
|
|
for item in array_content
|
|
):
|
|
compacted = ', '.join(
|
|
json.dumps(item, ensure_ascii=False) for item in array_content
|
|
)
|
|
if len(compacted) < 150:
|
|
return f'[{compacted}]'
|
|
except Exception:
|
|
pass
|
|
return match.group(0)
|
|
|
|
json_str = re.sub(
|
|
r'\[\s*\n\s*([^[\]]*?)\s*\n\s*\]',
|
|
compact_array,
|
|
json_str,
|
|
flags=re.DOTALL,
|
|
)
|
|
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
f.write(json_str)
|
|
if not json_str.endswith('\n'):
|
|
f.write('\n')
|
|
|
|
|
|
def merge_i18n_data(existing: Dict, new: Dict) -> Dict:
|
|
merged = existing.copy()
|
|
for key, value in new.items():
|
|
if key not in merged:
|
|
merged[key] = value
|
|
elif isinstance(merged[key], dict) and isinstance(value, dict):
|
|
merged[key] = merge_i18n_data(merged[key], value)
|
|
return merged
|
|
|
|
|
|
def find_template_in_category(template_name: str, category: Dict) -> Optional[Dict]:
|
|
if 'templates' not in category:
|
|
return None
|
|
for template in category['templates']:
|
|
if template.get('name') == template_name:
|
|
return template
|
|
return None
|
|
|
|
|
|
def prune_empty_categories(index_data: List[Dict]) -> List[Dict]:
|
|
return [c for c in index_data if c.get('templates')]
|
|
|
|
|
|
def find_templates_by_status(
|
|
index_data: List[Dict], status: str
|
|
) -> List[Tuple[str, Dict]]:
|
|
result: List[Tuple[str, Dict]] = []
|
|
for category in index_data:
|
|
for template in category.get('templates', []):
|
|
if template.get('status') == status:
|
|
name = template.get('name')
|
|
if name:
|
|
result.append((name, template))
|
|
return result
|
|
|
|
|
|
def move_media_files(
|
|
template_name: str,
|
|
src_dir: Path,
|
|
dst_dir: Path,
|
|
dry_run: bool,
|
|
) -> bool:
|
|
ok = True
|
|
|
|
json_src = src_dir / f'{template_name}.json'
|
|
json_dst = dst_dir / f'{template_name}.json'
|
|
if json_src.exists():
|
|
if json_dst.exists():
|
|
print(f' Skip: destination exists {json_dst.name}')
|
|
ok = False
|
|
else:
|
|
if dry_run:
|
|
print(f' Would move {json_src.name} -> {dst_dir.name}/')
|
|
else:
|
|
shutil.move(str(json_src), str(json_dst))
|
|
print(f' Moved {json_src.name} -> {dst_dir.name}/')
|
|
else:
|
|
print(f' Missing: {json_src.name}')
|
|
ok = False
|
|
|
|
for ext in ['.webp', '.mp3', '.mp4']:
|
|
i = 1
|
|
while True:
|
|
media_src = src_dir / f'{template_name}-{i}{ext}'
|
|
if not media_src.exists():
|
|
break
|
|
media_dst = dst_dir / media_src.name
|
|
if media_dst.exists():
|
|
print(f' Skip: destination exists {media_dst.name}')
|
|
ok = False
|
|
else:
|
|
if dry_run:
|
|
print(f' Would move {media_src.name} -> {dst_dir.name}/')
|
|
else:
|
|
shutil.move(str(media_src), str(media_dst))
|
|
print(f' Moved {media_src.name} -> {dst_dir.name}/')
|
|
i += 1
|
|
|
|
return ok
|
|
|
|
|
|
def restore_i18n(
|
|
template_name: str,
|
|
i18n_file: Path,
|
|
archived_i18n_file: Path,
|
|
dry_run: bool,
|
|
) -> None:
|
|
if not archived_i18n_file.exists():
|
|
return
|
|
|
|
i18n_data = load_json(i18n_file) if i18n_file.exists() else {'templates': {}}
|
|
archived_i18n = load_json(archived_i18n_file)
|
|
|
|
archived_templates = archived_i18n.get('templates', {})
|
|
if template_name not in archived_templates:
|
|
return
|
|
|
|
if 'templates' not in i18n_data:
|
|
i18n_data['templates'] = {}
|
|
|
|
template_i18n = {template_name: archived_templates[template_name]}
|
|
i18n_data['templates'] = merge_i18n_data(i18n_data['templates'], template_i18n)
|
|
|
|
if dry_run:
|
|
print(' Would move i18n entry archived -> scripts/i18n.json')
|
|
return
|
|
|
|
save_json(i18n_file, i18n_data)
|
|
del archived_templates[template_name]
|
|
archived_i18n['templates'] = archived_templates
|
|
save_json(archived_i18n_file, archived_i18n)
|
|
print(' Restored i18n to scripts/i18n.json')
|
|
|
|
|
|
def move_index_entry(
|
|
template_name: str,
|
|
src_index: List[Dict],
|
|
dst_index: List[Dict],
|
|
) -> Tuple[List[Dict], List[Dict], bool]:
|
|
moved_template = None
|
|
src_category = None
|
|
|
|
for category in src_index:
|
|
template = find_template_in_category(template_name, category)
|
|
if template:
|
|
moved_template = template
|
|
src_category = category
|
|
category['templates'] = [
|
|
t for t in category.get('templates', []) if t.get('name') != template_name
|
|
]
|
|
break
|
|
|
|
if not moved_template or not src_category:
|
|
return src_index, dst_index, False
|
|
|
|
# Ensure status is active when restoring
|
|
moved_template['status'] = 'active'
|
|
|
|
found_category = False
|
|
for dst_category in dst_index:
|
|
if (
|
|
dst_category.get('moduleName') == src_category.get('moduleName')
|
|
and dst_category.get('type') == src_category.get('type')
|
|
and dst_category.get('category') == src_category.get('category')
|
|
):
|
|
if not any(
|
|
t.get('name') == template_name for t in dst_category.get('templates', [])
|
|
):
|
|
dst_category.setdefault('templates', []).append(moved_template)
|
|
found_category = True
|
|
break
|
|
|
|
if not found_category:
|
|
dst_index.append(
|
|
{
|
|
'moduleName': src_category.get('moduleName'),
|
|
'type': src_category.get('type'),
|
|
'category': src_category.get('category'),
|
|
'icon': src_category.get('icon'),
|
|
'title': src_category.get('title'),
|
|
'templates': [moved_template],
|
|
}
|
|
)
|
|
|
|
return prune_empty_categories(src_index), dst_index, True
|
|
|
|
|
|
def restore_indices(
|
|
template_name: str,
|
|
templates_dir: Path,
|
|
archived_dir: Path,
|
|
dry_run: bool,
|
|
) -> None:
|
|
# main index
|
|
archived_index_file = archived_dir / 'index.json'
|
|
templates_index_file = templates_dir / 'index.json'
|
|
|
|
if not archived_index_file.exists():
|
|
print(' Missing: archived/index.json')
|
|
return
|
|
|
|
archived_index = load_json(archived_index_file)
|
|
templates_index = load_json(templates_index_file) if templates_index_file.exists() else []
|
|
|
|
archived_index, templates_index, moved = move_index_entry(
|
|
template_name, archived_index, templates_index
|
|
)
|
|
if moved:
|
|
if dry_run:
|
|
print(' Would move entry archived/index.json -> templates/index.json')
|
|
else:
|
|
save_json(archived_index_file, archived_index)
|
|
save_json(templates_index_file, templates_index)
|
|
print(' Restored entry to templates/index.json')
|
|
|
|
# locale indices
|
|
for locale_file in templates_dir.glob('index.*.json'):
|
|
if locale_file.name in ['index.json', 'index.schema.json']:
|
|
continue
|
|
|
|
archived_locale_file = archived_dir / locale_file.name
|
|
if not archived_locale_file.exists():
|
|
continue
|
|
|
|
src_locale = load_json(archived_locale_file)
|
|
dst_locale = load_json(locale_file)
|
|
src_locale, dst_locale, moved_locale = move_index_entry(
|
|
template_name, src_locale, dst_locale
|
|
)
|
|
if not moved_locale:
|
|
continue
|
|
|
|
if dry_run:
|
|
print(
|
|
f' Would move entry archived/{locale_file.name} -> templates/{locale_file.name}'
|
|
)
|
|
else:
|
|
save_json(archived_locale_file, src_locale)
|
|
save_json(locale_file, dst_locale)
|
|
print(f' Restored entry to templates/{locale_file.name}')
|
|
|
|
|
|
def restore_templates(base_dir: Path, restore_status: str, dry_run: bool) -> None:
|
|
templates_dir = base_dir / 'templates'
|
|
archived_dir = base_dir / 'archived'
|
|
scripts_dir = base_dir / 'scripts'
|
|
|
|
archived_index_file = archived_dir / 'index.json'
|
|
i18n_file = scripts_dir / 'i18n.json'
|
|
archived_i18n_file = archived_dir / 'archived_i18n.json'
|
|
|
|
if not archived_index_file.exists():
|
|
print(f'Archived index not found: {archived_index_file}')
|
|
return
|
|
|
|
archived_index = load_json(archived_index_file)
|
|
candidates = find_templates_by_status(archived_index, restore_status)
|
|
if not candidates:
|
|
print(f'No templates with status: "{restore_status}" found in archived/index.json')
|
|
return
|
|
|
|
print(f'Found {len(candidates)} template(s) to restore (status={restore_status}):')
|
|
for name, _ in candidates:
|
|
print(f' - {name}')
|
|
print()
|
|
|
|
for template_name, _ in candidates:
|
|
print(f'Restoring: {template_name}')
|
|
|
|
ok = move_media_files(
|
|
template_name, archived_dir, templates_dir, dry_run=dry_run
|
|
)
|
|
if not ok:
|
|
print(' Restore skipped for indices/i18n due to missing/conflicting files')
|
|
print()
|
|
continue
|
|
|
|
restore_i18n(
|
|
template_name,
|
|
i18n_file=i18n_file,
|
|
archived_i18n_file=archived_i18n_file,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
restore_indices(
|
|
template_name,
|
|
templates_dir=templates_dir,
|
|
archived_dir=archived_dir,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
print()
|
|
|
|
print('Restore complete.')
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description='Restore archived templates back into templates/ when status matches.'
|
|
)
|
|
parser.add_argument(
|
|
'--base-dir',
|
|
default=str(Path(__file__).parent.parent),
|
|
help='Project base dir (default: workflow_templates root)',
|
|
)
|
|
parser.add_argument(
|
|
'--status',
|
|
default='active',
|
|
help='Status value in archived/index.json used as restore trigger (default: active)',
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Print actions without modifying files',
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
restore_templates(Path(args.base_dir), restore_status=args.status, dry_run=args.dry_run)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
|