rlsbl 0.0.1 → 0.1.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.
@@ -0,0 +1,120 @@
1
+ """npm registry adapter for rlsbl."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+
7
+ NAME = "npm"
8
+
9
+
10
+ def read_version(dir_path):
11
+ """Read the version from package.json in the given directory."""
12
+ pkg_path = os.path.join(dir_path, "package.json")
13
+ with open(pkg_path, "r", encoding="utf-8") as f:
14
+ pkg = json.load(f)
15
+ if "version" not in pkg:
16
+ raise ValueError(f"No 'version' field in {pkg_path}")
17
+ return pkg["version"]
18
+
19
+
20
+ def write_version(dir_path, version):
21
+ """Write a new version to package.json, preserving formatting."""
22
+ pkg_path = os.path.join(dir_path, "package.json")
23
+ with open(pkg_path, "r", encoding="utf-8") as f:
24
+ raw = f.read()
25
+
26
+ # Detect indent: look for the first indented line
27
+ indent_match = re.search(r'^( +|\t+)"', raw, re.MULTILINE)
28
+ indent = indent_match.group(1) if indent_match else " "
29
+
30
+ pkg = json.loads(raw)
31
+ pkg["version"] = version
32
+
33
+ # Preserve trailing newline if present
34
+ trailing_newline = "\n" if raw.endswith("\n") else ""
35
+ output = json.dumps(pkg, indent=indent) + trailing_newline
36
+ # Atomic write: write to temp file, then rename
37
+ tmp_path = pkg_path + ".tmp"
38
+ with open(tmp_path, "w", encoding="utf-8") as f:
39
+ f.write(output)
40
+ os.replace(tmp_path, pkg_path)
41
+
42
+
43
+ def get_version_file():
44
+ """Returns the filename that holds the version for this registry."""
45
+ return "package.json"
46
+
47
+
48
+ def get_template_dir():
49
+ """Returns path to the npm-specific template directory."""
50
+ return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "npm")
51
+
52
+
53
+ def get_shared_template_dir():
54
+ """Returns path to the shared template directory."""
55
+ return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
56
+
57
+
58
+ def get_template_vars(dir_path):
59
+ """Extract template variables from the target project's package.json."""
60
+ pkg_path = os.path.join(dir_path, "package.json")
61
+ with open(pkg_path, "r", encoding="utf-8") as f:
62
+ pkg = json.load(f)
63
+
64
+ # Derive binCommand from the bin field (first key if object, or package name)
65
+ bin_command = pkg.get("name", "")
66
+ bin_field = pkg.get("bin")
67
+ if isinstance(bin_field, dict) and bin_field:
68
+ bin_command = next(iter(bin_field))
69
+ elif isinstance(bin_field, str):
70
+ bin_command = pkg.get("name", "")
71
+
72
+ # Derive repoName from repository field
73
+ repo_name = ""
74
+ repository = pkg.get("repository")
75
+ if repository:
76
+ url = repository if isinstance(repository, str) else (repository.get("url") or "")
77
+ match = re.search(r"github\.com[/:]([^/]+/[^/.]+)", url)
78
+ if match:
79
+ repo_name = match.group(1)
80
+
81
+ return {
82
+ "name": pkg.get("name", ""),
83
+ "version": pkg.get("version", "0.1.0"),
84
+ "binCommand": bin_command,
85
+ "author": pkg.get("author", ""),
86
+ "repoName": repo_name,
87
+ }
88
+
89
+
90
+ def get_template_mappings():
91
+ """Returns npm-specific template mappings (template file -> target path)."""
92
+ return [
93
+ {"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
94
+ {"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
95
+ ]
96
+
97
+
98
+ def get_shared_template_mappings():
99
+ """Returns shared template mappings."""
100
+ return [
101
+ {"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
102
+ {"template": "gitignore.tpl", "target": ".gitignore"},
103
+ {"template": "LICENSE.tpl", "target": "LICENSE"},
104
+ {"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
105
+ {"template": "check-prs.sh.tpl", "target": "scripts/check-prs.sh"},
106
+ {"template": "record-gif.sh.tpl", "target": "scripts/record-gif.sh"},
107
+ {"template": "pre-release.sh.tpl", "target": "scripts/pre-release.sh"},
108
+ {"template": "pre-push-hook.sh.tpl", "target": "scripts/pre-push-hook.sh"},
109
+ {"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
110
+ ]
111
+
112
+
113
+ def check_project_exists(dir_path):
114
+ """Returns True if a package.json exists in the given directory."""
115
+ return os.path.exists(os.path.join(dir_path, "package.json"))
116
+
117
+
118
+ def get_project_init_hint():
119
+ """Hint for users who haven't initialized their project yet."""
120
+ return 'Run "npm init" first'
@@ -0,0 +1,172 @@
1
+ """PyPI registry adapter for rlsbl."""
2
+
3
+ import os
4
+ import re
5
+ import tomllib
6
+
7
+ from ..utils import run
8
+
9
+ NAME = "pypi"
10
+
11
+
12
+ def read_version(dir_path):
13
+ """Read the version from pyproject.toml in the given directory."""
14
+ toml_path = os.path.join(dir_path, "pyproject.toml")
15
+ with open(toml_path, "rb") as f:
16
+ data = tomllib.load(f)
17
+ try:
18
+ return data["project"]["version"]
19
+ except KeyError:
20
+ raise ValueError(f"No [project].version in {toml_path}")
21
+
22
+
23
+ def write_version(dir_path, version):
24
+ """Write a new version to pyproject.toml using regex replacement.
25
+
26
+ tomllib is read-only (no stdlib TOML writer), so we use a regex to
27
+ replace the version string within the [project] section only,
28
+ preserving all other formatting.
29
+ """
30
+ toml_path = os.path.join(dir_path, "pyproject.toml")
31
+ with open(toml_path, "r", encoding="utf-8") as f:
32
+ content = f.read()
33
+
34
+ # Find [project] section boundaries to avoid matching version keys
35
+ # in other sections (e.g. [tool.something])
36
+ project_match = re.search(r"^\[project\]\s*$", content, re.MULTILINE)
37
+ if not project_match:
38
+ raise ValueError("No [project] section found in pyproject.toml")
39
+
40
+ section_start = project_match.end()
41
+ # Find next top-level section header or EOF
42
+ next_section = re.search(r"^\[", content[section_start:], re.MULTILINE)
43
+ section_end = section_start + next_section.start() if next_section else len(content)
44
+
45
+ # Replace version only within [project] section
46
+ section = content[section_start:section_end]
47
+ updated_section = re.sub(
48
+ r'^(version\s*=\s*)"[^"]+"',
49
+ rf'\g<1>"{version}"',
50
+ section,
51
+ count=1,
52
+ flags=re.MULTILINE,
53
+ )
54
+ updated = content[:section_start] + updated_section + content[section_end:]
55
+
56
+ # Atomic write: write to temp file, then rename
57
+ tmp_path = toml_path + ".tmp"
58
+ with open(tmp_path, "w", encoding="utf-8") as f:
59
+ f.write(updated)
60
+ os.replace(tmp_path, toml_path)
61
+
62
+
63
+ def get_version_file():
64
+ """Returns the filename that holds the version for this registry."""
65
+ return "pyproject.toml"
66
+
67
+
68
+ def get_template_dir():
69
+ """Returns path to the pypi-specific template directory."""
70
+ return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "pypi")
71
+
72
+
73
+ def get_shared_template_dir():
74
+ """Returns path to the shared template directory."""
75
+ return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
76
+
77
+
78
+ def get_template_vars(dir_path):
79
+ """Extract template variables from the target project's pyproject.toml."""
80
+ toml_path = os.path.join(dir_path, "pyproject.toml")
81
+ with open(toml_path, "rb") as f:
82
+ data = tomllib.load(f)
83
+
84
+ project = data.get("project", {})
85
+ name = project.get("name", "")
86
+ version = project.get("version", "0.1.0")
87
+
88
+ # Extract author -- fall back to git config
89
+ author = ""
90
+ try:
91
+ author = run("git", ["config", "user.name"])
92
+ except Exception:
93
+ pass
94
+
95
+ # Extract repo name from project.urls
96
+ repo_name = ""
97
+ urls = project.get("urls", {})
98
+ for url in urls.values():
99
+ match = re.search(r"github\.com/([^/\s\"]+/[^/\s\"]+)", url)
100
+ if match:
101
+ repo_name = match.group(1).removesuffix(".git")
102
+ break
103
+
104
+ # Derive binCommand from project.scripts (CLI entry points)
105
+ bin_command = ""
106
+ scripts = project.get("scripts", {})
107
+ if scripts:
108
+ bin_command = next(iter(scripts)) # first script entry
109
+
110
+ # Derive the actual Python import name.
111
+ # 1) Check hatch build config for an explicit packages list.
112
+ import_name = None
113
+ hatch = data.get("tool", {}).get("hatch", {})
114
+ packages = (
115
+ hatch.get("build", {}).get("targets", {}).get("wheel", {}).get("packages")
116
+ )
117
+ if packages and isinstance(packages, list) and len(packages) > 0:
118
+ # Strip src/ prefix (common hatch src layout) and convert path to module name
119
+ first_pkg = packages[0]
120
+ import_name = first_pkg.removeprefix("src/").replace("/", ".")
121
+
122
+ # 2) Fall back to filesystem detection, then underscore convention.
123
+ if not import_name:
124
+ underscored = name.replace("-", "_")
125
+ if os.path.isdir(os.path.join(dir_path, underscored)):
126
+ import_name = underscored
127
+ elif os.path.isdir(os.path.join(dir_path, name)):
128
+ import_name = name
129
+ else:
130
+ import_name = underscored # fallback to convention
131
+
132
+ return {
133
+ "name": name,
134
+ "version": version,
135
+ "binCommand": bin_command,
136
+ "author": author,
137
+ "repoName": repo_name,
138
+ "importName": import_name,
139
+ }
140
+
141
+
142
+ def get_template_mappings():
143
+ """Returns pypi-specific template mappings (template file -> target path)."""
144
+ return [
145
+ {"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
146
+ {"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
147
+ ]
148
+
149
+
150
+ def get_shared_template_mappings():
151
+ """Returns shared template mappings."""
152
+ return [
153
+ {"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
154
+ {"template": "gitignore.tpl", "target": ".gitignore"},
155
+ {"template": "LICENSE.tpl", "target": "LICENSE"},
156
+ {"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
157
+ {"template": "check-prs.sh.tpl", "target": "scripts/check-prs.sh"},
158
+ {"template": "record-gif.sh.tpl", "target": "scripts/record-gif.sh"},
159
+ {"template": "pre-release.sh.tpl", "target": "scripts/pre-release.sh"},
160
+ {"template": "pre-push-hook.sh.tpl", "target": "scripts/pre-push-hook.sh"},
161
+ {"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
162
+ ]
163
+
164
+
165
+ def check_project_exists(dir_path):
166
+ """Returns True if a pyproject.toml exists in the given directory."""
167
+ return os.path.exists(os.path.join(dir_path, "pyproject.toml"))
168
+
169
+
170
+ def get_project_init_hint():
171
+ """Hint for users who haven't initialized their project yet."""
172
+ return 'Run "uv init" first'
package/rlsbl/utils.py ADDED
@@ -0,0 +1,124 @@
1
+ """Git helpers, version bump, changelog extraction, and other shared utilities."""
2
+
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ def run(cmd, args=None, timeout=30):
11
+ """Run a command with args, return trimmed stdout. Raise on failure."""
12
+ full_cmd = [cmd] + (args or [])
13
+ result = subprocess.run(full_cmd, capture_output=True, text=True, check=True, timeout=timeout)
14
+ return result.stdout.strip()
15
+
16
+
17
+ def run_silent(cmd, args=None, timeout=30):
18
+ """Run a command suppressing stderr. Return trimmed stdout. Raise on failure."""
19
+ full_cmd = [cmd] + (args or [])
20
+ result = subprocess.run(
21
+ full_cmd, capture_output=True, text=True, check=True, timeout=timeout,
22
+ )
23
+ return result.stdout.strip()
24
+
25
+
26
+ def is_clean_tree():
27
+ """Returns True if the git working tree is clean (no uncommitted changes)."""
28
+ status = run("git", ["status", "--porcelain"])
29
+ return len(status) == 0
30
+
31
+
32
+ def get_current_branch():
33
+ """Returns the current git branch name."""
34
+ return run("git", ["rev-parse", "--abbrev-ref", "HEAD"])
35
+
36
+
37
+ def push_if_needed(branch):
38
+ """Push the branch to origin if local is ahead of remote."""
39
+ local = run("git", ["rev-parse", branch])
40
+ try:
41
+ remote = run("git", ["rev-parse", f"origin/{branch}"])
42
+ except subprocess.CalledProcessError:
43
+ # Remote branch doesn't exist yet; push it
44
+ run("git", ["push", "-u", "origin", branch])
45
+ return
46
+
47
+ if local != remote:
48
+ run("git", ["push", "origin", branch])
49
+
50
+
51
+ def extract_changelog_entry(changelog_path, version):
52
+ """Extract a changelog entry for a specific version.
53
+
54
+ Looks for a heading like '## 1.2.3' and captures everything
55
+ until the next heading or EOF.
56
+ """
57
+ with open(changelog_path, "r", encoding="utf-8") as f:
58
+ content = f.read()
59
+
60
+ escaped_version = re.escape(version)
61
+ header_pattern = re.compile(r"^## " + escaped_version + r"\s*$", re.MULTILINE)
62
+ match = header_pattern.search(content)
63
+ if not match:
64
+ return None
65
+
66
+ # Start after the matched header line
67
+ start_idx = match.end()
68
+ # Find the next "## " heading or use end of string
69
+ next_heading_idx = content.find("\n## ", start_idx)
70
+ end_idx = len(content) if next_heading_idx == -1 else next_heading_idx
71
+ entry = content[start_idx:end_idx].strip()
72
+ return entry or None
73
+
74
+
75
+ def check_gh_installed():
76
+ """Check that the gh CLI is installed."""
77
+ try:
78
+ run("gh", ["--version"])
79
+ return True
80
+ except (subprocess.CalledProcessError, FileNotFoundError):
81
+ return False
82
+
83
+
84
+ def check_gh_auth():
85
+ """Check that the gh CLI is authenticated."""
86
+ try:
87
+ run("gh", ["auth", "status"])
88
+ return True
89
+ except (subprocess.CalledProcessError, FileNotFoundError):
90
+ return False
91
+
92
+
93
+ def find_commit_tool():
94
+ """Detect safegit or fall back to git for committing.
95
+
96
+ Returns the path/name of the commit tool.
97
+ """
98
+ safegit_path = shutil.which("safegit")
99
+ if safegit_path:
100
+ return safegit_path
101
+ return "git"
102
+
103
+
104
+ def bump_version(version, bump_type):
105
+ """Bump a semver version string by the given type (patch, minor, major).
106
+
107
+ Returns the new version string.
108
+ """
109
+ parts = version.split(".")
110
+ if len(parts) != 3:
111
+ raise ValueError(f'Invalid semver version: "{version}"')
112
+ try:
113
+ major, minor, patch = (int(p) for p in parts)
114
+ except ValueError:
115
+ raise ValueError(f'Invalid semver version: "{version}"')
116
+
117
+ if bump_type == "major":
118
+ return f"{major + 1}.0.0"
119
+ elif bump_type == "minor":
120
+ return f"{major}.{minor + 1}.0"
121
+ elif bump_type == "patch":
122
+ return f"{major}.{minor}.{patch + 1}"
123
+ else:
124
+ raise ValueError(f'Invalid bump type: "{bump_type}". Use patch, minor, or major.')
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18, 20, 22]
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: actions/setup-node@v5
18
+ with:
19
+ node-version: ${{ matrix.node-version }}
20
+ - run: node -e "require('./package.json')"
21
+ - run: npm test --if-present
22
+ - run: npm audit --audit-level=moderate || true
@@ -0,0 +1,22 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-node@v5
17
+ with:
18
+ node-version: 22
19
+ registry-url: https://registry.npmjs.org
20
+ - run: npm publish --provenance --access public
21
+ env:
22
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: astral-sh/setup-uv@v8
18
+ - run: uv python install ${{ matrix.python-version }}
19
+ - run: uv sync
20
+ - run: uv run python -c "import {{importName}}"
@@ -0,0 +1,18 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: astral-sh/setup-uv@v8
17
+ - run: uv build
18
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## {{version}}
4
+
5
+ - Initial release
@@ -0,0 +1,20 @@
1
+ # {{name}}
2
+
3
+ ## Release workflow
4
+
5
+ This project uses [rlsbl](https://github.com/smm-h/rlsbl) for release orchestration.
6
+
7
+ - Update CHANGELOG.md with a `## X.Y.Z` entry describing changes
8
+ - Run `rlsbl <registry> release [patch|minor|major]` to bump version and create a GitHub Release
9
+ - CI handles publishing automatically via the publish workflow
10
+ - Never publish manually — always use `rlsbl <registry> release`
11
+ - Requires `NPM_TOKEN` secret on GitHub (for npm projects)
12
+ - Use `rlsbl <registry> release --dry-run` to preview a release without making changes
13
+
14
+ ## Conventions
15
+
16
+ - No tokens or secrets in command-line arguments (use env vars or config files)
17
+ - All file writes to shared state should be atomic (write to tmp, then rename)
18
+ - External calls (APIs, CLI tools) must have timeouts and graceful fallbacks
19
+ - Use `npm link` (npm) or `uv pip install -e .` (Python) for local development
20
+ - CI runs smoke tests on every push; manual testing for UI/UX changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{year}} {{author}}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # List open PRs for awareness at session start.
3
+ # Safe to run in hooks -- always exits 0.
4
+ cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
5
+ count=$(gh pr list --state open --json number --jq length 2>/dev/null) || exit 0
6
+ if [ "$count" -gt 0 ]; then
7
+ echo "Open PRs: $count"
8
+ gh pr list --state open
9
+ fi
10
+ exit 0
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "./scripts/check-prs.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ node_modules/
2
+ __pycache__/
3
+ *.pyc
4
+ *.log
5
+ .DS_Store
6
+ coverage/
7
+ dist/
8
+ *.egg-info/
9
+ .rlsbl-notes-*.tmp
10
+ .credentials.json
11
+ .*-cache.json
12
+ .env
13
+ .env.local
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-push hook: verify CHANGELOG.md has an entry for the current version.
3
+ # Install: cp scripts/pre-push-hook.sh .git/hooks/pre-push && chmod +x .git/hooks/pre-push
4
+
5
+ set -euo pipefail
6
+
7
+ # Detect project type and extract version
8
+ if [ -f package.json ]; then
9
+ VERSION=$(node -e "console.log(require('./package.json').version)" 2>/dev/null) || exit 0
10
+ elif [ -f pyproject.toml ]; then
11
+ VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)".*/\1/') || exit 0
12
+ else
13
+ exit 0
14
+ fi
15
+
16
+ if [ -z "$VERSION" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ # Check CHANGELOG.md has an entry for this version
21
+ if [ ! -f CHANGELOG.md ]; then
22
+ echo "Warning: CHANGELOG.md not found."
23
+ exit 0
24
+ fi
25
+
26
+ if ! grep -q "^## $VERSION" CHANGELOG.md; then
27
+ echo "Error: CHANGELOG.md has no entry for version $VERSION."
28
+ echo "Add a '## $VERSION' section before pushing."
29
+ exit 1
30
+ fi
31
+
32
+ # Check scaffolding freshness
33
+ if [ -f .rlsbl/version ]; then
34
+ SCAFFOLD_VER=$(cat .rlsbl/version | tr -d '[:space:]')
35
+ if command -v rlsbl &>/dev/null; then
36
+ CURRENT_VER=$(rlsbl --version 2>/dev/null | tr -d '[:space:]')
37
+ if [ -n "$CURRENT_VER" ] && [ "$SCAFFOLD_VER" != "$CURRENT_VER" ]; then
38
+ echo "Warning: scaffolding was generated by rlsbl $SCAFFOLD_VER but you have $CURRENT_VER installed."
39
+ echo "Run 'rlsbl scaffold --update' to update scaffolding."
40
+ fi
41
+ fi
42
+ fi
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-release validation hook.
3
+ # Runs before rlsbl creates a release. Exit non-zero to abort.
4
+ # Add your project-specific checks here (e.g., tests, linting, audit).
5
+
6
+ set -euo pipefail
7
+
8
+ echo "Running pre-release checks..."
9
+
10
+ # Example: run tests if available
11
+ # npm test 2>/dev/null || true
12
+
13
+ echo "Pre-release checks passed."