up-cli 0.1.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- up/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/core/checkpoint.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""Unified checkpoint management for up-cli.
|
|
2
|
+
|
|
3
|
+
This module provides a single implementation for Git checkpoints,
|
|
4
|
+
used by all commands that need to save/restore state:
|
|
5
|
+
- up save
|
|
6
|
+
- up reset
|
|
7
|
+
- up start (before AI operations)
|
|
8
|
+
- up agent (before merge)
|
|
9
|
+
|
|
10
|
+
Checkpoints are lightweight Git tags + metadata stored in .up/checkpoints/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
from dataclasses import dataclass, asdict
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, List
|
|
19
|
+
|
|
20
|
+
from up.core.state import get_state_manager, AgentState
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CheckpointMetadata:
|
|
25
|
+
"""Metadata for a checkpoint."""
|
|
26
|
+
id: str
|
|
27
|
+
commit_sha: str
|
|
28
|
+
tag_name: str
|
|
29
|
+
message: str
|
|
30
|
+
created_at: str
|
|
31
|
+
branch: str
|
|
32
|
+
files_changed: int = 0
|
|
33
|
+
task_id: Optional[str] = None
|
|
34
|
+
agent_id: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict) -> "CheckpointMetadata":
|
|
41
|
+
return cls(**{
|
|
42
|
+
k: v for k, v in data.items()
|
|
43
|
+
if k in cls.__dataclass_fields__
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CheckpointManager:
|
|
48
|
+
"""Manages Git checkpoints for up-cli.
|
|
49
|
+
|
|
50
|
+
Provides:
|
|
51
|
+
- Create checkpoints (commit + tag)
|
|
52
|
+
- Restore to checkpoints
|
|
53
|
+
- List available checkpoints
|
|
54
|
+
- Cleanup old checkpoints
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
CHECKPOINT_DIR = ".up/checkpoints"
|
|
58
|
+
TAG_PREFIX = "up-checkpoint"
|
|
59
|
+
|
|
60
|
+
def __init__(self, workspace: Optional[Path] = None):
|
|
61
|
+
"""Initialize checkpoint manager.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
workspace: Project root directory (defaults to cwd)
|
|
65
|
+
"""
|
|
66
|
+
self.workspace = workspace or Path.cwd()
|
|
67
|
+
self.checkpoint_dir = self.workspace / self.CHECKPOINT_DIR
|
|
68
|
+
self.state_manager = get_state_manager(workspace)
|
|
69
|
+
|
|
70
|
+
def _run_git(self, *args, check: bool = True, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
71
|
+
"""Run a git command with error handling.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
*args: Git command arguments
|
|
75
|
+
check: Raise GitError on failure
|
|
76
|
+
timeout: Command timeout in seconds
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
CompletedProcess result
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
GitError: If command fails and check=True
|
|
83
|
+
GitNotInstalledError: If git is not installed
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["git"] + list(args),
|
|
88
|
+
cwd=self.workspace,
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=timeout
|
|
92
|
+
)
|
|
93
|
+
if check and result.returncode != 0:
|
|
94
|
+
raise GitError(f"Git command failed: git {' '.join(args)}\n{result.stderr}")
|
|
95
|
+
return result
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
raise GitNotInstalledError(
|
|
98
|
+
"Git is not installed or not in PATH. "
|
|
99
|
+
"Please install git: https://git-scm.com/downloads"
|
|
100
|
+
)
|
|
101
|
+
except subprocess.TimeoutExpired:
|
|
102
|
+
raise GitError(f"Git command timed out after {timeout}s: git {' '.join(args)}")
|
|
103
|
+
|
|
104
|
+
def _is_git_repo(self) -> bool:
|
|
105
|
+
"""Check if workspace is a git repository."""
|
|
106
|
+
result = self._run_git("rev-parse", "--git-dir", check=False)
|
|
107
|
+
return result.returncode == 0
|
|
108
|
+
|
|
109
|
+
def _get_current_branch(self) -> str:
|
|
110
|
+
"""Get current branch name."""
|
|
111
|
+
result = self._run_git("rev-parse", "--abbrev-ref", "HEAD")
|
|
112
|
+
return result.stdout.strip()
|
|
113
|
+
|
|
114
|
+
def _get_head_sha(self) -> str:
|
|
115
|
+
"""Get current HEAD commit SHA."""
|
|
116
|
+
result = self._run_git("rev-parse", "HEAD")
|
|
117
|
+
return result.stdout.strip()
|
|
118
|
+
|
|
119
|
+
def _has_changes(self) -> bool:
|
|
120
|
+
"""Check if there are uncommitted changes."""
|
|
121
|
+
result = self._run_git("status", "--porcelain")
|
|
122
|
+
return bool(result.stdout.strip())
|
|
123
|
+
|
|
124
|
+
def _count_changed_files(self) -> int:
|
|
125
|
+
"""Count number of changed files."""
|
|
126
|
+
result = self._run_git("status", "--porcelain")
|
|
127
|
+
return len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0
|
|
128
|
+
|
|
129
|
+
def save(
|
|
130
|
+
self,
|
|
131
|
+
message: str = None,
|
|
132
|
+
task_id: str = None,
|
|
133
|
+
agent_id: str = None,
|
|
134
|
+
auto_commit: bool = True
|
|
135
|
+
) -> CheckpointMetadata:
|
|
136
|
+
"""Create a checkpoint.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
message: Optional checkpoint message
|
|
140
|
+
task_id: Associated task ID
|
|
141
|
+
agent_id: Associated agent ID (for worktrees)
|
|
142
|
+
auto_commit: Whether to commit dirty files
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
CheckpointMetadata for the created checkpoint
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
GitError: If git operations fail
|
|
149
|
+
NotAGitRepoError: If not in a git repository
|
|
150
|
+
"""
|
|
151
|
+
if not self._is_git_repo():
|
|
152
|
+
raise NotAGitRepoError("Not a git repository")
|
|
153
|
+
|
|
154
|
+
# Generate checkpoint ID
|
|
155
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
156
|
+
checkpoint_id = f"cp-{timestamp}"
|
|
157
|
+
if task_id:
|
|
158
|
+
checkpoint_id = f"cp-{task_id}-{timestamp}"
|
|
159
|
+
|
|
160
|
+
# Commit dirty files if requested
|
|
161
|
+
files_changed = 0
|
|
162
|
+
if auto_commit and self._has_changes():
|
|
163
|
+
files_changed = self._count_changed_files()
|
|
164
|
+
self._run_git("add", "-A")
|
|
165
|
+
commit_message = message or f"checkpoint: {checkpoint_id}"
|
|
166
|
+
self._run_git("commit", "-m", commit_message)
|
|
167
|
+
|
|
168
|
+
# Get commit info
|
|
169
|
+
commit_sha = self._get_head_sha()
|
|
170
|
+
branch = self._get_current_branch()
|
|
171
|
+
|
|
172
|
+
# Create lightweight tag
|
|
173
|
+
tag_name = f"{self.TAG_PREFIX}/{checkpoint_id}"
|
|
174
|
+
self._run_git("tag", tag_name, check=False) # May already exist
|
|
175
|
+
|
|
176
|
+
# Create metadata
|
|
177
|
+
metadata = CheckpointMetadata(
|
|
178
|
+
id=checkpoint_id,
|
|
179
|
+
commit_sha=commit_sha,
|
|
180
|
+
tag_name=tag_name,
|
|
181
|
+
message=message or f"Checkpoint before {task_id or 'AI operation'}",
|
|
182
|
+
created_at=datetime.now().isoformat(),
|
|
183
|
+
branch=branch,
|
|
184
|
+
files_changed=files_changed,
|
|
185
|
+
task_id=task_id,
|
|
186
|
+
agent_id=agent_id,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Save metadata
|
|
190
|
+
self._save_metadata(metadata)
|
|
191
|
+
|
|
192
|
+
# Update state
|
|
193
|
+
self.state_manager.add_checkpoint(checkpoint_id)
|
|
194
|
+
|
|
195
|
+
return metadata
|
|
196
|
+
|
|
197
|
+
def restore(
|
|
198
|
+
self,
|
|
199
|
+
checkpoint_id: str = None,
|
|
200
|
+
hard: bool = True
|
|
201
|
+
) -> CheckpointMetadata:
|
|
202
|
+
"""Restore to a checkpoint.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
checkpoint_id: Checkpoint to restore (defaults to last)
|
|
206
|
+
hard: Whether to use hard reset (discard changes)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
CheckpointMetadata of restored checkpoint
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
CheckpointNotFoundError: If checkpoint doesn't exist
|
|
213
|
+
"""
|
|
214
|
+
if not self._is_git_repo():
|
|
215
|
+
raise NotAGitRepoError("Not a git repository")
|
|
216
|
+
|
|
217
|
+
# Get checkpoint
|
|
218
|
+
if checkpoint_id:
|
|
219
|
+
metadata = self._load_metadata(checkpoint_id)
|
|
220
|
+
else:
|
|
221
|
+
# Get last checkpoint
|
|
222
|
+
last_id = self.state_manager.state.loop.last_checkpoint
|
|
223
|
+
if not last_id:
|
|
224
|
+
raise CheckpointNotFoundError("No checkpoints available")
|
|
225
|
+
metadata = self._load_metadata(last_id)
|
|
226
|
+
|
|
227
|
+
if metadata is None:
|
|
228
|
+
# Try to restore from tag directly
|
|
229
|
+
tag_name = f"{self.TAG_PREFIX}/{checkpoint_id}"
|
|
230
|
+
result = self._run_git("rev-parse", tag_name, check=False)
|
|
231
|
+
if result.returncode != 0:
|
|
232
|
+
raise CheckpointNotFoundError(f"Checkpoint not found: {checkpoint_id}")
|
|
233
|
+
|
|
234
|
+
commit_sha = result.stdout.strip()
|
|
235
|
+
metadata = CheckpointMetadata(
|
|
236
|
+
id=checkpoint_id,
|
|
237
|
+
commit_sha=commit_sha,
|
|
238
|
+
tag_name=tag_name,
|
|
239
|
+
message="Restored from tag",
|
|
240
|
+
created_at=datetime.now().isoformat(),
|
|
241
|
+
branch=self._get_current_branch(),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Perform reset
|
|
245
|
+
reset_type = "--hard" if hard else "--soft"
|
|
246
|
+
self._run_git("reset", reset_type, metadata.commit_sha)
|
|
247
|
+
|
|
248
|
+
# Record rollback
|
|
249
|
+
self.state_manager.record_rollback()
|
|
250
|
+
|
|
251
|
+
return metadata
|
|
252
|
+
|
|
253
|
+
def list_checkpoints(self, limit: int = 20) -> List[CheckpointMetadata]:
|
|
254
|
+
"""List available checkpoints.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
limit: Maximum number to return
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of checkpoint metadata, newest first
|
|
261
|
+
"""
|
|
262
|
+
checkpoints = []
|
|
263
|
+
|
|
264
|
+
# Get from state
|
|
265
|
+
checkpoint_ids = self.state_manager.state.checkpoints[-limit:]
|
|
266
|
+
checkpoint_ids.reverse() # Newest first
|
|
267
|
+
|
|
268
|
+
for cp_id in checkpoint_ids:
|
|
269
|
+
metadata = self._load_metadata(cp_id)
|
|
270
|
+
if metadata:
|
|
271
|
+
checkpoints.append(metadata)
|
|
272
|
+
|
|
273
|
+
return checkpoints
|
|
274
|
+
|
|
275
|
+
def get_last_checkpoint(self) -> Optional[CheckpointMetadata]:
|
|
276
|
+
"""Get the last checkpoint."""
|
|
277
|
+
last_id = self.state_manager.state.loop.last_checkpoint
|
|
278
|
+
if last_id:
|
|
279
|
+
return self._load_metadata(last_id)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def cleanup(self, keep: int = 20) -> int:
|
|
283
|
+
"""Remove old checkpoints.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
keep: Number of recent checkpoints to keep
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Number of checkpoints removed
|
|
290
|
+
"""
|
|
291
|
+
all_checkpoints = self.state_manager.state.checkpoints
|
|
292
|
+
if len(all_checkpoints) <= keep:
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
to_remove = all_checkpoints[:-keep]
|
|
296
|
+
removed = 0
|
|
297
|
+
|
|
298
|
+
for cp_id in to_remove:
|
|
299
|
+
# Remove tag
|
|
300
|
+
tag_name = f"{self.TAG_PREFIX}/{cp_id}"
|
|
301
|
+
self._run_git("tag", "-d", tag_name, check=False)
|
|
302
|
+
|
|
303
|
+
# Remove metadata file
|
|
304
|
+
metadata_file = self.checkpoint_dir / f"{cp_id}.json"
|
|
305
|
+
if metadata_file.exists():
|
|
306
|
+
metadata_file.unlink()
|
|
307
|
+
|
|
308
|
+
removed += 1
|
|
309
|
+
|
|
310
|
+
# Update state
|
|
311
|
+
self.state_manager.state.checkpoints = all_checkpoints[-keep:]
|
|
312
|
+
self.state_manager.save()
|
|
313
|
+
|
|
314
|
+
return removed
|
|
315
|
+
|
|
316
|
+
def diff_from_checkpoint(self, checkpoint_id: str = None) -> str:
|
|
317
|
+
"""Get diff from checkpoint to current state.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
checkpoint_id: Checkpoint to diff from (defaults to last)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Diff output as string
|
|
324
|
+
"""
|
|
325
|
+
if not checkpoint_id:
|
|
326
|
+
checkpoint_id = self.state_manager.state.loop.last_checkpoint
|
|
327
|
+
|
|
328
|
+
if not checkpoint_id:
|
|
329
|
+
# Diff from HEAD
|
|
330
|
+
result = self._run_git("diff", "HEAD")
|
|
331
|
+
return result.stdout
|
|
332
|
+
|
|
333
|
+
tag_name = f"{self.TAG_PREFIX}/{checkpoint_id}"
|
|
334
|
+
result = self._run_git("diff", tag_name, "HEAD", check=False)
|
|
335
|
+
|
|
336
|
+
if result.returncode != 0:
|
|
337
|
+
# Tag might not exist, try commit SHA from metadata
|
|
338
|
+
metadata = self._load_metadata(checkpoint_id)
|
|
339
|
+
if metadata:
|
|
340
|
+
result = self._run_git("diff", metadata.commit_sha, "HEAD")
|
|
341
|
+
return result.stdout
|
|
342
|
+
return ""
|
|
343
|
+
|
|
344
|
+
return result.stdout
|
|
345
|
+
|
|
346
|
+
def diff_stats(self, checkpoint_id: str = None) -> dict:
|
|
347
|
+
"""Get diff statistics from checkpoint.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Dict with files, insertions, deletions
|
|
351
|
+
"""
|
|
352
|
+
if not checkpoint_id:
|
|
353
|
+
checkpoint_id = self.state_manager.state.loop.last_checkpoint
|
|
354
|
+
|
|
355
|
+
if not checkpoint_id:
|
|
356
|
+
result = self._run_git("diff", "--stat", "HEAD")
|
|
357
|
+
else:
|
|
358
|
+
tag_name = f"{self.TAG_PREFIX}/{checkpoint_id}"
|
|
359
|
+
result = self._run_git("diff", "--stat", tag_name, "HEAD", check=False)
|
|
360
|
+
if result.returncode != 0:
|
|
361
|
+
return {"files": 0, "insertions": 0, "deletions": 0}
|
|
362
|
+
|
|
363
|
+
# Parse stat output
|
|
364
|
+
lines = result.stdout.strip().split("\n")
|
|
365
|
+
if not lines or not lines[-1]:
|
|
366
|
+
return {"files": 0, "insertions": 0, "deletions": 0}
|
|
367
|
+
|
|
368
|
+
# Last line has summary: "X files changed, Y insertions(+), Z deletions(-)"
|
|
369
|
+
import re
|
|
370
|
+
summary = lines[-1] if lines else ""
|
|
371
|
+
|
|
372
|
+
files = 0
|
|
373
|
+
insertions = 0
|
|
374
|
+
deletions = 0
|
|
375
|
+
|
|
376
|
+
files_match = re.search(r"(\d+) files? changed", summary)
|
|
377
|
+
if files_match:
|
|
378
|
+
files = int(files_match.group(1))
|
|
379
|
+
|
|
380
|
+
ins_match = re.search(r"(\d+) insertions?\(\+\)", summary)
|
|
381
|
+
if ins_match:
|
|
382
|
+
insertions = int(ins_match.group(1))
|
|
383
|
+
|
|
384
|
+
del_match = re.search(r"(\d+) deletions?\(-\)", summary)
|
|
385
|
+
if del_match:
|
|
386
|
+
deletions = int(del_match.group(1))
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"files": files,
|
|
390
|
+
"insertions": insertions,
|
|
391
|
+
"deletions": deletions,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
def _save_metadata(self, metadata: CheckpointMetadata) -> None:
|
|
395
|
+
"""Save checkpoint metadata to file."""
|
|
396
|
+
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
397
|
+
metadata_file = self.checkpoint_dir / f"{metadata.id}.json"
|
|
398
|
+
metadata_file.write_text(json.dumps(metadata.to_dict(), indent=2))
|
|
399
|
+
|
|
400
|
+
def _load_metadata(self, checkpoint_id: str) -> Optional[CheckpointMetadata]:
|
|
401
|
+
"""Load checkpoint metadata from file."""
|
|
402
|
+
metadata_file = self.checkpoint_dir / f"{checkpoint_id}.json"
|
|
403
|
+
if not metadata_file.exists():
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
data = json.loads(metadata_file.read_text())
|
|
408
|
+
return CheckpointMetadata.from_dict(data)
|
|
409
|
+
except (json.JSONDecodeError, KeyError):
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# =============================================================================
|
|
414
|
+
# Exceptions
|
|
415
|
+
# =============================================================================
|
|
416
|
+
|
|
417
|
+
class CheckpointError(Exception):
|
|
418
|
+
"""Base exception for checkpoint operations."""
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class GitError(CheckpointError):
|
|
423
|
+
"""Git operation failed."""
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class GitNotInstalledError(GitError):
|
|
428
|
+
"""Git is not installed or not in PATH."""
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class NotAGitRepoError(CheckpointError):
|
|
433
|
+
"""Not in a git repository."""
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class CheckpointNotFoundError(CheckpointError):
|
|
438
|
+
"""Checkpoint not found."""
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# =============================================================================
|
|
443
|
+
# Module-level convenience functions
|
|
444
|
+
# =============================================================================
|
|
445
|
+
|
|
446
|
+
_default_manager: Optional[CheckpointManager] = None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def get_checkpoint_manager(workspace: Optional[Path] = None) -> CheckpointManager:
|
|
450
|
+
"""Get or create the default checkpoint manager."""
|
|
451
|
+
global _default_manager
|
|
452
|
+
if _default_manager is None or (workspace and _default_manager.workspace != workspace):
|
|
453
|
+
_default_manager = CheckpointManager(workspace)
|
|
454
|
+
return _default_manager
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def save_checkpoint(
|
|
458
|
+
message: str = None,
|
|
459
|
+
task_id: str = None,
|
|
460
|
+
workspace: Optional[Path] = None
|
|
461
|
+
) -> CheckpointMetadata:
|
|
462
|
+
"""Create a checkpoint (convenience function)."""
|
|
463
|
+
return get_checkpoint_manager(workspace).save(message=message, task_id=task_id)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def restore_checkpoint(
|
|
467
|
+
checkpoint_id: str = None,
|
|
468
|
+
workspace: Optional[Path] = None
|
|
469
|
+
) -> CheckpointMetadata:
|
|
470
|
+
"""Restore to a checkpoint (convenience function)."""
|
|
471
|
+
return get_checkpoint_manager(workspace).restore(checkpoint_id=checkpoint_id)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_diff(
|
|
475
|
+
checkpoint_id: str = None,
|
|
476
|
+
workspace: Optional[Path] = None
|
|
477
|
+
) -> str:
|
|
478
|
+
"""Get diff from checkpoint (convenience function)."""
|
|
479
|
+
return get_checkpoint_manager(workspace).diff_from_checkpoint(checkpoint_id)
|