semver-ratchet 1.0.0 → 1.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.
package/README.md CHANGED
@@ -1,284 +1,256 @@
1
1
  # semver-ratchet (Node.js)
2
2
 
3
- Node.js implementation of the semver-ratchet versioning system.
3
+ Node.js implementation of forward-only versioning for Trunk-Based Development.
4
4
 
5
- ## Prerequisites
5
+ ## Dynamic Versioning
6
6
 
7
- ### Node.js Version Management (nvm)
7
+ semver-ratchet calculates versions dynamically from your git state.
8
8
 
9
- This project uses **nvm** (Node Version Manager) for managing Node.js versions.
9
+ ### Feature Branches: `0.{CRC32(branch_name)}.{GitDistance}`
10
10
 
11
- **Install nvm** (if not already installed):
11
+ Feature branches get ephemeral versions. The minor component is a CRC32 hash of the branch name, and the patch is the commit distance from the trunk branch.
12
12
 
13
- ```bash
14
- # macOS / Linux
15
- curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
16
-
17
- # Then restart your terminal or run:
18
- source ~/.bashrc # or ~/.zshrc for zsh
19
- ```
13
+ ### Main Branch: `Major.Minor.Patch`
20
14
 
21
- **Install and use Node.js**:
15
+ The main branch follows [Semantic Versioning 2.0.0](../docs/SEMVER.md). Bumps are determined by `[MAJOR]`, `[MINOR]`, or `[PATCH]` tags in commit messages.
22
16
 
23
- ```bash
24
- # Install latest LTS version
25
- nvm install --lts
17
+ ### Dynamic Version in package.json
26
18
 
27
- # Set as default
28
- nvm use --lts
19
+ Write version at build time:
29
20
 
30
- # Verify installation
31
- node --version
32
- npm --version
21
+ ```json
22
+ {
23
+ "name": "your-project",
24
+ "version": "0.0.0-dynamic",
25
+ "scripts": {
26
+ "prebuild": "semver-ratchet version > .version",
27
+ "build": "node build.js"
28
+ }
29
+ }
33
30
  ```
34
31
 
35
- **Project-specific Node.js version**:
32
+ Or use it as a module:
36
33
 
37
- ```bash
38
- # Create .nvmrc file with desired Node.js version
39
- echo "lts/*" > .nvmrc
34
+ ```javascript
35
+ import { getVersion } from 'semver-ratchet';
40
36
 
41
- # Automatically use the correct version when entering project directory
42
- nvm use
37
+ const version = getVersion();
38
+ console.log(`Building version: ${version}`);
43
39
  ```
44
40
 
45
- ## Installation
41
+ ---
46
42
 
47
- ### Development Setup
43
+ ## Installation
48
44
 
49
45
  ```bash
50
- cd node
46
+ npm install semver-ratchet
47
+ ```
51
48
 
52
- # Install dependencies
53
- npm install
49
+ ---
54
50
 
55
- # Run tests
56
- npm test
57
- ```
51
+ ## Usage
58
52
 
59
- ### As a Package
53
+ ### CLI
60
54
 
61
55
  ```bash
62
- npm install semver-ratchet
63
- ```
56
+ # Get current version
57
+ npx semver-ratchet version
64
58
 
65
- ## Usage
59
+ # With custom trunk name
60
+ npx semver-ratchet --trunk-name develop version
66
61
 
67
- ### As a Module
62
+ # Skip git history verification
63
+ npx semver-ratchet --no-verify version
64
+
65
+ # Display versioning info
66
+ npx semver-ratchet info
67
+
68
+ # Create and push tag
69
+ npx semver-ratchet tag --push
70
+ ```
71
+
72
+ ### ES Modules
68
73
 
69
74
  ```javascript
70
- import semverRatchet from 'semver-ratchet';
75
+ import {
76
+ getVersion,
77
+ getCompatibleVersions,
78
+ calculateCrc32Unsigned,
79
+ createGitTag,
80
+ pushGitTag,
81
+ isMainBranch,
82
+ getCurrentBranch,
83
+ verifyGitHistory,
84
+ } from 'semver-ratchet';
71
85
 
72
86
  // Get current version
73
- const version = semverRatchet.getVersion();
87
+ const version = getVersion();
74
88
  console.log(version); // e.g., "1.0.0" or "0.12345678.5"
75
89
 
76
- // Get compatible versions for dependency pinning
77
- const compatible = semverRatchet.getCompatibleVersions(version);
78
- console.log(compatible); // e.g., ["1.0.0", "1.0", "1"]
79
-
80
- // Get current branch
81
- const branch = semverRatchet.getBranch();
82
- console.log(branch);
90
+ // With custom trunk name
91
+ const version = getVersion("patch", true, "develop");
83
92
 
84
- // Check if on main branch
85
- const isMain = semverRatchet.isMainBranch();
86
- console.log(isMain);
93
+ // Get compatible versions for dependency pinning
94
+ const compatible = getCompatibleVersions("1.2.3");
95
+ console.log(compatible); // ["1.2.3", "1.2", "1"]
87
96
 
88
- // Calculate CRC32 of a string
89
- const crc = semverRatchet.calculateCrc32Unsigned('feature/my-branch');
90
- console.log(crc);
97
+ // Calculate CRC32 of a branch name
98
+ const crc = calculateCrc32Unsigned('feature/my-branch');
91
99
 
92
100
  // Create a git tag (main branch only)
93
- semverRatchet.createGitTag(version);
101
+ createGitTag(version);
94
102
 
95
103
  // Push tag to remote
96
- semverRatchet.pushGitTag(version);
104
+ pushGitTag(version);
97
105
  ```
98
106
 
99
107
  ### CommonJS
100
108
 
101
109
  ```javascript
102
- const semverRatchet = require('semver-ratchet');
103
-
104
- const version = semverRatchet.getVersion();
105
- console.log(version);
110
+ const { getVersion } = require('semver-ratchet');
111
+ const version = getVersion();
106
112
  ```
107
113
 
108
- ### CLI
114
+ ---
109
115
 
110
- ```bash
111
- # Get current version
112
- npx semver-ratchet version
116
+ ## Configuration
113
117
 
114
- # Get current branch
115
- npx semver-ratchet branch
118
+ ### Config Precedence Chain
116
119
 
117
- # Calculate CRC32 hash
118
- npx semver-ratchet crc32 feature/my-branch
120
+ ```
121
+ CLI flag > Environment variable > semver-ratchet.yaml > Default
122
+ ```
119
123
 
120
- # Validate commit message
121
- npx semver-ratchet validate "feat: add login [MINOR]"
124
+ ### CLI Flags
122
125
 
123
- # Check version monotonicity
124
- npx semver-ratchet check-monotonic
126
+ | Flag | Description | Default |
127
+ | :--- | :--- | :--- |
128
+ | `--trunk-name <name>` | Trunk branch name | `main` |
129
+ | `--default-bump {major,minor,patch}` | Default bump type | `patch` |
130
+ | `--no-verify` | Skip git history verification | `false` |
125
131
 
126
- # Display versioning information
127
- npx semver-ratchet info
132
+ ### Environment Variables
128
133
 
129
- # Generate compatible versions in CSV format
130
- npx semver-ratchet versions
134
+ | Variable | Description | Default |
135
+ | :--- | :--- | :--- |
136
+ | `RATCHET_TRUNK_NAME` | Trunk branch name | `main` |
137
+ | `RATCHET_DEFAULT_BUMP` | Default bump type | `minor` |
138
+ | `RATCHET_GIT_PATH` | Custom git binary path | `git` |
139
+ | `RATCHET_VERIFY_MINDEPTH` | Minimum git history depth | `50` |
140
+ | `RATCHET_MANUAL_VERSION` | Manual version override | *(none)* |
141
+ | `RATCHET_OVERRIDE` | *(deprecated)* Same as above | *(none)* |
131
142
 
132
- # Verify git history
133
- npx semver-ratchet verify
143
+ ### Config File (`semver-ratchet.yaml`)
134
144
 
135
- # Create and push git tag
136
- npx semver-ratchet tag --push
145
+ Place in your project root:
137
146
 
138
- # With custom options
139
- npx semver-ratchet version --default-bump minor
140
- npx semver-ratchet tag --force --push --remote upstream
147
+ ```yaml
148
+ trunkName: main
149
+ defaultBump: minor
150
+ verifyMindepth: 50
151
+ gitPath: /usr/local/bin/git
141
152
  ```
142
153
 
154
+ ---
155
+
143
156
  ## API Reference
144
157
 
145
- ### Functions
158
+ ### `getVersion(defaultBump?, verifyHistory?, trunkName?)`
146
159
 
147
- #### `getVersion(defaultBump?, verifyHistory?)`
148
160
  Get the appropriate version based on current branch.
149
161
 
150
162
  - **Parameters:**
151
- - `defaultBump` (string, optional): Default bump type - "major", "minor", or "patch" (default: "patch")
152
- - `verifyHistory` (boolean, optional): Verify sufficient git history exists (default: true)
153
- - **Returns:** string - Version string
163
+ - `defaultBump` (string, optional): Default bump type
164
+ - `verifyHistory` (boolean, optional): Verify git history (default: `true`)
165
+ - `trunkName` (string, optional): Trunk branch name override
166
+ - **Returns:** `string`
167
+
168
+ ### `calculateFeatureVersion(verifyHistory?, trunkName?)`
154
169
 
155
- #### `calculateFeatureVersion(verifyHistory?)`
156
170
  Calculate version for a feature branch.
157
171
 
158
- - **Returns:** string - Version in format "0.{CRC32}.{Distance}"
172
+ - **Returns:** `string` `0.{CRC32}.{Distance}`
173
+
174
+ ### `calculateMainVersion(defaultBump?, verifyHistory?, trunkName?)`
159
175
 
160
- #### `calculateMainVersion(defaultBump?, verifyHistory?)`
161
176
  Calculate version for main branch using SemVer.
162
177
 
163
- - **Returns:** string - Version in format "Major.Minor.Patch"
178
+ - **Returns:** `string` `Major.Minor.Patch`
179
+
180
+ ### `getCompatibleVersions(version)`
164
181
 
165
- #### `getCompatibleVersions(version)`
166
182
  Generate compatible version strings for dependency pinning.
167
183
 
168
- - **Parameters:**
169
- - `version` (string): Full version string
170
- - **Returns:** string[] - Array of compatible versions
184
+ - **Returns:** `string[]`
185
+
186
+ ### `getCurrentBranch()`
171
187
 
172
- #### `getCurrentBranch()`
173
188
  Get the current Git branch name.
174
189
 
175
- - **Returns:** string
190
+ - **Returns:** `string`
191
+
192
+ ### `isMainBranch(trunkName?)`
193
+
194
+ Check if current branch is the trunk branch.
176
195
 
177
- #### `isMainBranch()`
178
- Check if current branch is main or master.
196
+ - **Returns:** `boolean`
179
197
 
180
- - **Returns:** boolean
198
+ ### `calculateCrc32Unsigned(str)`
181
199
 
182
- #### `calculateCrc32Unsigned(str)`
183
200
  Calculate CRC32 checksum as unsigned 32-bit integer.
184
201
 
185
- - **Parameters:**
186
- - `str` (string): String to hash
187
- - **Returns:** number
202
+ - **Returns:** `number`
203
+
204
+ ### `parseSemver(version)`
188
205
 
189
- #### `parseSemver(version)`
190
206
  Parse a SemVer string into components.
191
207
 
192
- - **Parameters:**
193
- - `version` (string): Version string
194
- - **Returns:** object - { major, minor, patch }
208
+ - **Returns:** `{ major: number, minor: number, patch: number }`
195
209
 
196
- #### `compareSemver(v1, v2)`
197
- Compare two SemVer versions.
210
+ ### `compareSemver(v1, v2)`
198
211
 
199
- - **Parameters:**
200
- - `v1` (string): First version
201
- - `v2` (string): Second version
202
- - **Returns:** number - -1 if v1 < v2, 0 if equal, 1 if v1 > v2
212
+ Compare two SemVer versions.
203
213
 
204
- #### `determineVersionBump(commitMessages, defaultBump?)`
205
- Determine version bump based on commit messages.
214
+ - **Returns:** `number` — `-1`, `0`, or `1`
206
215
 
207
- - **Parameters:**
208
- - `commitMessages` (string[]): List of commit messages
209
- - `defaultBump` (string, optional): Default bump type
210
- - **Returns:** string - "major", "minor", or "patch"
216
+ ### `determineVersionBump(commitMessages, defaultBump?)`
211
217
 
212
- #### `createGitTag(version, force?)`
213
- Create a Git tag for the given version.
218
+ Determine version bump from commit messages.
214
219
 
215
- - **Parameters:**
216
- - `version` (string): Version string
217
- - `force` (boolean, optional): Overwrite existing tag
218
- - **Returns:** boolean
220
+ - **Returns:** `string` — `"major"`, `"minor"`, or `"patch"`
219
221
 
220
- #### `pushGitTag(version, remote?)`
221
- Push a Git tag to remote repository.
222
+ ### `createGitTag(version, force?)`
222
223
 
223
- - **Parameters:**
224
- - `version` (string): Version string
225
- - `remote` (string, optional): Remote name (default: "origin")
226
- - **Returns:** boolean
224
+ Create a Git tag for the given version.
227
225
 
228
- #### `verifyGitHistory(fetchDepth?)`
229
- Verify sufficient git history is available.
226
+ - **Returns:** `boolean`
230
227
 
231
- - **Parameters:**
232
- - `fetchDepth` (number, optional): Minimum commits required (default: 50)
233
- - **Returns:** boolean
228
+ ### `pushGitTag(version, remote?)`
234
229
 
235
- ## Package.json Configuration
230
+ Push a Git tag to remote repository.
236
231
 
237
- Add to your `package.json`:
232
+ - **Returns:** `boolean`
238
233
 
239
- ```json
240
- {
241
- "dependencies": {
242
- "semver-ratchet": "^0.2.0"
243
- },
244
- "scripts": {
245
- "version": "semver-ratchet version",
246
- "prepublish": "semver-ratchet tag --push"
247
- }
248
- }
249
- ```
234
+ ### `verifyGitHistory(fetchDepth?)`
250
235
 
251
- ## Environment Variables
236
+ Verify sufficient git history is available.
252
237
 
253
- - `RATCHET_OVERRIDE`: Override version completely (bypasses git). Useful for dev environments.
238
+ - **Returns:** `boolean`
254
239
 
255
- ```bash
256
- RATCHET_OVERRIDE=dev-local npx semver-ratchet version
257
- # Output: dev-local
258
- ```
240
+ ---
259
241
 
260
242
  ## Testing
261
243
 
262
- ### Run All Tests
263
-
264
244
  ```bash
245
+ # Run all tests
265
246
  npm test
266
- ```
267
-
268
- ### Run Tests in Watch Mode
269
247
 
270
- ```bash
271
- npm run test:watch
272
- ```
273
-
274
- ### Run Specific Test File
275
-
276
- ```bash
248
+ # Run specific test file
277
249
  node --test test/version.test.js
278
250
  ```
279
251
 
280
- **Note**: All tests use MockGitAdapter to avoid system git operations during testing, ensuring deterministic and isolated test results.
252
+ ---
281
253
 
282
254
  ## License
283
255
 
284
- MIT
256
+ MIT License. See [LICENSE](../LICENSE) for details.
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * semver-ratchet: Forward-only versioning for Trunk-Based Development.
3
- *
3
+ *
4
4
  * This module provides monotonic versioning based on Git history.
5
5
  * Feature branches use ephemeral versions (0.{CRC32}.{Distance}).
6
6
  * Main branch uses standard Semantic Versioning (Major.Minor.Patch).
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "semver-ratchet",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Forward-only versioning for Trunk-Based Development",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "semver-ratchet": "./bin/semver-ratchet"
8
+ "semver-ratchet": "bin/semver-ratchet"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/*.js",
@@ -29,7 +29,7 @@
29
29
  "license": "MIT",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/a-collet/semver-ratchet.git"
32
+ "url": "git+https://github.com/a-collet/semver-ratchet.git"
33
33
  },
34
34
  "bugs": {
35
35
  "url": "https://github.com/a-collet/semver-ratchet/issues"
package/src/cli.js CHANGED
@@ -24,6 +24,7 @@ function cmdVersion(_args) {
24
24
  const version = getVersion(
25
25
  _args.defaultBump || 'patch',
26
26
  !_args.noVerify,
27
+ _args.trunkName,
27
28
  );
28
29
  console.log(version);
29
30
  process.exit(0);
@@ -103,10 +104,11 @@ function cmdCheckMonotonic(_args) {
103
104
  const currentVersion = getVersion(
104
105
  _args.defaultBump || 'patch',
105
106
  !_args.noVerify,
107
+ _args.trunkName,
106
108
  );
107
109
 
108
110
  // For main branch, compare with latest tag
109
- if (isMainBranch()) {
111
+ if (isMainBranch(_args.trunkName)) {
110
112
  try {
111
113
  const adapter = getGitAdapter();
112
114
  const latestTag = adapter.getLatestTag();
@@ -151,21 +153,23 @@ function cmdInfo(_args) {
151
153
  const version = getVersion(
152
154
  _args.defaultBump || 'patch',
153
155
  !_args.noVerify,
156
+ _args.trunkName,
154
157
  );
155
- const branchType = isMainBranch() ? 'main' : 'feature';
158
+ const branchType = isMainBranch(_args.trunkName) ? 'main' : 'feature';
156
159
 
157
160
  console.log(`Branch: ${branch}`);
158
161
  console.log(`Type: ${branchType}`);
159
162
  console.log(`Version: ${version}`);
163
+ console.log(`Trunk name: ${_args.trunkName || 'main'}`);
160
164
  console.log(`Default bump: ${_args.defaultBump || 'patch'}`);
161
165
 
162
- if (!isMainBranch()) {
166
+ if (!isMainBranch(_args.trunkName)) {
163
167
  const crc32 = calculateCrc32Unsigned(branch);
164
168
  console.log(`Branch CRC32: ${crc32}`);
165
169
  }
166
170
 
167
171
  // Show compatible versions for main branch
168
- if (isMainBranch()) {
172
+ if (isMainBranch(_args.trunkName)) {
169
173
  try {
170
174
  parseSemver(version);
171
175
  const compatible = getCompatibleVersions(version);
@@ -187,6 +191,7 @@ function cmdVersions(_args) {
187
191
  const version = getVersion(
188
192
  _args.defaultBump || 'patch',
189
193
  !_args.noVerify,
194
+ _args.trunkName,
190
195
  );
191
196
 
192
197
  // Output as CSV
@@ -228,7 +233,7 @@ function cmdVerify(_args) {
228
233
  * @param {Object} _args - Command line arguments
229
234
  */
230
235
  function cmdTag(_args) {
231
- if (!isMainBranch()) {
236
+ if (!isMainBranch(_args.trunkName)) {
232
237
  console.log('ERROR: Git tags can only be created on main branch');
233
238
  process.exit(1);
234
239
  }
@@ -259,6 +264,7 @@ function parseArgs(argv) {
259
264
  command: null,
260
265
  defaultBump: 'patch',
261
266
  noVerify: false,
267
+ trunkName: undefined,
262
268
  };
263
269
 
264
270
  const commands = ['version', 'branch', 'crc32', 'validate', 'check-monotonic', 'info', 'versions', 'verify', 'tag'];
@@ -281,6 +287,12 @@ function parseArgs(argv) {
281
287
  continue;
282
288
  }
283
289
 
290
+ if (arg === '--trunk-name') {
291
+ args.trunkName = argv[++i];
292
+ i++;
293
+ continue;
294
+ }
295
+
284
296
  if (arg === '--no-verify') {
285
297
  args.noVerify = true;
286
298
  i++;
@@ -352,6 +364,7 @@ Commands:
352
364
 
353
365
  Global Options:
354
366
  --default-bump <type> Default version bump (major|minor|patch, default: patch)
367
+ --trunk-name <name> Trunk branch name (default: main)
355
368
  --no-verify Skip git history verification
356
369
 
357
370
  Tag Command Options:
@@ -365,6 +378,7 @@ Verify Command Options:
365
378
  Examples:
366
379
  semver-ratchet version
367
380
  semver-ratchet version --default-bump minor
381
+ semver-ratchet version --trunk-name develop
368
382
  semver-ratchet validate "feat: add login [MINOR]"
369
383
  semver-ratchet tag --push
370
384
  semver-ratchet crc32 feature/my-branch
@@ -428,4 +442,4 @@ export function main() {
428
442
  // Run if called directly
429
443
  if (process.argv[1] && process.argv[1].endsWith('cli.js')) {
430
444
  main();
431
- }
445
+ }
package/src/config.js CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Configuration module for semver-ratchet.
3
3
  *
4
- * Handles all environment variable configuration:
4
+ * Handles all configuration with precedence:
5
+ * CLI override > env var > config file > default
6
+ *
7
+ * Environment variables:
5
8
  * - RATCHET_GIT_PATH: Custom path to git binary
6
9
  * - RATCHET_TRUNK_NAME: Custom name for main/trunk branch
7
10
  * - RATCHET_DEFAULT_BUMP: Default version bump type
@@ -9,35 +12,64 @@
9
12
  * - RATCHET_MANUAL_VERSION: Manual version override (replaces RATCHET_OVERRIDE)
10
13
  */
11
14
 
15
+ import { loadConfigFile } from './config_file.js';
16
+
17
+ // Cache the loaded config file
18
+ let cachedConfig = null;
19
+
20
+ function getConfig() {
21
+ if (cachedConfig === null) {
22
+ cachedConfig = loadConfigFile();
23
+ }
24
+ return cachedConfig;
25
+ }
26
+
12
27
  /**
13
28
  * Get the path to the git binary.
29
+ * @param {string} [cliOverride] - CLI override value
14
30
  * @returns {string} Path to git binary (default: 'git')
15
31
  */
16
- export function getGitPath() {
17
- return process.env.RATCHET_GIT_PATH || 'git';
32
+ export function getGitPath(cliOverride) {
33
+ if (cliOverride) return cliOverride;
34
+ if (process.env.RATCHET_GIT_PATH) return process.env.RATCHET_GIT_PATH;
35
+ const config = getConfig();
36
+ if (config && config.gitPath) return config.gitPath;
37
+ return 'git';
18
38
  }
19
39
 
20
40
  /**
21
41
  * Get the name of the trunk/main branch.
42
+ * @param {string} [cliOverride] - CLI override value
22
43
  * @returns {string} Name of the trunk branch (default: 'main')
23
44
  */
24
- export function getTrunkName() {
25
- return process.env.RATCHET_TRUNK_NAME || 'main';
45
+ export function getTrunkName(cliOverride) {
46
+ if (cliOverride) return cliOverride;
47
+ if (process.env.RATCHET_TRUNK_NAME) return process.env.RATCHET_TRUNK_NAME;
48
+ const config = getConfig();
49
+ if (config && config.trunkName) return config.trunkName;
50
+ return 'main';
26
51
  }
27
52
 
28
53
  /**
29
54
  * Get the default version bump type.
55
+ * @param {string} [cliOverride] - CLI override value
30
56
  * @returns {string} Default bump type: 'major', 'minor', or 'patch' (default: 'minor')
31
57
  */
32
- export function getDefaultBump() {
33
- return process.env.RATCHET_DEFAULT_BUMP || 'minor';
58
+ export function getDefaultBump(cliOverride) {
59
+ if (cliOverride) return cliOverride;
60
+ if (process.env.RATCHET_DEFAULT_BUMP) return process.env.RATCHET_DEFAULT_BUMP;
61
+ const config = getConfig();
62
+ if (config && config.defaultBump) return config.defaultBump;
63
+ return 'minor';
34
64
  }
35
65
 
36
66
  /**
37
67
  * Get the minimum git history depth for verification.
68
+ * @param {number} [cliOverride] - CLI override value (-1 for no override)
38
69
  * @returns {number} Minimum number of commits required (default: 50)
39
70
  */
40
- export function getVerifyMinDepth() {
71
+ export function getVerifyMinDepth(cliOverride) {
72
+ if (cliOverride !== undefined && cliOverride >= 0) return cliOverride;
41
73
  const depthStr = process.env.RATCHET_VERIFY_MINDEPTH;
42
74
  if (depthStr !== undefined && depthStr !== null && depthStr !== '') {
43
75
  const depth = parseInt(depthStr, 10);
@@ -45,6 +77,8 @@ export function getVerifyMinDepth() {
45
77
  return depth;
46
78
  }
47
79
  }
80
+ const config = getConfig();
81
+ if (config && config.verifyMindepth > 0) return config.verifyMindepth;
48
82
  return 50;
49
83
  }
50
84
 
@@ -62,13 +96,13 @@ export function getManualVersion() {
62
96
  process.env.RATCHET_MANUAL_VERSION !== '') {
63
97
  return process.env.RATCHET_MANUAL_VERSION;
64
98
  }
65
-
99
+
66
100
  // Fallback to deprecated variable for backward compatibility
67
101
  if (process.env.RATCHET_OVERRIDE !== undefined &&
68
102
  process.env.RATCHET_OVERRIDE !== null &&
69
103
  process.env.RATCHET_OVERRIDE !== '') {
70
104
  return process.env.RATCHET_OVERRIDE;
71
105
  }
72
-
106
+
73
107
  return undefined;
74
- }
108
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Configuration file loader for semver-ratchet.
3
+ *
4
+ * Reads semver-ratchet.yaml from the project root and provides
5
+ * fallback configuration values.
6
+ */
7
+
8
+ import { readFileSync, statSync } from 'node:fs';
9
+ import { join, dirname } from 'node:path';
10
+
11
+ /**
12
+ * Find semver-ratchet.yaml by walking up from the current directory.
13
+ * @returns {string|null} Path to config file or null
14
+ */
15
+ function findConfigFile() {
16
+ let dir = process.cwd();
17
+ while (true) {
18
+ const candidate = join(dir, 'semver-ratchet.yaml');
19
+ try {
20
+ if (statSync(candidate).isFile()) {
21
+ return candidate;
22
+ }
23
+ } catch {
24
+ // File doesn't exist, continue
25
+ }
26
+
27
+ const parent = dirname(dir);
28
+ if (parent === dir) {
29
+ return null; // Reached root
30
+ }
31
+ dir = parent;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Parse a simple YAML file (key: value format).
37
+ * @param {string} content - YAML content
38
+ * @returns {Object} Parsed config object
39
+ */
40
+ function parseYAML(content) {
41
+ const config = {};
42
+ const lines = content.split('\n');
43
+
44
+ for (const line of lines) {
45
+ const trimmed = line.trim();
46
+ if (!trimmed || trimmed.startsWith('#')) {
47
+ continue;
48
+ }
49
+
50
+ const idx = trimmed.indexOf(':');
51
+ if (idx >= 0) {
52
+ const key = trimmed.substring(0, idx).trim();
53
+ let value = trimmed.substring(idx + 1).trim();
54
+
55
+ // Strip quotes
56
+ if ((value.startsWith('"') && value.endsWith('"')) ||
57
+ (value.startsWith("'") && value.endsWith("'"))) {
58
+ value = value.slice(1, -1);
59
+ }
60
+
61
+ config[key] = value;
62
+ }
63
+ }
64
+
65
+ return config;
66
+ }
67
+
68
+ /**
69
+ * Load configuration from semver-ratchet.yaml.
70
+ * @returns {Object|null} Config object or null if file not found
71
+ */
72
+ export function loadConfigFile() {
73
+ const path = findConfigFile();
74
+ if (!path) {
75
+ return null;
76
+ }
77
+
78
+ try {
79
+ const content = readFileSync(path, 'utf-8');
80
+ const config = parseYAML(content);
81
+
82
+ // Return null if no semver-ratchet keys found
83
+ if (!config.trunkName && !config.defaultBump &&
84
+ !config.verifyMindepth && !config.gitPath) {
85
+ return null;
86
+ }
87
+
88
+ // Normalize verifyMindepth to number
89
+ if (config.verifyMindepth) {
90
+ const depth = parseInt(config.verifyMindepth, 10);
91
+ config.verifyMindepth = isNaN(depth) ? 50 : depth;
92
+ }
93
+
94
+ return config;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
@@ -34,19 +34,21 @@ export class GitAdapter {
34
34
 
35
35
  /**
36
36
  * Check if current branch is main or master.
37
+ * @param {string} [_trunkName] - Trunk branch name to check against
37
38
  * @returns {boolean} True if on main/master branch
38
39
  * @throws {Error} If git operation fails
39
40
  */
40
- isMainBranch() {
41
+ isMainBranch(_trunkName) {
41
42
  throw new Error('isMainBranch() must be implemented by subclass');
42
43
  }
43
44
 
44
45
  /**
45
46
  * Get the number of commits between HEAD and main branch.
47
+ * @param {string} [_trunkName] - Trunk branch name
46
48
  * @returns {number} Number of commits
47
49
  * @throws {Error} If git operation fails
48
50
  */
49
- getGitDistance() {
51
+ getGitDistance(_trunkName) {
50
52
  throw new Error('getGitDistance() must be implemented by subclass');
51
53
  }
52
54
 
@@ -109,4 +111,4 @@ export class GitAdapter {
109
111
  pushTag(_version, _remote = 'origin') {
110
112
  throw new Error('pushTag() must be implemented by subclass');
111
113
  }
112
- }
114
+ }
package/src/git_mock.js CHANGED
@@ -205,18 +205,25 @@ export class MockGitAdapter extends GitAdapter {
205
205
 
206
206
  /**
207
207
  * Check if current branch is main or master.
208
- * @returns {boolean} True if on main/master branch
208
+ * @param {string} [trunkName] - Trunk branch name to check against
209
+ * @returns {boolean} True if on trunk branch
209
210
  */
210
- isMainBranch() {
211
- return this._currentBranch.toLowerCase() === 'main' || this._currentBranch.toLowerCase() === 'master';
211
+ isMainBranch(trunkName) {
212
+ if (trunkName) {
213
+ return this._currentBranch.toLowerCase() === trunkName.toLowerCase();
214
+ }
215
+ // Backward compatibility: check both main and master
216
+ return this._currentBranch.toLowerCase() === 'main' ||
217
+ this._currentBranch.toLowerCase() === 'master';
212
218
  }
213
219
 
214
220
  /**
215
- * Calculate the number of commits between HEAD and main branch.
221
+ * Calculate the number of commits between HEAD and trunk branch.
222
+ * @param {string} [trunkName] - Trunk branch name
216
223
  * @returns {number} Number of commits
217
224
  */
218
- getGitDistance() {
219
- if (this.isMainBranch()) {
225
+ getGitDistance(trunkName) {
226
+ if (this.isMainBranch(trunkName)) {
220
227
  return 0;
221
228
  }
222
229
 
@@ -345,4 +352,4 @@ export class MockGitAdapter extends GitAdapter {
345
352
  getBranches() {
346
353
  return Object.keys(this._branches);
347
354
  }
348
- }
355
+ }
package/src/git_real.js CHANGED
@@ -39,28 +39,35 @@ export class RealGitAdapter extends GitAdapter {
39
39
 
40
40
  /**
41
41
  * Check if current branch is main or master.
42
- * @returns {boolean} True if on main/master branch
42
+ * @param {string} [trunkName] - Trunk branch name to check against
43
+ * @returns {boolean} True if on trunk branch
43
44
  * @throws {Error} If git operation fails
44
45
  */
45
- isMainBranch() {
46
+ isMainBranch(trunkName) {
46
47
  const branch = this.getCurrentBranch();
48
+ if (trunkName) {
49
+ return branch.toLowerCase() === trunkName.toLowerCase();
50
+ }
51
+ // Backward compatibility: check both main and master
47
52
  return branch.toLowerCase() === 'main' || branch.toLowerCase() === 'master';
48
53
  }
49
54
 
50
55
  /**
51
- * Calculate the number of commits between HEAD and main branch.
56
+ * Calculate the number of commits between HEAD and trunk branch.
57
+ * @param {string} [trunkName] - Trunk branch name
52
58
  * @returns {number} Number of commits
53
59
  * @throws {Error} If git operation fails
54
60
  */
55
- getGitDistance() {
56
- if (this.isMainBranch()) {
61
+ getGitDistance(trunkName = 'main') {
62
+ const trunk = trunkName || 'main';
63
+ if (this.isMainBranch(trunk)) {
57
64
  return 0;
58
65
  }
59
66
 
60
67
  try {
61
- // Count commits on current branch not on main
68
+ // Count commits on current branch not on trunk
62
69
  this.getCurrentBranch();
63
- const output = execGit('rev-list --count HEAD ^origin/main 2>/dev/null || rev-list --count HEAD');
70
+ const output = execGit(`rev-list --count HEAD ^origin/${trunk} 2>/dev/null || rev-list --count HEAD`);
64
71
  return parseInt(output, 10) || 0;
65
72
  } catch {
66
73
  // Fallback: try a simpler approach
@@ -207,4 +214,4 @@ export class RealGitAdapter extends GitAdapter {
207
214
  throw new Error(`Failed to push tag: ${error.message}`);
208
215
  }
209
216
  }
210
- }
217
+ }
package/src/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { RealGitAdapter } from './git_real.js';
9
- import { getManualVersion, getDefaultBump, getVerifyMinDepth } from './config.js';
9
+ import { getManualVersion, getDefaultBump, getVerifyMinDepth, getTrunkName } from './config.js';
10
10
 
11
11
  // Global git adapter instance (can be overridden for testing)
12
12
  let _gitAdapter = null;
@@ -147,10 +147,12 @@ export function getCompatibleVersions(version) {
147
147
 
148
148
  /**
149
149
  * Check if current branch is main or master.
150
- * @returns {boolean} True if on main/master branch
150
+ * @param {string} [trunkNameOverride] - CLI override for trunk name
151
+ * @returns {boolean} True if on trunk branch
151
152
  */
152
- export function isMainBranch() {
153
- return getGitAdapter().isMainBranch();
153
+ export function isMainBranch(trunkNameOverride) {
154
+ const trunkName = trunkNameOverride !== undefined ? getTrunkName(trunkNameOverride) : undefined;
155
+ return getGitAdapter().isMainBranch(trunkName);
154
156
  }
155
157
 
156
158
  /**
@@ -204,14 +206,16 @@ export function determineVersionBump(messages, defaultBump = 'patch') {
204
206
  * Calculate the main branch version based on tags and commits.
205
207
  * @param {string} defaultBump - Default bump type (major|minor|patch)
206
208
  * @param {boolean} verifyHistory - Whether to verify git history
209
+ * @param {string} [trunkNameOverride] - CLI override for trunk name
207
210
  * @returns {string} SemVer version string
208
211
  * @throws {Error} If on feature branch or git operations fail
209
212
  */
210
- export function calculateMainVersion(defaultBump = 'patch', verifyHistory = true) {
213
+ export function calculateMainVersion(defaultBump = 'patch', verifyHistory = true, trunkNameOverride) {
211
214
  const adapter = getGitAdapter();
215
+ const trunkName = trunkNameOverride !== undefined ? getTrunkName(trunkNameOverride) : undefined;
212
216
 
213
217
  // Verify we're on main branch
214
- if (!adapter.isMainBranch()) {
218
+ if (!adapter.isMainBranch(trunkName)) {
215
219
  throw new Error('Cannot calculate main version on feature branch');
216
220
  }
217
221
 
@@ -258,20 +262,22 @@ export function calculateMainVersion(defaultBump = 'patch', verifyHistory = true
258
262
  * Calculate the feature branch version.
259
263
  * @param {string} _defaultBump - Default bump type (not used for feature branches)
260
264
  * @param {boolean} _verifyHistory - Whether to verify git history
265
+ * @param {string} [trunkNameOverride] - CLI override for trunk name
261
266
  * @returns {string} Feature version string (0.{CRC}.{Distance})
262
267
  * @throws {Error} If on main branch
263
268
  */
264
- export function calculateFeatureVersion(_defaultBump = 'patch', _verifyHistory = true) {
269
+ export function calculateFeatureVersion(_defaultBump = 'patch', _verifyHistory = true, trunkNameOverride) {
265
270
  const adapter = getGitAdapter();
271
+ const trunkName = trunkNameOverride !== undefined ? getTrunkName(trunkNameOverride) : undefined;
266
272
 
267
273
  // Verify we're NOT on main branch
268
- if (adapter.isMainBranch()) {
274
+ if (adapter.isMainBranch(trunkName)) {
269
275
  throw new Error('Cannot calculate feature version on main branch');
270
276
  }
271
277
 
272
278
  const branchName = adapter.getCurrentBranch();
273
279
  const crc = calculateCrc32Unsigned(branchName);
274
- const distance = adapter.getGitDistance();
280
+ const distance = adapter.getGitDistance(trunkName);
275
281
 
276
282
  return `0.${crc}.${distance}`;
277
283
  }
@@ -280,9 +286,10 @@ export function calculateFeatureVersion(_defaultBump = 'patch', _verifyHistory =
280
286
  * Get the current version based on git state.
281
287
  * @param {string} [defaultBump] - Default bump type for main branch (uses env/config if not provided)
282
288
  * @param {boolean} [verifyHistory=true] - Whether to verify git history
289
+ * @param {string} [trunkNameOverride] - CLI override for trunk name
283
290
  * @returns {string} Version string
284
291
  */
285
- export function getVersion(defaultBump, _verifyHistory = true) {
292
+ export function getVersion(defaultBump, _verifyHistory = true, trunkNameOverride) {
286
293
  // Check for manual version override first
287
294
  const manualVersion = getManualVersion();
288
295
  if (manualVersion !== undefined) {
@@ -295,11 +302,12 @@ export function getVersion(defaultBump, _verifyHistory = true) {
295
302
  }
296
303
 
297
304
  const adapter = getGitAdapter();
305
+ const trunkName = trunkNameOverride !== undefined ? getTrunkName(trunkNameOverride) : undefined;
298
306
 
299
- if (adapter.isMainBranch()) {
300
- return calculateMainVersion(defaultBump, _verifyHistory);
307
+ if (adapter.isMainBranch(trunkName)) {
308
+ return calculateMainVersion(defaultBump, _verifyHistory, trunkNameOverride);
301
309
  } else {
302
- return calculateFeatureVersion(defaultBump, _verifyHistory);
310
+ return calculateFeatureVersion(defaultBump, _verifyHistory, trunkNameOverride);
303
311
  }
304
312
  }
305
313