@trac3er/oh-my-god 2.0.0 → 2.0.2
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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session State Snapshot System for OMG
|
|
4
|
+
|
|
5
|
+
Captures `.omg/state/` directory, compresses it, versions snapshots,
|
|
6
|
+
and stores them in `.omg/state/snapshots/`.
|
|
7
|
+
|
|
8
|
+
Feature flag: OMG_SNAPSHOT_ENABLED (default: False)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tarfile
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
# Lazy import from hooks
|
|
20
|
+
def _get_feature_flag_enabled() -> bool:
|
|
21
|
+
"""Check if snapshot feature is enabled."""
|
|
22
|
+
env_val = os.environ.get("OMG_SNAPSHOT_ENABLED", "").lower()
|
|
23
|
+
if env_val in ("0", "false", "no"):
|
|
24
|
+
return False
|
|
25
|
+
if env_val in ("1", "true", "yes"):
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
# Lazy import from hooks
|
|
29
|
+
hooks_dir = os.path.normpath(
|
|
30
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
|
|
31
|
+
)
|
|
32
|
+
if hooks_dir not in sys.path:
|
|
33
|
+
sys.path.insert(0, hooks_dir)
|
|
34
|
+
try:
|
|
35
|
+
from _common import get_feature_flag # type: ignore[import-untyped]
|
|
36
|
+
return get_feature_flag("SNAPSHOT", default=False)
|
|
37
|
+
except ImportError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_atomic_json_write():
|
|
42
|
+
"""Lazy-import atomic_json_write from hooks/_common.py."""
|
|
43
|
+
hooks_dir = os.path.normpath(
|
|
44
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
|
|
45
|
+
)
|
|
46
|
+
if hooks_dir not in sys.path:
|
|
47
|
+
sys.path.insert(0, hooks_dir)
|
|
48
|
+
try:
|
|
49
|
+
from _common import atomic_json_write # type: ignore[import-untyped]
|
|
50
|
+
return atomic_json_write
|
|
51
|
+
except ImportError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_snapshot(name: Optional[str] = None, state_dir: str = ".omg/state") -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Capture `.omg/state/` directory and create a compressed snapshot.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name: Optional name suffix for the snapshot
|
|
61
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Snapshot metadata dict with keys: id, name, created_at, files_count, compressed_size, state_dir
|
|
65
|
+
or {"skipped": True} if feature flag is disabled
|
|
66
|
+
"""
|
|
67
|
+
if not _get_feature_flag_enabled():
|
|
68
|
+
return {"skipped": True}
|
|
69
|
+
|
|
70
|
+
# Generate snapshot ID
|
|
71
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
72
|
+
snapshot_id = f"{timestamp}_{name}" if name else timestamp
|
|
73
|
+
|
|
74
|
+
# Ensure snapshots directory exists
|
|
75
|
+
snapshots_dir = os.path.join(state_dir, "snapshots")
|
|
76
|
+
os.makedirs(snapshots_dir, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
# Paths for snapshot files
|
|
79
|
+
snapshot_tar_path = os.path.join(snapshots_dir, f"{snapshot_id}.tar.gz")
|
|
80
|
+
snapshot_meta_path = os.path.join(snapshots_dir, f"{snapshot_id}.json")
|
|
81
|
+
|
|
82
|
+
# Files to exclude
|
|
83
|
+
exclude_patterns = {
|
|
84
|
+
"snapshots", # Don't snapshot the snapshots directory itself
|
|
85
|
+
"credentials.enc",
|
|
86
|
+
"credentials.meta",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Create tar.gz archive
|
|
90
|
+
files_count = 0
|
|
91
|
+
try:
|
|
92
|
+
with tarfile.open(snapshot_tar_path, "w:gz") as tar:
|
|
93
|
+
for root, dirs, files in os.walk(state_dir):
|
|
94
|
+
# Filter out excluded directories
|
|
95
|
+
dirs[:] = [d for d in dirs if d not in exclude_patterns]
|
|
96
|
+
|
|
97
|
+
for file in files:
|
|
98
|
+
file_path = os.path.join(root, file)
|
|
99
|
+
# Skip excluded files
|
|
100
|
+
if file in exclude_patterns:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Calculate arcname (relative path in archive)
|
|
104
|
+
arcname = os.path.relpath(file_path, state_dir)
|
|
105
|
+
tar.add(file_path, arcname=arcname)
|
|
106
|
+
files_count += 1
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"[OMG] Error creating snapshot: {e}", file=sys.stderr)
|
|
110
|
+
return {"error": str(e)}
|
|
111
|
+
|
|
112
|
+
# Get compressed size
|
|
113
|
+
compressed_size = os.path.getsize(snapshot_tar_path) if os.path.exists(snapshot_tar_path) else 0
|
|
114
|
+
|
|
115
|
+
# Create metadata
|
|
116
|
+
metadata = {
|
|
117
|
+
"id": snapshot_id,
|
|
118
|
+
"name": name or "",
|
|
119
|
+
"created_at": datetime.now().isoformat(),
|
|
120
|
+
"files_count": files_count,
|
|
121
|
+
"compressed_size": compressed_size,
|
|
122
|
+
"state_dir": state_dir,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Write metadata atomically
|
|
126
|
+
atomic_json_write = _get_atomic_json_write()
|
|
127
|
+
if atomic_json_write:
|
|
128
|
+
atomic_json_write(snapshot_meta_path, metadata)
|
|
129
|
+
else:
|
|
130
|
+
# Fallback: write without atomic guarantee
|
|
131
|
+
try:
|
|
132
|
+
with open(snapshot_meta_path, "w", encoding="utf-8") as f:
|
|
133
|
+
json.dump(metadata, f, separators=(",", ":"))
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"[OMG] Error writing metadata: {e}", file=sys.stderr)
|
|
136
|
+
|
|
137
|
+
return metadata
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def list_snapshots(state_dir: str = ".omg/state") -> List[Dict[str, Any]]:
|
|
141
|
+
"""
|
|
142
|
+
List all available snapshots.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of snapshot metadata dicts, sorted by created_at descending (newest first)
|
|
149
|
+
"""
|
|
150
|
+
snapshots_dir = os.path.join(state_dir, "snapshots")
|
|
151
|
+
if not os.path.isdir(snapshots_dir):
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
snapshots = []
|
|
155
|
+
try:
|
|
156
|
+
for file in os.listdir(snapshots_dir):
|
|
157
|
+
if file.endswith(".json"):
|
|
158
|
+
meta_path = os.path.join(snapshots_dir, file)
|
|
159
|
+
try:
|
|
160
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
161
|
+
metadata = json.load(f)
|
|
162
|
+
snapshots.append(metadata)
|
|
163
|
+
except (json.JSONDecodeError, OSError):
|
|
164
|
+
pass # Skip invalid metadata files
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
# Sort by created_at descending (newest first)
|
|
169
|
+
snapshots.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
170
|
+
return snapshots
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def restore_snapshot(snapshot_id: str, state_dir: str = ".omg/state") -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Restore a snapshot to the state directory.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
snapshot_id: ID of the snapshot to restore
|
|
179
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if restored successfully, False if snapshot not found
|
|
183
|
+
"""
|
|
184
|
+
snapshots_dir = os.path.join(state_dir, "snapshots")
|
|
185
|
+
snapshot_tar_path = os.path.join(snapshots_dir, f"{snapshot_id}.tar.gz")
|
|
186
|
+
|
|
187
|
+
if not os.path.exists(snapshot_tar_path):
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
with tarfile.open(snapshot_tar_path, "r:gz") as tar:
|
|
192
|
+
# Use filter='data' for Python 3.14+ compatibility
|
|
193
|
+
try:
|
|
194
|
+
tar.extractall(path=state_dir, filter='data')
|
|
195
|
+
except TypeError:
|
|
196
|
+
# Fallback for Python < 3.12
|
|
197
|
+
tar.extractall(path=state_dir)
|
|
198
|
+
return True
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"[OMG] Error restoring snapshot: {e}", file=sys.stderr)
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def delete_snapshot(snapshot_id: str, state_dir: str = ".omg/state") -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Delete a snapshot.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
snapshot_id: ID of the snapshot to delete
|
|
210
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if deleted successfully, False if snapshot not found
|
|
214
|
+
"""
|
|
215
|
+
snapshots_dir = os.path.join(state_dir, "snapshots")
|
|
216
|
+
snapshot_tar_path = os.path.join(snapshots_dir, f"{snapshot_id}.tar.gz")
|
|
217
|
+
snapshot_meta_path = os.path.join(snapshots_dir, f"{snapshot_id}.json")
|
|
218
|
+
|
|
219
|
+
deleted = False
|
|
220
|
+
try:
|
|
221
|
+
if os.path.exists(snapshot_tar_path):
|
|
222
|
+
os.remove(snapshot_tar_path)
|
|
223
|
+
deleted = True
|
|
224
|
+
except OSError as e:
|
|
225
|
+
print(f"[OMG] Error deleting snapshot tar: {e}", file=sys.stderr)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if os.path.exists(snapshot_meta_path):
|
|
229
|
+
os.remove(snapshot_meta_path)
|
|
230
|
+
deleted = True
|
|
231
|
+
except OSError as e:
|
|
232
|
+
print(f"[OMG] Error deleting snapshot metadata: {e}", file=sys.stderr)
|
|
233
|
+
|
|
234
|
+
return deleted
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# --- Branch / Fork API ---
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _get_branching_flag_enabled() -> bool:
|
|
241
|
+
"""Check if branching feature is enabled."""
|
|
242
|
+
env_val = os.environ.get("OMG_BRANCHING_ENABLED", "").lower()
|
|
243
|
+
if env_val in ("0", "false", "no"):
|
|
244
|
+
return False
|
|
245
|
+
if env_val in ("1", "true", "yes"):
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
# Lazy import from hooks
|
|
249
|
+
hooks_dir = os.path.normpath(
|
|
250
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
|
|
251
|
+
)
|
|
252
|
+
if hooks_dir not in sys.path:
|
|
253
|
+
sys.path.insert(0, hooks_dir)
|
|
254
|
+
try:
|
|
255
|
+
from _common import get_feature_flag # type: ignore[import-untyped]
|
|
256
|
+
return get_feature_flag("BRANCHING", default=False)
|
|
257
|
+
except ImportError:
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def create_branch(
|
|
262
|
+
name: str,
|
|
263
|
+
from_snapshot_id: Optional[str] = None,
|
|
264
|
+
state_dir: str = ".omg/state",
|
|
265
|
+
) -> Dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Create a named branch from a snapshot or the current state.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: Branch name (must be non-empty, no slashes)
|
|
271
|
+
from_snapshot_id: Optional snapshot ID to branch from.
|
|
272
|
+
If provided, restores that snapshot first.
|
|
273
|
+
Otherwise, creates a new snapshot automatically.
|
|
274
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Branch metadata dict with keys: name, snapshot_id, created_at,
|
|
278
|
+
parent_branch, status.
|
|
279
|
+
Or {"skipped": True} if feature flag is disabled.
|
|
280
|
+
"""
|
|
281
|
+
if not _get_branching_flag_enabled():
|
|
282
|
+
return {"skipped": True}
|
|
283
|
+
|
|
284
|
+
if not name or "/" in name:
|
|
285
|
+
return {"error": "Invalid branch name: must be non-empty with no slashes"}
|
|
286
|
+
|
|
287
|
+
# Resolve source snapshot
|
|
288
|
+
if from_snapshot_id:
|
|
289
|
+
# Verify snapshot exists before restoring
|
|
290
|
+
snapshots_dir = os.path.join(state_dir, "snapshots")
|
|
291
|
+
tar_path = os.path.join(snapshots_dir, f"{from_snapshot_id}.tar.gz")
|
|
292
|
+
if not os.path.exists(tar_path):
|
|
293
|
+
return {"error": f"Snapshot not found: {from_snapshot_id}"}
|
|
294
|
+
restore_snapshot(from_snapshot_id, state_dir=state_dir)
|
|
295
|
+
snapshot_id = from_snapshot_id
|
|
296
|
+
else:
|
|
297
|
+
# Create a fresh snapshot for this branch
|
|
298
|
+
snap = create_snapshot(name=name, state_dir=state_dir)
|
|
299
|
+
if snap.get("error") or snap.get("skipped"):
|
|
300
|
+
return snap
|
|
301
|
+
snapshot_id = snap["id"]
|
|
302
|
+
|
|
303
|
+
# Read current branch (parent)
|
|
304
|
+
current_branch_path = os.path.join(state_dir, "current_branch.json")
|
|
305
|
+
parent_branch: Optional[str] = None
|
|
306
|
+
if os.path.exists(current_branch_path):
|
|
307
|
+
try:
|
|
308
|
+
with open(current_branch_path, "r", encoding="utf-8") as f:
|
|
309
|
+
cb = json.load(f)
|
|
310
|
+
parent_branch = cb.get("name")
|
|
311
|
+
except (json.JSONDecodeError, OSError):
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# Build branch metadata
|
|
315
|
+
metadata: Dict[str, Any] = {
|
|
316
|
+
"name": name,
|
|
317
|
+
"snapshot_id": snapshot_id,
|
|
318
|
+
"created_at": datetime.now().isoformat(),
|
|
319
|
+
"parent_branch": parent_branch,
|
|
320
|
+
"status": "active",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Write branch metadata
|
|
324
|
+
branches_dir = os.path.join(state_dir, "branches")
|
|
325
|
+
os.makedirs(branches_dir, exist_ok=True)
|
|
326
|
+
branch_path = os.path.join(branches_dir, f"{name}.json")
|
|
327
|
+
|
|
328
|
+
atomic_json_write = _get_atomic_json_write()
|
|
329
|
+
if atomic_json_write:
|
|
330
|
+
atomic_json_write(branch_path, metadata)
|
|
331
|
+
else:
|
|
332
|
+
try:
|
|
333
|
+
with open(branch_path, "w", encoding="utf-8") as f:
|
|
334
|
+
json.dump(metadata, f, separators=(",", ":"))
|
|
335
|
+
except Exception as e:
|
|
336
|
+
print(f"[OMG] Error writing branch metadata: {e}", file=sys.stderr)
|
|
337
|
+
return {"error": str(e)}
|
|
338
|
+
|
|
339
|
+
# Update current branch tracker
|
|
340
|
+
_update_current_branch(name, state_dir=state_dir)
|
|
341
|
+
|
|
342
|
+
return metadata
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def list_branches(state_dir: str = ".omg/state") -> List[Dict[str, Any]]:
|
|
346
|
+
"""
|
|
347
|
+
List all branches with metadata.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of branch metadata dicts, sorted by created_at descending (newest first)
|
|
354
|
+
"""
|
|
355
|
+
branches_dir = os.path.join(state_dir, "branches")
|
|
356
|
+
if not os.path.isdir(branches_dir):
|
|
357
|
+
return []
|
|
358
|
+
|
|
359
|
+
branches: List[Dict[str, Any]] = []
|
|
360
|
+
try:
|
|
361
|
+
for file in os.listdir(branches_dir):
|
|
362
|
+
if file.endswith(".json"):
|
|
363
|
+
branch_path = os.path.join(branches_dir, file)
|
|
364
|
+
try:
|
|
365
|
+
with open(branch_path, "r", encoding="utf-8") as f:
|
|
366
|
+
metadata = json.load(f)
|
|
367
|
+
branches.append(metadata)
|
|
368
|
+
except (json.JSONDecodeError, OSError):
|
|
369
|
+
pass # Skip invalid metadata files
|
|
370
|
+
except OSError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
# Sort by created_at descending (newest first)
|
|
374
|
+
branches.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
375
|
+
return branches
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def switch_branch(name: str, state_dir: str = ".omg/state") -> bool:
|
|
379
|
+
"""
|
|
380
|
+
Switch to a named branch by restoring its snapshot.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
name: Branch name to switch to
|
|
384
|
+
state_dir: Path to the state directory (default: ".omg/state")
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
True if switched successfully, False otherwise
|
|
388
|
+
"""
|
|
389
|
+
branches_dir = os.path.join(state_dir, "branches")
|
|
390
|
+
branch_path = os.path.join(branches_dir, f"{name}.json")
|
|
391
|
+
|
|
392
|
+
if not os.path.exists(branch_path):
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
with open(branch_path, "r", encoding="utf-8") as f:
|
|
397
|
+
branch_meta = json.load(f)
|
|
398
|
+
except (json.JSONDecodeError, OSError):
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
snapshot_id = branch_meta.get("snapshot_id")
|
|
402
|
+
if not snapshot_id:
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
if not restore_snapshot(snapshot_id, state_dir=state_dir):
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
_update_current_branch(name, state_dir=state_dir)
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _update_current_branch(name: str, state_dir: str = ".omg/state") -> None:
|
|
413
|
+
"""Update the current branch tracker file."""
|
|
414
|
+
current_branch_path = os.path.join(state_dir, "current_branch.json")
|
|
415
|
+
data = {"name": name, "switched_at": datetime.now().isoformat()}
|
|
416
|
+
atomic_json_write = _get_atomic_json_write()
|
|
417
|
+
if atomic_json_write:
|
|
418
|
+
atomic_json_write(current_branch_path, data)
|
|
419
|
+
else:
|
|
420
|
+
try:
|
|
421
|
+
with open(current_branch_path, "w", encoding="utf-8") as f:
|
|
422
|
+
json.dump(data, f, separators=(",", ":"))
|
|
423
|
+
except Exception as e:
|
|
424
|
+
print(f"[OMG] Error updating current branch: {e}", file=sys.stderr)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# --- Merge API ---
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _get_merge_flag_enabled() -> bool:
|
|
431
|
+
"""Check if merge feature is enabled."""
|
|
432
|
+
env_val = os.environ.get("OMG_MERGE_ENABLED", "").lower()
|
|
433
|
+
if env_val in ("0", "false", "no"):
|
|
434
|
+
return False
|
|
435
|
+
if env_val in ("1", "true", "yes"):
|
|
436
|
+
return True
|
|
437
|
+
|
|
438
|
+
# Lazy import from hooks
|
|
439
|
+
hooks_dir = os.path.normpath(
|
|
440
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
|
|
441
|
+
)
|
|
442
|
+
if hooks_dir not in sys.path:
|
|
443
|
+
sys.path.insert(0, hooks_dir)
|
|
444
|
+
try:
|
|
445
|
+
from _common import get_feature_flag # type: ignore[import-untyped]
|
|
446
|
+
return get_feature_flag("MERGE", default=False)
|
|
447
|
+
except ImportError:
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _load_branch_state(branch_name: str, state_dir: str = ".omg/state") -> Optional[Dict[str, Any]]:
|
|
452
|
+
"""Load a branch's metadata as a flat state dict.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
branch_name: Name of the branch to load
|
|
456
|
+
state_dir: Path to the state directory
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Branch metadata dict, or None if branch does not exist or is invalid.
|
|
460
|
+
"""
|
|
461
|
+
branch_path = os.path.join(state_dir, "branches", f"{branch_name}.json")
|
|
462
|
+
if not os.path.exists(branch_path):
|
|
463
|
+
return None
|
|
464
|
+
try:
|
|
465
|
+
with open(branch_path, "r", encoding="utf-8") as f:
|
|
466
|
+
return json.load(f)
|
|
467
|
+
except (json.JSONDecodeError, OSError):
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def detect_merge_conflicts(
|
|
472
|
+
source_state: Dict[str, Any], target_state: Dict[str, Any]
|
|
473
|
+
) -> List[Dict[str, Any]]:
|
|
474
|
+
"""Compare two state dicts and find keys where both sides have different values.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
source_state: State dict from the source branch
|
|
478
|
+
target_state: State dict from the target branch
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
List of conflict dicts with keys: key, source_value, target_value, conflict_type.
|
|
482
|
+
conflict_type is "value_conflict" when both sides changed the same key
|
|
483
|
+
to different values.
|
|
484
|
+
"""
|
|
485
|
+
conflicts: List[Dict[str, Any]] = []
|
|
486
|
+
# Find keys present in both dicts with different values
|
|
487
|
+
common_keys = set(source_state.keys()) & set(target_state.keys())
|
|
488
|
+
for key in sorted(common_keys):
|
|
489
|
+
source_val = source_state[key]
|
|
490
|
+
target_val = target_state[key]
|
|
491
|
+
if source_val != target_val:
|
|
492
|
+
conflicts.append({
|
|
493
|
+
"key": key,
|
|
494
|
+
"source_value": source_val,
|
|
495
|
+
"target_value": target_val,
|
|
496
|
+
"conflict_type": "value_conflict",
|
|
497
|
+
})
|
|
498
|
+
return conflicts
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def preview_merge(
|
|
502
|
+
source_branch: str,
|
|
503
|
+
target_branch: str = "main",
|
|
504
|
+
state_dir: str = ".omg/state",
|
|
505
|
+
) -> Dict[str, Any]:
|
|
506
|
+
"""Preview a merge without applying changes.
|
|
507
|
+
|
|
508
|
+
Loads both branch snapshot states (as flat JSON dicts from snapshot
|
|
509
|
+
metadata) and detects conflicts.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
source_branch: Branch to merge from
|
|
513
|
+
target_branch: Branch to merge into (default: "main")
|
|
514
|
+
state_dir: Path to the state directory
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Preview dict with keys: source, target, conflicts, changes, preview.
|
|
518
|
+
Or {"skipped": True} if feature flag is disabled.
|
|
519
|
+
Or {"error": ...} if a branch cannot be found.
|
|
520
|
+
"""
|
|
521
|
+
if not _get_merge_flag_enabled():
|
|
522
|
+
return {"skipped": True}
|
|
523
|
+
|
|
524
|
+
source_state = _load_branch_state(source_branch, state_dir=state_dir)
|
|
525
|
+
if source_state is None:
|
|
526
|
+
return {"error": f"Source branch not found: {source_branch}"}
|
|
527
|
+
|
|
528
|
+
target_state = _load_branch_state(target_branch, state_dir=state_dir)
|
|
529
|
+
if target_state is None:
|
|
530
|
+
return {"error": f"Target branch not found: {target_branch}"}
|
|
531
|
+
|
|
532
|
+
conflicts = detect_merge_conflicts(source_state, target_state)
|
|
533
|
+
|
|
534
|
+
# Count keys that exist only in source (net new changes)
|
|
535
|
+
source_only_keys = set(source_state.keys()) - set(target_state.keys())
|
|
536
|
+
changes = len(source_only_keys) + len(conflicts)
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
"source": source_branch,
|
|
540
|
+
"target": target_branch,
|
|
541
|
+
"conflicts": conflicts,
|
|
542
|
+
"changes": changes,
|
|
543
|
+
"preview": True,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def merge_branch(
|
|
548
|
+
source_branch: str,
|
|
549
|
+
target_branch: str = "main",
|
|
550
|
+
state_dir: str = ".omg/state",
|
|
551
|
+
) -> Dict[str, Any]:
|
|
552
|
+
"""Merge source branch state into target branch.
|
|
553
|
+
|
|
554
|
+
Uses last-write-wins strategy when there are no conflicts.
|
|
555
|
+
If conflicts exist, the merge is aborted and conflicts are returned.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
source_branch: Branch to merge from
|
|
559
|
+
target_branch: Branch to merge into (default: "main")
|
|
560
|
+
state_dir: Path to the state directory
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Result dict with keys: merged, conflicts, changes_applied.
|
|
564
|
+
Or {"skipped": True} if feature flag is disabled.
|
|
565
|
+
Or {"error": ...} on failure.
|
|
566
|
+
"""
|
|
567
|
+
if not _get_merge_flag_enabled():
|
|
568
|
+
return {"skipped": True}
|
|
569
|
+
|
|
570
|
+
preview = preview_merge(source_branch, target_branch, state_dir=state_dir)
|
|
571
|
+
if preview.get("error"):
|
|
572
|
+
return preview
|
|
573
|
+
|
|
574
|
+
conflicts = preview.get("conflicts", [])
|
|
575
|
+
if conflicts:
|
|
576
|
+
return {
|
|
577
|
+
"merged": False,
|
|
578
|
+
"conflicts": conflicts,
|
|
579
|
+
"changes_applied": 0,
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# --- Apply merge: last-write-wins (source on top of target) ---
|
|
583
|
+
source_state = _load_branch_state(source_branch, state_dir=state_dir)
|
|
584
|
+
target_state = _load_branch_state(target_branch, state_dir=state_dir)
|
|
585
|
+
if source_state is None or target_state is None:
|
|
586
|
+
return {"error": "Branch state became unavailable during merge"}
|
|
587
|
+
|
|
588
|
+
merged_state = {**target_state, **source_state}
|
|
589
|
+
# Preserve target branch name and update status
|
|
590
|
+
merged_state["name"] = target_branch
|
|
591
|
+
merged_state["status"] = "active"
|
|
592
|
+
|
|
593
|
+
# Count actual changes applied
|
|
594
|
+
source_only_keys = set(source_state.keys()) - set(target_state.keys())
|
|
595
|
+
changes_applied = len(source_only_keys)
|
|
596
|
+
|
|
597
|
+
# Write merged state to target branch file
|
|
598
|
+
target_branch_path = os.path.join(state_dir, "branches", f"{target_branch}.json")
|
|
599
|
+
atomic_json_write = _get_atomic_json_write()
|
|
600
|
+
if atomic_json_write:
|
|
601
|
+
atomic_json_write(target_branch_path, merged_state)
|
|
602
|
+
else:
|
|
603
|
+
try:
|
|
604
|
+
os.makedirs(os.path.dirname(target_branch_path), exist_ok=True)
|
|
605
|
+
with open(target_branch_path, "w", encoding="utf-8") as f:
|
|
606
|
+
json.dump(merged_state, f, separators=(",", ":"))
|
|
607
|
+
except Exception as e:
|
|
608
|
+
return {"error": f"Failed to write merged state: {e}"}
|
|
609
|
+
|
|
610
|
+
# Mark source branch as merged
|
|
611
|
+
source_branch_path = os.path.join(state_dir, "branches", f"{source_branch}.json")
|
|
612
|
+
if source_state:
|
|
613
|
+
source_state["status"] = "merged"
|
|
614
|
+
source_state["merged_into"] = target_branch
|
|
615
|
+
source_state["merged_at"] = datetime.now().isoformat()
|
|
616
|
+
if atomic_json_write:
|
|
617
|
+
atomic_json_write(source_branch_path, source_state)
|
|
618
|
+
else:
|
|
619
|
+
try:
|
|
620
|
+
with open(source_branch_path, "w", encoding="utf-8") as f:
|
|
621
|
+
json.dump(source_state, f, separators=(",", ":"))
|
|
622
|
+
except Exception as e:
|
|
623
|
+
print(f"[OMG] Error updating source branch status: {e}", file=sys.stderr)
|
|
624
|
+
|
|
625
|
+
# Update current_branch.json to reflect merged state
|
|
626
|
+
_update_current_branch(target_branch, state_dir=state_dir)
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
"merged": True,
|
|
630
|
+
"conflicts": [],
|
|
631
|
+
"changes_applied": changes_applied,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
def main():
|
|
635
|
+
"""CLI entry point."""
|
|
636
|
+
if len(sys.argv) < 2:
|
|
637
|
+
print(
|
|
638
|
+
"Usage: python3 session_snapshot.py <command> [options]",
|
|
639
|
+
file=sys.stderr,
|
|
640
|
+
)
|
|
641
|
+
print("Commands:", file=sys.stderr)
|
|
642
|
+
print(" create [--name NAME] Create a snapshot", file=sys.stderr)
|
|
643
|
+
print(" list List all snapshots", file=sys.stderr)
|
|
644
|
+
print(" restore <snapshot_id> Restore a snapshot", file=sys.stderr)
|
|
645
|
+
print(" delete <snapshot_id> Delete a snapshot", file=sys.stderr)
|
|
646
|
+
print(" branch <name> Create a branch", file=sys.stderr)
|
|
647
|
+
print(" branches List all branches", file=sys.stderr)
|
|
648
|
+
print(" switch <name> Switch to a branch", file=sys.stderr)
|
|
649
|
+
print(" merge <source> [--into <target>] Merge branches", file=sys.stderr)
|
|
650
|
+
print(" merge-preview <source> [--into <target>] Preview merge", file=sys.stderr)
|
|
651
|
+
sys.exit(1)
|
|
652
|
+
|
|
653
|
+
command = sys.argv[1]
|
|
654
|
+
|
|
655
|
+
if command == "create":
|
|
656
|
+
name = None
|
|
657
|
+
if len(sys.argv) > 3 and sys.argv[2] == "--name":
|
|
658
|
+
name = sys.argv[3]
|
|
659
|
+
result = create_snapshot(name=name)
|
|
660
|
+
print(json.dumps(result, indent=2))
|
|
661
|
+
|
|
662
|
+
elif command == "list":
|
|
663
|
+
snapshots = list_snapshots()
|
|
664
|
+
print(json.dumps(snapshots, indent=2))
|
|
665
|
+
|
|
666
|
+
elif command == "restore":
|
|
667
|
+
if len(sys.argv) < 3:
|
|
668
|
+
print("Usage: python3 session_snapshot.py restore <snapshot_id>", file=sys.stderr)
|
|
669
|
+
sys.exit(1)
|
|
670
|
+
snapshot_id = sys.argv[2]
|
|
671
|
+
success = restore_snapshot(snapshot_id)
|
|
672
|
+
result = {"success": success, "snapshot_id": snapshot_id}
|
|
673
|
+
print(json.dumps(result, indent=2))
|
|
674
|
+
|
|
675
|
+
elif command == "delete":
|
|
676
|
+
if len(sys.argv) < 3:
|
|
677
|
+
print("Usage: python3 session_snapshot.py delete <snapshot_id>", file=sys.stderr)
|
|
678
|
+
sys.exit(1)
|
|
679
|
+
snapshot_id = sys.argv[2]
|
|
680
|
+
success = delete_snapshot(snapshot_id)
|
|
681
|
+
result = {"success": success, "snapshot_id": snapshot_id}
|
|
682
|
+
print(json.dumps(result, indent=2))
|
|
683
|
+
|
|
684
|
+
elif command == "branch":
|
|
685
|
+
if len(sys.argv) < 3:
|
|
686
|
+
print("Usage: python3 session_snapshot.py branch <name> [--from <snapshot_id>]", file=sys.stderr)
|
|
687
|
+
sys.exit(1)
|
|
688
|
+
branch_name = sys.argv[2]
|
|
689
|
+
from_id = None
|
|
690
|
+
if len(sys.argv) > 4 and sys.argv[3] == "--from":
|
|
691
|
+
from_id = sys.argv[4]
|
|
692
|
+
result = create_branch(branch_name, from_snapshot_id=from_id)
|
|
693
|
+
print(json.dumps(result, indent=2))
|
|
694
|
+
|
|
695
|
+
elif command == "branches":
|
|
696
|
+
branches = list_branches()
|
|
697
|
+
print(json.dumps(branches, indent=2))
|
|
698
|
+
|
|
699
|
+
elif command == "switch":
|
|
700
|
+
if len(sys.argv) < 3:
|
|
701
|
+
print("Usage: python3 session_snapshot.py switch <name>", file=sys.stderr)
|
|
702
|
+
sys.exit(1)
|
|
703
|
+
branch_name = sys.argv[2]
|
|
704
|
+
success = switch_branch(branch_name)
|
|
705
|
+
result = {"success": success, "branch": branch_name}
|
|
706
|
+
print(json.dumps(result, indent=2))
|
|
707
|
+
|
|
708
|
+
elif command == "merge":
|
|
709
|
+
if len(sys.argv) < 3:
|
|
710
|
+
print("Usage: python3 session_snapshot.py merge <source> [--into <target>]", file=sys.stderr)
|
|
711
|
+
sys.exit(1)
|
|
712
|
+
source = sys.argv[2]
|
|
713
|
+
target = "main"
|
|
714
|
+
if len(sys.argv) > 4 and sys.argv[3] == "--into":
|
|
715
|
+
target = sys.argv[4]
|
|
716
|
+
result = merge_branch(source, target_branch=target)
|
|
717
|
+
print(json.dumps(result, indent=2))
|
|
718
|
+
|
|
719
|
+
elif command == "merge-preview":
|
|
720
|
+
if len(sys.argv) < 3:
|
|
721
|
+
print("Usage: python3 session_snapshot.py merge-preview <source> [--into <target>]", file=sys.stderr)
|
|
722
|
+
sys.exit(1)
|
|
723
|
+
source = sys.argv[2]
|
|
724
|
+
target = "main"
|
|
725
|
+
if len(sys.argv) > 4 and sys.argv[3] == "--into":
|
|
726
|
+
target = sys.argv[4]
|
|
727
|
+
result = preview_merge(source, target_branch=target)
|
|
728
|
+
print(json.dumps(result, indent=2))
|
|
729
|
+
|
|
730
|
+
else:
|
|
731
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
732
|
+
sys.exit(1)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
if __name__ == "__main__":
|
|
736
|
+
main()
|