File size: 6,591 Bytes
5a03a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f3e893e
5a03a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c138f41
 
 
5a03a94
c138f41
5a03a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python3
"""Restore project files from a saved checkpoint.

Supports restoring individual checkpoints, rolling back to the latest checkpoint
for a specific phase, or listing available checkpoints.

Usage:
    python3 scripts/checkpoint_restore.py --id ckpt-20260523-220000      # Restore specific checkpoint
    python3 scripts/checkpoint_restore.py --latest                        # Restore latest checkpoint
    python3 scripts/checkpoint_restore.py --phase 0                       # Restore latest Phase 0 checkpoint
    python3 scripts/checkpoint_restore.py --list                          # List available checkpoints
    python3 scripts/checkpoint_restore.py --id ckpt-xxx --dry-run         # Preview without restoring

Checkpoints stored in: .checkpoints/<checkpoint_id>/
"""

import argparse
import json
import os
import shutil
import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).parent.parent
CHECKPOINT_DIR = PROJECT_ROOT / ".checkpoints"


def load_index() -> dict:
    """Load the checkpoint index file."""
    index_path = CHECKPOINT_DIR / "index.json"
    if not index_path.exists():
        print("ERROR: No checkpoints found (index.json missing)", file=sys.stderr)
        sys.exit(1)
    with open(index_path) as f:
        return json.load(f)


def load_checkpoint_meta(checkpoint_id: str) -> dict:
    """Load metadata for a specific checkpoint."""
    meta_path = CHECKPOINT_DIR / checkpoint_id / "checkpoint.json"
    if not meta_path.exists():
        print(f"ERROR: Checkpoint '{checkpoint_id}' not found", file=sys.stderr)
        sys.exit(1)
    with open(meta_path) as f:
        return json.load(f)


def list_checkpoints():
    """List all checkpoints with details."""
    index = load_index()
    ckpts = index.get("checkpoints", [])
    if not ckpts:
        print("No checkpoints found")
        return

    print(f"Available checkpoints ({len(ckpts)}):")
    print(f"{'ID':<30} {'Phase':<12} {'Files':<6} {'Message'}")
    print("-" * 80)
    for c in ckpts:
        cid = c["id"]
        phase = c.get("phase", "?")
        count = c.get("files_count", "?")
        msg = c.get("message", "")[:40]
        print(f"{cid:<30} {phase:<12} {count:<6} {msg}")


def find_checkpoint(checkpoint_id: str = None, phase: str = None, latest: bool = False) -> str:
    """Find a checkpoint ID based on search criteria."""
    index = load_index()
    ckpts = index.get("checkpoints", [])

    if checkpoint_id:
        return checkpoint_id

    if latest:
        if not ckpts:
            print("ERROR: No checkpoints available", file=sys.stderr)
            sys.exit(1)
        return ckpts[0]["id"]

    if phase:
        phase_ckpts = [c for c in ckpts if c.get("phase") == phase]
        if not phase_ckpts:
            print(f"ERROR: No checkpoints found for phase '{phase}'", file=sys.stderr)
            sys.exit(1)
        return phase_ckpts[0]["id"]

    return ckpts[0]["id"]


def restore_checkpoint(checkpoint_id: str, dry_run: bool = False):
    """Restore files from a checkpoint.

    Args:
        checkpoint_id: The checkpoint ID to restore from.
        dry_run: If True, only show what would be restored without copying.
    """
    meta = load_checkpoint_meta(checkpoint_id)
    ckpt_dir = CHECKPOINT_DIR / checkpoint_id

    print(f"Checkpoint: {checkpoint_id}")
    print(f"  Phase: {meta['phase']}")
    print(f"  Timestamp: {meta['timestamp']}")
    print(f"  Message: {meta.get('message', '')}")
    print()

    if dry_run:
        print("DRY RUN — No files will be modified")
        print()

    files_to_restore = meta.get("files_backed_up", [])
    restored = []
    skipped = []

    for rel_path in files_to_restore:
        src = ckpt_dir / rel_path
        dst = PROJECT_ROOT / rel_path

        if not src.exists():
            skipped.append((rel_path, "source missing in checkpoint"))
            continue

        if dry_run:
            dst_exists = dst.exists()
            dst_size = os.path.getsize(dst) if dst_exists else 0
            src_size = os.path.getsize(src)
            print(f"  Would restore: {rel_path}")
            print(f"    From: {src_size} bytes (checkpoint)")
            print(f"    To:   {dst_size} bytes {'(existing)' if dst_exists else '(new file)'}")
            restored.append(rel_path)
            continue

        # Make backup of current file before overwriting?
        # No — this is a restore, the checkpoint is the reference.
        # If user wants to keep current state, they should have saved a checkpoint first.
        os.makedirs(dst.parent, exist_ok=True)
        try:
            shutil.copy2(src, dst)
            restored.append(rel_path)
            print(f"  ✓ {rel_path}")
        except Exception as e:
            skipped.append((rel_path, str(e)))

    if not dry_run:
        print()
        print(f"Restore complete: {len(restored)} files restored")
        if skipped:
            print(f"  {len(skipped)} files skipped:")
            for f, reason in skipped:
                print(f"    ✗ {f} ({reason})")

        # Update current checkpoint metadata
        print()
        print(f"Restored from checkpoint: {checkpoint_id}")
        print(f"  Phase: {meta['phase']}")
        print(f"  Original timestamp: {meta['timestamp']}")

        # If there are app files, suggest restart
        has_app = any(f.startswith("app/") for f in restored)
        if has_app:
            print()
            print("⚠️  App files were restored. Restart the API server:")
    else:
        print()
        print(f"Dry run complete: {len(restored)} files would be restored")
        if skipped:
            print(f"  {len(skipped)} files would be skipped")


def main():
    parser = argparse.ArgumentParser(
        description="Restore EUR-Lex AI Chat project from a saved checkpoint"
    )
    parser.add_argument("--id", help="Checkpoint ID to restore")
    parser.add_argument("--latest", action="store_true", help="Restore latest checkpoint")
    parser.add_argument("--phase", help="Restore latest checkpoint for a specific phase")
    parser.add_argument("--list", action="store_true", help="List available checkpoints")
    parser.add_argument("--dry-run", action="store_true", help="Preview without restoring")

    args = parser.parse_args()

    if args.list:
        list_checkpoints()
        return

    checkpoint_id = find_checkpoint(
        checkpoint_id=args.id,
        phase=args.phase,
        latest=args.latest,
    )

    restore_checkpoint(checkpoint_id, dry_run=args.dry_run)


if __name__ == "__main__":
    main()