@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,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Theme Engine for OMG
|
|
4
|
+
|
|
5
|
+
Provides terminal capability detection, theme definition format,
|
|
6
|
+
color scheme application, and preference management.
|
|
7
|
+
|
|
8
|
+
Feature flag: OMG_THEMES_ENABLED (default: False)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import datetime
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import yaml
|
|
21
|
+
except ImportError:
|
|
22
|
+
yaml = None
|
|
23
|
+
|
|
24
|
+
# --- Lazy imports for hooks/_common.py ---
|
|
25
|
+
|
|
26
|
+
_get_feature_flag = None
|
|
27
|
+
_atomic_json_write = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_imports():
|
|
31
|
+
"""Lazy import feature flag and atomic write from hooks/_common.py."""
|
|
32
|
+
global _get_feature_flag, _atomic_json_write
|
|
33
|
+
if _get_feature_flag is not None:
|
|
34
|
+
return
|
|
35
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
36
|
+
if repo_root not in sys.path:
|
|
37
|
+
sys.path.insert(0, repo_root)
|
|
38
|
+
try:
|
|
39
|
+
from hooks._common import get_feature_flag as _gff
|
|
40
|
+
from hooks._common import atomic_json_write as _ajw
|
|
41
|
+
_get_feature_flag = _gff
|
|
42
|
+
_atomic_json_write = _ajw
|
|
43
|
+
except ImportError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_themes_enabled() -> bool:
|
|
48
|
+
"""Check if themes are enabled via feature flag."""
|
|
49
|
+
_ensure_imports()
|
|
50
|
+
if _get_feature_flag:
|
|
51
|
+
return _get_feature_flag("THEMES", default=False)
|
|
52
|
+
# Fallback if _common.py is not available
|
|
53
|
+
env_val = os.environ.get("OMG_THEMES_ENABLED", "").lower()
|
|
54
|
+
if env_val in ("1", "true", "yes"):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Theme:
|
|
61
|
+
"""Theme definition."""
|
|
62
|
+
name: str
|
|
63
|
+
variant: str
|
|
64
|
+
colors: Dict[str, str]
|
|
65
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Theme":
|
|
69
|
+
"""Create a Theme from a dictionary."""
|
|
70
|
+
return cls(
|
|
71
|
+
name=data.get("name", "unknown"),
|
|
72
|
+
variant=data.get("variant", "dark"),
|
|
73
|
+
colors=data.get("colors", {}),
|
|
74
|
+
metadata=data.get("metadata", {})
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ThemeEngine:
|
|
79
|
+
"""Engine for managing and applying themes."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, project_dir: Optional[str] = None):
|
|
82
|
+
self.project_dir = project_dir or os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
83
|
+
self.themes_dir = os.path.join(self.project_dir, "config", "themes")
|
|
84
|
+
self.state_file = os.path.join(self.project_dir, ".omg", "state", "theme.json")
|
|
85
|
+
|
|
86
|
+
def detect_capabilities(self) -> Dict[str, bool]:
|
|
87
|
+
"""Detect terminal capabilities."""
|
|
88
|
+
if not is_themes_enabled():
|
|
89
|
+
return {"truecolor": False, "256color": False, "basic": False, "dark_mode": True}
|
|
90
|
+
|
|
91
|
+
colorterm = os.environ.get("COLORTERM", "").lower()
|
|
92
|
+
term = os.environ.get("TERM", "").lower()
|
|
93
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
94
|
+
|
|
95
|
+
truecolor = colorterm in ("truecolor", "24bit")
|
|
96
|
+
color256 = "256color" in term or truecolor
|
|
97
|
+
basic = "color" in term or color256
|
|
98
|
+
|
|
99
|
+
# Dark mode detection
|
|
100
|
+
dark_mode = True
|
|
101
|
+
colorfgbg = os.environ.get("COLORFGBG", "")
|
|
102
|
+
if colorfgbg:
|
|
103
|
+
parts = colorfgbg.split(";")
|
|
104
|
+
if len(parts) >= 2:
|
|
105
|
+
try:
|
|
106
|
+
bg = int(parts[-1])
|
|
107
|
+
if bg >= 8:
|
|
108
|
+
dark_mode = False
|
|
109
|
+
except ValueError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
if term_program == "iterm.app":
|
|
113
|
+
# iTerm2 specific detection could go here, but default to dark
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"truecolor": truecolor,
|
|
118
|
+
"256color": color256,
|
|
119
|
+
"basic": basic,
|
|
120
|
+
"dark_mode": dark_mode
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def load_theme(self, name: str) -> Optional[Theme]:
|
|
124
|
+
"""Load a theme by name from config/themes/."""
|
|
125
|
+
if not is_themes_enabled() or not yaml:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
theme_path = os.path.join(self.themes_dir, f"{name}.yaml")
|
|
129
|
+
if not os.path.exists(theme_path):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
with open(theme_path, "r", encoding="utf-8") as f:
|
|
134
|
+
data = yaml.safe_load(f)
|
|
135
|
+
if data:
|
|
136
|
+
return Theme.from_dict(data)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def _hex_to_rgb(self, hex_color: str) -> tuple:
|
|
142
|
+
"""Convert hex color to RGB tuple."""
|
|
143
|
+
hex_color = hex_color.lstrip("#")
|
|
144
|
+
if len(hex_color) == 3:
|
|
145
|
+
hex_color = "".join(c + c for c in hex_color)
|
|
146
|
+
if len(hex_color) != 6:
|
|
147
|
+
return (0, 0, 0)
|
|
148
|
+
try:
|
|
149
|
+
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
150
|
+
except ValueError:
|
|
151
|
+
return (0, 0, 0)
|
|
152
|
+
|
|
153
|
+
def _rgb_to_256(self, r: int, g: int, b: int) -> int:
|
|
154
|
+
"""Convert RGB to nearest 256-color index."""
|
|
155
|
+
if r == g == b:
|
|
156
|
+
if r < 8:
|
|
157
|
+
return 16
|
|
158
|
+
if r > 248:
|
|
159
|
+
return 231
|
|
160
|
+
return round(((r - 8) / 247) * 24) + 232
|
|
161
|
+
|
|
162
|
+
r_idx = int(round(r / 255.0 * 5))
|
|
163
|
+
g_idx = int(round(g / 255.0 * 5))
|
|
164
|
+
b_idx = int(round(b / 255.0 * 5))
|
|
165
|
+
return 16 + 36 * r_idx + 6 * g_idx + b_idx
|
|
166
|
+
|
|
167
|
+
def apply_theme(self, theme: Theme) -> Dict[str, str]:
|
|
168
|
+
"""Return ANSI escape codes for the theme colors."""
|
|
169
|
+
if not is_themes_enabled():
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
caps = self.detect_capabilities()
|
|
173
|
+
if not caps["basic"]:
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
ansi_codes = {}
|
|
177
|
+
for key, hex_color in theme.colors.items():
|
|
178
|
+
if not hex_color.startswith("#"):
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
r, g, b = self._hex_to_rgb(hex_color)
|
|
182
|
+
|
|
183
|
+
if caps["truecolor"]:
|
|
184
|
+
ansi_codes[key] = f"\033[38;2;{r};{g};{b}m"
|
|
185
|
+
elif caps["256color"]:
|
|
186
|
+
color_idx = self._rgb_to_256(r, g, b)
|
|
187
|
+
ansi_codes[key] = f"\033[38;5;{color_idx}m"
|
|
188
|
+
else:
|
|
189
|
+
# Basic 8-color fallback (simplified)
|
|
190
|
+
ansi_codes[key] = "\033[39m" # Default foreground
|
|
191
|
+
|
|
192
|
+
return ansi_codes
|
|
193
|
+
|
|
194
|
+
def get_available_themes(self) -> List[str]:
|
|
195
|
+
"""List available themes in config/themes/."""
|
|
196
|
+
if not is_themes_enabled() or not os.path.exists(self.themes_dir):
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
themes = []
|
|
200
|
+
try:
|
|
201
|
+
for filename in os.listdir(self.themes_dir):
|
|
202
|
+
if filename.endswith(".yaml") or filename.endswith(".yml"):
|
|
203
|
+
themes.append(os.path.splitext(filename)[0])
|
|
204
|
+
except OSError:
|
|
205
|
+
pass
|
|
206
|
+
return sorted(themes)
|
|
207
|
+
|
|
208
|
+
def save_preference(self, theme_name: str) -> bool:
|
|
209
|
+
"""Save theme preference to .omg/state/theme.json."""
|
|
210
|
+
if not is_themes_enabled():
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
_ensure_imports()
|
|
214
|
+
if not _atomic_json_write:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
data = {
|
|
218
|
+
"theme": theme_name,
|
|
219
|
+
"set_at": datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
_atomic_json_write(self.state_file, data)
|
|
224
|
+
return True
|
|
225
|
+
except Exception:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def get_preference(self) -> Optional[str]:
|
|
229
|
+
"""Read theme preference from .omg/state/theme.json."""
|
|
230
|
+
if not is_themes_enabled():
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
if not os.path.exists(self.state_file):
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
with open(self.state_file, "r", encoding="utf-8") as f:
|
|
238
|
+
data = json.load(f)
|
|
239
|
+
return data.get("theme")
|
|
240
|
+
except Exception:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
"""CLI entry point."""
|
|
246
|
+
parser = argparse.ArgumentParser(description="OMG Theme Engine")
|
|
247
|
+
parser.add_argument("--detect-capabilities", action="store_true", help="Detect terminal capabilities")
|
|
248
|
+
parser.add_argument("--list", action="store_true", help="List available themes")
|
|
249
|
+
parser.add_argument("--apply", metavar="THEME", help="Apply a theme and show colors")
|
|
250
|
+
parser.add_argument("--set", metavar="THEME", help="Set theme preference")
|
|
251
|
+
parser.add_argument("--get", action="store_true", help="Get current theme preference")
|
|
252
|
+
|
|
253
|
+
args = parser.parse_args()
|
|
254
|
+
|
|
255
|
+
if not is_themes_enabled():
|
|
256
|
+
print("Themes are disabled. Set OMG_THEMES_ENABLED=true to enable.")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
engine = ThemeEngine()
|
|
260
|
+
|
|
261
|
+
if args.detect_capabilities:
|
|
262
|
+
caps = engine.detect_capabilities()
|
|
263
|
+
print(json.dumps(caps, indent=2))
|
|
264
|
+
elif args.list:
|
|
265
|
+
themes = engine.get_available_themes()
|
|
266
|
+
for theme in themes:
|
|
267
|
+
print(theme)
|
|
268
|
+
elif args.apply:
|
|
269
|
+
theme = engine.load_theme(args.apply)
|
|
270
|
+
if theme:
|
|
271
|
+
codes = engine.apply_theme(theme)
|
|
272
|
+
for name, code in codes.items():
|
|
273
|
+
print(f"{code}{name}\033[0m")
|
|
274
|
+
else:
|
|
275
|
+
print(f"Theme '{args.apply}' not found.")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
elif args.set:
|
|
278
|
+
if engine.save_preference(args.set):
|
|
279
|
+
print(f"Theme preference set to '{args.set}'")
|
|
280
|
+
else:
|
|
281
|
+
print("Failed to save preference.")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
elif args.get:
|
|
284
|
+
pref = engine.get_preference()
|
|
285
|
+
if pref:
|
|
286
|
+
print(pref)
|
|
287
|
+
else:
|
|
288
|
+
print("No theme preference set.")
|
|
289
|
+
else:
|
|
290
|
+
parser.print_help()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import argparse
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Dict, Any, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
def is_themes_enabled() -> bool:
|
|
9
|
+
"""Lazy import feature flag."""
|
|
10
|
+
try:
|
|
11
|
+
from hooks._common import get_feature_flag
|
|
12
|
+
return get_feature_flag("THEMES", default=False)
|
|
13
|
+
except ImportError:
|
|
14
|
+
return os.environ.get("OMG_THEMES_ENABLED", "").lower() in ("1", "true", "yes")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from tools.theme_engine import ThemeEngine, Theme
|
|
18
|
+
except ImportError:
|
|
19
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
20
|
+
from tools.theme_engine import ThemeEngine, Theme
|
|
21
|
+
|
|
22
|
+
class ThemeSelector:
|
|
23
|
+
"""Interactive theme selection, preview, and auto-detection."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, engine: Optional[ThemeEngine] = None):
|
|
26
|
+
self.engine = engine or ThemeEngine()
|
|
27
|
+
|
|
28
|
+
def list_themes(self) -> Union[List[str], Dict[str, str]]:
|
|
29
|
+
"""Returns sorted list of available theme names."""
|
|
30
|
+
if not is_themes_enabled():
|
|
31
|
+
return {"error": "Themes are disabled"}
|
|
32
|
+
return sorted(self.engine.get_available_themes())
|
|
33
|
+
|
|
34
|
+
def preview_theme(self, name: str) -> Dict[str, Any]:
|
|
35
|
+
"""Returns preview info {name, colors, ansi_preview: str} without applying."""
|
|
36
|
+
if not is_themes_enabled():
|
|
37
|
+
return {"error": "Themes are disabled"}
|
|
38
|
+
|
|
39
|
+
theme = self.engine.load_theme(name)
|
|
40
|
+
if not theme:
|
|
41
|
+
return {"error": f"Theme '{name}' not found"}
|
|
42
|
+
|
|
43
|
+
ansi_codes = self.engine.apply_theme(theme)
|
|
44
|
+
|
|
45
|
+
preview_lines = [f"Preview for {theme.name}:"]
|
|
46
|
+
for color_name, hex_val in theme.colors.items():
|
|
47
|
+
ansi = ansi_codes.get(color_name, "")
|
|
48
|
+
reset = "\033[0m" if ansi else ""
|
|
49
|
+
preview_lines.append(f"{ansi}██ {color_name}: {hex_val}{reset}")
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"name": theme.name,
|
|
53
|
+
"colors": theme.colors,
|
|
54
|
+
"ansi_preview": "\n".join(preview_lines)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def set_theme(self, name: str) -> Dict[str, Any]:
|
|
58
|
+
"""Applies + persists theme, returns {success, theme, applied_at}."""
|
|
59
|
+
if not is_themes_enabled():
|
|
60
|
+
return {"error": "Themes are disabled"}
|
|
61
|
+
|
|
62
|
+
theme = self.engine.load_theme(name)
|
|
63
|
+
if not theme:
|
|
64
|
+
return {"error": f"Theme '{name}' not found"}
|
|
65
|
+
|
|
66
|
+
self.engine.apply_theme(theme)
|
|
67
|
+
success = self.engine.save_preference(name)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"success": success,
|
|
71
|
+
"theme": theme.name,
|
|
72
|
+
"applied_at": datetime.now(timezone.utc).isoformat()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def get_current_theme(self) -> Union[Optional[str], Dict[str, str]]:
|
|
76
|
+
"""Reads current theme from .omg/state/theme.json."""
|
|
77
|
+
if not is_themes_enabled():
|
|
78
|
+
return {"error": "Themes are disabled"}
|
|
79
|
+
return self.engine.get_preference()
|
|
80
|
+
|
|
81
|
+
def auto_detect_theme(self) -> Union[str, Dict[str, str]]:
|
|
82
|
+
"""Detects dark/light mode, returns appropriate default theme name."""
|
|
83
|
+
if not is_themes_enabled():
|
|
84
|
+
return {"error": "Themes are disabled"}
|
|
85
|
+
|
|
86
|
+
caps = self.engine.detect_capabilities()
|
|
87
|
+
if caps.get("dark_mode", True):
|
|
88
|
+
return "catppuccin-mocha"
|
|
89
|
+
else:
|
|
90
|
+
return "catppuccin-latte"
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
parser = argparse.ArgumentParser(description="OMG Theme Selector")
|
|
94
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
95
|
+
group.add_argument("--list", action="store_true", help="List available themes")
|
|
96
|
+
group.add_argument("--preview", type=str, metavar="NAME", help="Preview a theme")
|
|
97
|
+
group.add_argument("--set", type=str, metavar="NAME", help="Set a theme")
|
|
98
|
+
group.add_argument("--auto", action="store_true", help="Auto-detect theme")
|
|
99
|
+
group.add_argument("--current", action="store_true", help="Get current theme")
|
|
100
|
+
|
|
101
|
+
args = parser.parse_args()
|
|
102
|
+
|
|
103
|
+
selector = ThemeSelector()
|
|
104
|
+
|
|
105
|
+
if args.list:
|
|
106
|
+
result = selector.list_themes()
|
|
107
|
+
if isinstance(result, dict) and "error" in result:
|
|
108
|
+
print(json.dumps(result))
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
for theme in result:
|
|
111
|
+
print(theme)
|
|
112
|
+
elif args.preview:
|
|
113
|
+
result = selector.preview_theme(args.preview)
|
|
114
|
+
if "error" in result:
|
|
115
|
+
print(json.dumps(result))
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
print(result["ansi_preview"])
|
|
118
|
+
elif args.set:
|
|
119
|
+
result = selector.set_theme(args.set)
|
|
120
|
+
print(json.dumps(result))
|
|
121
|
+
if "error" in result or not result.get("success"):
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
elif args.auto:
|
|
124
|
+
result = selector.auto_detect_theme()
|
|
125
|
+
if isinstance(result, dict) and "error" in result:
|
|
126
|
+
print(json.dumps(result))
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
print(result)
|
|
129
|
+
elif args.current:
|
|
130
|
+
result = selector.get_current_theme()
|
|
131
|
+
if isinstance(result, dict) and "error" in result:
|
|
132
|
+
print(json.dumps(result))
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
print(result if result else "None")
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|