@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.
Files changed (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. 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()