rlsbl 0.8.3 → 0.9.0

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 (37) hide show
  1. package/package.json +3 -5
  2. package/rlsbl/__init__.py +0 -247
  3. package/rlsbl/__main__.py +0 -4
  4. package/rlsbl/commands/__init__.py +0 -0
  5. package/rlsbl/commands/check.py +0 -229
  6. package/rlsbl/commands/config.py +0 -67
  7. package/rlsbl/commands/discover.py +0 -198
  8. package/rlsbl/commands/init_cmd.py +0 -518
  9. package/rlsbl/commands/pre_push_check.py +0 -46
  10. package/rlsbl/commands/record_gif.py +0 -92
  11. package/rlsbl/commands/release.py +0 -287
  12. package/rlsbl/commands/status.py +0 -76
  13. package/rlsbl/commands/undo.py +0 -74
  14. package/rlsbl/commands/watch.py +0 -125
  15. package/rlsbl/config.py +0 -57
  16. package/rlsbl/registries/__init__.py +0 -5
  17. package/rlsbl/registries/go.py +0 -123
  18. package/rlsbl/registries/npm.py +0 -119
  19. package/rlsbl/registries/pypi.py +0 -171
  20. package/rlsbl/tagging.py +0 -207
  21. package/rlsbl/templates/go/VERSION.tpl +0 -1
  22. package/rlsbl/templates/go/ci.yml.tpl +0 -18
  23. package/rlsbl/templates/go/goreleaser.yml.tpl +0 -25
  24. package/rlsbl/templates/go/publish.yml.tpl +0 -25
  25. package/rlsbl/templates/merged/publish.yml.tpl +0 -30
  26. package/rlsbl/templates/npm/ci.yml.tpl +0 -22
  27. package/rlsbl/templates/npm/publish.yml.tpl +0 -22
  28. package/rlsbl/templates/pypi/ci.yml.tpl +0 -20
  29. package/rlsbl/templates/pypi/publish.yml.tpl +0 -18
  30. package/rlsbl/templates/shared/CHANGELOG.md.tpl +0 -5
  31. package/rlsbl/templates/shared/CLAUDE.md.tpl +0 -20
  32. package/rlsbl/templates/shared/LICENSE.tpl +0 -21
  33. package/rlsbl/templates/shared/claude-settings.json.tpl +0 -3
  34. package/rlsbl/templates/shared/gitignore.tpl +0 -14
  35. package/rlsbl/templates/shared/hooks/post-release.sh.tpl +0 -8
  36. package/rlsbl/templates/shared/hooks/pre-release.sh.tpl +0 -31
  37. package/rlsbl/utils.py +0 -131
@@ -1,287 +0,0 @@
1
- """Release command: bump version, commit, push, create GitHub Release."""
2
-
3
- import os
4
- import sys
5
- import time
6
-
7
- from ..config import should_tag
8
- from ..registries import REGISTRIES
9
- from ..tagging import ensure_github_topic, ensure_npm_keyword, ensure_pypi_keyword
10
- from ..utils import (
11
- bump_version,
12
- check_gh_auth,
13
- check_gh_installed,
14
- extract_changelog_entry,
15
- find_commit_tool,
16
- get_current_branch,
17
- get_push_timeout,
18
- is_clean_tree,
19
- push_if_needed,
20
- run,
21
- )
22
-
23
- VALID_BUMP_TYPES = ("patch", "minor", "major")
24
-
25
-
26
- def run_cmd(registry, args, flags):
27
- """Release command handler.
28
-
29
- Bumps version, commits, pushes, and creates a GitHub Release.
30
- """
31
- quiet = flags.get("quiet", False)
32
-
33
- def log(msg):
34
- if not quiet:
35
- print(msg)
36
-
37
- reg = REGISTRIES[registry]
38
-
39
- # Check prerequisites
40
- if not check_gh_installed():
41
- print("Error: gh CLI is not installed. Install it from https://cli.github.com", file=sys.stderr)
42
- sys.exit(1)
43
- if not check_gh_auth():
44
- print('Error: gh CLI is not authenticated. Run "gh auth login" first.', file=sys.stderr)
45
- sys.exit(1)
46
-
47
- # Clean working tree
48
- if not is_clean_tree():
49
- print("Error: working tree is not clean. Commit your changes first.", file=sys.stderr)
50
- sys.exit(1)
51
-
52
- # Branch check
53
- branch = get_current_branch()
54
- if branch not in ("main", "master"):
55
- print(f'Warning: you are on branch "{branch}", not main/master.', file=sys.stderr)
56
-
57
- # Current version
58
- current_version = reg.read_version(".")
59
- log(f"Current version: {current_version}")
60
-
61
- # If the current version has never been tagged, release it as-is (bootstrap)
62
- current_tag = f"v{current_version}"
63
- current_tag_exists = len(run("git", ["tag", "-l", current_tag])) > 0
64
-
65
- if not current_tag_exists:
66
- new_version = current_version
67
- bump_type = None
68
- tag = current_tag
69
- if args:
70
- log(f"First release: releasing {new_version} as-is (bump type ignored)")
71
- else:
72
- log(f"First release: {new_version}")
73
- else:
74
- bump_type = args[0] if args else "patch"
75
- if bump_type not in VALID_BUMP_TYPES:
76
- print(
77
- f'Error: invalid bump type "{bump_type}". Use: {", ".join(VALID_BUMP_TYPES)}',
78
- file=sys.stderr,
79
- )
80
- sys.exit(1)
81
-
82
- new_version = bump_version(current_version, bump_type)
83
- tag = f"v{new_version}"
84
- log(f"New version: {new_version} ({bump_type})")
85
-
86
- # Check tag doesn't already exist
87
- tag_output = run("git", ["tag", "-l", tag])
88
- if len(tag_output) > 0:
89
- print(f'Error: tag "{tag}" already exists.', file=sys.stderr)
90
- sys.exit(1)
91
-
92
- # Validate changelog entry
93
- changelog_path = os.path.join(".", "CHANGELOG.md")
94
- if not os.path.exists(changelog_path):
95
- print(
96
- f"Error: CHANGELOG.md not found. Create one with a ## {new_version} section.",
97
- file=sys.stderr,
98
- )
99
- sys.exit(1)
100
- changelog_entry = extract_changelog_entry(changelog_path, new_version)
101
- if not changelog_entry:
102
- print(
103
- f"Error: no changelog entry found for version {new_version} in CHANGELOG.md.",
104
- file=sys.stderr,
105
- )
106
- print(f'Add a "## {new_version}" section describing the changes.', file=sys.stderr)
107
- sys.exit(1)
108
- if len(changelog_entry.strip()) < 10:
109
- print(
110
- f"Warning: changelog entry for {new_version} is very short. Consider adding more detail.",
111
- file=sys.stderr,
112
- )
113
-
114
- # Run pre-release hook if present
115
- pre_release_script = os.path.join(".", ".rlsbl", "hooks", "pre-release.sh")
116
- if os.path.exists(pre_release_script):
117
- log("Running pre-release hook...")
118
- try:
119
- run("bash", [pre_release_script])
120
- except Exception:
121
- print("Error: pre-release hook failed. Fix the issues and try again.", file=sys.stderr)
122
- sys.exit(1)
123
-
124
- # Dry run: print summary and return
125
- if flags.get("dry-run", False):
126
- log("\n--- Dry run summary ---")
127
- log(f"Registry: {registry}")
128
- if bump_type:
129
- log(f"Bump: {current_version} -> {new_version} ({bump_type})")
130
- else:
131
- log(f"Version: {new_version} (first release)")
132
- log(f"Tag: {tag}")
133
- log(f"Branch: {branch}")
134
- # Show other version files that would be synced
135
- other_files = []
136
- for name, other_reg in REGISTRIES.items():
137
- if name == registry:
138
- continue
139
- if other_reg.check_project_exists("."):
140
- other_file = other_reg.get_version_file()
141
- if other_file:
142
- other_files.append(other_file)
143
- if other_files:
144
- log(f"Sync to: {', '.join(other_files)}")
145
- log(f"Changelog:\n{changelog_entry}")
146
- log("--- No changes made ---")
147
- return
148
-
149
- # Pre-compute which files will be modified
150
- version_file = reg.get_version_file()
151
- files_to_commit = []
152
- if version_file:
153
- files_to_commit.append(version_file)
154
- for name, other_reg in REGISTRIES.items():
155
- if name == registry:
156
- continue
157
- if other_reg.check_project_exists("."):
158
- other_file = other_reg.get_version_file()
159
- if other_file:
160
- files_to_commit.append(other_file)
161
-
162
- # Confirmation prompt (skip with --yes)
163
- if not flags.get("yes"):
164
- bump_label = f" ({bump_type})" if bump_type else ""
165
- print(f"\nAbout to release {new_version}{bump_label} on {branch}")
166
- print(f" Tag: {tag}")
167
- if files_to_commit:
168
- print(f" Files: {', '.join(files_to_commit)}")
169
- else:
170
- print(" Files: (none -- version is the git tag)")
171
- try:
172
- answer = input("Proceed? [y/N] ").strip().lower()
173
- except (EOFError, KeyboardInterrupt):
174
- print("\nAborted.")
175
- sys.exit(1)
176
- if answer != "y":
177
- print("Aborted.")
178
- sys.exit(0)
179
-
180
- # Write new version to version files (skip if version didn't change, e.g. first release)
181
- if new_version != current_version:
182
- if version_file:
183
- reg.write_version(".", new_version)
184
- log(f"Updated version in {version_file}")
185
-
186
- # Sync version to all other recognized version files
187
- for name, other_reg in REGISTRIES.items():
188
- if name == registry:
189
- continue
190
- if other_reg.check_project_exists("."):
191
- other_file = other_reg.get_version_file()
192
- if other_file:
193
- other_reg.write_version(".", new_version)
194
- log(f"Synced version to {other_file}")
195
-
196
- # Ecosystem tagging: add keyword to manifests if enabled (after confirmation)
197
- if should_tag(flags):
198
- try:
199
- if REGISTRIES["npm"].check_project_exists("."):
200
- if ensure_npm_keyword(".", quiet=quiet):
201
- if "package.json" not in files_to_commit:
202
- files_to_commit.append("package.json")
203
- except Exception:
204
- pass
205
- try:
206
- if REGISTRIES["pypi"].check_project_exists("."):
207
- if ensure_pypi_keyword(".", quiet=quiet):
208
- if "pyproject.toml" not in files_to_commit:
209
- files_to_commit.append("pyproject.toml")
210
- except Exception:
211
- pass
212
-
213
- # Update .rlsbl/version marker so it's included in the release commit
214
- rlsbl_version_marker = os.path.join(".rlsbl", "version")
215
- if os.path.exists(os.path.dirname(rlsbl_version_marker)):
216
- try:
217
- from .. import __version__ as rlsbl_ver
218
- with open(rlsbl_version_marker, "w") as f:
219
- f.write(rlsbl_ver + "\n")
220
- if rlsbl_version_marker not in files_to_commit:
221
- files_to_commit.append(rlsbl_version_marker)
222
- except Exception:
223
- pass
224
-
225
- # Commit if anything was actually modified (version bump or tagging)
226
- needs_commit = new_version != current_version or not is_clean_tree()
227
- if files_to_commit and needs_commit:
228
- commit_tool = find_commit_tool()
229
- if commit_tool == "safegit":
230
- run(commit_tool, ["commit", "-m", tag, "--", *files_to_commit])
231
- else:
232
- run("git", ["add", *files_to_commit])
233
- run("git", ["commit", "-m", tag])
234
- log(f"Committed: {tag}")
235
- elif not needs_commit:
236
- log("No changes to commit")
237
-
238
- # Create local git tag
239
- run("git", ["tag", tag])
240
- log(f"Tagged: {tag}")
241
-
242
- # Push commits and tag
243
- push_timeout = get_push_timeout()
244
- if push_timeout != 120:
245
- log(f"Push timeout: {push_timeout}s (from RLSBL_PUSH_TIMEOUT)")
246
- push_if_needed(branch)
247
- run("git", ["push", "origin", tag], timeout=push_timeout)
248
- log(f"Pushed to origin/{branch}")
249
-
250
- # Create GitHub Release using a temp notes file
251
- notes_file = f".rlsbl-notes-{int(time.time() * 1000)}.tmp"
252
- writing_file = notes_file + ".writing"
253
- try:
254
- with open(writing_file, "w", encoding="utf-8") as f:
255
- f.write(changelog_entry)
256
- os.rename(writing_file, notes_file)
257
- run("gh", ["release", "create", tag, "--title", tag, "--notes-file", notes_file])
258
- log(f"Created GitHub Release: {tag}")
259
- finally:
260
- # Clean up temp files even if gh release fails
261
- for tmp in (notes_file, writing_file):
262
- if os.path.exists(tmp):
263
- os.unlink(tmp)
264
-
265
- # Ecosystem tagging: add GitHub topic after release is created
266
- if should_tag(flags):
267
- ensure_github_topic(quiet=quiet)
268
-
269
- # Run post-release hook if present (non-fatal: release is already complete)
270
- post_release_script = os.path.join(".", ".rlsbl", "hooks", "post-release.sh")
271
- if os.path.exists(post_release_script):
272
- log("Running post-release hook...")
273
- try:
274
- env = os.environ.copy()
275
- env["RLSBL_VERSION"] = new_version
276
- run("bash", [post_release_script], env=env)
277
- except Exception as e:
278
- print(f"Warning: post-release hook failed: {e}", file=sys.stderr)
279
-
280
- # Hint: how to watch CI for this release
281
- try:
282
- commit_sha = run("git", ["rev-parse", "HEAD"])
283
- log(f"Watch CI: rlsbl watch {commit_sha}")
284
- except Exception:
285
- pass
286
-
287
- log(f"\nRelease {new_version} complete!")
@@ -1,76 +0,0 @@
1
- """Status command: show project status summary."""
2
-
3
- import os
4
- import sys
5
-
6
- from ..registries import REGISTRIES
7
- from ..utils import (
8
- extract_changelog_entry,
9
- get_current_branch,
10
- is_clean_tree,
11
- run,
12
- )
13
-
14
-
15
- def run_cmd(registry, args, flags):
16
- """Status command handler.
17
-
18
- Shows a quick 'where am I' summary: package info, git state, changelog, CI.
19
- """
20
- reg = REGISTRIES[registry]
21
-
22
- if not reg.check_project_exists("."):
23
- print(f"No {registry} project found in current directory.", file=sys.stderr)
24
- sys.exit(1)
25
-
26
- version = reg.read_version(".")
27
- vars_dict = reg.get_template_vars(".")
28
- name = vars_dict.get("name") or "(unknown)"
29
-
30
- print(f"Package: {name}")
31
-
32
- # Show version info for all detected registries
33
- for r_name, r_mod in REGISTRIES.items():
34
- if r_mod.check_project_exists("."):
35
- ver = r_mod.read_version(".")
36
- file = r_mod.get_version_file() or "git tag"
37
- print(f"Version: {ver} ({r_name}, {file})")
38
-
39
- # Git info
40
- try:
41
- branch = get_current_branch()
42
- print(f"Branch: {branch}")
43
- except Exception:
44
- print("Branch: (not a git repo)")
45
-
46
- # Last tag
47
- try:
48
- last_tag = run("git", ["describe", "--tags", "--abbrev=0"])
49
- print(f"Last tag: {last_tag}")
50
- except Exception:
51
- print("Last tag: (none)")
52
-
53
- # Clean tree
54
- try:
55
- print(f"Clean: {'yes' if is_clean_tree() else 'no'}")
56
- except Exception:
57
- print("Clean: (unknown)")
58
-
59
- # Changelog
60
- changelog_path = "CHANGELOG.md"
61
- if os.path.exists(changelog_path):
62
- entry = extract_changelog_entry(changelog_path, version)
63
- if entry:
64
- print(f"Changelog: has entry for {version}")
65
- else:
66
- print(f"Changelog: no entry for {version}")
67
- else:
68
- print("Changelog: (not found)")
69
-
70
- # CI workflows
71
- ci_exists = os.path.exists(".github/workflows/ci.yml")
72
- publish_exists = os.path.exists(".github/workflows/publish.yml") or os.path.exists(
73
- ".github/workflows/workflow.yml"
74
- )
75
- print(f"CI: {'yes' if ci_exists else 'missing'}")
76
- print(f"Publish: {'yes' if publish_exists else 'missing'}")
@@ -1,74 +0,0 @@
1
- """Undo command: revert the last release."""
2
-
3
- import sys
4
-
5
- from ..utils import run, check_gh_installed, check_gh_auth, get_push_timeout, is_clean_tree
6
-
7
-
8
- def run_cmd(registry, args, flags):
9
- if not check_gh_installed():
10
- print("Error: gh CLI is not installed.", file=sys.stderr)
11
- sys.exit(1)
12
- if not check_gh_auth():
13
- print("Error: gh CLI is not authenticated.", file=sys.stderr)
14
- sys.exit(1)
15
-
16
- if not is_clean_tree():
17
- print("Error: working tree is not clean. Commit your changes first.", file=sys.stderr)
18
- sys.exit(1)
19
-
20
- # Find the latest tag
21
- try:
22
- tag = run("git", ["describe", "--tags", "--abbrev=0"])
23
- except Exception:
24
- print("Error: no tags found. Nothing to undo.", file=sys.stderr)
25
- sys.exit(1)
26
-
27
- print(f"This will undo release {tag}:")
28
- print(f" - Delete git tag {tag} (local + remote)")
29
- print(f" - Revert the version bump commit")
30
- print(f" - Delete the GitHub Release for {tag}")
31
-
32
- if not flags.get("yes"):
33
- try:
34
- answer = input("\nThis is destructive. Proceed? [y/N] ").strip().lower()
35
- except (EOFError, KeyboardInterrupt):
36
- print("\nAborted.")
37
- sys.exit(1)
38
- if answer != "y":
39
- print("Aborted.")
40
- sys.exit(0)
41
-
42
- # Delete GitHub Release
43
- try:
44
- run("gh", ["release", "delete", tag, "--yes"])
45
- print(f"Deleted GitHub Release: {tag}")
46
- except Exception as e:
47
- print(f"Warning: could not delete GitHub Release: {e}")
48
-
49
- # Delete remote tag
50
- try:
51
- run("git", ["push", "origin", f":{tag}"], timeout=get_push_timeout())
52
- print(f"Deleted remote tag: {tag}")
53
- except Exception as e:
54
- print(f"Warning: could not delete remote tag: {e}")
55
-
56
- # Delete local tag
57
- try:
58
- run("git", ["tag", "-d", tag])
59
- print(f"Deleted local tag: {tag}")
60
- except Exception as e:
61
- print(f"Warning: could not delete local tag: {e}")
62
-
63
- # Revert the version bump commit (should be HEAD)
64
- try:
65
- head_msg = run("git", ["log", "-1", "--format=%s"])
66
- if head_msg == tag:
67
- run("git", ["revert", "--no-edit", "HEAD"])
68
- print(f"Reverted commit: {head_msg}")
69
- else:
70
- print(f"Warning: HEAD commit ({head_msg}) doesn't match tag ({tag}). Skipping revert.")
71
- except Exception as e:
72
- print(f"Warning: could not revert commit: {e}")
73
-
74
- print(f"\nUndo complete. Run 'git push' to sync the revert.")
@@ -1,125 +0,0 @@
1
- """Watch command: monitor CI runs for a commit and report results."""
2
-
3
- import json
4
- import shutil
5
- import subprocess
6
- import sys
7
- import time
8
-
9
- from ..utils import run
10
-
11
-
12
- def _notify(title, body):
13
- """Send a desktop notification. Non-fatal if unavailable."""
14
- try:
15
- if sys.platform == "darwin":
16
- # Escape double quotes to prevent AppleScript injection
17
- escaped_title = title.replace('"', '\\"')
18
- escaped_body = body.replace('"', '\\"')
19
- subprocess.run(
20
- ["osascript", "-e",
21
- f'display notification "{escaped_body}" with title "{escaped_title}"'],
22
- timeout=5, capture_output=True,
23
- )
24
- elif shutil.which("notify-send"):
25
- subprocess.run(
26
- ["notify-send", "-u", "normal", title, body],
27
- timeout=5, capture_output=True,
28
- )
29
- except Exception:
30
- pass
31
-
32
-
33
- def run_cmd(registry, args, flags):
34
- """Watch all CI runs for a commit until they complete.
35
-
36
- Usage: rlsbl watch [<commit-sha>]
37
- Defaults to HEAD if no commit SHA is provided.
38
- """
39
- try:
40
- # Get commit SHA (resolve short SHAs -- gh requires full 40-char)
41
- if args:
42
- try:
43
- commit_sha = run("git", ["rev-parse", args[0]])
44
- except Exception:
45
- commit_sha = args[0]
46
- else:
47
- try:
48
- commit_sha = run("git", ["rev-parse", "HEAD"])
49
- except Exception:
50
- print("Error: not a git repository and no commit SHA provided.", file=sys.stderr)
51
- sys.exit(1)
52
-
53
- # Get repo info for display and URLs
54
- try:
55
- repo_info = run("gh", ["repo", "view", "--json", "nameWithOwner,name"])
56
- info = json.loads(repo_info)
57
- repo_slug = info.get("nameWithOwner", "")
58
- repo_name = info.get("name", "")
59
- except Exception:
60
- print("Error: could not get repo info. Is gh installed and authenticated?", file=sys.stderr)
61
- sys.exit(1)
62
-
63
- # Try to find a tag for this commit for nicer display
64
- try:
65
- tag = run("git", ["describe", "--tags", "--exact-match", commit_sha])
66
- except Exception:
67
- tag = commit_sha[:12]
68
-
69
- label = f"{repo_name} {tag}" if repo_name else tag
70
-
71
- # Poll until at least one run appears (retry up to 30s)
72
- runs = []
73
- for _ in range(15):
74
- try:
75
- raw = run("gh", ["run", "list", "--commit", commit_sha,
76
- "--json", "databaseId,name,status"])
77
- parsed = json.loads(raw)
78
- if parsed:
79
- runs = parsed
80
- break
81
- except Exception:
82
- pass
83
- time.sleep(2)
84
-
85
- if not runs:
86
- print(f"rlsbl: {label}: no CI runs found after 30s", file=sys.stderr)
87
- sys.exit(1)
88
-
89
- print(f"rlsbl: {label}: found {len(runs)} CI run(s), watching...", file=sys.stderr)
90
-
91
- # Watch each run sequentially, collecting results
92
- any_failed = False
93
- for ci_run in runs:
94
- run_id = str(ci_run["databaseId"])
95
- workflow_name = ci_run.get("name", f"run {run_id}")
96
-
97
- try:
98
- # gh run watch blocks until the run completes;
99
- # --exit-status makes it exit 1 on failure; check=True raises
100
- # CalledProcessError so we can distinguish pass from fail
101
- subprocess.run(
102
- ["gh", "run", "watch", run_id, "--exit-status"],
103
- capture_output=True, text=True, timeout=3600, check=True,
104
- )
105
- print(f"rlsbl: {label}: {workflow_name} passed", file=sys.stderr)
106
- except subprocess.CalledProcessError:
107
- any_failed = True
108
- print(f"rlsbl: {label}: {workflow_name} FAILED", file=sys.stderr)
109
- if repo_slug:
110
- print(f"rlsbl: https://github.com/{repo_slug}/actions/runs/{run_id}",
111
- file=sys.stderr)
112
- except subprocess.TimeoutExpired:
113
- any_failed = True
114
- print(f"rlsbl: {label}: {workflow_name} timed out after 1h", file=sys.stderr)
115
-
116
- # Desktop notification for overall result
117
- if any_failed:
118
- _notify(f"{label}: CI FAILED", "One or more workflows failed")
119
- else:
120
- _notify(f"{label}: CI passed", "All workflows passed")
121
-
122
- sys.exit(1 if any_failed else 0)
123
- except KeyboardInterrupt:
124
- print("\nWatch cancelled.", file=sys.stderr)
125
- sys.exit(130)
package/rlsbl/config.py DELETED
@@ -1,57 +0,0 @@
1
- """Config reading for the tag feature (ecosystem discoverability).
2
-
3
- Precedence (highest to lowest):
4
- 1. CLI flag (--no-tag)
5
- 2. Project-level: .rlsbl/config.json
6
- 3. User-level: ~/.rlsbl/config.json
7
- 4. Default: True (tagging enabled)
8
- """
9
-
10
- import json
11
- import os
12
-
13
-
14
- def _project_config():
15
- """Resolve project config path at call time (respects cwd changes)."""
16
- return os.path.join(".rlsbl", "config.json")
17
-
18
- USER_CONFIG = os.path.expanduser("~/.rlsbl/config.json")
19
-
20
-
21
- def read_json_config(path):
22
- """Safely read a JSON file, returning {} on missing or malformed."""
23
- try:
24
- with open(path, "r", encoding="utf-8") as f:
25
- return json.load(f)
26
- except (OSError, json.JSONDecodeError):
27
- return {}
28
-
29
-
30
- def should_tag(flags):
31
- """Returns True if tagging is enabled, checking flag > project > user > default."""
32
- # CLI flag takes highest precedence
33
- if flags.get("no-tag"):
34
- return False
35
-
36
- # Project-level config
37
- project = read_json_config(_project_config())
38
- if "tag" in project:
39
- return bool(project["tag"])
40
-
41
- # User-level config
42
- user = read_json_config(USER_CONFIG)
43
- if "tag" in user:
44
- return bool(user["tag"])
45
-
46
- # Default: tagging enabled
47
- return True
48
-
49
-
50
- def write_project_config(key, value):
51
- """Write or update a key in .rlsbl/config.json (creates dir if needed)."""
52
- os.makedirs(os.path.dirname(_project_config()), exist_ok=True)
53
- existing = read_json_config(_project_config())
54
- existing[key] = value
55
- with open(_project_config(), "w", encoding="utf-8") as f:
56
- json.dump(existing, f, indent=2)
57
- f.write("\n")
@@ -1,5 +0,0 @@
1
- """Registry lookup for rlsbl."""
2
-
3
- from . import go, npm, pypi
4
-
5
- REGISTRIES = {"npm": npm, "pypi": pypi, "go": go}