semver-ratchet 1.0.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/src/cli.js ADDED
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Command-line interface for semver-ratchet.
3
+ */
4
+
5
+ import {
6
+ getVersion,
7
+ getCurrentBranch,
8
+ calculateCrc32Unsigned,
9
+ determineVersionBump,
10
+ getCompatibleVersions,
11
+ verifyGitHistory,
12
+ createGitTag,
13
+ pushGitTag,
14
+ isMainBranch,
15
+ parseSemver,
16
+ getGitAdapter,
17
+ } from './version.js';
18
+
19
+ /**
20
+ * Display the current version.
21
+ * @param {Object} _args - Command line arguments
22
+ */
23
+ function cmdVersion(_args) {
24
+ const version = getVersion(
25
+ _args.defaultBump || 'patch',
26
+ !_args.noVerify,
27
+ );
28
+ console.log(version);
29
+ process.exit(0);
30
+ }
31
+
32
+ /**
33
+ * Display the current branch name.
34
+ * @param {Object} _args - Command line arguments
35
+ */
36
+ function cmdBranch(_args) {
37
+ const branch = getCurrentBranch();
38
+ console.log(branch);
39
+ process.exit(0);
40
+ }
41
+
42
+ /**
43
+ * Calculate CRC32 of a string (useful for branch name hashing).
44
+ * @param {Object} _args - Command line arguments
45
+ */
46
+ function cmdCrc32(_args) {
47
+ const value = calculateCrc32Unsigned(_args.string);
48
+ console.log(value);
49
+ process.exit(0);
50
+ }
51
+
52
+ /**
53
+ * Validate a commit message for SemVer bump.
54
+ * @param {Object} _args - Command line arguments
55
+ */
56
+ function cmdValidate(_args) {
57
+ const message = _args.message;
58
+ const messages = [message];
59
+ const bumpType = determineVersionBump(messages, _args.defaultBump || 'patch');
60
+
61
+ // Check for valid tags
62
+ if (message.toUpperCase().includes('[MAJOR]')) {
63
+ console.log(`Valid: ${bumpType.toUpperCase()} bump`);
64
+ console.log(' Tag: [MAJOR]');
65
+ process.exit(0);
66
+ } else if (message.toUpperCase().includes('[MINOR]')) {
67
+ console.log(`Valid: ${bumpType.toUpperCase()} bump`);
68
+ console.log(' Tag: [MINOR]');
69
+ process.exit(0);
70
+ } else if (message.toUpperCase().includes('[PATCH]')) {
71
+ console.log(`Valid: ${bumpType.toUpperCase()} bump`);
72
+ console.log(' Tag: [PATCH]');
73
+ process.exit(0);
74
+ } else {
75
+ console.log(`Valid: ${bumpType.toUpperCase()} bump (default: ${_args.defaultBump || 'patch'})`);
76
+ console.log(` Tag: [${(_args.defaultBump || 'patch').toUpperCase()}] (implicit)`);
77
+ process.exit(0);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Compare two SemVer versions.
83
+ * @param {string} v1 - First version
84
+ * @param {string} v2 - Second version
85
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
86
+ */
87
+ function compareSemver(v1, v2) {
88
+ const { major: m1, minor: n1, patch: p1 } = parseSemver(v1);
89
+ const { major: m2, minor: n2, patch: p2 } = parseSemver(v2);
90
+
91
+ if (m1 !== m2) return m1 < m2 ? -1 : 1;
92
+ if (n1 !== n2) return n1 < n2 ? -1 : 1;
93
+ if (p1 !== p2) return p1 < p2 ? -1 : 1;
94
+ return 0;
95
+ }
96
+
97
+ /**
98
+ * Check if version is monotonic (increasing).
99
+ * @param {Object} _args - Command line arguments
100
+ */
101
+ function cmdCheckMonotonic(_args) {
102
+ try {
103
+ const currentVersion = getVersion(
104
+ _args.defaultBump || 'patch',
105
+ !_args.noVerify,
106
+ );
107
+
108
+ // For main branch, compare with latest tag
109
+ if (isMainBranch()) {
110
+ try {
111
+ const adapter = getGitAdapter();
112
+ const latestTag = adapter.getLatestTag();
113
+
114
+ if (latestTag) {
115
+ const comparison = compareSemver(currentVersion, latestTag);
116
+
117
+ if (comparison > 0) {
118
+ console.log(`Monotonic: ${latestTag} -> ${currentVersion}`);
119
+ process.exit(0);
120
+ } else {
121
+ console.log(`ERROR: Version decreased or stayed same: ${latestTag} -> ${currentVersion}`);
122
+ process.exit(1);
123
+ }
124
+ } else {
125
+ // No tags yet, assume monotonic
126
+ console.log(`Monotonic: No previous tags, starting at ${currentVersion}`);
127
+ process.exit(0);
128
+ }
129
+ } catch {
130
+ // No tags yet, assume monotonic
131
+ console.log(`Monotonic: No previous tags, starting at ${currentVersion}`);
132
+ process.exit(0);
133
+ }
134
+ } else {
135
+ // Feature branches are always monotonic within their branch
136
+ console.log(`Monotonic: Feature branch version ${currentVersion}`);
137
+ process.exit(0);
138
+ }
139
+ } catch (error) {
140
+ console.log(`ERROR: ${error.message}`);
141
+ process.exit(1);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Display versioning information.
147
+ * @param {Object} _args - Command line arguments
148
+ */
149
+ function cmdInfo(_args) {
150
+ const branch = getCurrentBranch();
151
+ const version = getVersion(
152
+ _args.defaultBump || 'patch',
153
+ !_args.noVerify,
154
+ );
155
+ const branchType = isMainBranch() ? 'main' : 'feature';
156
+
157
+ console.log(`Branch: ${branch}`);
158
+ console.log(`Type: ${branchType}`);
159
+ console.log(`Version: ${version}`);
160
+ console.log(`Default bump: ${_args.defaultBump || 'patch'}`);
161
+
162
+ if (!isMainBranch()) {
163
+ const crc32 = calculateCrc32Unsigned(branch);
164
+ console.log(`Branch CRC32: ${crc32}`);
165
+ }
166
+
167
+ // Show compatible versions for main branch
168
+ if (isMainBranch()) {
169
+ try {
170
+ parseSemver(version);
171
+ const compatible = getCompatibleVersions(version);
172
+ console.log(`Compatible versions: ${compatible.join(', ')}`);
173
+ } catch {
174
+ // Not a valid semver, skip
175
+ }
176
+ }
177
+
178
+ process.exit(0);
179
+ }
180
+
181
+ /**
182
+ * Generate compatible versions in CSV format.
183
+ * @param {Object} _args - Command line arguments
184
+ */
185
+ function cmdVersions(_args) {
186
+ try {
187
+ const version = getVersion(
188
+ _args.defaultBump || 'patch',
189
+ !_args.noVerify,
190
+ );
191
+
192
+ // Output as CSV
193
+ console.log('version,type,description');
194
+
195
+ const parts = version.split('.');
196
+ const major = parts[0];
197
+ const minor = parts[1];
198
+
199
+ console.log(`${version},full,Exact version ${version}`);
200
+ console.log(`${major}.${minor},minor,Compatible with ${major}.${minor}.x`);
201
+ console.log(`${major},major,Compatible with ${major}.x.x`);
202
+
203
+ process.exit(0);
204
+ } catch (error) {
205
+ console.log(`ERROR: ${error.message}`);
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Verify git history is sufficient for version calculation.
212
+ * @param {Object} _args - Command line arguments
213
+ */
214
+ function cmdVerify(_args) {
215
+ try {
216
+ const depth = parseInt(_args.depth, 10) || 50;
217
+ verifyGitHistory(depth);
218
+ console.log('OK: Sufficient git history available');
219
+ process.exit(0);
220
+ } catch (error) {
221
+ console.log(`ERROR: ${error.message}`);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Create and optionally push a Git tag for the current version.
228
+ * @param {Object} _args - Command line arguments
229
+ */
230
+ function cmdTag(_args) {
231
+ if (!isMainBranch()) {
232
+ console.log('ERROR: Git tags can only be created on main branch');
233
+ process.exit(1);
234
+ }
235
+
236
+ try {
237
+ // Create tag (version is calculated internally)
238
+ createGitTag(_args.defaultBump || 'patch', _args.force);
239
+
240
+ // Push tag if requested
241
+ if (_args.push) {
242
+ pushGitTag(_args.defaultBump || 'patch', _args.remote || 'origin');
243
+ }
244
+
245
+ process.exit(0);
246
+ } catch (error) {
247
+ console.log(`ERROR: ${error.message}`);
248
+ process.exit(1);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Parse command line arguments.
254
+ * @param {string[]} argv - Command line arguments
255
+ * @returns {Object} Parsed arguments
256
+ */
257
+ function parseArgs(argv) {
258
+ const args = {
259
+ command: null,
260
+ defaultBump: 'patch',
261
+ noVerify: false,
262
+ };
263
+
264
+ const commands = ['version', 'branch', 'crc32', 'validate', 'check-monotonic', 'info', 'versions', 'verify', 'tag'];
265
+
266
+ let i = 2; // Skip node and script name
267
+ while (i < argv.length) {
268
+ const arg = argv[i];
269
+
270
+ // Check for commands
271
+ if (commands.includes(arg)) {
272
+ args.command = arg;
273
+ i++;
274
+ continue;
275
+ }
276
+
277
+ // Check for flags
278
+ if (arg === '--default-bump') {
279
+ args.defaultBump = argv[++i];
280
+ i++;
281
+ continue;
282
+ }
283
+
284
+ if (arg === '--no-verify') {
285
+ args.noVerify = true;
286
+ i++;
287
+ continue;
288
+ }
289
+
290
+ if (arg === '--force') {
291
+ args.force = true;
292
+ i++;
293
+ continue;
294
+ }
295
+
296
+ if (arg === '--push') {
297
+ args.push = true;
298
+ i++;
299
+ continue;
300
+ }
301
+
302
+ if (arg === '--remote') {
303
+ args.remote = argv[++i];
304
+ i++;
305
+ continue;
306
+ }
307
+
308
+ if (arg === '--depth') {
309
+ args.depth = argv[++i];
310
+ i++;
311
+ continue;
312
+ }
313
+
314
+ // Positional arguments
315
+ if (args.command === 'crc32' && !args.string && !arg.startsWith('--')) {
316
+ args.string = arg;
317
+ i++;
318
+ continue;
319
+ }
320
+
321
+ if (args.command === 'validate' && !args.message && !arg.startsWith('--')) {
322
+ args.message = arg;
323
+ i++;
324
+ continue;
325
+ }
326
+
327
+ i++;
328
+ }
329
+
330
+ return args;
331
+ }
332
+
333
+ /**
334
+ * Print usage information.
335
+ */
336
+ function printUsage() {
337
+ console.log(`
338
+ Usage: semver-ratchet [options] <command>
339
+
340
+ Forward-only versioning for Trunk-Based Development
341
+
342
+ Commands:
343
+ version Get current version
344
+ branch Get current branch name
345
+ crc32 <string> Calculate CRC32 of a string
346
+ validate <msg> Validate commit message for SemVer bump
347
+ check-monotonic Check version monotonicity
348
+ info Display versioning information
349
+ versions Generate compatible versions in CSV format
350
+ verify Verify git history is sufficient
351
+ tag Create and push a Git tag for the current version
352
+
353
+ Global Options:
354
+ --default-bump <type> Default version bump (major|minor|patch, default: patch)
355
+ --no-verify Skip git history verification
356
+
357
+ Tag Command Options:
358
+ --force Overwrite existing tag if it exists
359
+ --push Push tag to remote after creating
360
+ --remote <name> Remote name to push to (default: origin)
361
+
362
+ Verify Command Options:
363
+ --depth <n> Minimum required commit depth (default: 50)
364
+
365
+ Examples:
366
+ semver-ratchet version
367
+ semver-ratchet version --default-bump minor
368
+ semver-ratchet validate "feat: add login [MINOR]"
369
+ semver-ratchet tag --push
370
+ semver-ratchet crc32 feature/my-branch
371
+ `);
372
+ }
373
+
374
+ /**
375
+ * Main entry point for CLI.
376
+ */
377
+ export function main() {
378
+ const args = parseArgs(process.argv);
379
+
380
+ if (!args.command) {
381
+ printUsage();
382
+ process.exit(1);
383
+ }
384
+
385
+ switch (args.command) {
386
+ case 'version':
387
+ cmdVersion(args);
388
+ break;
389
+ case 'branch':
390
+ cmdBranch(args);
391
+ break;
392
+ case 'crc32':
393
+ if (!args.string) {
394
+ console.log('ERROR: crc32 command requires a string argument');
395
+ process.exit(1);
396
+ }
397
+ cmdCrc32(args);
398
+ break;
399
+ case 'validate':
400
+ if (!args.message) {
401
+ console.log('ERROR: validate command requires a message argument');
402
+ process.exit(1);
403
+ }
404
+ cmdValidate(args);
405
+ break;
406
+ case 'check-monotonic':
407
+ cmdCheckMonotonic(args);
408
+ break;
409
+ case 'info':
410
+ cmdInfo(args);
411
+ break;
412
+ case 'versions':
413
+ cmdVersions(args);
414
+ break;
415
+ case 'verify':
416
+ cmdVerify(args);
417
+ break;
418
+ case 'tag':
419
+ cmdTag(args);
420
+ break;
421
+ default:
422
+ console.log(`Unknown command: ${args.command}`);
423
+ printUsage();
424
+ process.exit(1);
425
+ }
426
+ }
427
+
428
+ // Run if called directly
429
+ if (process.argv[1] && process.argv[1].endsWith('cli.js')) {
430
+ main();
431
+ }
package/src/config.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Configuration module for semver-ratchet.
3
+ *
4
+ * Handles all environment variable configuration:
5
+ * - RATCHET_GIT_PATH: Custom path to git binary
6
+ * - RATCHET_TRUNK_NAME: Custom name for main/trunk branch
7
+ * - RATCHET_DEFAULT_BUMP: Default version bump type
8
+ * - RATCHET_VERIFY_MINDEPTH: Minimum git history depth
9
+ * - RATCHET_MANUAL_VERSION: Manual version override (replaces RATCHET_OVERRIDE)
10
+ */
11
+
12
+ /**
13
+ * Get the path to the git binary.
14
+ * @returns {string} Path to git binary (default: 'git')
15
+ */
16
+ export function getGitPath() {
17
+ return process.env.RATCHET_GIT_PATH || 'git';
18
+ }
19
+
20
+ /**
21
+ * Get the name of the trunk/main branch.
22
+ * @returns {string} Name of the trunk branch (default: 'main')
23
+ */
24
+ export function getTrunkName() {
25
+ return process.env.RATCHET_TRUNK_NAME || 'main';
26
+ }
27
+
28
+ /**
29
+ * Get the default version bump type.
30
+ * @returns {string} Default bump type: 'major', 'minor', or 'patch' (default: 'minor')
31
+ */
32
+ export function getDefaultBump() {
33
+ return process.env.RATCHET_DEFAULT_BUMP || 'minor';
34
+ }
35
+
36
+ /**
37
+ * Get the minimum git history depth for verification.
38
+ * @returns {number} Minimum number of commits required (default: 50)
39
+ */
40
+ export function getVerifyMinDepth() {
41
+ const depthStr = process.env.RATCHET_VERIFY_MINDEPTH;
42
+ if (depthStr !== undefined && depthStr !== null && depthStr !== '') {
43
+ const depth = parseInt(depthStr, 10);
44
+ if (!isNaN(depth) && depth >= 0) {
45
+ return depth;
46
+ }
47
+ }
48
+ return 50;
49
+ }
50
+
51
+ /**
52
+ * Get the manual version override.
53
+ * @returns {string|undefined} Manual version string or undefined
54
+ *
55
+ * Note: This replaces the deprecated RATCHET_OVERRIDE variable.
56
+ * RATCHET_OVERRIDE is still supported for backward compatibility.
57
+ */
58
+ export function getManualVersion() {
59
+ // Check for new variable first
60
+ if (process.env.RATCHET_MANUAL_VERSION !== undefined &&
61
+ process.env.RATCHET_MANUAL_VERSION !== null &&
62
+ process.env.RATCHET_MANUAL_VERSION !== '') {
63
+ return process.env.RATCHET_MANUAL_VERSION;
64
+ }
65
+
66
+ // Fallback to deprecated variable for backward compatibility
67
+ if (process.env.RATCHET_OVERRIDE !== undefined &&
68
+ process.env.RATCHET_OVERRIDE !== null &&
69
+ process.env.RATCHET_OVERRIDE !== '') {
70
+ return process.env.RATCHET_OVERRIDE;
71
+ }
72
+
73
+ return undefined;
74
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Git Adapter Interface for semver-ratchet.
3
+ *
4
+ * This module defines the interface that all git adapters must implement.
5
+ * Both RealGitAdapter and MockGitAdapter implement this interface.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} GitAdapter
10
+ * @property {function(): string} getCurrentBranch - Get the current branch name
11
+ * @property {function(): boolean} isMainBranch - Check if current branch is main/master
12
+ * @property {function(): number} getGitDistance - Get number of commits from main
13
+ * @property {function(): string|null} getLatestTag - Get the latest SemVer tag
14
+ * @property {function(): string[]} getCommitMessages - Get commit messages since tag
15
+ * @property {function(number): boolean} verifyGitHistory - Verify enough history exists
16
+ * @property {function(string): boolean} tagExists - Check if a tag exists
17
+ * @property {function(string, boolean): boolean} createTag - Create a git tag
18
+ * @property {function(string, string): void} pushTag - Push a tag to remote
19
+ */
20
+
21
+ /**
22
+ * Abstract base class for Git adapters.
23
+ * All git adapter implementations must extend this class.
24
+ */
25
+ export class GitAdapter {
26
+ /**
27
+ * Get the current branch name.
28
+ * @returns {string} The branch name
29
+ * @throws {Error} If git operation fails
30
+ */
31
+ getCurrentBranch() {
32
+ throw new Error('getCurrentBranch() must be implemented by subclass');
33
+ }
34
+
35
+ /**
36
+ * Check if current branch is main or master.
37
+ * @returns {boolean} True if on main/master branch
38
+ * @throws {Error} If git operation fails
39
+ */
40
+ isMainBranch() {
41
+ throw new Error('isMainBranch() must be implemented by subclass');
42
+ }
43
+
44
+ /**
45
+ * Get the number of commits between HEAD and main branch.
46
+ * @returns {number} Number of commits
47
+ * @throws {Error} If git operation fails
48
+ */
49
+ getGitDistance() {
50
+ throw new Error('getGitDistance() must be implemented by subclass');
51
+ }
52
+
53
+ /**
54
+ * Get the latest SemVer tag on the current branch.
55
+ * @returns {string|null} Tag version without 'v' prefix, or null if no tags
56
+ * @throws {Error} If git operation fails
57
+ */
58
+ getLatestTag() {
59
+ throw new Error('getLatestTag() must be implemented by subclass');
60
+ }
61
+
62
+ /**
63
+ * Get commit messages since a specific tag or all commits.
64
+ * @param {string} [_sinceTag] - Optional tag to start from (without 'v' prefix)
65
+ * @returns {string[]} List of commit messages
66
+ * @throws {Error} If git operation fails
67
+ */
68
+ getCommitMessages(_sinceTag = null) {
69
+ throw new Error('getCommitMessages() must be implemented by subclass');
70
+ }
71
+
72
+ /**
73
+ * Verify that enough git history is available.
74
+ * @param {number} [_fetchDepth] - Minimum number of commits required
75
+ * @returns {boolean} True if sufficient history exists
76
+ * @throws {Error} If history is insufficient
77
+ */
78
+ verifyGitHistory(_fetchDepth = 50) {
79
+ throw new Error('verifyGitHistory() must be implemented by subclass');
80
+ }
81
+
82
+ /**
83
+ * Check if a tag exists for the given version.
84
+ * @param {string} _version - Version string (without 'v' prefix)
85
+ * @returns {boolean} True if tag exists
86
+ * @throws {Error} If git operation fails
87
+ */
88
+ tagExists(_version) {
89
+ throw new Error('tagExists() must be implemented by subclass');
90
+ }
91
+
92
+ /**
93
+ * Create a git tag for the specified version.
94
+ * @param {string} _version - Version string (without 'v' prefix)
95
+ * @param {boolean} [_force] - If true, overwrite existing tag
96
+ * @returns {boolean} True if tag was created successfully
97
+ * @throws {Error} If git operation fails
98
+ */
99
+ createTag(_version, _force = false) {
100
+ throw new Error('createTag() must be implemented by subclass');
101
+ }
102
+
103
+ /**
104
+ * Push a tag to the remote repository.
105
+ * @param {string} _version - Version string (without 'v' prefix)
106
+ * @param {string} [_remote] - Remote name to push to
107
+ * @throws {Error} If git operation fails
108
+ */
109
+ pushTag(_version, _remote = 'origin') {
110
+ throw new Error('pushTag() must be implemented by subclass');
111
+ }
112
+ }