@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
package/OMG-setup.sh CHANGED
@@ -3,6 +3,19 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
6
+ BACKUP_TS="$(date +%Y%m%d_%H%M%S)"
7
+ BACKUP_DIR="$CLAUDE_DIR/.omg-backup-$BACKUP_TS"
8
+ VERSION="2.0.2"
9
+
10
+ PLUGIN_NAME="omg"
11
+ PLUGIN_MARKETPLACE="omg"
12
+ LEGACY_PLUGIN_MARKETPLACE="oh-advanced-layer"
13
+ PLUGIN_REF="${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}"
14
+ LEGACY_PLUGIN_REF="${PLUGIN_NAME}@${LEGACY_PLUGIN_MARKETPLACE}"
15
+ PLUGIN_CACHE_DIR="$CLAUDE_DIR/plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME"
16
+ LEGACY_PLUGIN_CACHE_DIR="$CLAUDE_DIR/plugins/cache/$LEGACY_PLUGIN_MARKETPLACE/$PLUGIN_NAME"
17
+ PLUGIN_BUNDLE_MARKER_FILE=".omg-plugin-bundle"
18
+
6
19
  ACTION="install"
7
20
  ACTION_EXPLICIT=false
8
21
  DRY_RUN=false
@@ -11,421 +24,1254 @@ MERGE_POLICY="ask"
11
24
  FRESH_INSTALL=false
12
25
  INSTALL_AS_PLUGIN=false
13
26
  USE_SYMLINK=false
14
- SKIP_CODEX_SKILLS=false
15
- MANIFEST_PATH="$CLAUDE_DIR/.omg-manifest"
16
- PLUGIN_CACHE_DIR="$CLAUDE_DIR/plugins/cache/oh-advanced-layer/omg"
17
- PLUGIN_MARKER="$PLUGIN_CACHE_DIR/.omg-plugin-bundle"
18
- VERSION=""
19
- TMP_MANIFEST=()
27
+ ADOPTION_MODE="omg-only"
28
+ ADOPT_MODE="auto"
29
+ OMG_PRESET="safe"
30
+ ERRORS=0
31
+ OMG_MANIFEST="$CLAUDE_DIR/.omg-manifest"
32
+ NEW_MANIFEST_ENTRIES=()
33
+
34
+ V3_RULES=(
35
+ "00-truth-evidence.md" "01-enforcement-map.md" "02-doc-check.md"
36
+ "03-working-memory.md" "04-quality-gate.md" "05-structured-reports.md"
37
+ "06-infra-safety.md" "07-cross-model.md" "08-big-picture.md"
38
+ "09-surgical-changes.md" "10-code-simplifier.md" "11-dependency-safety.md"
39
+ "12-circuit-breaker.md" "13-planning-checklist.md" "14-auto-commands.md"
40
+ "15-context-management.md" "16-honest-testing.md" "17-ensemble-collaboration.md"
41
+ "18-collaborative-solving.md" "19-outside-in.md" "20-project-identity.md"
42
+ "21-verified-claims.md" "22-auto-plugin-mcp.md"
43
+ )
44
+ V3_AGENTS_REMOVE=(cross-validator.md dependency-guardian.md infra-guardian.md perf-analyst.md ui-reviewer.md)
45
+ OLD_OMG_AGENTS=(architect.md critic.md executor.md qa-tester.md escalation-router.md)
46
+ V3_COMMANDS_REMOVE=(cross-review.md simplify.md)
47
+ V4_COMMANDS_REMOVE=(
48
+ code-review.md deep-plan.md domain-init.md escalate.md handoff.md
49
+ health-check.md learn.md project-init.md security-review.md
50
+ )
51
+
52
+ # Dynamic hook discovery — no hardcoded list.
53
+ # Used by remove_omg_files() as fallback when manifest is absent.
54
+ build_omg_hooks_list() {
55
+ OMG_HOOKS=()
56
+ for f in "$SCRIPT_DIR"/hooks/*.py; do
57
+ [ -f "$f" ] && OMG_HOOKS+=("$(basename "$f")")
58
+ done
59
+ }
20
60
 
21
61
  usage() {
22
- cat <<'EOF'
62
+ cat <<EOF
23
63
  OMG Setup Manager
24
64
 
25
65
  Usage:
26
- ./OMG-setup.sh <action> [options]
27
- ./OMG-setup.sh [options]
66
+ ./OMG-setup.sh <action> [OPTIONS]
67
+ ./OMG-setup.sh [OPTIONS] # defaults to install
68
+ ./OMG-setup.sh # interactive menu in terminal mode
28
69
 
29
70
  Actions:
30
- install Install or update OMG
31
- update Alias of install
32
- reinstall Uninstall then install
33
- uninstall Remove OMG-managed files
71
+ install Install or upgrade OMG components
72
+ update Alias of install (explicit update mode)
73
+ reinstall Clean reinstall (remove OMG files, then install)
74
+ uninstall Remove OMG-managed files from ~/.claude
34
75
 
35
76
  Options:
36
- --fresh
37
- --symlink
77
+ --fresh For install/update: clean reinstall before install
78
+ --symlink Use symlinks instead of copies (dev mode - live updates)
38
79
  --install-as-plugin
39
- --skip-codex-skills
40
- --dry-run
41
- --non-interactive
42
- --merge-policy=ask|apply|skip
43
- -h, --help
80
+ Install plugin bundle (plugin.json + MCP + HUD) together
81
+ --dry-run Show what would happen without writing files
82
+ --non-interactive Skip prompts (CI/automation mode)
83
+ --merge-policy=X Settings merge: ask (default), apply, skip
84
+ --mode=omg-only|coexist
85
+ Native OMG adoption mode for overlapping ecosystems
86
+ --adopt=auto Detect OMG-adjacent ecosystems during install/update
87
+ --preset=safe|balanced|interop|labs
88
+ User-facing preset for managed OMG features
89
+ -h, --help Show this help
90
+
91
+ Examples:
92
+ ./OMG-setup.sh install
93
+ ./OMG-setup.sh install --symlink # Dev mode: live updates from repo
94
+ ./OMG-setup.sh install --install-as-plugin
95
+ ./OMG-setup.sh install --mode=coexist --preset=interop
96
+ ./OMG-setup.sh update --non-interactive --merge-policy=apply
97
+ ./OMG-setup.sh reinstall --dry-run
98
+ ./OMG-setup.sh uninstall --dry-run
44
99
  EOF
45
100
  }
46
101
 
47
- say() {
48
- printf '%s\n' "$*"
102
+ is_standalone_installed() {
103
+ [ -f "$CLAUDE_DIR/hooks/.omg-version" ] || [ -d "$CLAUDE_DIR/omg-runtime" ]
49
104
  }
50
105
 
51
- record_manifest() {
52
- TMP_MANIFEST+=("$1")
106
+ is_plugin_installed() {
107
+ local marker_new="$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
108
+ local marker_legacy="$LEGACY_PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
109
+ if [ -f "$marker_new" ] || [ -f "$marker_legacy" ]; then
110
+ return 0
111
+ fi
112
+ local installed_plugins="$CLAUDE_DIR/plugins/installed_plugins.json"
113
+ if [ -f "$installed_plugins" ] && grep -Eq "\"$PLUGIN_REF\"|\"$LEGACY_PLUGIN_REF\"" "$installed_plugins" 2>/dev/null; then
114
+ return 0
115
+ fi
116
+ return 1
53
117
  }
54
118
 
55
- run_or_echo() {
56
- if $DRY_RUN; then
57
- say "DRY RUN: $*"
58
- return 0
59
- fi
60
- "$@"
61
- }
119
+ prompt_start_action() {
120
+ if $ACTION_EXPLICIT || $NON_INTERACTIVE || $DRY_RUN; then
121
+ return 0
122
+ fi
62
123
 
63
- copy_file() {
64
- local src="$1"
65
- local dst="$2"
66
- local mode="${3:-644}"
67
- if $DRY_RUN; then
68
- say "DRY RUN: install $src -> $dst"
69
- record_manifest "$dst"
70
- return 0
71
- fi
72
- mkdir -p "$(dirname "$dst")"
73
- if $USE_SYMLINK; then
74
- ln -sfn "$src" "$dst"
75
- else
76
- cp "$src" "$dst"
77
- fi
78
- chmod "$mode" "$dst"
79
- record_manifest "$dst"
80
- }
124
+ local standalone_installed=false
125
+ local plugin_installed=false
126
+ is_standalone_installed && standalone_installed=true
127
+ is_plugin_installed && plugin_installed=true
128
+
129
+ echo ""
130
+ echo "Select OMG setup action:"
131
+ echo " 1. Install standalone"
132
+ if $standalone_installed; then
133
+ echo " 2. Update standalone"
134
+ fi
135
+ echo " 3. Install as plugin"
136
+ if $plugin_installed; then
137
+ echo " 4. Update plugin install"
138
+ fi
139
+ echo " 5. Uninstall"
140
+ echo " 0. Cancel"
141
+ echo ""
81
142
 
82
- copy_dir_glob() {
83
- local src_dir="$1"
84
- local pattern="$2"
85
- local dst_dir="$3"
86
- local mode="${4:-644}"
87
- shopt -s nullglob
88
- for src in "$src_dir"/$pattern; do
89
- [ -e "$src" ] || continue
90
- copy_file "$src" "$dst_dir/$(basename "$src")" "$mode"
91
- done
92
- shopt -u nullglob
143
+ read -p "Choose [1/2/3/4/5/0]: " -r
144
+ case "${REPLY:-}" in
145
+ 1)
146
+ ACTION="install"
147
+ INSTALL_AS_PLUGIN=false
148
+ ;;
149
+ 2)
150
+ if $standalone_installed; then
151
+ ACTION="update"
152
+ INSTALL_AS_PLUGIN=false
153
+ else
154
+ echo "Standalone update unavailable (not installed)."
155
+ exit 1
156
+ fi
157
+ ;;
158
+ 3)
159
+ ACTION="install"
160
+ INSTALL_AS_PLUGIN=true
161
+ ;;
162
+ 4)
163
+ if $plugin_installed; then
164
+ ACTION="update"
165
+ INSTALL_AS_PLUGIN=true
166
+ else
167
+ echo "Plugin update unavailable (plugin install not detected)."
168
+ exit 1
169
+ fi
170
+ ;;
171
+ 5)
172
+ ACTION="uninstall"
173
+ ;;
174
+ 0)
175
+ echo "Cancelled by user."
176
+ exit 0
177
+ ;;
178
+ *)
179
+ echo "Invalid selection."
180
+ exit 1
181
+ ;;
182
+ esac
93
183
  }
94
184
 
95
- copy_tree() {
96
- local src_dir="$1"
97
- local dst_dir="$2"
98
- local selector="${3:-all}"
99
- while IFS= read -r src; do
100
- case "$selector" in
101
- markdown)
102
- [[ "$src" == *.md ]] || continue
103
- ;;
104
- ts-only)
105
- [[ "$src" == *.ts ]] || continue
106
- ;;
107
- runtime)
108
- [[ "$src" == *.ts || "$src" == *.json || "$src" == *.sh ]] || continue
109
- ;;
110
- all)
111
- ;;
112
- *)
113
- ;;
185
+ parse_args() {
186
+ if [ $# -gt 0 ]; then
187
+ case "$1" in
188
+ install|update|reinstall|uninstall)
189
+ ACTION="$1"
190
+ ACTION_EXPLICIT=true
191
+ shift
192
+ ;;
193
+ help|-h|--help)
194
+ usage
195
+ exit 0
196
+ ;;
197
+ esac
198
+ fi
199
+
200
+ for arg in "$@"; do
201
+ case "$arg" in
202
+ --dry-run) DRY_RUN=true ;;
203
+ --symlink) USE_SYMLINK=true ;;
204
+ --non-interactive) NON_INTERACTIVE=true ;;
205
+ --fresh) FRESH_INSTALL=true ;;
206
+ --install-as-plugin) INSTALL_AS_PLUGIN=true ;;
207
+ --merge-policy=*) MERGE_POLICY="${arg#*=}" ;;
208
+ --mode=*) ADOPTION_MODE="${arg#*=}" ;;
209
+ --adopt=*) ADOPT_MODE="${arg#*=}" ;;
210
+ --preset=*) OMG_PRESET="${arg#*=}" ;;
211
+ --help|-h)
212
+ usage
213
+ exit 0
214
+ ;;
215
+ *)
216
+ echo "Unknown option: $arg"
217
+ echo ""
218
+ usage
219
+ exit 1
220
+ ;;
221
+ esac
222
+ done
223
+
224
+ if [ "$ACTION" = "reinstall" ]; then
225
+ FRESH_INSTALL=true
226
+ fi
227
+
228
+ if [ ! -t 0 ] || [ -n "${npm_lifecycle_event:-}" ] || [ -n "${npm_execpath:-}" ]; then
229
+ NON_INTERACTIVE=true
230
+ fi
231
+
232
+ # Auto-enable plugin mode for npm installs
233
+ if [ -n "${npm_execpath:-}" ] || [ -n "${npm_lifecycle_event:-}" ]; then
234
+ INSTALL_AS_PLUGIN=true
235
+ fi
236
+
237
+ case "$ADOPTION_MODE" in
238
+ omg-only|coexist) ;;
239
+ *)
240
+ echo "Unknown adoption mode: $ADOPTION_MODE"
241
+ exit 1
242
+ ;;
243
+ esac
244
+
245
+ case "$ADOPT_MODE" in
246
+ auto) ;;
247
+ *)
248
+ echo "Unknown adoption detector mode: $ADOPT_MODE"
249
+ exit 1
250
+ ;;
114
251
  esac
115
- local rel="${src#$src_dir/}"
116
- local mode=644
117
- case "$src" in
118
- *.sh|*.ts|*.mjs) mode=755 ;;
252
+
253
+ case "$OMG_PRESET" in
254
+ safe|balanced|interop|labs) ;;
255
+ *)
256
+ echo "Unknown OMG preset: $OMG_PRESET"
257
+ exit 1
258
+ ;;
119
259
  esac
120
- copy_file "$src" "$dst_dir/$rel" "$mode"
121
- done < <(find "$src_dir" -type f | sort)
122
260
  }
123
261
 
124
- is_standalone_installed() {
125
- [ -f "$MANIFEST_PATH" ] || [ -d "$CLAUDE_DIR/omg-runtime" ]
262
+ preflight() {
263
+ echo "Pre-flight checks..."
264
+ if ! command -v python3 &>/dev/null; then
265
+ echo " ❌ python3 not found. Install: https://www.python.org/downloads/"
266
+ exit 1
267
+ fi
268
+ local py_ver py_maj py_min
269
+ py_ver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
270
+ py_maj=$(echo "$py_ver" | cut -d. -f1)
271
+ py_min=$(echo "$py_ver" | cut -d. -f2)
272
+ if [ "$py_maj" -lt 3 ] || { [ "$py_maj" -eq 3 ] && [ "$py_min" -lt 8 ]; }; then
273
+ echo " ❌ Python $py_ver found, 3.8+ required"
274
+ exit 1
275
+ fi
276
+ echo " ✓ Python $py_ver"
126
277
  }
127
278
 
128
- is_plugin_installed() {
129
- [ -f "$PLUGIN_MARKER" ]
279
+ ensure_backup() {
280
+ if ! $DRY_RUN; then
281
+ mkdir -p "$BACKUP_DIR"
282
+ for dir in rules hooks agents commands; do
283
+ [ -d "$CLAUDE_DIR/$dir" ] && cp -r "$CLAUDE_DIR/$dir" "$BACKUP_DIR/$dir" 2>/dev/null || true
284
+ done
285
+ [ -f "$CLAUDE_DIR/settings.json" ] && cp "$CLAUDE_DIR/settings.json" "$BACKUP_DIR/"
286
+ prune_old_backups
287
+ fi
130
288
  }
131
289
 
132
- prompt_start_action() {
133
- if $ACTION_EXPLICIT || $NON_INTERACTIVE || $DRY_RUN; then
134
- return 0
135
- fi
136
-
137
- local standalone_installed=false
138
- local plugin_installed=false
139
- is_standalone_installed && standalone_installed=true
140
- is_plugin_installed && plugin_installed=true
141
-
142
- say ""
143
- say "Select OMG setup action:"
144
- say " 1. Install standalone"
145
- if $standalone_installed; then
146
- say " 2. Update standalone"
147
- fi
148
- say " 3. Install as plugin"
149
- if $plugin_installed; then
150
- say " 4. Update plugin install"
151
- fi
152
- say " 5. Uninstall"
153
- say " 0. Cancel"
154
- say ""
155
- read -r -p "Choose [1/2/3/4/5/0]: " choice
156
- case "${choice:-}" in
157
- 1) ACTION="install"; INSTALL_AS_PLUGIN=false ;;
158
- 2) ACTION="update"; INSTALL_AS_PLUGIN=false ;;
159
- 3) ACTION="install"; INSTALL_AS_PLUGIN=true ;;
160
- 4) ACTION="update"; INSTALL_AS_PLUGIN=true ;;
161
- 5) ACTION="uninstall" ;;
162
- 0) say "Cancelled by user."; exit 0 ;;
163
- *) say "Invalid selection."; exit 1 ;;
164
- esac
290
+ prune_old_backups() {
291
+ local backups=()
292
+ while IFS= read -r path; do
293
+ backups+=("$path")
294
+ done < <(find "$CLAUDE_DIR" -maxdepth 1 -type d -name ".omg-backup-*" | sort)
295
+
296
+ local total=${#backups[@]}
297
+ if [ "$total" -le 2 ]; then
298
+ return 0
299
+ fi
300
+
301
+ local remove_count=$((total - 2))
302
+ for old in "${backups[@]:0:$remove_count}"; do
303
+ [[ "$old" == "$CLAUDE_DIR"/* ]] || {
304
+ echo "ERROR: backup prune target outside expected directory: $old" >&2
305
+ exit 1
306
+ }
307
+ rm -rf "$old"
308
+ done
165
309
  }
166
310
 
167
- parse_args() {
168
- if [ $# -gt 0 ]; then
169
- case "$1" in
170
- install|update|reinstall|uninstall)
171
- ACTION="$1"
172
- ACTION_EXPLICIT=true
173
- shift
174
- ;;
175
- -h|--help|help)
176
- usage
177
- exit 0
178
- ;;
179
- esac
180
- fi
181
-
182
- for arg in "$@"; do
183
- case "$arg" in
184
- --dry-run) DRY_RUN=true ;;
185
- --symlink) USE_SYMLINK=true ;;
186
- --non-interactive) NON_INTERACTIVE=true ;;
187
- --fresh) FRESH_INSTALL=true ;;
188
- --install-as-plugin) INSTALL_AS_PLUGIN=true ;;
189
- --skip-codex-skills) SKIP_CODEX_SKILLS=true ;;
190
- --merge-policy=*) MERGE_POLICY="${arg#*=}" ;;
191
- -h|--help)
192
- usage
193
- exit 0
194
- ;;
195
- *)
196
- say "Unknown option: $arg"
197
- usage
198
- exit 1
199
- ;;
200
- esac
201
- done
311
+ is_omg_managed_command_file() {
312
+ local file="$1"
313
+ if [ ! -f "$file" ]; then
314
+ return 1
315
+ fi
202
316
 
203
- if [ "$ACTION" = "reinstall" ]; then
204
- FRESH_INSTALL=true
205
- ACTION="install"
206
- fi
317
+ if grep -q "OMG-AUTO-COMPAT-ALIAS" "$file" 2>/dev/null; then
318
+ return 0
319
+ fi
320
+ if grep -q "OMG-MANAGED-COMMAND" "$file" 2>/dev/null; then
321
+ return 0
322
+ fi
207
323
 
208
- if [ ! -t 0 ] || [ -n "${npm_execpath:-}" ] || [ -n "${npm_lifecycle_event:-}" ]; then
209
- NON_INTERACTIVE=true
210
- fi
324
+ local base
325
+ base="$(basename "$file")"
326
+ if [[ "$base" == OMG:* ]] && grep -q "/OMG:" "$file" 2>/dev/null; then
327
+ return 0
328
+ fi
329
+ return 1
330
+ }
211
331
 
212
- if [ -n "${npm_execpath:-}" ] || [ -n "${npm_lifecycle_event:-}" ]; then
213
- INSTALL_AS_PLUGIN=true
214
- fi
332
+ mark_omg_managed_command_file() {
333
+ local file="$1"
334
+ if [ ! -f "$file" ]; then
335
+ return 0
336
+ fi
337
+ if ! grep -q "OMG-MANAGED-COMMAND" "$file" 2>/dev/null; then
338
+ printf "\n<!-- OMG-MANAGED-COMMAND -->\n" >> "$file"
339
+ fi
215
340
  }
216
341
 
217
- preflight() {
218
- say "═══════════════════════════════════════════════════════════════"
219
- say " OMG Setup Manager — $ACTION"
220
- say "═══════════════════════════════════════════════════════════════"
221
- say ""
222
- say "Pre-flight checks..."
223
- if ! command -v bun >/dev/null 2>&1; then
224
- say " ✗ bun not found. Install Bun first: https://bun.sh"
225
- exit 1
226
- fi
227
- VERSION="$(bun -e 'import { readFileSync } from "node:fs"; console.log(JSON.parse(readFileSync(process.argv[1], "utf8")).version);' "$SCRIPT_DIR/package.json")"
228
- say " ✓ Bun $(bun --version)"
342
+ # Install a file or directory - either copy or symlink based on USE_SYMLINK
343
+ # Usage: install_file <source> <target> [type: file|dir]
344
+ install_file() {
345
+ local src="$1"
346
+ local target="$2"
347
+ local type="${3:-file}"
348
+
349
+ if $USE_SYMLINK; then
350
+ # In symlink mode, create symlink from target -> source
351
+ # First remove existing file/dir if present
352
+ if [ -e "$target" ] || [ -L "$target" ]; then
353
+ rm -rf "$target"
354
+ fi
355
+ ln -s "$src" "$target"
356
+ else
357
+ # In copy mode, do regular copy
358
+ if [ "$type" = "dir" ]; then
359
+ cp -R "$src" "$target"
360
+ else
361
+ cp "$src" "$target"
362
+ fi
363
+ fi
229
364
  }
230
365
 
231
- backup_existing() {
232
- local backup_dir="$CLAUDE_DIR/.omg-backup-$(date +%Y%m%d_%H%M%S)"
233
- if $DRY_RUN; then
234
- say " DRY RUN: backup -> $backup_dir"
235
- return 0
236
- fi
237
- mkdir -p "$backup_dir"
238
- for path in settings.json commands agents hooks omg-runtime rules templates; do
239
- if [ -e "$CLAUDE_DIR/$path" ]; then
240
- cp -R "$CLAUDE_DIR/$path" "$backup_dir/" 2>/dev/null || true
241
- fi
242
- done
243
- say " ✓ Backup: $backup_dir"
366
+ track_file() {
367
+ NEW_MANIFEST_ENTRIES+=("$1")
244
368
  }
245
369
 
246
- remove_manifest_paths() {
247
- if [ ! -f "$MANIFEST_PATH" ]; then
248
- return 0
249
- fi
250
- while IFS= read -r path; do
251
- [ -n "$path" ] || continue
252
- if $DRY_RUN; then
253
- say "DRY RUN: remove $path"
370
+ reconcile_stale_files() {
371
+ if [ ! -f "$OMG_MANIFEST" ]; then
372
+ echo " (no previous manifest — first install, skipping reconciliation)"
373
+ return 0
374
+ fi
375
+ local stale=0
376
+ while IFS= read -r old_entry; do
377
+ [ -n "$old_entry" ] || continue
378
+ [[ "$old_entry" == "#"* ]] && continue
379
+ local found=false
380
+ for new_entry in "${NEW_MANIFEST_ENTRIES[@]}"; do
381
+ if [ "$old_entry" = "$new_entry" ]; then
382
+ found=true
383
+ break
384
+ fi
385
+ done
386
+ if ! $found; then
387
+ local target="$CLAUDE_DIR/$old_entry"
388
+ if [ -f "$target" ]; then
389
+ if ! $DRY_RUN; then
390
+ rm -f "$target"
391
+ fi
392
+ echo " - $old_entry (removed from source)"
393
+ stale=$((stale + 1))
394
+ fi
395
+ fi
396
+ done < "$OMG_MANIFEST"
397
+ if [ $stale -eq 0 ]; then
398
+ echo " (no stale files)"
399
+ elif $DRY_RUN; then
400
+ echo " (dry-run: would remove $stale stale file(s))"
254
401
  else
255
- rm -rf "$path"
402
+ echo " ✓ Cleaned $stale stale file(s)"
256
403
  fi
257
- done < "$MANIFEST_PATH"
258
- if ! $DRY_RUN; then
259
- rm -f "$MANIFEST_PATH"
260
- fi
261
404
  }
262
405
 
263
- remove_legacy_runtime_files() {
264
- local legacy_ext="py"
265
- local dirs=(
266
- "$CLAUDE_DIR/hooks"
267
- "$CLAUDE_DIR/omg-runtime/scripts"
268
- "$CLAUDE_DIR/omg-runtime/runtime"
269
- "$CLAUDE_DIR/omg-runtime/tools"
270
- "$CLAUDE_DIR/omg-runtime/control_plane"
271
- "$CLAUDE_DIR/omg-runtime/lab"
272
- "$CLAUDE_DIR/omg-runtime/omg_natives"
273
- "$CLAUDE_DIR/omg-runtime/registry"
274
- )
275
- local dir
276
- local file
277
- for dir in "${dirs[@]}"; do
278
- [ -d "$dir" ] || continue
279
- while IFS= read -r file; do
280
- if $DRY_RUN; then
281
- say "DRY RUN: remove legacy $file"
282
- else
283
- rm -f "$file"
284
- fi
285
- done < <(find "$dir" -maxdepth 1 -type f -name "*.${legacy_ext}" | sort)
286
- done
406
+ write_omg_manifest() {
407
+ if ! $DRY_RUN; then
408
+ printf '%s\n' "${NEW_MANIFEST_ENTRIES[@]}" | sort > "$OMG_MANIFEST"
409
+ fi
287
410
  }
288
411
 
289
- merge_settings() {
290
- local source="$SCRIPT_DIR/settings.json"
291
- local target="$CLAUDE_DIR/settings.json"
292
-
293
- if [ ! -f "$target" ]; then
294
- copy_file "$source" "$target" 644
295
- return 0
296
- fi
297
-
298
- case "$MERGE_POLICY" in
299
- skip)
300
- say " ~ Settings merge skipped"
301
- return 0
302
- ;;
303
- apply)
304
- ;;
305
- ask)
306
- if $NON_INTERACTIVE; then
307
- :
308
- else
309
- read -r -p "Apply settings merge? [Y/n]: " reply
310
- case "${reply:-Y}" in
311
- n|N) say " ~ Settings merge skipped"; return 0 ;;
312
- esac
313
- fi
314
- ;;
315
- *)
316
- say "Invalid merge policy: $MERGE_POLICY"
317
- exit 1
318
- ;;
319
- esac
320
-
321
- if $DRY_RUN; then
322
- say "DRY RUN: merge settings $target <= $source"
323
- else
324
- bun "$SCRIPT_DIR/scripts/settings-merge.ts" "$target" "$source"
325
- say " ✓ Settings merged (auto)"
326
- fi
327
- record_manifest "$target"
412
+ prune_plugin_mcp_from_settings() {
413
+ local mcp_path="$CLAUDE_DIR/.mcp.json"
414
+ if [ ! -f "$mcp_path" ]; then
415
+ return 0
416
+ fi
417
+ python3 - "$mcp_path" <<'PY'
418
+ import json
419
+ import sys
420
+ from pathlib import Path
421
+
422
+ path = Path(sys.argv[1])
423
+ try:
424
+ data = json.loads(path.read_text(encoding="utf-8"))
425
+ except Exception:
426
+ print("0")
427
+ raise SystemExit(0)
428
+
429
+ servers = data.get("mcpServers")
430
+ if not isinstance(servers, dict):
431
+ print("0")
432
+ raise SystemExit(0)
433
+
434
+ removed = 0
435
+ for key in ("context7", "filesystem", "websearch", "chrome-devtools"):
436
+ if key in servers:
437
+ servers.pop(key, None)
438
+ removed += 1
439
+
440
+ if removed:
441
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
442
+
443
+ print(str(removed))
444
+ PY
445
+ }
446
+
447
+ merge_plugin_mcp_into_settings() {
448
+ local mcp_path="$CLAUDE_DIR/.mcp.json"
449
+ local source_mcp_path="$SCRIPT_DIR/.mcp.json"
450
+ python3 - "$mcp_path" "$source_mcp_path" <<'PY'
451
+ import json
452
+ import sys
453
+ from pathlib import Path
454
+
455
+ mcp_path = Path(sys.argv[1])
456
+ source_mcp_path = Path(sys.argv[2])
457
+
458
+ mcp_config = {}
459
+ if mcp_path.exists():
460
+ try:
461
+ mcp_config = json.loads(mcp_path.read_text(encoding="utf-8"))
462
+ except Exception:
463
+ mcp_config = {}
464
+ if not isinstance(mcp_config, dict):
465
+ mcp_config = {}
466
+
467
+ try:
468
+ source_mcp = json.loads(source_mcp_path.read_text(encoding="utf-8"))
469
+ except Exception:
470
+ source_mcp = {}
471
+
472
+ incoming = source_mcp.get("mcpServers") if isinstance(source_mcp, dict) else {}
473
+ if not isinstance(incoming, dict):
474
+ incoming = {}
475
+
476
+ servers = mcp_config.get("mcpServers")
477
+ if not isinstance(servers, dict):
478
+ servers = {}
479
+ for key, value in incoming.items():
480
+ servers[key] = value
481
+ mcp_config["mcpServers"] = servers
482
+
483
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
484
+ mcp_path.write_text(json.dumps(mcp_config, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
485
+ print(str(len(incoming)))
486
+ PY
487
+ }
488
+
489
+ write_plugin_mcp_file() {
490
+ local target_path="$1"
491
+ local source_mcp_path="$SCRIPT_DIR/.mcp.json"
492
+ python3 - "$target_path" "$source_mcp_path" <<'PY'
493
+ import json
494
+ import sys
495
+ from pathlib import Path
496
+
497
+ target_path = Path(sys.argv[1])
498
+ source_mcp_path = Path(sys.argv[2])
499
+
500
+ try:
501
+ source_mcp = json.loads(source_mcp_path.read_text(encoding="utf-8"))
502
+ except Exception:
503
+ source_mcp = {}
504
+
505
+ mcp_servers = source_mcp.get("mcpServers") if isinstance(source_mcp, dict) else {}
506
+ if not isinstance(mcp_servers, dict):
507
+ mcp_servers = {}
508
+
509
+ payload = {"mcpServers": mcp_servers}
510
+ target_path.parent.mkdir(parents=True, exist_ok=True)
511
+ target_path.write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
512
+ print(str(len(mcp_servers)))
513
+ PY
514
+ }
515
+
516
+ register_plugin_in_registry() {
517
+ local plugin_ref="$1"
518
+ local install_path="$2"
519
+ local version="$3"
520
+ local settings_path="$CLAUDE_DIR/settings.json"
521
+ local installed_plugins_path="$CLAUDE_DIR/plugins/installed_plugins.json"
522
+
523
+ python3 - "$settings_path" "$installed_plugins_path" "$plugin_ref" "$install_path" "$version" <<'PY'
524
+ import json
525
+ import sys
526
+ from pathlib import Path
527
+ from datetime import datetime, timezone
528
+
529
+ settings_path = Path(sys.argv[1])
530
+ installed_plugins_path = Path(sys.argv[2])
531
+ plugin_ref = sys.argv[3]
532
+ install_path = sys.argv[4]
533
+ version = sys.argv[5]
534
+ now = datetime.now(timezone.utc).isoformat()
535
+
536
+ settings = {}
537
+ if settings_path.exists():
538
+ try:
539
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
540
+ except Exception:
541
+ settings = {}
542
+ if not isinstance(settings, dict):
543
+ settings = {}
544
+ enabled = settings.get("enabledPlugins")
545
+ if not isinstance(enabled, dict):
546
+ enabled = {}
547
+ enabled[plugin_ref] = True
548
+ settings["enabledPlugins"] = enabled
549
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
550
+ settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
551
+
552
+ installed = {}
553
+ if installed_plugins_path.exists():
554
+ try:
555
+ installed = json.loads(installed_plugins_path.read_text(encoding="utf-8"))
556
+ except Exception:
557
+ installed = {}
558
+ if not isinstance(installed, dict):
559
+ installed = {}
560
+ installed["version"] = 2
561
+ plugins = installed.get("plugins")
562
+ if not isinstance(plugins, dict):
563
+ plugins = {}
564
+ plugins[plugin_ref] = [{
565
+ "scope": "user",
566
+ "installPath": install_path,
567
+ "version": version,
568
+ "installedAt": now,
569
+ "lastUpdated": now,
570
+ }]
571
+ installed["plugins"] = plugins
572
+ installed_plugins_path.parent.mkdir(parents=True, exist_ok=True)
573
+ installed_plugins_path.write_text(json.dumps(installed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
574
+ PY
575
+ }
576
+
577
+ unregister_plugin_from_registry() {
578
+ local plugin_ref="$1"
579
+ local settings_path="$CLAUDE_DIR/settings.json"
580
+ local installed_plugins_path="$CLAUDE_DIR/plugins/installed_plugins.json"
581
+
582
+ python3 - "$settings_path" "$installed_plugins_path" "$plugin_ref" <<'PY'
583
+ import json
584
+ import sys
585
+ from pathlib import Path
586
+
587
+ settings_path = Path(sys.argv[1])
588
+ installed_plugins_path = Path(sys.argv[2])
589
+ plugin_ref = sys.argv[3]
590
+
591
+ if settings_path.exists():
592
+ try:
593
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
594
+ except Exception:
595
+ settings = {}
596
+ if isinstance(settings, dict):
597
+ enabled = settings.get("enabledPlugins")
598
+ if isinstance(enabled, dict) and plugin_ref in enabled:
599
+ enabled.pop(plugin_ref, None)
600
+ settings["enabledPlugins"] = enabled
601
+ settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
602
+
603
+ if installed_plugins_path.exists():
604
+ try:
605
+ installed = json.loads(installed_plugins_path.read_text(encoding="utf-8"))
606
+ except Exception:
607
+ installed = {}
608
+ if isinstance(installed, dict):
609
+ plugins = installed.get("plugins")
610
+ if isinstance(plugins, dict) and plugin_ref in plugins:
611
+ plugins.pop(plugin_ref, None)
612
+ installed["plugins"] = plugins
613
+ installed_plugins_path.write_text(json.dumps(installed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
614
+ PY
615
+ }
616
+
617
+ apply_omg_preset_to_settings() {
618
+ local settings_path="$1"
619
+ local preset="$2"
620
+
621
+ if [ ! -f "$settings_path" ]; then
622
+ return 0
623
+ fi
624
+
625
+ python3 - "$SCRIPT_DIR" "$settings_path" "$preset" <<'PY'
626
+ import json
627
+ import sys
628
+ from pathlib import Path
629
+
630
+ root = Path(sys.argv[1])
631
+ settings_path = Path(sys.argv[2])
632
+ preset = sys.argv[3]
633
+
634
+ sys.path.insert(0, str(root))
635
+
636
+ from runtime.adoption import CANONICAL_VERSION, get_preset_features, resolve_preset
637
+
638
+ try:
639
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
640
+ except Exception:
641
+ settings = {}
642
+
643
+ if not isinstance(settings, dict):
644
+ settings = {}
645
+
646
+ omg = settings.get("_omg")
647
+ if not isinstance(omg, dict):
648
+ omg = {}
649
+
650
+ features = omg.get("features")
651
+ if not isinstance(features, dict):
652
+ features = {}
653
+
654
+ features.update(get_preset_features(resolve_preset(preset)))
655
+ omg["features"] = features
656
+ omg["preset"] = resolve_preset(preset)
657
+ omg["_version"] = CANONICAL_VERSION
658
+ settings["_omg"] = omg
659
+
660
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
661
+ settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
662
+ PY
328
663
  }
329
664
 
330
- write_manifest() {
331
- if $DRY_RUN; then
332
- return 0
333
- fi
334
- printf '%s\n' "${TMP_MANIFEST[@]}" | awk 'NF' | sort -u > "$MANIFEST_PATH"
665
+ write_native_adoption_report() {
666
+ python3 - "$SCRIPT_DIR" "$CLAUDE_DIR" "$ADOPTION_MODE" "$ADOPT_MODE" "$OMG_PRESET" <<'PY'
667
+ import sys
668
+ from pathlib import Path
669
+
670
+ root = Path(sys.argv[1])
671
+ project_dir = Path(sys.argv[2])
672
+ mode = sys.argv[3]
673
+ adopt = sys.argv[4]
674
+ preset = sys.argv[5]
675
+
676
+ sys.path.insert(0, str(root))
677
+
678
+ from runtime.adoption import build_adoption_report, write_adoption_report
679
+
680
+ report = build_adoption_report(project_dir, requested_mode=mode, preset=preset, adopt=adopt)
681
+ path = write_adoption_report(project_dir, report)
682
+ print(path)
683
+ PY
684
+ }
685
+
686
+ apply_adoption_mode_marker() {
687
+ if [ "$ADOPTION_MODE" = "coexist" ]; then
688
+ if ! $DRY_RUN; then
689
+ mkdir -p "$CLAUDE_DIR/hooks"
690
+ printf '%s\n' "coexist" > "$CLAUDE_DIR/hooks/.omg-coexist"
691
+ fi
692
+ track_file "hooks/.omg-coexist"
693
+ else
694
+ if ! $DRY_RUN; then
695
+ rm -f "$CLAUDE_DIR/hooks/.omg-coexist"
696
+ fi
697
+ fi
698
+ }
699
+
700
+ remove_omg_files() {
701
+ if ! $DRY_RUN; then
702
+ local plugin_bundle_marker="$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
703
+ local plugin_bundle_marker_legacy="$LEGACY_PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
704
+ local remove_plugin_managed_mcp=false
705
+ if [ -f "$plugin_bundle_marker" ] || [ -f "$plugin_bundle_marker_legacy" ]; then
706
+ remove_plugin_managed_mcp=true
707
+ fi
708
+
709
+ # Use manifest for precise removal if available.
710
+ if [ -f "$OMG_MANIFEST" ]; then
711
+ while IFS= read -r entry; do
712
+ [ -n "$entry" ] || continue
713
+ [[ "$entry" == "#"* ]] && continue
714
+ rm -f "$CLAUDE_DIR/$entry"
715
+ done < "$OMG_MANIFEST"
716
+ rm -f "$OMG_MANIFEST"
717
+ fi
718
+
719
+ # Also remove by pattern (covers pre-manifest installs + compat aliases).
720
+ build_omg_hooks_list
721
+ for h in "${OMG_HOOKS[@]}"; do
722
+ rm -f "$CLAUDE_DIR/hooks/$h"
723
+ done
724
+ rm -f "$CLAUDE_DIR/hooks/.omg-version" "$CLAUDE_DIR/hooks/.omg-coexist"
725
+
726
+ # Remove OMG rules and old v3 rule set.
727
+ for r in "$CLAUDE_DIR"/rules/0[0-4]-*.md; do
728
+ [ -f "$r" ] && rm "$r"
729
+ done
730
+ for rule in "${V3_RULES[@]}"; do
731
+ rm -f "$CLAUDE_DIR/rules/$rule"
732
+ done
733
+
734
+ # Remove OMG agents, commands, templates.
735
+ rm -f "$CLAUDE_DIR"/agents/omg-*.md
736
+ if [ -d "$CLAUDE_DIR/commands" ]; then
737
+ while IFS= read -r cmd_path; do
738
+ if [ -n "$cmd_path" ] && is_omg_managed_command_file "$cmd_path"; then
739
+ rm -f "$cmd_path"
740
+ fi
741
+ done < <(find "$CLAUDE_DIR/commands" -maxdepth 1 -type f -name "*.md" 2>/dev/null | sort)
742
+ fi
743
+ [[ "$CLAUDE_DIR/templates/omg" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/templates/omg" >&2; exit 1; }
744
+ rm -rf "$CLAUDE_DIR/templates/omg"
745
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
746
+ rm -rf "$CLAUDE_DIR/omg-runtime"
747
+
748
+ [[ "$PLUGIN_CACHE_DIR" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $PLUGIN_CACHE_DIR" >&2; exit 1; }
749
+ rm -rf "$PLUGIN_CACHE_DIR"
750
+ [[ "$LEGACY_PLUGIN_CACHE_DIR" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $LEGACY_PLUGIN_CACHE_DIR" >&2; exit 1; }
751
+ rm -rf "$LEGACY_PLUGIN_CACHE_DIR"
752
+ rm -f "$CLAUDE_DIR/hud/omg-hud.mjs"
753
+ unregister_plugin_from_registry "$PLUGIN_REF"
754
+ unregister_plugin_from_registry "$LEGACY_PLUGIN_REF"
755
+
756
+ if $remove_plugin_managed_mcp; then
757
+ local pruned_mcp=0
758
+ pruned_mcp=$(prune_plugin_mcp_from_settings)
759
+ if [ "${pruned_mcp:-0}" -gt 0 ]; then
760
+ echo " ✓ Plugin-managed MCP servers removed from .mcp.json ($pruned_mcp)"
761
+ fi
762
+ fi
763
+ fi
335
764
  }
336
765
 
337
766
  install_plugin_bundle() {
338
- say " Plugin bundle mode enabled: install plugin + MCP + HUD together"
339
- if ! $DRY_RUN; then
340
- mkdir -p "$PLUGIN_CACHE_DIR"
341
- fi
342
- copy_tree "$SCRIPT_DIR/.claude-plugin" "$PLUGIN_CACHE_DIR/.claude-plugin" ""
343
- copy_file "$SCRIPT_DIR/.mcp.json" "$PLUGIN_CACHE_DIR/.mcp.json" 644
344
- copy_tree "$SCRIPT_DIR/hud" "$PLUGIN_CACHE_DIR/hud" ""
345
- if $DRY_RUN; then
346
- say "DRY RUN: write $PLUGIN_MARKER"
347
- else
348
- printf '%s\n' "omg-plugin-bundle-v2" > "$PLUGIN_MARKER"
349
- fi
350
- record_manifest "$PLUGIN_CACHE_DIR"
767
+ local plugin_ref="$PLUGIN_REF"
768
+ local plugin_root="$PLUGIN_CACHE_DIR/$VERSION"
769
+ local plugin_manifest_src="$SCRIPT_DIR/.claude-plugin/plugin.json"
770
+ local plugin_manifest_target="$plugin_root/.claude-plugin/plugin.json"
771
+ local plugin_mcp_target="$plugin_root/.mcp.json"
772
+ local hud_src="$SCRIPT_DIR/hud/omg-hud.mjs"
773
+ local hud_target="$CLAUDE_DIR/hud/omg-hud.mjs"
774
+
775
+ echo " Plugin bundle mode enabled: install plugin + MCP + HUD together"
776
+ if $DRY_RUN; then
777
+ echo " (would install plugin bundle under $plugin_root and deploy HUD to $hud_target)"
778
+ echo " (would register plugin in ~/.claude/plugins/installed_plugins.json and enable it in settings.json)"
779
+ echo " (would merge plugin MCP servers into .mcp.json)"
780
+ return 0
781
+ fi
782
+
783
+ mkdir -p "$plugin_root/.claude-plugin"
784
+ mkdir -p "$CLAUDE_DIR/hud"
785
+ cp "$plugin_manifest_src" "$plugin_manifest_target"
786
+
787
+ # Provide a fallback .mcp.json if not shipped in npm package
788
+ if [ ! -f "$SCRIPT_DIR/.mcp.json" ]; then
789
+ local _fallback_mcp_dir
790
+ _fallback_mcp_dir=$(mktemp -d)
791
+ cat > "$_fallback_mcp_dir/.mcp.json" <<'FALLBACK_MCP'
792
+ {
793
+ "mcpServers": {
794
+ "context7": {
795
+ "command": "npx",
796
+ "args": ["@upstash/context7-mcp@2.1.3"]
797
+ },
798
+ "filesystem": {
799
+ "command": "npx",
800
+ "args": ["@modelcontextprotocol/server-filesystem@2026.1.14", "."]
801
+ },
802
+ "websearch": {
803
+ "command": "npx",
804
+ "args": ["@zhafron/mcp-web-search@1.2.2"]
805
+ },
806
+ "chrome-devtools": {
807
+ "command": "npx",
808
+ "args": ["chrome-devtools-mcp@0.19.0"]
809
+ }
810
+ }
351
811
  }
812
+ FALLBACK_MCP
813
+ SCRIPT_DIR="$_fallback_mcp_dir" write_plugin_mcp_file "$plugin_mcp_target" >/dev/null
814
+ rm -rf "$_fallback_mcp_dir"
815
+ else
816
+ write_plugin_mcp_file "$plugin_mcp_target" >/dev/null
817
+ fi
818
+
819
+ cp "$hud_src" "$hud_target"
820
+ mkdir -p "$PLUGIN_CACHE_DIR"
821
+ printf '%s\n' "omg-plugin-bundle-v1" > "$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
822
+
823
+ unregister_plugin_from_registry "$LEGACY_PLUGIN_REF"
824
+ register_plugin_in_registry "$plugin_ref" "$plugin_root" "$VERSION"
825
+ merge_plugin_mcp_into_settings >/dev/null
352
826
 
353
- install_runtime() {
354
- say "Step 1/4: Install core runtime..."
355
- copy_tree "$SCRIPT_DIR/commands" "$CLAUDE_DIR/commands" "markdown"
356
- copy_tree "$SCRIPT_DIR/agents" "$CLAUDE_DIR/agents" "markdown"
357
- copy_tree "$SCRIPT_DIR/rules" "$CLAUDE_DIR/rules" "markdown"
358
- copy_tree "$SCRIPT_DIR/templates" "$CLAUDE_DIR/templates/omg" "all"
359
- copy_tree "$SCRIPT_DIR/hooks" "$CLAUDE_DIR/hooks" "ts-only"
360
- copy_tree "$SCRIPT_DIR/runtime" "$CLAUDE_DIR/omg-runtime/runtime" "runtime"
361
- copy_tree "$SCRIPT_DIR/scripts" "$CLAUDE_DIR/omg-runtime/scripts" "runtime"
362
- copy_tree "$SCRIPT_DIR/tools" "$CLAUDE_DIR/omg-runtime/tools" "runtime"
363
- copy_tree "$SCRIPT_DIR/control_plane" "$CLAUDE_DIR/omg-runtime/control_plane" "runtime"
364
- copy_tree "$SCRIPT_DIR/lab" "$CLAUDE_DIR/omg-runtime/lab" "runtime"
365
- copy_tree "$SCRIPT_DIR/registry" "$CLAUDE_DIR/omg-runtime/registry" "runtime"
366
- copy_tree "$SCRIPT_DIR/omg_natives" "$CLAUDE_DIR/omg-runtime/omg_natives" "runtime"
367
- copy_tree "$SCRIPT_DIR/hud" "$CLAUDE_DIR/omg-runtime/hud" "all"
368
- copy_file "$SCRIPT_DIR/.mcp.json" "$CLAUDE_DIR/omg-runtime/.mcp.json" 644
369
- copy_file "$SCRIPT_DIR/package.json" "$CLAUDE_DIR/omg-runtime/package.json" 644
370
- copy_file "$SCRIPT_DIR/tsconfig.json" "$CLAUDE_DIR/omg-runtime/tsconfig.json" 644
371
- copy_file "$SCRIPT_DIR/bunfig.toml" "$CLAUDE_DIR/omg-runtime/bunfig.toml" 644
827
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$VERSION/.claude-plugin/plugin.json"
828
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$VERSION/.mcp.json"
829
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$PLUGIN_BUNDLE_MARKER_FILE"
830
+ track_file "hud/omg-hud.mjs"
831
+ echo " Plugin bundle installed and registered in Claude plugin settings"
372
832
  }
373
833
 
374
- install_action() {
375
- if $FRESH_INSTALL; then
376
- uninstall_action
377
- fi
378
- backup_existing
379
- remove_legacy_runtime_files
380
- install_runtime
381
- say "Step 2/4: Merge settings and templates..."
382
- merge_settings
383
- say "Step 3/4: Install portable runtime metadata..."
384
- if ! $DRY_RUN; then
385
- mkdir -p "$CLAUDE_DIR/hooks" "$CLAUDE_DIR/omg-runtime"
386
- mkdir -p "$CLAUDE_DIR/omg-runtime"
387
- printf '%s\n' "omg-v2-$VERSION" > "$CLAUDE_DIR/hooks/.omg-version"
388
- fi
389
- record_manifest "$CLAUDE_DIR/omg-runtime"
390
- record_manifest "$CLAUDE_DIR/hooks/.omg-version"
391
- if $INSTALL_AS_PLUGIN; then
392
- say "Step 4/4: Install plugin bundle..."
393
- install_plugin_bundle
394
- else
395
- say "Step 4/4: Plugin bundle skipped"
396
- fi
397
- write_manifest
398
- say ""
399
- say "═══════════════════════════════════════════════════════════════"
400
- say " ✅ OMG Bun install completed successfully"
401
- say "═══════════════════════════════════════════════════════════════"
834
+ run_uninstall() {
835
+ echo "═══════════════════════════════════════════════════════════════"
836
+ echo " OMG Setup Manager — uninstall"
837
+ echo "═══════════════════════════════════════════════════════════════"
838
+ echo ""
839
+
840
+ if $DRY_RUN; then
841
+ echo " *** DRY RUN no files will be changed ***"
842
+ echo ""
843
+ fi
844
+
845
+ preflight
846
+
847
+ local existing_ver=""
848
+ if [ -f "$CLAUDE_DIR/hooks/.omg-version" ]; then
849
+ existing_ver=$(cat "$CLAUDE_DIR/hooks/.omg-version" 2>/dev/null || echo "")
850
+ fi
851
+ if [ -n "$existing_ver" ]; then
852
+ echo " Existing OMG install: $existing_ver"
853
+ else
854
+ echo " ~ No .omg-version marker found; uninstall will still remove known OMG files."
855
+ fi
856
+
857
+ if ! $NON_INTERACTIVE && ! $DRY_RUN; then
858
+ read -p "Proceed with uninstall? [y/N] " -n 1 -r
859
+ echo ""
860
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
861
+ echo "Cancelled."
862
+ exit 0
863
+ fi
864
+ fi
865
+
866
+ echo ""
867
+ echo "Uninstall: removing OMG-managed files from $CLAUDE_DIR"
868
+ ensure_backup
869
+ if ! $DRY_RUN; then
870
+ echo " ✓ Backup: $BACKUP_DIR"
871
+ else
872
+ echo " (would backup to $BACKUP_DIR)"
873
+ fi
874
+ remove_omg_files
875
+ if $DRY_RUN; then
876
+ echo " (would remove OMG hooks/rules/agents/commands/templates)"
877
+ else
878
+ echo " ✓ Removed OMG hooks/rules/agents/commands/templates"
879
+ fi
880
+
881
+ echo ""
882
+ echo "Uninstall complete."
883
+ echo " ✓ If plugin bundle was installed, plugin + MCP + HUD were removed together"
884
+ echo "Preserved:"
885
+ echo " - $CLAUDE_DIR/settings.json"
886
+ echo " - project .omg/ data"
887
+ echo " - non-OMG custom files"
402
888
  }
403
889
 
404
- uninstall_action() {
405
- say "Uninstalling OMG Bun runtime..."
406
- remove_manifest_paths
407
- if $DRY_RUN; then
408
- say "DRY RUN: remove $PLUGIN_CACHE_DIR"
409
- else
410
- rm -rf "$PLUGIN_CACHE_DIR"
411
- rm -rf "$CLAUDE_DIR/omg-runtime"
412
- rm -f "$CLAUDE_DIR/hooks/.omg-version"
413
- fi
414
- say " Uninstall complete"
890
+ run_install_like() {
891
+ local existing_ver=""
892
+ local removed=0
893
+ local installed_rules=0
894
+ local installed_hooks=0
895
+ local hook_errors=0
896
+ local installed_agents=0
897
+ local installed_cmds=0
898
+
899
+ echo "═══════════════════════════════════════════════════════════════"
900
+ echo " OMG Setup Manager — $ACTION"
901
+ echo "═══════════════════════════════════════════════════════════════"
902
+ echo ""
903
+
904
+ if $DRY_RUN; then
905
+ echo " *** DRY RUN — no files will be changed ***"
906
+ echo ""
907
+ fi
908
+
909
+ preflight
910
+
911
+ if [ -f "$CLAUDE_DIR/hooks/.omg-version" ]; then
912
+ existing_ver=$(cat "$CLAUDE_DIR/hooks/.omg-version" 2>/dev/null || echo "")
913
+ fi
914
+
915
+ if [ "$ACTION" = "update" ] && [ -z "$existing_ver" ]; then
916
+ echo " ~ No existing OMG install detected. update will proceed as install."
917
+ fi
918
+
919
+ if [ -n "$existing_ver" ]; then
920
+ echo " ✓ Existing: $existing_ver → target $VERSION"
921
+ else
922
+ echo " ✓ Fresh install"
923
+ fi
924
+ echo " ✓ Command surface: /OMG:setup and /OMG:crazy are the primary native front door"
925
+ echo " ✓ Adoption mode: $ADOPTION_MODE"
926
+ echo " ✓ Preset: $OMG_PRESET"
927
+
928
+ if $FRESH_INSTALL; then
929
+ echo ""
930
+ echo "Fresh/reinstall mode: remove OMG files before install."
931
+ if ! $NON_INTERACTIVE && ! $DRY_RUN; then
932
+ read -p "Proceed with fresh cleanup? [y/N] " -n 1 -r
933
+ echo ""
934
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
935
+ echo "Cancelled."
936
+ exit 0
937
+ fi
938
+ fi
939
+
940
+ ensure_backup
941
+ if ! $DRY_RUN; then
942
+ echo " ✓ Backup: $BACKUP_DIR"
943
+ else
944
+ echo " (would backup to $BACKUP_DIR)"
945
+ fi
946
+ remove_omg_files
947
+ if $DRY_RUN; then
948
+ echo " (would remove OMG hooks/rules/agents/commands/templates)"
949
+ else
950
+ echo " ✓ Clean slate ready"
951
+ fi
952
+ existing_ver=""
953
+ fi
954
+
955
+ if [ -n "$existing_ver" ] && ! $FRESH_INSTALL; then
956
+ echo ""
957
+ echo "Step 0/7: Backup existing installation..."
958
+ ensure_backup
959
+ if ! $DRY_RUN; then
960
+ echo " ✓ Backup: $BACKUP_DIR"
961
+ else
962
+ echo " (would backup to $BACKUP_DIR)"
963
+ fi
964
+ fi
965
+
966
+ echo ""
967
+ echo "Step 1/7: Remove deprecated files..."
968
+
969
+ for rule in "${V3_RULES[@]}"; do
970
+ target="$CLAUDE_DIR/rules/$rule"
971
+ if [ -f "$target" ]; then
972
+ ! $DRY_RUN && rm "$target"
973
+ echo " - rules/$rule"
974
+ removed=$((removed + 1))
975
+ fi
976
+ done
977
+
978
+ for agent in "${V3_AGENTS_REMOVE[@]}"; do
979
+ target="$CLAUDE_DIR/agents/$agent"
980
+ if [ -f "$target" ]; then
981
+ ! $DRY_RUN && rm "$target"
982
+ echo " - agents/$agent (v3 deprecated)"
983
+ removed=$((removed + 1))
984
+ fi
985
+ done
986
+
987
+ for agent in "${OLD_OMG_AGENTS[@]}"; do
988
+ target="$CLAUDE_DIR/agents/$agent"
989
+ if [ -f "$target" ]; then
990
+ if grep -q "OMG\|omg\|circuit.breaker\|escalat" "$target" 2>/dev/null; then
991
+ ! $DRY_RUN && rm "$target"
992
+ echo " - agents/$agent (renamed to omg-$agent)"
993
+ removed=$((removed + 1))
994
+ else
995
+ echo " ~ agents/$agent (kept — appears to be non-OMG/custom)"
996
+ fi
997
+ fi
998
+ done
999
+
1000
+ for cmd in "${V3_COMMANDS_REMOVE[@]}" "${V4_COMMANDS_REMOVE[@]}"; do
1001
+ target="$CLAUDE_DIR/commands/$cmd"
1002
+ if [ -f "$target" ]; then
1003
+ if is_omg_managed_command_file "$target"; then
1004
+ ! $DRY_RUN && rm "$target"
1005
+ echo " - commands/$cmd (v4 → OMG:$cmd)"
1006
+ removed=$((removed + 1))
1007
+ else
1008
+ echo " ~ commands/$cmd (kept — appears to be non-OMG/custom)"
1009
+ fi
1010
+ fi
1011
+ done
1012
+
1013
+ if compgen -G "$CLAUDE_DIR/commands/*omc*.md" > /dev/null; then
1014
+ for cmd in "$CLAUDE_DIR"/commands/*omc*.md; do
1015
+ [ -f "$cmd" ] || continue
1016
+ if is_omg_managed_command_file "$cmd"; then
1017
+ ! $DRY_RUN && rm "$cmd"
1018
+ echo " - commands/$(basename "$cmd") (removed legacy command)"
1019
+ removed=$((removed + 1))
1020
+ fi
1021
+ done
1022
+ fi
1023
+
1024
+ if [ -d "$CLAUDE_DIR/hooks/__pycache__" ]; then
1025
+ ! $DRY_RUN && rm -rf "$CLAUDE_DIR/hooks/__pycache__"
1026
+ removed=$((removed + 1))
1027
+ fi
1028
+ ! $DRY_RUN && find "$CLAUDE_DIR/hooks/" -name "*.pyc" -delete 2>/dev/null || true
1029
+ [ $removed -eq 0 ] && echo " (nothing to remove)" || echo " ✓ Removed $removed deprecated files"
1030
+
1031
+ echo ""
1032
+ echo "Step 2/7: Core Rules → $CLAUDE_DIR/rules/"
1033
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/rules"
1034
+ for f in "$SCRIPT_DIR"/rules/core/*.md; do
1035
+ name=$(basename "$f")
1036
+ target="$CLAUDE_DIR/rules/$name"
1037
+ ! $DRY_RUN && cp "$f" "$target"
1038
+ installed_rules=$((installed_rules + 1))
1039
+ track_file "rules/$name"
1040
+ done
1041
+ echo " ✓ $installed_rules core rules"
1042
+
1043
+ echo ""
1044
+ echo "Step 3/7: Hooks → $CLAUDE_DIR/hooks/"
1045
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/hooks"
1046
+ for f in "$SCRIPT_DIR"/hooks/*.py; do
1047
+ name=$(basename "$f")
1048
+ target="$CLAUDE_DIR/hooks/$name"
1049
+ if ! $DRY_RUN; then
1050
+ install_file "$f" "$target"
1051
+ if ! $USE_SYMLINK; then
1052
+ chmod +x "$target"
1053
+ fi
1054
+ fi
1055
+ if python3 -c "import py_compile; py_compile.compile('$f', doraise=True)" 2>/dev/null; then
1056
+ echo " ✓ $name"
1057
+ else
1058
+ echo " ❌ $name (SYNTAX ERROR)"
1059
+ hook_errors=$((hook_errors + 1))
1060
+ ERRORS=$((ERRORS + 1))
1061
+ fi
1062
+ installed_hooks=$((installed_hooks + 1))
1063
+ track_file "hooks/$name"
1064
+ done
1065
+ ! $DRY_RUN && echo "$VERSION" > "$CLAUDE_DIR/hooks/.omg-version"
1066
+ apply_adoption_mode_marker
1067
+ echo " ✓ $installed_hooks hooks ($hook_errors errors)"
1068
+
1069
+ echo ""
1070
+ echo "Step 4/7: Agents → $CLAUDE_DIR/agents/"
1071
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/agents"
1072
+ for f in "$SCRIPT_DIR"/agents/*.md; do
1073
+ name=$(basename "$f")
1074
+ target="$CLAUDE_DIR/agents/$name"
1075
+ if ! $DRY_RUN; then
1076
+ [ -f "$target" ] && [ -z "$existing_ver" ] && cp "$target" "$target.bak.$BACKUP_TS"
1077
+ install_file "$f" "$target"
1078
+ fi
1079
+ echo " ✓ $name"
1080
+ installed_agents=$((installed_agents + 1))
1081
+ track_file "agents/$name"
1082
+ done
1083
+ echo " ✓ $installed_agents agents"
1084
+
1085
+ echo ""
1086
+ echo "Step 5/7: Commands → $CLAUDE_DIR/commands/"
1087
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/commands"
1088
+ for f in "$SCRIPT_DIR"/commands/*.md; do
1089
+ name=$(basename "$f")
1090
+ if [[ "$name" == *omc* ]]; then
1091
+ echo " - /$(basename "$name" .md) (skipped: legacy alias commands are unsupported)"
1092
+ continue
1093
+ fi
1094
+ target="$CLAUDE_DIR/commands/$name"
1095
+ if [ -f "$target" ] && ! is_omg_managed_command_file "$target"; then
1096
+ echo " ~ /$(basename "$name" .md) (kept existing custom command)"
1097
+ continue
1098
+ fi
1099
+ if ! $DRY_RUN; then
1100
+ install_file "$f" "$target"
1101
+ if ! $USE_SYMLINK; then
1102
+ mark_omg_managed_command_file "$target"
1103
+ fi
1104
+ fi
1105
+ echo " ✓ /$(basename "$name" .md)"
1106
+ installed_cmds=$((installed_cmds + 1))
1107
+ track_file "commands/$name"
1108
+ done
1109
+ echo ""
1110
+ echo "Step 6/7: Settings + Templates..."
1111
+ MERGE="$SCRIPT_DIR/scripts/settings-merge.py"
1112
+ TARGET="$CLAUDE_DIR/settings.json"
1113
+ SOURCE="$SCRIPT_DIR/settings.json"
1114
+ if ! $DRY_RUN; then
1115
+ if [ ! -f "$TARGET" ]; then
1116
+ cp "$SOURCE" "$TARGET"
1117
+ apply_omg_preset_to_settings "$TARGET" "$OMG_PRESET"
1118
+ echo " ✓ Created settings.json"
1119
+ else
1120
+ if [ "$MERGE_POLICY" = "skip" ]; then
1121
+ apply_omg_preset_to_settings "$TARGET" "$OMG_PRESET"
1122
+ echo " ⊘ Skipped settings merge (--merge-policy=skip)"
1123
+ elif [ "$MERGE_POLICY" = "apply" ] || $NON_INTERACTIVE; then
1124
+ python3 "$MERGE" "$TARGET" "$SOURCE"
1125
+ apply_omg_preset_to_settings "$TARGET" "$OMG_PRESET"
1126
+ echo " ✓ Settings merged (auto)"
1127
+ else
1128
+ echo " Merging settings.json..."
1129
+ dry_run_preview="$(python3 "$MERGE" "$TARGET" "$SOURCE" --dry-run 2>&1)"
1130
+ printf '%s\n' "$dry_run_preview" | sed -n '1,5p' | sed 's/^/ /'
1131
+ echo ""
1132
+ if read -p " Apply merge? [Y/n] " -n 1 -r 2>/dev/null; then
1133
+ echo ""
1134
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
1135
+ python3 "$MERGE" "$TARGET" "$SOURCE"
1136
+ apply_omg_preset_to_settings "$TARGET" "$OMG_PRESET"
1137
+ echo " ✓ Settings merged"
1138
+ else
1139
+ echo " ⊘ Skipped (manual merge needed)"
1140
+ fi
1141
+ else
1142
+ # read failed — only auto-apply if we can confirm non-interactive context
1143
+ # non-interactive fallback: check for clear non-interactive indicators
1144
+ if [ ! -t 0 ] || [ -n "${npm_lifecycle_event:-}" ] || [ -n "${npm_execpath:-}" ]; then
1145
+ python3 "$MERGE" "$TARGET" "$SOURCE"
1146
+ apply_omg_preset_to_settings "$TARGET" "$OMG_PRESET"
1147
+ echo " ✓ Settings merged (auto — non-interactive fallback)"
1148
+ else
1149
+ echo " ⚠ Could not read input. Skipping merge to be safe."
1150
+ echo " Run manually: ./OMG-setup.sh update --merge-policy=apply"
1151
+ fi
1152
+ fi
1153
+ fi
1154
+ fi
1155
+
1156
+ if $USE_SYMLINK; then
1157
+ # In symlink mode, link entire directories for templates
1158
+ if [ -e "$CLAUDE_DIR/templates/omg" ] || [ -L "$CLAUDE_DIR/templates/omg" ]; then
1159
+ rm -rf "$CLAUDE_DIR/templates/omg"
1160
+ fi
1161
+ ln -s "$SCRIPT_DIR/templates" "$CLAUDE_DIR/templates/omg"
1162
+ # Also link contextual rules
1163
+ mkdir -p "$CLAUDE_DIR/templates/omg/contextual-rules"
1164
+ for cr in "$SCRIPT_DIR"/rules/contextual/*.md; do
1165
+ [ -f "$cr" ] && track_file "templates/omg/contextual-rules/$(basename "$cr")"
1166
+ done
1167
+ else
1168
+ mkdir -p "$CLAUDE_DIR/templates/omg"
1169
+ cp "$SCRIPT_DIR"/templates/* "$CLAUDE_DIR/templates/omg/" 2>/dev/null || true
1170
+ for t in "$SCRIPT_DIR"/templates/*; do
1171
+ [ -f "$t" ] && track_file "templates/omg/$(basename "$t")"
1172
+ done
1173
+ mkdir -p "$CLAUDE_DIR/templates/omg/contextual-rules"
1174
+ cp "$SCRIPT_DIR"/rules/contextual/*.md "$CLAUDE_DIR/templates/omg/contextual-rules/" 2>/dev/null || true
1175
+ for cr in "$SCRIPT_DIR"/rules/contextual/*.md; do
1176
+ [ -f "$cr" ] && track_file "templates/omg/contextual-rules/$(basename "$cr")"
1177
+ done
1178
+ fi
1179
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/memory"
1180
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/learnings"
1181
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/ledger"
1182
+ echo " \u2713 State directory templates (memory, learnings, ledger)"
1183
+ echo " \u2713 Templates + contextual rules"
1184
+
1185
+ if $USE_SYMLINK; then
1186
+ # In symlink mode, link runtime directories instead of copying
1187
+ mkdir -p "$CLAUDE_DIR/omg-runtime/scripts"
1188
+ install_file "$SCRIPT_DIR/scripts/omg.py" "$CLAUDE_DIR/omg-runtime/scripts/omg.py"
1189
+
1190
+ [[ "$CLAUDE_DIR/omg-runtime/runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/runtime" >&2; exit 1; }
1191
+ [[ "$CLAUDE_DIR/omg-runtime/hooks" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/hooks" >&2; exit 1; }
1192
+ [[ "$CLAUDE_DIR/omg-runtime/lab" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/lab" >&2; exit 1; }
1193
+
1194
+ rm -rf "$CLAUDE_DIR/omg-runtime/runtime" "$CLAUDE_DIR/omg-runtime/hooks" "$CLAUDE_DIR/omg-runtime/lab"
1195
+ ln -s "$SCRIPT_DIR/runtime" "$CLAUDE_DIR/omg-runtime/runtime"
1196
+ ln -s "$SCRIPT_DIR/hooks" "$CLAUDE_DIR/omg-runtime/hooks"
1197
+ ln -s "$SCRIPT_DIR/lab" "$CLAUDE_DIR/omg-runtime/lab"
1198
+
1199
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
1200
+ find "$CLAUDE_DIR/omg-runtime" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
1201
+ find "$CLAUDE_DIR/omg-runtime" -name "*.pyc" -delete 2>/dev/null || true
1202
+ echo " ✓ Portable runtime → $CLAUDE_DIR/omg-runtime (symlinked to $SCRIPT_DIR)"
1203
+ else
1204
+ mkdir -p "$CLAUDE_DIR/omg-runtime/scripts"
1205
+ cp "$SCRIPT_DIR/scripts/omg.py" "$CLAUDE_DIR/omg-runtime/scripts/omg.py"
1206
+ [[ "$CLAUDE_DIR/omg-runtime/runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime/runtime" >&2; exit 1; }
1207
+ rm -rf "$CLAUDE_DIR/omg-runtime/runtime" "$CLAUDE_DIR/omg-runtime/hooks" "$CLAUDE_DIR/omg-runtime/lab"
1208
+ cp -R "$SCRIPT_DIR/runtime" "$CLAUDE_DIR/omg-runtime/"
1209
+ cp -R "$SCRIPT_DIR/hooks" "$CLAUDE_DIR/omg-runtime/"
1210
+ cp -R "$SCRIPT_DIR/lab" "$CLAUDE_DIR/omg-runtime/"
1211
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
1212
+ find "$CLAUDE_DIR/omg-runtime" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
1213
+ find "$CLAUDE_DIR/omg-runtime" -name "*.pyc" -delete 2>/dev/null || true
1214
+ echo " ✓ Portable runtime → $CLAUDE_DIR/omg-runtime"
1215
+ fi
1216
+ if $INSTALL_AS_PLUGIN; then
1217
+ install_plugin_bundle
1218
+ fi
1219
+
1220
+ local adoption_report_path=""
1221
+ adoption_report_path=$(write_native_adoption_report)
1222
+ echo " ✓ Adoption report → $adoption_report_path"
1223
+ else
1224
+ echo " (would merge settings.json + copy templates + provision portable runtime)"
1225
+ if $INSTALL_AS_PLUGIN; then
1226
+ install_plugin_bundle
1227
+ fi
1228
+ echo " (would write adoption report and apply preset/mode markers)"
1229
+ fi
1230
+
1231
+
1232
+ echo ""
1233
+ echo "Step 7/7: Reconcile stale files..."
1234
+ reconcile_stale_files
1235
+ write_omg_manifest
1236
+
1237
+ echo ""
1238
+ echo "═══════════════════════════════════════════════════════════════"
1239
+ if [ $ERRORS -eq 0 ]; then
1240
+ echo " ✅ OMG ${VERSION} ${ACTION} completed successfully"
1241
+ else
1242
+ echo " ⚠ OMG ${VERSION} ${ACTION} completed with $ERRORS error(s)"
1243
+ fi
1244
+ echo "═══════════════════════════════════════════════════════════════"
1245
+ echo ""
1246
+ echo " Files: $installed_rules rules, $installed_hooks hooks, $installed_agents agents, $installed_cmds commands"
1247
+ echo " Version: $VERSION"
1248
+ if $USE_SYMLINK; then
1249
+ echo " Mode: Symlink (live updates from $SCRIPT_DIR)"
1250
+ echo " Source: $SCRIPT_DIR"
1251
+ elif $FRESH_INSTALL; then
1252
+ echo " Mode: Fresh reinstall"
1253
+ elif [ -n "$existing_ver" ]; then
1254
+ echo " Upgraded: $existing_ver → $VERSION"
1255
+ echo " Backup: $BACKUP_DIR"
1256
+ fi
1257
+ echo ""
1258
+ if $DRY_RUN; then
1259
+ echo " *** DRY RUN — no files were changed ***"
1260
+ echo " Run without --dry-run to apply changes."
1261
+ fi
415
1262
  }
416
1263
 
417
1264
  main() {
418
- parse_args "$@"
419
- prompt_start_action
420
- preflight
421
- case "$ACTION" in
422
- install|update) install_action ;;
423
- uninstall) uninstall_action ;;
424
- *)
425
- usage
426
- exit 1
427
- ;;
428
- esac
1265
+ parse_args "$@"
1266
+ prompt_start_action
1267
+ case "$ACTION" in
1268
+ uninstall) run_uninstall ;;
1269
+ install|update|reinstall) run_install_like ;;
1270
+ *)
1271
+ usage
1272
+ exit 1
1273
+ ;;
1274
+ esac
429
1275
  }
430
1276
 
431
1277
  main "$@"