vnxt 1.9.1 → 1.13.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 (3) hide show
  1. package/README.md +73 -36
  2. package/package.json +1 -1
  3. package/vnxt.js +465 -360
package/README.md CHANGED
@@ -22,42 +22,56 @@ A lightweight CLI tool for automated version bumping with changelog generation a
22
22
 
23
23
  ## <img src="./docs/logos/caret-38x38.png" width="24" align="center"> Installation
24
24
 
25
- ### Global Installation
25
+ ### npm (all platforms)
26
26
 
27
- **Bash/PowerShell:**
28
27
  ```bash
29
28
  npm install -g vnxt
30
29
  ```
31
30
 
32
- After installation, you can use either `vnxt` or the shorter alias `vx`:
31
+ ### Scoop (Windows)
32
+
33
+ ```powershell
34
+ scoop bucket add vnxt https://github.com/n-orrow/scoop-vnxt
35
+ scoop install vnxt
36
+ ```
37
+
38
+ ### Chocolatey (Windows)
39
+
40
+ > ⏳ Pending moderation — will be available on the Chocolatey community repository shortly.
41
+
42
+ ```powershell
43
+ choco install vnxt
44
+ ```
45
+
46
+ ### Homebrew (macOS/Linux)
47
+
33
48
  ```bash
34
- vnxt --help
35
- vx --help # Same thing, shorter!
49
+ brew tap n-orrow/vnxt
50
+ brew install vnxt
36
51
  ```
37
52
 
38
- ### Local Installation (from source)
53
+ ### From Source
39
54
 
40
55
  **Bash/macOS/Linux:**
41
56
  ```bash
42
- # Clone the repository
43
57
  git clone https://github.com/n-orrow/vnxt.git
44
58
  cd vnxt
45
-
46
- # Install globally via npm link
47
59
  chmod +x vnxt.js
48
60
  npm link
49
61
  ```
50
62
 
51
63
  **PowerShell/Windows:**
52
64
  ```powershell
53
- # Clone the repository
54
65
  git clone https://github.com/n-orrow/vnxt.git
55
66
  cd vnxt
56
-
57
- # Install globally via npm link
58
67
  npm link
59
68
  ```
60
69
 
70
+ After installation, you can use either `vnxt` or the shorter alias `vx`:
71
+ ```bash
72
+ vx --help
73
+ ```
74
+
61
75
  ## <img src="./docs/logos/caret-38x38.png" width="24" align="center"> Usage
62
76
 
63
77
  ### Basic Examples
@@ -89,18 +103,19 @@ All options work with both `vnxt` and `vx`:
89
103
  ```
90
104
  -m, --message <msg> Commit message (required unless using interactive mode)
91
105
  -t, --type <type> Version type: patch, minor, major (auto-detected from message)
92
- -v, --version <ver> Set specific version (e.g., 2.0.0-beta.1)
93
- -V, --version Show vnxt version
94
- -p, --push Push to remote with tags
106
+ -sv, --set-version <v> Set a specific version (e.g., 2.0.0-beta.1)
107
+ -vv, --vnxt-version Show the installed vnxt version
108
+ -gv, --get-version Show the current project's version
109
+ -p, --push Push to remote with tags
95
110
  -dnp, --no-push Prevent auto-push (overrides config)
96
- --publish Push and trigger npm publish via GitHub Actions
97
- -c, --changelog Update CHANGELOG.md
98
- -d, --dry-run Show what would happen without making changes
99
- -a, --all [mode] Stage files before versioning (prompts if no mode)
100
- Modes: tracked, all, interactive (i), patch (p)
101
- -r, --release Generate release notes file
102
- -q, --quiet Minimal output (errors only)
103
- -h, --help Show help message
111
+ --publish Push and trigger npm publish via GitHub Actions (implies --push)
112
+ -c, --changelog Update CHANGELOG.md
113
+ -d, --dry-run Show what would happen without making changes
114
+ -a, --all [mode] Stage files before versioning (prompts if no mode)
115
+ Modes: tracked, all, interactive (i), patch (p)
116
+ -r, --release Generate release notes file (saved to release-notes/)
117
+ -q, --quiet Minimal output (errors only)
118
+ -h, --help Show help message
104
119
  ```
105
120
 
106
121
  ### Automatic Version Detection
@@ -112,7 +127,7 @@ vnxt automatically detects the version bump type from your commit message:
112
127
  - `patch:` → **patch** version bump
113
128
  - `feat:` or `feature:` → **minor** version bump
114
129
  - `fix:` → **patch** version bump
115
- - `BREAKING:` or contains `BREAKING` → **major** version bump
130
+ - `BREAKING` (anywhere in message) or `breaking:` → **major** version bump
116
131
 
117
132
  You can override this with the `-t` flag.
118
133
 
@@ -144,8 +159,8 @@ Set a specific version number (useful for pre-releases):
144
159
 
145
160
  **Bash/PowerShell:**
146
161
  ```bash
147
- vx -v 2.0.0-beta.1 -m "beta: initial release candidate"
148
- vx -v 1.5.0-rc.2 -m "release candidate 2"
162
+ vx -sv 2.0.0-beta.1 -m "beta: initial release candidate"
163
+ vx -sv 1.5.0-rc.2 -m "release candidate 2"
149
164
  ```
150
165
 
151
166
  ### Changelog Generation
@@ -177,19 +192,25 @@ Generate a formatted release notes file:
177
192
  vx -m "feat: major feature release" -r
178
193
  ```
179
194
 
180
- Creates `release-notes-v1.2.0.md`:
195
+ You'll be prompted to add optional context before the file is created. Release notes are saved to `release-notes/v1.2.0.md` (respects your `tagPrefix` config):
181
196
  ```markdown
182
197
  # Release v1.2.0
183
198
 
184
- Released: 2024-02-10
199
+ Released: 2024-02-10 at 14:32:00 UTC
200
+ Author: Your Name
185
201
 
186
202
  ## Changes
187
203
  feat: major feature release
188
204
 
189
205
  ## Installation
190
206
  npm install your-package@1.2.0
207
+
208
+ ## Full Changelog
209
+ See CHANGELOG.md for complete version history.
191
210
  ```
192
211
 
212
+ **Note:** `--publish` automatically generates release notes too, so `-r` is only needed for standalone bumps where you want the file without publishing.
213
+
193
214
  ### File Staging Options
194
215
 
195
216
  vnxt offers flexible file staging with the `-a` flag:
@@ -237,14 +258,28 @@ Display the installed vnxt version:
237
258
 
238
259
  **Bash/PowerShell:**
239
260
  ```bash
240
- vx -V
261
+ vx -vv
262
+ # or
263
+ vnxt --vnxt-version
264
+ ```
265
+
266
+ Output:
267
+ ```
268
+ vnxt v1.9.3
269
+ ```
270
+
271
+ Display the current project's version:
272
+
273
+ **Bash/PowerShell:**
274
+ ```bash
275
+ vx -gv
241
276
  # or
242
- vnxt --version
277
+ vnxt --get-version
243
278
  ```
244
279
 
245
280
  Output:
246
281
  ```
247
- vnxt v1.8.0
282
+ my-package v2.4.1
248
283
  ```
249
284
 
250
285
  ### npm Publish
@@ -257,10 +292,12 @@ vx -m "feat: new feature" --publish
257
292
  ```
258
293
 
259
294
  This will:
260
- 1. Bump the version and commit
261
- 2. Push with a standard `v*` tag
262
- 3. Push an additional `publish/v*` tag
263
- 4. GitHub Actions detects the `publish/v*` tag and publishes to npm automatically
295
+ 1. Prompt for optional release notes context
296
+ 2. Bump the version and commit
297
+ 3. Auto-generate a release notes file in `release-notes/`
298
+ 4. Push with a standard `v*` tag
299
+ 5. Push an additional `publish/v*` tag
300
+ 6. GitHub Actions detects the `publish/v*` tag and publishes to npm automatically
264
301
 
265
302
  This means you can batch up multiple changes without publishing to npm each time:
266
303
  ```bash
@@ -417,7 +454,7 @@ vx -m "feat: final feature" --publish
417
454
 
418
455
  **Bash/PowerShell:**
419
456
  ```bash
420
- vx -V
457
+ vx -vv
421
458
  ```
422
459
 
423
460
  ## <img src="./docs/logos/caret-38x38.png" width="24" align="center"> Troubleshooting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vnxt",
3
- "version": "1.9.1",
3
+ "version": "1.13.0",
4
4
  "description": "Version incrementation CLI tool with built in git commit, push and changelog generation",
5
5
  "main": "vnxt.js",
6
6
  "bin": {
package/vnxt.js CHANGED
@@ -1,16 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const {execSync} = require('child_process');
3
+ // =============================================================================
4
+ // TODOs
5
+ // -----------------------------------------------------------------------------
6
+ // 1. Nothing comes to mind right now
7
+ // =============================================================================
8
+
9
+ // =============================================================================
10
+ // Imports & Constants
11
+ // =============================================================================
12
+
13
+ const {execSync, execFileSync} = require('child_process');
4
14
  const fs = require('fs');
15
+ const path = require('path');
5
16
  const readline = require('readline');
6
17
 
7
- // ANSI color codes for terminal output
8
18
  const colors = {
9
19
  reset: '\x1b[0m',
10
20
  bright: '\x1b[1m',
11
21
  dim: '\x1b[2m',
12
-
13
- // Foreground colors
14
22
  red: '\x1b[31m',
15
23
  green: '\x1b[32m',
16
24
  yellow: '\x1b[33m',
@@ -20,10 +28,13 @@ const colors = {
20
28
  gray: '\x1b[90m'
21
29
  };
22
30
 
23
- // Quiet mode flag
31
+ const args = process.argv.slice(2);
24
32
  let quietMode = false;
25
33
 
26
- // Helper to log with colors (respects quiet mode and colors config)
34
+ // =============================================================================
35
+ // Logging
36
+ // =============================================================================
37
+
27
38
  function log(message, color = '') {
28
39
  if (quietMode) return;
29
40
  if (color && colors[color] && config.colors) {
@@ -34,8 +45,6 @@ function log(message, color = '') {
34
45
  }
35
46
 
36
47
  function logError(message) {
37
- // Errors always show, even in quiet mode
38
- // Colors can be disabled for errors too
39
48
  if (config.colors) {
40
49
  console.error(`${colors.red}${message}${colors.reset}`);
41
50
  } else {
@@ -43,10 +52,10 @@ function logError(message) {
43
52
  }
44
53
  }
45
54
 
46
- // Parse command line arguments
47
- const args = process.argv.slice(2);
55
+ // =============================================================================
56
+ // Argument Helpers
57
+ // =============================================================================
48
58
 
49
- // Helper to parse flags
50
59
  function getFlag(flag, short) {
51
60
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : args.indexOf(short);
52
61
  if (index === -1) return null;
@@ -54,351 +63,382 @@ function getFlag(flag, short) {
54
63
  }
55
64
 
56
65
  function hasFlag(flag, short) {
57
- return args.includes(flag) || args.includes(short);
66
+ return args.includes(flag) || (short ? args.includes(short) : false);
58
67
  }
59
68
 
60
- // Load config file if exists
61
- let config = {
62
- autoChangelog: true,
63
- defaultType: 'patch',
64
- requireCleanWorkingDir: false,
65
- autoPush: true,
66
- defaultStageMode: 'tracked',
67
- tagPrefix: 'v',
68
- colors: true
69
- };
69
+ // =============================================================================
70
+ // Load Config
71
+ // =============================================================================
72
+
73
+ function loadConfig() {
74
+ const defaults = {
75
+ autoChangelog: true,
76
+ defaultType: 'patch',
77
+ requireCleanWorkingDir: false,
78
+ autoPush: true,
79
+ defaultStageMode: 'tracked',
80
+ tagPrefix: 'v',
81
+ colors: true
82
+ };
83
+
84
+ if (fs.existsSync('.vnxtrc.json')) {
85
+ const userConfig = JSON.parse(fs.readFileSync('.vnxtrc.json', 'utf8'));
86
+ return {...defaults, ...userConfig};
87
+ }
70
88
 
71
- if (fs.existsSync('.vnxtrc.json')) {
72
- const userConfig = JSON.parse(fs.readFileSync('.vnxtrc.json', 'utf8'));
73
- config = {...config, ...userConfig};
89
+ return defaults;
74
90
  }
75
91
 
76
- // Check for --version flag
77
- if (args.includes('--version') || args.includes('-V')) {
78
- const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
79
- console.log(`vnxt v${pkg.version}`);
80
- process.exit(0);
81
- }
92
+ const config = loadConfig();
82
93
 
83
- // Check for --quiet flag
84
- if (args.includes('--quiet') || args.includes('-q')) {
85
- quietMode = true;
86
- }
94
+ // =============================================================================
95
+ // Handle Quick Flags (exit immediately)
96
+ // =============================================================================
87
97
 
88
- // Check if in a git repository
89
- if (!fs.existsSync('.git')) {
90
- logError(' Not a git repository. Run `git init` first.');
91
- process.exit(1);
98
+ function handleQuickFlags() {
99
+ // -vv / --vnxt-version: show vnxt's own installed version
100
+ if (args.includes('--vnxt-version') || args.includes('-vv')) {
101
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
102
+ console.log(`vnxt v${pkg.version}`);
103
+ process.exit(0);
104
+ }
105
+
106
+ // -gv / --get-version: show the current project's version
107
+ if (args.includes('--get-version') || args.includes('-gv')) {
108
+ if (!fs.existsSync('./package.json')) {
109
+ console.error('❌ No package.json found in current directory.');
110
+ process.exit(1);
111
+ }
112
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
113
+ console.log(`${pkg.name} v${pkg.version}`);
114
+ process.exit(0);
115
+ }
116
+
117
+ // -h / --help
118
+ if (hasFlag('--help', '-h')) {
119
+ printHelp();
120
+ process.exit(0);
121
+ }
92
122
  }
93
123
 
94
- // Parse arguments
95
- let message = getFlag('--message', '-m');
96
- let type = getFlag('--type', '-t') || config.defaultType;
97
- let customVersion = getFlag('--version', '-v');
98
- let dryRun = hasFlag('--dry-run', '-d');
99
- let noPush = hasFlag('--no-push', '-dnp');
100
- let publishToNpm = hasFlag('--publish');
101
- let push = noPush ? false : (hasFlag('--push', '-p') || publishToNpm || config.autoPush);
102
- let generateChangelog = hasFlag('--changelog', '-c') || config.autoChangelog;
103
- const addAllFlag = getFlag('--all', '-a');
104
- let addMode = null;
105
- let promptForStaging = false;
106
- if (addAllFlag) {
107
- if (typeof addAllFlag === 'string') {
108
- const mode = addAllFlag.toLowerCase();
109
- if (['tracked', 'all', 'a', 'interactive', 'i', 'patch', 'p'].includes(mode)) {
110
- if (mode === 'a') addMode = 'all';
111
- else if (mode === 'i') addMode = 'interactive';
112
- else if (mode === 'p') addMode = 'patch';
113
- else addMode = mode;
124
+ // =============================================================================
125
+ // Parse Args
126
+ // =============================================================================
127
+
128
+ function parseArgs() {
129
+ if (args.includes('--quiet') || args.includes('-q')) {
130
+ quietMode = true;
131
+ }
132
+
133
+ const addAllFlag = getFlag('--all', '-a');
134
+ let addMode = null;
135
+ let promptForStaging = false;
136
+
137
+ if (addAllFlag) {
138
+ if (typeof addAllFlag === 'string') {
139
+ const mode = addAllFlag.toLowerCase();
140
+ const modeMap = { a: 'all', i: 'interactive', p: 'patch' };
141
+ const valid = ['tracked', 'all', 'interactive', 'patch', ...Object.keys(modeMap)];
142
+ if (!valid.includes(mode)) {
143
+ logError(`Error: Invalid add mode '${addAllFlag}'. Use: tracked, all, interactive (i), or patch (p)`);
144
+ process.exit(1);
145
+ }
146
+ addMode = modeMap[mode] || mode;
114
147
  } else {
115
- logError(`Error: Invalid add mode '${addAllFlag}'. Use: tracked, all, interactive (i), or patch (p)`);
116
- process.exit(1);
148
+ promptForStaging = true;
117
149
  }
118
- } else {
119
- promptForStaging = true;
120
150
  }
151
+
152
+ const noPush = hasFlag('--no-push', '-dnp');
153
+ const publishToNpm = hasFlag('--publish');
154
+
155
+ return {
156
+ message: getFlag('--message', '-m'),
157
+ type: getFlag('--type', '-t') || config.defaultType,
158
+ customVersion: getFlag('--set-version', '-sv'),
159
+ dryRun: hasFlag('--dry-run', '-d'),
160
+ noPush,
161
+ publishToNpm,
162
+ push: noPush ? false : (hasFlag('--push', '-p') || publishToNpm || config.autoPush),
163
+ generateChangelog: hasFlag('--changelog', '-c') || config.autoChangelog,
164
+ generateReleaseNotes: hasFlag('--release', '-r'),
165
+ addMode,
166
+ promptForStaging
167
+ };
121
168
  }
122
- let generateReleaseNotes = hasFlag('--release', '-r');
123
169
 
124
- // Interactive mode helper
125
- async function prompt(question) {
126
- const rl = readline.createInterface({
127
- input: process.stdin, output: process.stdout
128
- });
170
+ // =============================================================================
171
+ // Interactive Prompt Helper
172
+ // =============================================================================
129
173
 
174
+ async function prompt(question) {
175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
130
176
  return new Promise(resolve => {
131
- rl.question(question, answer => {
132
- rl.close();
133
- resolve(answer);
134
- });
177
+ rl.question(question, answer => { rl.close(); resolve(answer); });
135
178
  });
136
179
  }
137
180
 
138
- // Main function
139
- async function main() {
140
- try {
141
- // Interactive mode if no message provided
142
- if (!message) {
143
- log('🤔 Interactive mode\n', 'cyan');
181
+ // =============================================================================
182
+ // Interactive Mode
183
+ // =============================================================================
144
184
 
145
- message = await prompt('Commit message: ');
146
- if (!message) {
147
- logError('Error: Commit message is required');
148
- process.exit(1);
149
- }
185
+ async function runInteractiveMode(opts) {
186
+ log('🤔 Interactive mode\n', 'cyan');
150
187
 
151
- const typeInput = await prompt('Version type (patch/minor/major) [auto-detect]: ');
152
- if (typeInput && ['patch', 'minor', 'major'].includes(typeInput)) {
153
- type = typeInput;
154
- }
188
+ opts.message = await prompt('Commit message: ');
189
+ if (!opts.message) {
190
+ logError('Error: Commit message is required');
191
+ process.exit(1);
192
+ }
155
193
 
156
- const changelogInput = await prompt('Update CHANGELOG.md? (y/n) [n]: ');
157
- generateChangelog = changelogInput.toLowerCase() === 'y' || changelogInput.toLowerCase() === 'yes' || generateChangelog;
194
+ const typeInput = await prompt('Version type (patch/minor/major) [auto-detect]: ');
195
+ if (typeInput && ['patch', 'minor', 'major'].includes(typeInput)) {
196
+ opts.type = typeInput;
197
+ }
158
198
 
159
- const releaseNotesInput = await prompt('Generate release notes? (y/n) [n]: ');
160
- generateReleaseNotes = releaseNotesInput.toLowerCase() === 'y' || releaseNotesInput.toLowerCase() === 'yes';
199
+ const changelogInput = await prompt('Update CHANGELOG.md? (y/n) [n]: ');
200
+ opts.generateChangelog = changelogInput.toLowerCase() === 'y' || changelogInput.toLowerCase() === 'yes' || opts.generateChangelog;
161
201
 
162
- const pushInput = await prompt('Push to remote? (y/n) [n]: ');
163
- push = pushInput.toLowerCase() === 'y' || pushInput.toLowerCase() === 'yes' || push;
202
+ const publishInput = await prompt('Publish to npm? (y/n) [n]: ');
203
+ if (publishInput.toLowerCase() === 'y' || publishInput.toLowerCase() === 'yes') {
204
+ opts.publishToNpm = true;
205
+ opts.generateReleaseNotes = true;
206
+ }
164
207
 
165
- const dryRunInput = await prompt('Dry run (preview only)? (y/n) [n]: ');
166
- dryRun = dryRunInput.toLowerCase() === 'y' || dryRunInput.toLowerCase() === 'yes';
208
+ const pushInput = await prompt('Push to remote? (y/n) [n]: ');
209
+ opts.push = pushInput.toLowerCase() === 'y' || pushInput.toLowerCase() === 'yes' || opts.push;
167
210
 
168
- log(''); // Blank line before proceeding
169
- }
211
+ const dryRunInput = await prompt('Dry run (preview only)? (y/n) [n]: ');
212
+ opts.dryRun = dryRunInput.toLowerCase() === 'y' || dryRunInput.toLowerCase() === 'yes';
170
213
 
171
- // Auto-detect version type from conventional commit format
172
- if (!customVersion && !getFlag('--type', '-t')) {
173
- if (message.startsWith('major:') || message.startsWith('MAJOR:')) {
174
- type = 'major';
175
- log('📝 Auto-detected: major version bump', 'cyan');
176
- } else if (message.startsWith('minor:') || message.startsWith('MINOR:')) {
177
- type = 'minor';
178
- log('📝 Auto-detected: minor version bump', 'cyan');
179
- } else if (message.startsWith('patch:') || message.startsWith('PATCH:')) {
180
- type = 'patch';
181
- log('📝 Auto-detected: patch version bump', 'cyan');
182
- } else if (message.startsWith('feat:') || message.startsWith('feature:')) {
183
- type = 'minor';
184
- log('📝 Auto-detected: minor version bump (feature)', 'cyan');
185
- } else if (message.startsWith('fix:')) {
186
- type = 'patch';
187
- log('📝 Auto-detected: patch version bump (fix)', 'cyan');
188
- } else if (message.includes('BREAKING') || message.startsWith('breaking:')) {
189
- type = 'major';
190
- log('📝 Auto-detected: major version bump (breaking change)', 'cyan');
191
- }
192
- }
214
+ log('');
215
+ }
193
216
 
194
- // Validate version type
195
- if (!customVersion && !['patch', 'minor', 'major'].includes(type)) {
196
- logError('Error: Version type must be patch, minor, or major');
197
- process.exit(1);
217
+ // =============================================================================
218
+ // Detect Version Type
219
+ // =============================================================================
220
+
221
+ function detectVersionType(message, currentType) {
222
+ const rules = [
223
+ { prefixes: ['major:', 'MAJOR:'], type: 'major', label: 'major version bump' },
224
+ { prefixes: ['minor:', 'MINOR:'], type: 'minor', label: 'minor version bump' },
225
+ { prefixes: ['patch:', 'PATCH:'], type: 'patch', label: 'patch version bump' },
226
+ { prefixes: ['feat:', 'feature:'], type: 'minor', label: 'minor version bump (feature)' },
227
+ { prefixes: ['fix:'], type: 'patch', label: 'patch version bump (fix)' },
228
+ { prefixes: ['breaking:'], type: 'major', label: 'major version bump (breaking change)' },
229
+ ];
230
+
231
+ for (const rule of rules) {
232
+ if (rule.prefixes.some(p => message.startsWith(p))) {
233
+ log(`📝 Auto-detected: ${rule.label}`, 'cyan');
234
+ return rule.type;
198
235
  }
236
+ }
199
237
 
200
- // AUTO-REQUIRE RELEASE NOTES for --publish
201
- let releaseNotesContext = '';
202
- const requireReleaseNotes = !generateReleaseNotes && publishToNpm;
203
- if (requireReleaseNotes) {
204
- generateReleaseNotes = true;
205
- if (!quietMode) {
206
- log(`\n📋 Release notes required for --publish.`, 'yellow');
207
- releaseNotesContext = await prompt(' Add context (press Enter to skip): ');
208
- if (releaseNotesContext) log('');
209
- }
210
- } else if (generateReleaseNotes && !quietMode) {
211
- // -r flag was passed explicitly - still offer context prompt
212
- releaseNotesContext = await prompt('\n📋 Add context to release notes (press Enter to skip): ');
213
- if (releaseNotesContext) log('');
214
- }
238
+ // Special case: BREAKING anywhere in message
239
+ if (message.includes('BREAKING')) {
240
+ log('📝 Auto-detected: major version bump (breaking change)', 'cyan');
241
+ return 'major';
242
+ }
243
+
244
+ return currentType;
245
+ }
215
246
 
216
- // PRE-FLIGHT CHECKS
217
- log('\n🔍 Running pre-flight checks...\n', 'cyan');
218
-
219
- // Check for uncommitted changes OR if user requested staging prompt
220
- if ((config.requireCleanWorkingDir && !addMode) || promptForStaging) {
221
- const status = execSync('git status --porcelain --untracked-files=no').toString().trim();
222
- if (status || promptForStaging) {
223
- if (status) {
224
- log('⚠️ You have uncommitted changes.\n', 'yellow');
225
- }
226
- log('📁 How would you like to stage files?\n');
227
- log(' 1. Tracked files only (git add -u)');
228
- log(' 2. All changes (git add -A)');
229
- log(' 3. Interactive selection (git add -i)');
230
- log(' 4. Patch mode (git add -p)');
231
- log(' 5. Skip staging (continue without staging)\n');
232
-
233
- const choice = await prompt('Select [1-5]: ');
234
-
235
- if (choice === '1') {
236
- addMode = 'tracked';
237
- } else if (choice === '2') {
238
- addMode = 'all';
239
- } else if (choice === '3') {
240
- addMode = 'interactive';
241
- } else if (choice === '4') {
242
- addMode = 'patch';
243
- } else if (choice === '5') {
244
- log('⚠️ Skipping file staging. Ensure files are staged manually.', 'yellow');
245
- } else {
246
- logError('Invalid choice. Exiting.');
247
- process.exit(1);
248
- }
249
- log('');
247
+ // =============================================================================
248
+ // Pre-flight Checks
249
+ // =============================================================================
250
+
251
+ async function runPreflightChecks(opts) {
252
+ log('\n🔍 Running pre-flight checks...\n', 'cyan');
253
+
254
+ // Staging prompt if requested
255
+ if ((config.requireCleanWorkingDir && !opts.addMode) || opts.promptForStaging) {
256
+ const status = execSync('git status --porcelain --untracked-files=no').toString().trim();
257
+ if (status || opts.promptForStaging) {
258
+ if (status) log('⚠️ You have uncommitted changes.\n', 'yellow');
259
+
260
+ log('📁 How would you like to stage files?\n');
261
+ log(' 1. Tracked files only (git add -u)');
262
+ log(' 2. All changes (git add -A)');
263
+ log(' 3. Interactive selection (git add -i)');
264
+ log(' 4. Patch mode (git add -p)');
265
+ log(' 5. Skip staging (continue without staging)\n');
266
+
267
+ const choice = await prompt('Select [1-5]: ');
268
+ const choiceMap = { '1': 'tracked', '2': 'all', '3': 'interactive', '4': 'patch' };
269
+
270
+ if (choiceMap[choice]) {
271
+ opts.addMode = choiceMap[choice];
272
+ } else if (choice === '5') {
273
+ log('⚠️ Skipping file staging. Ensure files are staged manually.', 'yellow');
274
+ } else {
275
+ logError('Invalid choice. Exiting.');
276
+ process.exit(1);
250
277
  }
278
+ log('');
251
279
  }
280
+ }
252
281
 
253
- // Check current branch
254
- const branch = execSync('git branch --show-current').toString().trim();
255
- if (branch !== 'main' && branch !== 'master') {
256
- log(`⚠️ Warning: You're on branch '${branch}', not main/master`, 'yellow');
257
- }
282
+ // Branch check
283
+ const branch = execSync('git branch --show-current').toString().trim();
284
+ if (branch !== 'main' && branch !== 'master') {
285
+ log(`⚠️ Warning: You're on branch '${branch}', not main/master`, 'yellow');
286
+ }
258
287
 
259
- // Check if remote exists
260
- try {
261
- execSync('git remote get-url origin', {stdio: 'pipe'});
262
- } catch {
263
- if (push) {
264
- logError('❌ Error: No remote repository configured, cannot push');
265
- process.exit(1);
266
- }
267
- log('⚠️ Warning: No remote repository configured', 'yellow');
288
+ // Remote check
289
+ try {
290
+ execSync('git remote get-url origin', {stdio: 'pipe'});
291
+ } catch {
292
+ if (opts.push) {
293
+ logError('❌ Error: No remote repository configured, cannot push');
294
+ process.exit(1);
268
295
  }
296
+ log('⚠️ Warning: No remote repository configured', 'yellow');
297
+ }
269
298
 
270
- log('✅ Pre-flight checks passed\n', 'green');
271
-
272
- // DRY RUN MODE
273
- if (dryRun) {
274
- log('🔬 DRY RUN MODE - No changes will be made\n', 'yellow');
275
- log('Would perform the following actions:');
276
-
277
- if (addMode) {
278
- const modeDescriptions = {
279
- 'tracked': 'Stage tracked files only (git add -u)',
280
- 'all': 'Stage all changes (git add -A)',
281
- 'interactive': 'Interactive selection (git add -i)',
282
- 'patch': 'Patch mode (git add -p)'
283
- };
284
- log(` 1. ${modeDescriptions[addMode]}`);
285
- }
299
+ log('✅ Pre-flight checks passed\n', 'green');
300
+ return branch;
301
+ }
286
302
 
287
- log(` 2. Bump ${type} version`);
288
- log(` 3. Commit with message: "${message}"`);
289
- log(' 4. Create git tag with annotation');
303
+ // =============================================================================
304
+ // Dry Run
305
+ // =============================================================================
306
+
307
+ function runDryRun(opts) {
308
+ log('🔬 DRY RUN MODE - No changes will be made\n', 'yellow');
309
+ log('Would perform the following actions:');
310
+
311
+ if (opts.addMode) {
312
+ const modeDescriptions = {
313
+ tracked: 'Stage tracked files only (git add -u)',
314
+ all: 'Stage all changes (git add -A)',
315
+ interactive: 'Interactive selection (git add -i)',
316
+ patch: 'Patch mode (git add -p)'
317
+ };
318
+ log(` 1. ${modeDescriptions[opts.addMode]}`);
319
+ }
290
320
 
291
- if (generateChangelog) {
292
- log(' 5. Update CHANGELOG.md');
293
- } else {
294
- log(' 5. (Skipping changelog - use --changelog to enable)');
295
- }
321
+ log(` 2. Bump ${opts.type} version`);
322
+ log(` 3. Commit with message: "${opts.message}"`);
323
+ log(' 4. Create git tag with annotation');
324
+ log(opts.generateChangelog ? ' 5. Update CHANGELOG.md' : ' 5. (Skipping changelog - use --changelog to enable)');
325
+ log(opts.generateReleaseNotes ? ' 6. Generate release notes file' : ' 6. (Skipping release notes - use --release to enable)');
326
+ log(opts.push ? ' 7. Push to remote with tags' : ' 7. (Skipping push - use --push to enable)');
296
327
 
297
- if (generateReleaseNotes) {
298
- log(' 6. Generate release notes file');
299
- } else {
300
- log(' 6. (Skipping release notes - use --release to enable)');
301
- }
328
+ log('\n✓ Dry run complete. Use without -d to apply changes.', 'green');
329
+ process.exit(0);
330
+ }
302
331
 
303
- if (push) {
304
- log(' 7. Push to remote with tags');
305
- } else {
306
- log(' 7. (Skipping push - use --push to enable)');
307
- }
332
+ // =============================================================================
333
+ // Stage Files
334
+ // =============================================================================
335
+
336
+ function stageFiles(addMode) {
337
+ log('📦 Staging files...', 'cyan');
338
+ const modeCommands = {
339
+ tracked: 'git add -u',
340
+ all: 'git add -A',
341
+ interactive: 'git add -i',
342
+ patch: 'git add -p'
343
+ };
344
+ execSync(modeCommands[addMode], {stdio: 'inherit'});
345
+ }
308
346
 
309
- log('\n✓ Dry run complete. Use without -d to apply changes.', 'green');
310
- process.exit(0);
311
- }
347
+ // =============================================================================
348
+ // Bump Version
349
+ // =============================================================================
312
350
 
313
- // STAGE FILES if requested
314
- if (addMode) {
315
- log('📦 Staging files...', 'cyan');
316
-
317
- if (addMode === 'tracked') {
318
- execSync('git add -u', {stdio: 'inherit'});
319
- } else if (addMode === 'all') {
320
- execSync('git add -A', {stdio: 'inherit'});
321
- } else if (addMode === 'interactive') {
322
- execSync('git add -i', {stdio: 'inherit'});
323
- } else if (addMode === 'patch') {
324
- execSync('git add -p', {stdio: 'inherit'});
325
- }
326
- }
327
- // BUMP VERSION
328
- log(`\n🔼 Bumping version...`, 'cyan');
351
+ function bumpVersion(opts) {
352
+ log('\n🔼 Bumping version...', 'cyan');
329
353
 
330
- // Get current version before bump
331
- const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
332
- const oldVersion = packageJson.version;
354
+ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
355
+ const oldVersion = packageJson.version;
333
356
 
334
- // Always disable npm's git integration and handle it ourselves
335
- if (customVersion) {
336
- execSync(`npm version ${customVersion} --git-tag-version=false`, {stdio: quietMode ? 'pipe' : 'inherit'});
337
- } else {
338
- execSync(`npm version ${type} --git-tag-version=false`, {stdio: quietMode ? 'pipe' : 'inherit'});
339
- }
357
+ const versionArg = opts.customVersion || opts.type;
358
+ execSync(`npm version ${versionArg} --git-tag-version=false`, {stdio: quietMode ? 'pipe' : 'inherit'});
340
359
 
341
- // Get new version
342
- const newVersion = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
360
+ const newVersion = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
343
361
 
344
- // Stage package files
345
- execSync('git add package.json', {stdio: 'pipe'});
346
- if (fs.existsSync('package-lock.json')) {
347
- execSync('git add package-lock.json', {stdio: 'pipe'});
348
- }
362
+ // Stage package files and commit
363
+ execSync('git add package.json', {stdio: 'pipe'});
364
+ if (fs.existsSync('package-lock.json')) {
365
+ execSync('git add package-lock.json', {stdio: 'pipe'});
366
+ }
367
+ execFileSync('git', ['commit', '-m', opts.message], {stdio: quietMode ? 'pipe' : 'inherit'});
349
368
 
350
- // Commit with user's message
351
- execSync(`git commit -m "${message}"`, {stdio: quietMode ? 'pipe' : 'inherit'});
369
+ // Create annotated tag
370
+ log('🏷️ Adding tag annotation...', 'cyan');
371
+ const tagMessage = `Version ${newVersion}\n\n${opts.message}`;
372
+ execFileSync('git', ['tag', '-a', `${config.tagPrefix}${newVersion}`, '-m', tagMessage], {stdio: 'pipe'});
352
373
 
353
- // Create annotated tag
354
- log('🏷️ Adding tag annotation...', 'cyan');
355
- const tagMessage = `Version ${newVersion}\n\n${message}`;
356
- execSync(`git tag -a ${config.tagPrefix}${newVersion} -m "${tagMessage}"`, {stdio: 'pipe'});
374
+ return { oldVersion, newVersion, packageJson };
375
+ }
357
376
 
358
- // GENERATE CHANGELOG
359
- if (generateChangelog) {
360
- log('📄 Updating CHANGELOG.md...', 'cyan');
361
- const date = new Date().toISOString().split('T')[0];
362
- const changelogEntry = `\n## [${newVersion}] - ${date}\n- ${message}\n`;
377
+ // =============================================================================
378
+ // Generate Changelog
379
+ // =============================================================================
363
380
 
364
- let changelog = '# Changelog\n';
365
- if (fs.existsSync('CHANGELOG.md')) {
366
- changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
367
- }
381
+ function generateChangelog(newVersion, message) {
382
+ log('📄 Updating CHANGELOG.md...', 'cyan');
368
383
 
369
- // Insert new entry after the title
370
- const lines = changelog.split('\n');
371
- const titleIndex = lines.findIndex(line => line.startsWith('# Changelog'));
372
- lines.splice(titleIndex + 1, 0, changelogEntry);
384
+ const date = new Date().toISOString().split('T')[0];
385
+ const entry = `\n## [${newVersion}] - ${date}\n- ${message}\n`;
373
386
 
374
- fs.writeFileSync('CHANGELOG.md', lines.join('\n'));
387
+ let changelog = '# Changelog\n';
388
+ if (fs.existsSync('CHANGELOG.md')) {
389
+ changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
390
+ }
375
391
 
376
- // Stage the changelog
377
- execSync('git add CHANGELOG.md', {stdio: 'pipe'});
378
- execSync(`git commit --amend --no-edit`, {stdio: 'pipe'});
379
- }
392
+ const lines = changelog.split('\n');
393
+ const titleIndex = lines.findIndex(line => line.startsWith('# Changelog'));
394
+ lines.splice(titleIndex + 1, 0, entry);
395
+ fs.writeFileSync('CHANGELOG.md', lines.join('\n'));
380
396
 
381
- // GENERATE RELEASE NOTES
382
- if (generateReleaseNotes) {
383
- log('📋 Generating release notes...', 'cyan');
397
+ execSync('git add CHANGELOG.md', {stdio: 'pipe'});
398
+ execSync('git commit --amend --no-edit', {stdio: 'pipe'});
399
+ }
384
400
 
385
- const date = new Date();
386
- const timestamp = date.toISOString().replace('T', ' ').split('.')[0] + ' UTC';
387
- const dateShort = date.toISOString().split('T')[0];
401
+ // =============================================================================
402
+ // Generate Release Notes
403
+ // =============================================================================
388
404
 
389
- let author = '';
390
- try {
391
- author = execSync('git config user.name', {stdio: 'pipe'}).toString().trim();
392
- } catch {
393
- author = '';
405
+ function generateReleaseNotes(newVersion, message, context, packageJson, isPublish = false) {
406
+ log('📋 Generating release notes...', 'cyan');
407
+
408
+ const date = new Date();
409
+ const timestamp = date.toISOString().replace('T', ' ').split('.')[0] + ' UTC';
410
+ const dateShort = date.toISOString().split('T')[0];
411
+
412
+ let author = '';
413
+ try { author = execSync('git config user.name', {stdio: 'pipe'}).toString().trim(); } catch {}
414
+
415
+ // If publishing, gather all commits since the last publish/v* tag
416
+ let changes = message;
417
+ if (isPublish) {
418
+ try {
419
+ const lastPublishTag = execSync(
420
+ 'git tag --list "publish/v*" --sort=-version:refname',
421
+ {stdio: 'pipe'}
422
+ ).toString().trim().split('\n').filter(Boolean)[0];
423
+
424
+ if (lastPublishTag) {
425
+ const commits = execSync(
426
+ `git log ${lastPublishTag}..HEAD --pretty=format:"- %s"`,
427
+ {stdio: 'pipe'}
428
+ ).toString().trim();
429
+ if (commits) changes = commits;
394
430
  }
431
+ } catch {
432
+ // Fall back to current message if git log fails
433
+ }
434
+ }
395
435
 
396
- const releaseNotes = `# Release ${config.tagPrefix}${newVersion}
436
+ const notes = `# Release ${config.tagPrefix}${newVersion}
397
437
 
398
438
  Released: ${dateShort} at ${timestamp.split(' ')[1]}${author ? `\nAuthor: ${author}` : ''}
399
439
 
400
440
  ## Changes
401
- ${message}${releaseNotesContext ? `\n\n## Release Notes\n${releaseNotesContext}` : ''}
441
+ ${changes}${context ? `\n\n## Release Notes\n${context}` : ''}
402
442
 
403
443
  ## Installation
404
444
  \`\`\`bash
@@ -406,81 +446,79 @@ npm install ${packageJson.name}@${newVersion}
406
446
  \`\`\`
407
447
 
408
448
  ## Full Changelog
409
- See [CHANGELOG.md](./CHANGELOG.md) for complete version history.
449
+ See [CHANGELOG.md](../CHANGELOG.md) for complete version history.
410
450
  `;
411
451
 
412
- const releaseNotesDir = 'release-notes';
413
- if (!fs.existsSync(releaseNotesDir)) {
414
- fs.mkdirSync(releaseNotesDir);
415
- }
452
+ const dir = 'release-notes';
453
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
416
454
 
417
- const filename = `${releaseNotesDir}/${config.tagPrefix}${newVersion}.md`;
418
- fs.writeFileSync(filename, releaseNotes);
419
- log(` Created: ${filename}`);
420
-
421
- // Stage and amend commit to include release notes
422
- execSync(`git add ${filename}`, {stdio: 'pipe'});
423
- execSync(`git commit --amend --no-edit`, {stdio: 'pipe'});
424
- }
455
+ const filename = `${dir}/${config.tagPrefix}${newVersion}.md`;
456
+ fs.writeFileSync(filename, notes);
457
+ log(` Created: ${filename}`);
425
458
 
426
- // PUSH TO REMOTE
427
- if (push) {
428
- log('🚀 Pushing to remote...', 'cyan');
429
- execSync('git push --follow-tags', {stdio: quietMode ? 'pipe' : 'inherit'});
430
-
431
- // If --publish flag, also push a publish/v* tag to trigger npm workflow
432
- if (publishToNpm) {
433
- log('📦 Pushing publish tag to trigger npm release...', 'cyan');
434
- const publishTag = `publish/${config.tagPrefix}${newVersion}`;
435
- execSync(`git tag ${publishTag}`, {stdio: 'pipe'});
436
- execSync(`git push origin ${publishTag}`, {stdio: quietMode ? 'pipe' : 'inherit'});
437
- }
438
- }
439
-
440
- // STATS/SUMMARY
441
- log('\n📊 Summary:', 'cyan');
442
- log('━'.repeat(50), 'gray');
459
+ execSync(`git add ${filename}`, {stdio: 'pipe'});
460
+ execSync('git commit --amend --no-edit', {stdio: 'pipe'});
461
+ }
443
462
 
444
- log(`\n📦 Version: ${oldVersion} → ${newVersion}`, 'green');
445
- log(`💬 Message: ${message}`);
446
- log(`🏷️ Tag: ${config.tagPrefix}${newVersion}`);
447
- log(`🌿 Branch: ${branch}`);
463
+ // =============================================================================
464
+ // Push to Remote
465
+ // =============================================================================
448
466
 
449
- if (generateChangelog) {
450
- log(`📄 Changelog: Updated`);
451
- }
467
+ function pushToRemote(opts, newVersion) {
468
+ log('🚀 Pushing to remote...', 'cyan');
469
+ execSync('git push --follow-tags', {stdio: quietMode ? 'pipe' : 'inherit'});
452
470
 
453
- if (generateReleaseNotes) {
454
- log(`📋 Release notes: Generated`);
455
- }
471
+ if (opts.publishToNpm) {
472
+ log('📦 Pushing publish tag to trigger npm release...', 'cyan');
473
+ const publishTag = `publish/${config.tagPrefix}${newVersion}`;
474
+ execSync(`git tag ${publishTag}`, {stdio: 'pipe'});
475
+ execSync(`git push origin ${publishTag}`, {stdio: quietMode ? 'pipe' : 'inherit'});
476
+ }
477
+ }
456
478
 
457
- if (push) {
458
- log(`🚀 Remote: Pushed with tags`, 'green');
459
- if (publishToNpm) {
460
- log(`📦 npm: Publishing triggered (publish/${config.tagPrefix}${newVersion})`, 'green');
461
- }
462
- } else {
463
- log(`📍 Remote: Not pushed (use --push to enable)`, 'gray');
479
+ // =============================================================================
480
+ // Print Summary
481
+ // =============================================================================
482
+
483
+ function printSummary(opts, oldVersion, newVersion, branch) {
484
+ log('\n📊 Summary:', 'cyan');
485
+ log('━'.repeat(50), 'gray');
486
+ log(`\n📦 Version: ${oldVersion} → ${newVersion}`, 'green');
487
+ log(`💬 Message: ${opts.message}`);
488
+ log(`🏷️ Tag: ${config.tagPrefix}${newVersion}`);
489
+ log(`🌿 Branch: ${branch}`);
490
+
491
+ if (opts.generateChangelog) log('📄 Changelog: Updated');
492
+ if (opts.generateReleaseNotes) log('📋 Release notes: Generated');
493
+
494
+ if (opts.push) {
495
+ log('🚀 Remote: Pushed with tags', 'green');
496
+ if (opts.publishToNpm) {
497
+ log(`📦 npm: Publishing triggered (publish/${config.tagPrefix}${newVersion})`, 'green');
464
498
  }
499
+ } else {
500
+ log('📍 Remote: Not pushed (use --push to enable)', 'gray');
501
+ }
465
502
 
466
- // Show files changed
467
- if (!quietMode) {
503
+ if (!quietMode) {
504
+ try {
468
505
  log('\n📝 Files changed:');
469
506
  const diff = execSync('git diff HEAD~1 --stat').toString();
470
507
  console.log(diff);
508
+ } catch {
509
+ // No previous commit to diff against
471
510
  }
472
-
473
- log('━'.repeat(50), 'gray');
474
- log('\n✅ Version bump complete!\n', 'green');
475
-
476
- } catch (error) {
477
- logError('\n❌ Error: ' + error.message);
478
- process.exit(1);
479
511
  }
512
+
513
+ log('━'.repeat(50), 'gray');
514
+ log('\n✅ Version bump complete!\n', 'green');
480
515
  }
481
516
 
482
- // Show help
483
- if (hasFlag('--help', '-h')) {
517
+ // =============================================================================
518
+ // Help
519
+ // =============================================================================
520
+
521
+ function printHelp() {
484
522
  console.log(`
485
523
  vnxt (vx) - Version Bump CLI Tool
486
524
 
@@ -491,17 +529,18 @@ Usage:
491
529
  Options:
492
530
  -m, --message <msg> Commit message (required, or use interactive mode)
493
531
  -t, --type <type> Version type: patch, minor, major (auto-detected from message)
494
- -v, --version <ver> Set specific version (e.g., 2.0.0-beta.1)
495
- -V, --version Show vnxt version
532
+ -sv, --set-version <v> Set a specific version (e.g., 2.0.0-beta.1)
533
+ -gv, --get-version Show the current project's version
534
+ -vv, --vnxt-version Show the installed vnxt version
496
535
  -p, --push Push to remote with tags
497
536
  -dnp, --no-push Prevent auto-push (overrides config)
498
- --publish Push and trigger npm publish via GitHub Actions
537
+ --publish Push and trigger npm publish via GitHub Actions (implies --push)
499
538
  -c, --changelog Update CHANGELOG.md
500
539
  -d, --dry-run Show what would happen without making changes
501
540
  -a, --all [mode] Stage files before versioning
502
541
  Modes: tracked (default), all, interactive (i), patch (p)
503
542
  If no mode specified, prompts interactively
504
- -r, --release Generate release notes file
543
+ -r, --release Generate release notes file (saved to release-notes/)
505
544
  -q, --quiet Minimal output (errors only)
506
545
  -h, --help Show this help message
507
546
 
@@ -510,7 +549,7 @@ Auto-detection:
510
549
  - "minor:" → minor version
511
550
  - "patch:" → patch version
512
551
  - "feat:" or "feature:" → minor version
513
- - "fix:" → patch version
552
+ - "fix:" → patch version
514
553
  - "BREAKING" or "breaking:" → major version
515
554
 
516
555
  Configuration:
@@ -526,11 +565,12 @@ Configuration:
526
565
  }
527
566
 
528
567
  Examples:
529
- vx -V # Show version
568
+ vx -vv # Show vnxt version
569
+ vx -gv # Show current project version
530
570
  vx -m "fix: resolve bug" # Auto-pushes with autoPush: true
531
571
  vx -m "feat: add new feature" # Auto-pushes with autoPush: true
532
572
  vx -m "fix: bug" -dnp # Don't push (override)
533
- vx -v 2.0.0-beta.1 -m "beta release"
573
+ vx -sv 2.0.0-beta.1 -m "beta release"
534
574
  vx -m "test" -d
535
575
  vx -m "fix: bug" -a # Interactive prompt for staging
536
576
  vx -m "fix: bug" -a tracked # Stage tracked files only
@@ -539,10 +579,75 @@ Examples:
539
579
  vx -m "fix: bug" -a p # Patch mode
540
580
  vx -m "fix: bug" -q # Quiet mode (minimal output)
541
581
  vx -m "feat: new feature" --publish # Bump, push and trigger npm publish
582
+ vx -m "fix: bug" -r # Generate release notes in release-notes/
542
583
  vx # Interactive mode
543
584
  `);
544
- process.exit(0);
545
585
  }
546
586
 
547
- // Run main function
587
+ // =============================================================================
588
+ // Main
589
+ // =============================================================================
590
+
591
+ async function main() {
592
+ try {
593
+ handleQuickFlags();
594
+
595
+ // Git repo check
596
+ if (!fs.existsSync('.git')) {
597
+ logError('❌ Not a git repository. Run `git init` first.');
598
+ process.exit(1);
599
+ }
600
+
601
+ const opts = parseArgs();
602
+
603
+ // Interactive mode if no message provided
604
+ if (!opts.message) {
605
+ await runInteractiveMode(opts);
606
+ }
607
+
608
+ // Auto-detect version type from commit message
609
+ if (!opts.customVersion && !getFlag('--type', '-t')) {
610
+ opts.type = detectVersionType(opts.message, opts.type);
611
+ }
612
+
613
+ // Validate version type
614
+ if (!opts.customVersion && !['patch', 'minor', 'major'].includes(opts.type)) {
615
+ logError('Error: Version type must be patch, minor, or major');
616
+ process.exit(1);
617
+ }
618
+
619
+ // Release notes context prompt
620
+ let releaseNotesContext = '';
621
+ if (!opts.generateReleaseNotes && opts.publishToNpm) {
622
+ opts.generateReleaseNotes = true;
623
+ if (!quietMode) {
624
+ log('\n📋 Release notes required for --publish.', 'yellow');
625
+ releaseNotesContext = await prompt(' Add context (press Enter to skip): ');
626
+ if (releaseNotesContext) log('');
627
+ }
628
+ } else if (opts.generateReleaseNotes && !quietMode) {
629
+ releaseNotesContext = await prompt('\n📋 Add context to release notes (press Enter to skip): ');
630
+ if (releaseNotesContext) log('');
631
+ }
632
+
633
+ const branch = await runPreflightChecks(opts);
634
+
635
+ if (opts.dryRun) runDryRun(opts);
636
+
637
+ if (opts.addMode) stageFiles(opts.addMode);
638
+
639
+ const { oldVersion, newVersion, packageJson } = bumpVersion(opts);
640
+
641
+ if (opts.generateChangelog) generateChangelog(newVersion, opts.message);
642
+ if (opts.generateReleaseNotes) generateReleaseNotes(newVersion, opts.message, releaseNotesContext, packageJson, opts.publishToNpm);
643
+ if (opts.push) pushToRemote(opts, newVersion);
644
+
645
+ printSummary(opts, oldVersion, newVersion, branch);
646
+
647
+ } catch (error) {
648
+ logError('\n❌ Error: ' + error.message);
649
+ process.exit(1);
650
+ }
651
+ }
652
+
548
653
  main();