@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,464 @@
1
+ #!/usr/bin/env python3
2
+ """OMG Migration Script — Migrate OMC settings to OMG.
3
+
4
+ Handles:
5
+ 1. Back up current ~/.claude/settings.json
6
+ 2. Deploy OMG hooks to ~/.claude/hooks/
7
+ 3. Deduplicate hooks in settings.json (remove double-registered entries)
8
+ 4. Fix common format bugs (ConfigChange wrapper, etc.)
9
+ 5. Switch statusLine from OMC HUD to OMG HUD
10
+ 6. Remove dead/erroring plugins
11
+ 7. Deploy OMG HUD to ~/.claude/hud/
12
+
13
+ Idempotent — safe to run multiple times. Always creates a backup first.
14
+
15
+ Usage:
16
+ python3 scripts/migrate-legacy.py # full migration
17
+ python3 scripts/migrate-legacy.py --dry-run # preview changes only
18
+ python3 scripts/migrate-legacy.py --hooks-only # only deploy hooks
19
+ python3 scripts/migrate-legacy.py --hud-only # only switch HUD
20
+ python3 scripts/migrate-legacy.py --restore BACKUP # restore a backup
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import shutil
28
+ import sys
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ # ── Paths ────────────────────────────────────────────────────────────────────
34
+ OMG_ROOT = Path(__file__).resolve().parents[1]
35
+ HOME = Path.home()
36
+ CLAUDE_DIR = HOME / ".claude"
37
+ SETTINGS_PATH = CLAUDE_DIR / "settings.json"
38
+ HOOKS_DIR = CLAUDE_DIR / "hooks"
39
+ HUD_DIR = CLAUDE_DIR / "hud"
40
+ BACKUP_DIR = CLAUDE_DIR / ".omg-backups"
41
+
42
+ OMG_HOOKS_DIR = OMG_ROOT / "hooks"
43
+ OMG_HUD_SRC = OMG_ROOT / "hud" / "omg-hud.mjs"
44
+ OMG_VERSION_TAG = "omg-v1"
45
+
46
+ # ── Hook canonical definitions ───────────────────────────────────────────────
47
+ # Single source of truth for which hooks run on which events.
48
+ # This eliminates duplication regardless of how many times migration runs.
49
+ CANONICAL_HOOKS: dict[str, list[dict[str, Any]]] = {
50
+ "PreToolUse": [
51
+ {
52
+ "matcher": "Bash",
53
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/firewall.py"', "timeout": 5}],
54
+ },
55
+ {
56
+ "matcher": "Read|Write|Edit|MultiEdit",
57
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/secret-guard.py"', "timeout": 5}],
58
+ },
59
+ ],
60
+ "PostToolUse": [
61
+ {
62
+ "matcher": "Bash|Write|Edit|MultiEdit",
63
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/tool-ledger.py"', "timeout": 10}],
64
+ },
65
+ {
66
+ "matcher": "Bash",
67
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/circuit-breaker.py"', "timeout": 10}],
68
+ },
69
+ {
70
+ "matcher": "Write|Edit|MultiEdit",
71
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/post-write.py"', "timeout": 30}],
72
+ },
73
+ {
74
+ "matcher": "Write|Edit|MultiEdit",
75
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/shadow_manager.py"', "timeout": 15}],
76
+ },
77
+ ],
78
+ "Stop": [
79
+ {
80
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/quality-gate.py"', "timeout": 10}],
81
+ },
82
+ {
83
+ "hooks": [
84
+ {"type": "command", "command": 'python3 "$HOME/.claude/hooks/stop-gate.py"', "timeout": 15},
85
+ {"type": "command", "command": 'python3 "$HOME/.claude/hooks/test-validator.py"', "timeout": 30},
86
+ {"type": "command", "command": 'python3 "$HOME/.claude/hooks/quality-runner.py"', "timeout": 180},
87
+ ],
88
+ },
89
+ ],
90
+ "PreCompact": [
91
+ {
92
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/pre-compact.py"', "timeout": 15}],
93
+ },
94
+ ],
95
+ "SessionStart": [
96
+ {
97
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/session-start.py"', "timeout": 10}],
98
+ },
99
+ ],
100
+ "UserPromptSubmit": [
101
+ {
102
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/prompt-enhancer.py"', "timeout": 10}],
103
+ },
104
+ ],
105
+ "ConfigChange": [
106
+ {
107
+ "hooks": [{"type": "command", "command": 'python3 "$HOME/.claude/hooks/config-guard.py"', "timeout": 10}],
108
+ },
109
+ ],
110
+ }
111
+
112
+ # Plugins to remove (known dead/erroring)
113
+ DEAD_PLUGINS = [
114
+ "claude-delegator@jarrodwatts-claude-delegator",
115
+ ]
116
+
117
+ # ── Helpers ──────────────────────────────────────────────────────────────────
118
+
119
+ def timestamp() -> str:
120
+ return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
121
+
122
+
123
+ def backup_settings() -> Path | None:
124
+ """Create a timestamped backup of settings.json. Returns backup path."""
125
+ if not SETTINGS_PATH.exists():
126
+ return None
127
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
128
+ dest = BACKUP_DIR / f"settings-{timestamp()}.json"
129
+ shutil.copy2(SETTINGS_PATH, dest)
130
+ return dest
131
+
132
+
133
+ def load_settings() -> dict[str, Any]:
134
+ if not SETTINGS_PATH.exists():
135
+ return {"$schema": "https://json.schemastore.org/claude-code-settings.json"}
136
+ with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
137
+ return json.load(f)
138
+
139
+
140
+ def save_settings(settings: dict[str, Any]) -> None:
141
+ with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
142
+ json.dump(settings, f, indent=2, ensure_ascii=False)
143
+ f.write("\n")
144
+
145
+
146
+ # ── Migration steps ──────────────────────────────────────────────────────────
147
+
148
+ def step_deploy_hooks(dry_run: bool) -> list[str]:
149
+ """Copy OMG hook files to ~/.claude/hooks/."""
150
+ changes: list[str] = []
151
+ HOOKS_DIR.mkdir(parents=True, exist_ok=True)
152
+
153
+ hook_files = [f for f in OMG_HOOKS_DIR.iterdir() if f.suffix == ".py" and not f.name.startswith("__")]
154
+ for src in sorted(hook_files):
155
+ dest = HOOKS_DIR / src.name
156
+ # Check if update needed
157
+ if dest.exists():
158
+ src_content = src.read_bytes()
159
+ dest_content = dest.read_bytes()
160
+ if src_content == dest_content:
161
+ continue
162
+ changes.append(f" hooks/{src.name} -> ~/.claude/hooks/{src.name}")
163
+ if not dry_run:
164
+ shutil.copy2(src, dest)
165
+ dest.chmod(0o755)
166
+
167
+ # Write version marker
168
+ version_file = HOOKS_DIR / ".omg-version"
169
+ tag = f"{OMG_VERSION_TAG}-{timestamp()[:8]}"
170
+ if not dry_run:
171
+ version_file.write_text(tag + "\n", encoding="utf-8")
172
+ changes.append(f" .omg-version = {tag}")
173
+
174
+ # Write coexist marker
175
+ coexist_file = HOOKS_DIR / ".omg-coexist"
176
+ if not dry_run:
177
+ coexist_file.write_text("omc-coexist\n", encoding="utf-8")
178
+
179
+ return changes
180
+
181
+
182
+ def step_deduplicate_hooks(settings: dict[str, Any], dry_run: bool) -> list[str]:
183
+ """Replace all hook entries with the canonical set."""
184
+ changes: list[str] = []
185
+ old_hooks = settings.get("hooks", {})
186
+
187
+ # Count old entries for reporting
188
+ old_count = sum(
189
+ len(v) if isinstance(v, list) else 1
190
+ for v in old_hooks.values()
191
+ )
192
+ new_count = sum(len(v) for v in CANONICAL_HOOKS.values())
193
+
194
+ if old_hooks != CANONICAL_HOOKS:
195
+ changes.append(f" hooks: {old_count} entries -> {new_count} canonical entries")
196
+ if not dry_run:
197
+ settings["hooks"] = CANONICAL_HOOKS
198
+
199
+ return changes
200
+
201
+
202
+ def step_switch_hud(settings: dict[str, Any], dry_run: bool) -> list[str]:
203
+ """Switch statusLine to OMG HUD."""
204
+ changes: list[str] = []
205
+
206
+ # Deploy OMG HUD file
207
+ HUD_DIR.mkdir(parents=True, exist_ok=True)
208
+ hud_dest = HUD_DIR / "omg-hud.mjs"
209
+ if OMG_HUD_SRC.exists():
210
+ if hud_dest.exists():
211
+ if OMG_HUD_SRC.read_bytes() != hud_dest.read_bytes():
212
+ changes.append(f" hud/omg-hud.mjs updated")
213
+ if not dry_run:
214
+ shutil.copy2(OMG_HUD_SRC, hud_dest)
215
+ else:
216
+ changes.append(f" hud/omg-hud.mjs deployed")
217
+ if not dry_run:
218
+ shutil.copy2(OMG_HUD_SRC, hud_dest)
219
+
220
+ # Update statusLine in settings
221
+ old_sl = settings.get("statusLine", {})
222
+ new_sl = {"type": "command", "command": "node ~/.claude/hud/omg-hud.mjs"}
223
+ if old_sl != new_sl:
224
+ old_cmd = old_sl.get("command", "(none)")
225
+ changes.append(f" statusLine: {old_cmd} -> node ~/.claude/hud/omg-hud.mjs")
226
+ if not dry_run:
227
+ settings["statusLine"] = new_sl
228
+
229
+ return changes
230
+
231
+
232
+ def step_clean_plugins(settings: dict[str, Any], dry_run: bool) -> list[str]:
233
+ """Remove dead/erroring plugins."""
234
+ changes: list[str] = []
235
+ plugins = settings.get("enabledPlugins", {})
236
+
237
+ for plugin_id in DEAD_PLUGINS:
238
+ if plugin_id in plugins:
239
+ changes.append(f" removed plugin: {plugin_id}")
240
+ if not dry_run:
241
+ del plugins[plugin_id]
242
+
243
+ return changes
244
+
245
+
246
+ def step_fix_plugin_manifests(dry_run: bool) -> list[str]:
247
+ """Fix known plugin manifest issues (unrecognized keys)."""
248
+ changes: list[str] = []
249
+
250
+ # Fix legacy plugin manifest
251
+ manifest_path = CLAUDE_DIR / "plugins" / "cache" / "claude-plugins-official" / "omg-superpowers"
252
+ if manifest_path.exists():
253
+ for version_dir in manifest_path.iterdir():
254
+ plugin_json = version_dir / ".claude-plugin" / "plugin.json"
255
+ if plugin_json.exists():
256
+ try:
257
+ data = json.loads(plugin_json.read_text(encoding="utf-8"))
258
+ bad_keys = [k for k in ("category", "source") if k in data]
259
+ if bad_keys:
260
+ changes.append(f" fixed {plugin_json.relative_to(CLAUDE_DIR)}: removed {bad_keys}")
261
+ if not dry_run:
262
+ for k in bad_keys:
263
+ del data[k]
264
+ plugin_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
265
+ except (json.JSONDecodeError, OSError):
266
+ pass
267
+
268
+ return changes
269
+
270
+
271
+ def step_add_comment(settings: dict[str, Any], dry_run: bool) -> list[str]:
272
+ """Add OMG migration marker comment."""
273
+ changes: list[str] = []
274
+ marker = f"OMG v1 migrated {timestamp()[:8]}. Hooks canonical. HUD standalone."
275
+ old_comment = settings.get("_comment", "")
276
+ if "OMG" not in old_comment:
277
+ changes.append(f" _comment updated")
278
+ if not dry_run:
279
+ settings["_comment"] = marker
280
+ return changes
281
+
282
+
283
+ # ── Remove hooks from project settings.json ──────────────────────────────────
284
+
285
+ def step_clean_project_settings(dry_run: bool) -> list[str]:
286
+ """Remove hooks from OMG project settings.json to prevent double-execution."""
287
+ changes: list[str] = []
288
+ project_settings = OMG_ROOT / "settings.json"
289
+ if not project_settings.exists():
290
+ return changes
291
+
292
+ try:
293
+ data = json.loads(project_settings.read_text(encoding="utf-8"))
294
+ except (json.JSONDecodeError, OSError):
295
+ return changes
296
+
297
+ if "hooks" in data:
298
+ hook_count = sum(len(v) if isinstance(v, list) else 1 for v in data["hooks"].values())
299
+ changes.append(f" project settings.json: removed {hook_count} hook entries (already in user-level)")
300
+ if not dry_run:
301
+ del data["hooks"]
302
+ project_settings.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
303
+
304
+ return changes
305
+
306
+
307
+ # ── Restore ──────────────────────────────────────────────────────────────────
308
+
309
+ def restore_backup(backup_path: str) -> None:
310
+ p = Path(backup_path)
311
+ if not p.exists():
312
+ print(f"ERROR: backup not found: {p}", file=sys.stderr)
313
+ sys.exit(1)
314
+ # Validate JSON
315
+ try:
316
+ json.loads(p.read_text(encoding="utf-8"))
317
+ except json.JSONDecodeError as e:
318
+ print(f"ERROR: invalid JSON in backup: {e}", file=sys.stderr)
319
+ sys.exit(1)
320
+ shutil.copy2(p, SETTINGS_PATH)
321
+ print(f"Restored {SETTINGS_PATH} from {p}")
322
+
323
+
324
+ # ── Main ─────────────────────────────────────────────────────────────────────
325
+
326
+ def run_migration(
327
+ dry_run: bool = False,
328
+ hooks_only: bool = False,
329
+ hud_only: bool = False,
330
+ ) -> int:
331
+ print(f"{'[DRY RUN] ' if dry_run else ''}OMG Migration v1")
332
+ print(f" OMG root: {OMG_ROOT}")
333
+ print(f" Claude: {CLAUDE_DIR}")
334
+ print()
335
+
336
+ # Backup
337
+ if not dry_run:
338
+ backup = backup_settings()
339
+ if backup:
340
+ print(f"Backup: {backup}")
341
+ print()
342
+
343
+ settings = load_settings()
344
+ all_changes: list[str] = []
345
+
346
+ # Step 1: Deploy hooks
347
+ if not hud_only:
348
+ print("Step 1: Deploy hooks")
349
+ changes = step_deploy_hooks(dry_run)
350
+ all_changes.extend(changes)
351
+ for c in changes:
352
+ print(c)
353
+ if not changes:
354
+ print(" (no changes)")
355
+ print()
356
+
357
+ # Step 2: Deduplicate hooks in settings
358
+ if not hud_only:
359
+ print("Step 2: Deduplicate hooks in settings.json")
360
+ changes = step_deduplicate_hooks(settings, dry_run)
361
+ all_changes.extend(changes)
362
+ for c in changes:
363
+ print(c)
364
+ if not changes:
365
+ print(" (already canonical)")
366
+ print()
367
+
368
+ # Step 3: Switch HUD
369
+ if not hooks_only:
370
+ print("Step 3: Switch to OMG HUD")
371
+ changes = step_switch_hud(settings, dry_run)
372
+ all_changes.extend(changes)
373
+ for c in changes:
374
+ print(c)
375
+ if not changes:
376
+ print(" (already using OMG HUD)")
377
+ print()
378
+
379
+ # Step 4: Clean plugins
380
+ if not hooks_only and not hud_only:
381
+ print("Step 4: Clean dead plugins")
382
+ changes = step_clean_plugins(settings, dry_run)
383
+ all_changes.extend(changes)
384
+ for c in changes:
385
+ print(c)
386
+ if not changes:
387
+ print(" (none to remove)")
388
+ print()
389
+
390
+ # Step 5: Fix plugin manifests
391
+ if not hooks_only and not hud_only:
392
+ print("Step 5: Fix plugin manifests")
393
+ changes = step_fix_plugin_manifests(dry_run)
394
+ all_changes.extend(changes)
395
+ for c in changes:
396
+ print(c)
397
+ if not changes:
398
+ print(" (none to fix)")
399
+ print()
400
+
401
+ # Step 6: Clean project settings
402
+ if not hud_only:
403
+ print("Step 6: Clean project settings.json")
404
+ changes = step_clean_project_settings(dry_run)
405
+ all_changes.extend(changes)
406
+ for c in changes:
407
+ print(c)
408
+ if not changes:
409
+ print(" (already clean)")
410
+ print()
411
+
412
+ # Step 7: Add migration marker
413
+ if not hooks_only and not hud_only:
414
+ print("Step 7: Add migration marker")
415
+ changes = step_add_comment(settings, dry_run)
416
+ all_changes.extend(changes)
417
+ for c in changes:
418
+ print(c)
419
+ if not changes:
420
+ print(" (already marked)")
421
+ print()
422
+
423
+ # Save
424
+ if not dry_run and all_changes:
425
+ save_settings(settings)
426
+ print(f"Saved {SETTINGS_PATH}")
427
+
428
+ # Summary
429
+ print(f"\n{'[DRY RUN] ' if dry_run else ''}Migration complete: {len(all_changes)} changes")
430
+ if dry_run and all_changes:
431
+ print(" Run without --dry-run to apply changes.")
432
+
433
+ return 0
434
+
435
+
436
+ def build_parser() -> argparse.ArgumentParser:
437
+ parser = argparse.ArgumentParser(
438
+ prog="migrate-legacy",
439
+ description="Migrate OMC settings to OMG. Idempotent — safe to run multiple times.",
440
+ )
441
+ parser.add_argument("--dry-run", action="store_true", help="Preview changes without applying")
442
+ parser.add_argument("--hooks-only", action="store_true", help="Only deploy hooks")
443
+ parser.add_argument("--hud-only", action="store_true", help="Only switch HUD")
444
+ parser.add_argument("--restore", metavar="BACKUP", help="Restore a backup file")
445
+ return parser
446
+
447
+
448
+ def main(argv: list[str] | None = None) -> int:
449
+ parser = build_parser()
450
+ args = parser.parse_args(argv)
451
+
452
+ if args.restore:
453
+ restore_backup(args.restore)
454
+ return 0
455
+
456
+ return run_migration(
457
+ dry_run=args.dry_run,
458
+ hooks_only=args.hooks_only,
459
+ hud_only=args.hud_only,
460
+ )
461
+
462
+
463
+ if __name__ == "__main__":
464
+ raise SystemExit(main())
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ """Legacy wrapper for `legacy_to_omg_migrate.py`."""
3
+ from __future__ import annotations
4
+
5
+ import runpy
6
+ from pathlib import Path
7
+
8
+
9
+ if __name__ == "__main__":
10
+ target = Path(__file__).resolve().with_name("legacy_to_omg_migrate.py")
11
+ runpy.run_path(str(target), run_name="__main__")
12
+