@trac3er/oh-my-god 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Changelog Generator for OMG
|
|
4
|
+
|
|
5
|
+
Parses conventional commits from git log and generates/updates CHANGELOG.md
|
|
6
|
+
in Keep-a-Changelog format.
|
|
7
|
+
|
|
8
|
+
Feature flag: OMG_CHANGELOG_ENABLED (default: False)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import date
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
# Lazy imports
|
|
18
|
+
_git_inspector = None
|
|
19
|
+
_get_feature_flag = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_imports():
|
|
23
|
+
"""Lazy import git_inspector and feature flag helper."""
|
|
24
|
+
global _git_inspector, _get_feature_flag
|
|
25
|
+
if _git_inspector is None:
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
27
|
+
from tools import git_inspector as _gi
|
|
28
|
+
from hooks._common import get_feature_flag as _gff
|
|
29
|
+
_git_inspector = _gi
|
|
30
|
+
_get_feature_flag = _gff
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_enabled() -> bool:
|
|
34
|
+
"""Check if changelog feature is enabled."""
|
|
35
|
+
_ensure_imports()
|
|
36
|
+
return _get_feature_flag("GIT_WORKFLOW", default=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Supported conventional commit types
|
|
40
|
+
CONVENTIONAL_TYPES = frozenset({
|
|
41
|
+
"feat", "fix", "docs", "style", "refactor",
|
|
42
|
+
"test", "chore", "perf", "ci", "build", "sec",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
# Regex for conventional commit: type(scope): description OR type: description
|
|
46
|
+
_CONVENTIONAL_RE = re.compile(
|
|
47
|
+
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?!?:\s*(?P<description>.+)$"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Changelog section groupings
|
|
51
|
+
_TYPE_TO_SECTION = {
|
|
52
|
+
"feat": "Added",
|
|
53
|
+
"fix": "Fixed",
|
|
54
|
+
"refactor": "Changed",
|
|
55
|
+
"perf": "Changed",
|
|
56
|
+
"docs": "Changed",
|
|
57
|
+
"style": "Changed",
|
|
58
|
+
"build": "Changed",
|
|
59
|
+
"ci": "Changed",
|
|
60
|
+
"chore": "Other",
|
|
61
|
+
"test": "Other",
|
|
62
|
+
"sec": "Security",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_SECTION_ORDER = ["Added", "Fixed", "Changed", "Deprecated", "Removed", "Security", "Other"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_commit_log(cwd: str = ".") -> List[Dict[str, Any]]:
|
|
69
|
+
"""Parse git log for conventional commits.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cwd: Working directory (default: current directory)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of dicts with keys: type, scope, description, hash, author, date, breaking
|
|
76
|
+
Returns empty list if OMG_CHANGELOG_ENABLED is False or no conventional commits found.
|
|
77
|
+
"""
|
|
78
|
+
if not _is_enabled():
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
_ensure_imports()
|
|
82
|
+
raw_commits = _git_inspector.git_log(cwd, n=100)
|
|
83
|
+
|
|
84
|
+
if not raw_commits:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
parsed = []
|
|
88
|
+
for commit in raw_commits:
|
|
89
|
+
subject = commit.get("subject", "").strip()
|
|
90
|
+
if not subject:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
match = _CONVENTIONAL_RE.match(subject)
|
|
94
|
+
if not match:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
commit_type = match.group("type").lower()
|
|
98
|
+
if commit_type not in CONVENTIONAL_TYPES:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Detect breaking changes
|
|
102
|
+
breaking = "BREAKING CHANGE" in subject or "!" in subject.split(":")[0]
|
|
103
|
+
|
|
104
|
+
parsed.append({
|
|
105
|
+
"type": commit_type,
|
|
106
|
+
"scope": match.group("scope") or "",
|
|
107
|
+
"description": match.group("description").strip(),
|
|
108
|
+
"hash": commit.get("hash", "")[:7],
|
|
109
|
+
"author": commit.get("author", ""),
|
|
110
|
+
"date": commit.get("date", ""),
|
|
111
|
+
"breaking": breaking,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return parsed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def generate_changelog_entry(
|
|
118
|
+
commits: List[Dict[str, Any]],
|
|
119
|
+
version: str = "Unreleased",
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Format a Keep-a-Changelog section from parsed commits.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
commits: List of parsed commit dicts from parse_commit_log()
|
|
125
|
+
version: Version label (default: "Unreleased")
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Formatted changelog section string.
|
|
129
|
+
Returns empty string if commits list is empty.
|
|
130
|
+
"""
|
|
131
|
+
if not commits:
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
today = date.today().isoformat()
|
|
135
|
+
header = f"## [{version}] - {today}"
|
|
136
|
+
|
|
137
|
+
# Group commits by section
|
|
138
|
+
sections: Dict[str, List[str]] = {s: [] for s in _SECTION_ORDER}
|
|
139
|
+
|
|
140
|
+
for commit in commits:
|
|
141
|
+
section = _TYPE_TO_SECTION.get(commit["type"], "Other")
|
|
142
|
+
scope = commit.get("scope", "")
|
|
143
|
+
description = commit["description"]
|
|
144
|
+
short_hash = commit.get("hash", "")
|
|
145
|
+
|
|
146
|
+
if scope:
|
|
147
|
+
entry = f"- **{scope}**: {description}"
|
|
148
|
+
else:
|
|
149
|
+
entry = f"- {description}"
|
|
150
|
+
|
|
151
|
+
if short_hash:
|
|
152
|
+
entry += f" (#{short_hash})"
|
|
153
|
+
|
|
154
|
+
if commit.get("breaking"):
|
|
155
|
+
entry += " **[BREAKING]**"
|
|
156
|
+
|
|
157
|
+
sections[section].append(entry)
|
|
158
|
+
|
|
159
|
+
lines = [header, ""]
|
|
160
|
+
|
|
161
|
+
for section_name in _SECTION_ORDER:
|
|
162
|
+
entries = sections[section_name]
|
|
163
|
+
if not entries:
|
|
164
|
+
continue
|
|
165
|
+
lines.append(f"### {section_name}")
|
|
166
|
+
lines.extend(entries)
|
|
167
|
+
lines.append("")
|
|
168
|
+
|
|
169
|
+
# Strip trailing blank line
|
|
170
|
+
while lines and lines[-1] == "":
|
|
171
|
+
lines.pop()
|
|
172
|
+
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def update_changelog(cwd: str = ".", version: str = None) -> bool:
|
|
177
|
+
"""Parse commits and prepend a new entry to CHANGELOG.md.
|
|
178
|
+
|
|
179
|
+
Reads existing CHANGELOG.md (or creates a new one). Inserts the new
|
|
180
|
+
entry after the top-level `# Changelog` header without overwriting
|
|
181
|
+
existing sections.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
cwd: Working directory (default: current directory)
|
|
185
|
+
version: Version label (default: "Unreleased")
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True on success, False on failure or if no commits to add.
|
|
189
|
+
"""
|
|
190
|
+
commits = parse_commit_log(cwd)
|
|
191
|
+
if not commits:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
entry = generate_changelog_entry(commits, version=version or "Unreleased")
|
|
195
|
+
if not entry:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
changelog_path = os.path.join(cwd, "CHANGELOG.md")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if os.path.exists(changelog_path):
|
|
202
|
+
with open(changelog_path, "r", encoding="utf-8") as f:
|
|
203
|
+
existing = f.read()
|
|
204
|
+
else:
|
|
205
|
+
existing = "# Changelog\n\nAll notable changes to this project will be documented here.\n"
|
|
206
|
+
|
|
207
|
+
# Find insertion point: after the first `# Changelog` header line
|
|
208
|
+
lines = existing.splitlines(keepends=True)
|
|
209
|
+
insert_idx = 0
|
|
210
|
+
for i, line in enumerate(lines):
|
|
211
|
+
if line.startswith("# "):
|
|
212
|
+
insert_idx = i + 1
|
|
213
|
+
# Skip blank lines immediately after the header
|
|
214
|
+
while insert_idx < len(lines) and lines[insert_idx].strip() == "":
|
|
215
|
+
insert_idx += 1
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
new_block = entry + "\n\n"
|
|
219
|
+
lines.insert(insert_idx, new_block)
|
|
220
|
+
new_content = "".join(lines)
|
|
221
|
+
|
|
222
|
+
with open(changelog_path, "w", encoding="utf-8") as f:
|
|
223
|
+
f.write(new_content)
|
|
224
|
+
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
except OSError:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _dry_run(cwd: str = ".", version: str = None) -> str:
|
|
232
|
+
"""Return the changelog entry that would be written, without modifying any file."""
|
|
233
|
+
commits = parse_commit_log(cwd)
|
|
234
|
+
if not commits:
|
|
235
|
+
return "[OMG Changelog] No conventional commits found (or feature flag disabled)."
|
|
236
|
+
return generate_changelog_entry(commits, version=version or "Unreleased")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main():
|
|
240
|
+
"""CLI entry point."""
|
|
241
|
+
import argparse
|
|
242
|
+
|
|
243
|
+
parser = argparse.ArgumentParser(
|
|
244
|
+
description="OMG Changelog Generator — parses conventional commits and updates CHANGELOG.md"
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument("--cwd", default=".", help="Working directory (default: .)")
|
|
247
|
+
parser.add_argument("--version", default=None, help="Version label (default: Unreleased)")
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--dry-run",
|
|
250
|
+
action="store_true",
|
|
251
|
+
help="Print the changelog entry without writing to file",
|
|
252
|
+
)
|
|
253
|
+
args = parser.parse_args()
|
|
254
|
+
|
|
255
|
+
if args.dry_run:
|
|
256
|
+
print(_dry_run(cwd=args.cwd, version=args.version))
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
success = update_changelog(cwd=args.cwd, version=args.version)
|
|
260
|
+
if success:
|
|
261
|
+
print("[OMG Changelog] CHANGELOG.md updated successfully.")
|
|
262
|
+
else:
|
|
263
|
+
print("[OMG Changelog] No changes written (no commits or feature flag disabled).")
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
_SYNTH_TYPE_TO_SECTION = {
|
|
268
|
+
"feat": "Features",
|
|
269
|
+
"fix": "Bug Fixes",
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_SYNTH_SECTION_ORDER = ["Features", "Bug Fixes", "Breaking Changes", "Other"]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def synthesize_changelog(commits: List[Dict[str, Any]]) -> str:
|
|
276
|
+
"""Public API: group raw commit dicts by type into markdown sections.
|
|
277
|
+
|
|
278
|
+
Accepts dicts with ``message`` (str), optional ``hash`` and ``files``.
|
|
279
|
+
"""
|
|
280
|
+
if not commits:
|
|
281
|
+
return ""
|
|
282
|
+
|
|
283
|
+
sections: Dict[str, List[str]] = {s: [] for s in _SYNTH_SECTION_ORDER}
|
|
284
|
+
|
|
285
|
+
for commit in commits:
|
|
286
|
+
message = commit.get("message", "").strip()
|
|
287
|
+
if not message:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
is_breaking = message.startswith("BREAKING CHANGE")
|
|
291
|
+
if not is_breaking:
|
|
292
|
+
colon_idx = message.find(":")
|
|
293
|
+
if colon_idx > 0 and "!" in message[:colon_idx]:
|
|
294
|
+
is_breaking = True
|
|
295
|
+
|
|
296
|
+
if is_breaking:
|
|
297
|
+
sections["Breaking Changes"].append(f"- {message}")
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
match = _CONVENTIONAL_RE.match(message)
|
|
301
|
+
if match:
|
|
302
|
+
commit_type = match.group("type").lower()
|
|
303
|
+
section = _SYNTH_TYPE_TO_SECTION.get(commit_type, "Other")
|
|
304
|
+
sections[section].append(f"- {message}")
|
|
305
|
+
else:
|
|
306
|
+
sections["Other"].append(f"- {message}")
|
|
307
|
+
|
|
308
|
+
lines: List[str] = ["## Changes", ""]
|
|
309
|
+
|
|
310
|
+
for section_name in _SYNTH_SECTION_ORDER:
|
|
311
|
+
entries = sections[section_name]
|
|
312
|
+
if not entries:
|
|
313
|
+
continue
|
|
314
|
+
lines.append(f"### {section_name}")
|
|
315
|
+
lines.extend(entries)
|
|
316
|
+
lines.append("")
|
|
317
|
+
|
|
318
|
+
while lines and lines[-1] == "":
|
|
319
|
+
lines.pop()
|
|
320
|
+
|
|
321
|
+
if len(lines) <= 1:
|
|
322
|
+
return ""
|
|
323
|
+
|
|
324
|
+
return "\n".join(lines)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def write_changelog(
|
|
328
|
+
commits: List[Dict[str, Any]],
|
|
329
|
+
output_path: Optional[str] = None,
|
|
330
|
+
) -> str:
|
|
331
|
+
"""Synthesize changelog; write to *output_path* when given, else return string."""
|
|
332
|
+
content = synthesize_changelog(commits)
|
|
333
|
+
if not content:
|
|
334
|
+
return ""
|
|
335
|
+
|
|
336
|
+
if output_path is not None:
|
|
337
|
+
parent = os.path.dirname(output_path)
|
|
338
|
+
if parent:
|
|
339
|
+
os.makedirs(parent, exist_ok=True)
|
|
340
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
341
|
+
f.write(content)
|
|
342
|
+
|
|
343
|
+
return content
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
main()
|