vnxt 1.9.0 → 1.9.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/vnxt.js +446 -353
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vnxt",
3
- "version": "1.9.0",
3
+ "version": "1.9.5",
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,361 @@ 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
+ // =============================================================================
97
+
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
+ }
87
116
 
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);
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'));
396
+
397
+ execSync('git add CHANGELOG.md', {stdio: 'pipe'});
398
+ execSync('git commit --amend --no-edit', {stdio: 'pipe'});
399
+ }
380
400
 
381
- // GENERATE RELEASE NOTES
382
- if (generateReleaseNotes) {
383
- log('šŸ“‹ Generating release notes...', 'cyan');
401
+ // =============================================================================
402
+ // Generate Release Notes
403
+ // =============================================================================
384
404
 
385
- const date = new Date();
386
- const timestamp = date.toISOString().replace('T', ' ').split('.')[0] + ' UTC';
387
- const dateShort = date.toISOString().split('T')[0];
405
+ function generateReleaseNotes(newVersion, message, context, packageJson) {
406
+ log('šŸ“‹ Generating release notes...', 'cyan');
388
407
 
389
- let author = '';
390
- try {
391
- author = execSync('git config user.name', {stdio: 'pipe'}).toString().trim();
392
- } catch {
393
- author = '';
394
- }
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 {}
395
414
 
396
- const releaseNotes = `# Release ${config.tagPrefix}${newVersion}
415
+ const notes = `# Release ${config.tagPrefix}${newVersion}
397
416
 
398
417
  Released: ${dateShort} at ${timestamp.split(' ')[1]}${author ? `\nAuthor: ${author}` : ''}
399
418
 
400
419
  ## Changes
401
- ${message}${releaseNotesContext ? `\n\n## Release Notes\n${releaseNotesContext}` : ''}
420
+ ${message}${context ? `\n\n## Release Notes\n${context}` : ''}
402
421
 
403
422
  ## Installation
404
423
  \`\`\`bash
@@ -406,72 +425,79 @@ npm install ${packageJson.name}@${newVersion}
406
425
  \`\`\`
407
426
 
408
427
  ## Full Changelog
409
- See [CHANGELOG.md](./CHANGELOG.md) for complete version history.
428
+ See [CHANGELOG.md](../CHANGELOG.md) for complete version history.
410
429
  `;
411
430
 
412
- const filename = `release-notes-${config.tagPrefix}${newVersion}.md`;
413
- fs.writeFileSync(filename, releaseNotes);
414
- log(` Created: ${filename}`);
415
- }
431
+ const dir = 'release-notes';
432
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
416
433
 
417
- // PUSH TO REMOTE
418
- if (push) {
419
- log('šŸš€ Pushing to remote...', 'cyan');
420
- execSync('git push --follow-tags', {stdio: quietMode ? 'pipe' : 'inherit'});
421
-
422
- // If --publish flag, also push a publish/v* tag to trigger npm workflow
423
- if (publishToNpm) {
424
- log('šŸ“¦ Pushing publish tag to trigger npm release...', 'cyan');
425
- const publishTag = `publish/${config.tagPrefix}${newVersion}`;
426
- execSync(`git tag ${publishTag}`, {stdio: 'pipe'});
427
- execSync(`git push origin ${publishTag}`, {stdio: quietMode ? 'pipe' : 'inherit'});
428
- }
429
- }
434
+ const filename = `${dir}/${config.tagPrefix}${newVersion}.md`;
435
+ fs.writeFileSync(filename, notes);
436
+ log(` Created: ${filename}`);
430
437
 
431
- // STATS/SUMMARY
432
- log('\nšŸ“Š Summary:', 'cyan');
433
- log('━'.repeat(50), 'gray');
438
+ execSync(`git add ${filename}`, {stdio: 'pipe'});
439
+ execSync('git commit --amend --no-edit', {stdio: 'pipe'});
440
+ }
434
441
 
435
- log(`\nšŸ“¦ Version: ${oldVersion} → ${newVersion}`, 'green');
436
- log(`šŸ’¬ Message: ${message}`);
437
- log(`šŸ·ļø Tag: ${config.tagPrefix}${newVersion}`);
438
- log(`🌿 Branch: ${branch}`);
442
+ // =============================================================================
443
+ // Push to Remote
444
+ // =============================================================================
439
445
 
440
- if (generateChangelog) {
441
- log(`šŸ“„ Changelog: Updated`);
442
- }
446
+ function pushToRemote(opts, newVersion) {
447
+ log('šŸš€ Pushing to remote...', 'cyan');
448
+ execSync('git push --follow-tags', {stdio: quietMode ? 'pipe' : 'inherit'});
443
449
 
444
- if (generateReleaseNotes) {
445
- log(`šŸ“‹ Release notes: Generated`);
446
- }
450
+ if (opts.publishToNpm) {
451
+ log('šŸ“¦ Pushing publish tag to trigger npm release...', 'cyan');
452
+ const publishTag = `publish/${config.tagPrefix}${newVersion}`;
453
+ execSync(`git tag ${publishTag}`, {stdio: 'pipe'});
454
+ execSync(`git push origin ${publishTag}`, {stdio: quietMode ? 'pipe' : 'inherit'});
455
+ }
456
+ }
447
457
 
448
- if (push) {
449
- log(`šŸš€ Remote: Pushed with tags`, 'green');
450
- if (publishToNpm) {
451
- log(`šŸ“¦ npm: Publishing triggered (publish/${config.tagPrefix}${newVersion})`, 'green');
452
- }
453
- } else {
454
- log(`šŸ“ Remote: Not pushed (use --push to enable)`, 'gray');
458
+ // =============================================================================
459
+ // Print Summary
460
+ // =============================================================================
461
+
462
+ function printSummary(opts, oldVersion, newVersion, branch) {
463
+ log('\nšŸ“Š Summary:', 'cyan');
464
+ log('━'.repeat(50), 'gray');
465
+ log(`\nšŸ“¦ Version: ${oldVersion} → ${newVersion}`, 'green');
466
+ log(`šŸ’¬ Message: ${opts.message}`);
467
+ log(`šŸ·ļø Tag: ${config.tagPrefix}${newVersion}`);
468
+ log(`🌿 Branch: ${branch}`);
469
+
470
+ if (opts.generateChangelog) log('šŸ“„ Changelog: Updated');
471
+ if (opts.generateReleaseNotes) log('šŸ“‹ Release notes: Generated');
472
+
473
+ if (opts.push) {
474
+ log('šŸš€ Remote: Pushed with tags', 'green');
475
+ if (opts.publishToNpm) {
476
+ log(`šŸ“¦ npm: Publishing triggered (publish/${config.tagPrefix}${newVersion})`, 'green');
455
477
  }
478
+ } else {
479
+ log('šŸ“ Remote: Not pushed (use --push to enable)', 'gray');
480
+ }
456
481
 
457
- // Show files changed
458
- if (!quietMode) {
482
+ if (!quietMode) {
483
+ try {
459
484
  log('\nšŸ“ Files changed:');
460
485
  const diff = execSync('git diff HEAD~1 --stat').toString();
461
486
  console.log(diff);
487
+ } catch {
488
+ // No previous commit to diff against
462
489
  }
463
-
464
- log('━'.repeat(50), 'gray');
465
- log('\nāœ… Version bump complete!\n', 'green');
466
-
467
- } catch (error) {
468
- logError('\nāŒ Error: ' + error.message);
469
- process.exit(1);
470
490
  }
491
+
492
+ log('━'.repeat(50), 'gray');
493
+ log('\nāœ… Version bump complete!\n', 'green');
471
494
  }
472
495
 
473
- // Show help
474
- if (hasFlag('--help', '-h')) {
496
+ // =============================================================================
497
+ // Help
498
+ // =============================================================================
499
+
500
+ function printHelp() {
475
501
  console.log(`
476
502
  vnxt (vx) - Version Bump CLI Tool
477
503
 
@@ -482,17 +508,18 @@ Usage:
482
508
  Options:
483
509
  -m, --message <msg> Commit message (required, or use interactive mode)
484
510
  -t, --type <type> Version type: patch, minor, major (auto-detected from message)
485
- -v, --version <ver> Set specific version (e.g., 2.0.0-beta.1)
486
- -V, --version Show vnxt version
511
+ -sv, --set-version <v> Set a specific version (e.g., 2.0.0-beta.1)
512
+ -gv, --get-version Show the current project's version
513
+ -vv, --vnxt-version Show the installed vnxt version
487
514
  -p, --push Push to remote with tags
488
515
  -dnp, --no-push Prevent auto-push (overrides config)
489
- --publish Push and trigger npm publish via GitHub Actions
516
+ --publish Push and trigger npm publish via GitHub Actions (implies --push)
490
517
  -c, --changelog Update CHANGELOG.md
491
518
  -d, --dry-run Show what would happen without making changes
492
519
  -a, --all [mode] Stage files before versioning
493
520
  Modes: tracked (default), all, interactive (i), patch (p)
494
521
  If no mode specified, prompts interactively
495
- -r, --release Generate release notes file
522
+ -r, --release Generate release notes file (saved to release-notes/)
496
523
  -q, --quiet Minimal output (errors only)
497
524
  -h, --help Show this help message
498
525
 
@@ -501,7 +528,7 @@ Auto-detection:
501
528
  - "minor:" → minor version
502
529
  - "patch:" → patch version
503
530
  - "feat:" or "feature:" → minor version
504
- - "fix:" → patch version
531
+ - "fix:" → patch version
505
532
  - "BREAKING" or "breaking:" → major version
506
533
 
507
534
  Configuration:
@@ -517,11 +544,12 @@ Configuration:
517
544
  }
518
545
 
519
546
  Examples:
520
- vx -V # Show version
547
+ vx -vv # Show vnxt version
548
+ vx -gv # Show current project version
521
549
  vx -m "fix: resolve bug" # Auto-pushes with autoPush: true
522
550
  vx -m "feat: add new feature" # Auto-pushes with autoPush: true
523
551
  vx -m "fix: bug" -dnp # Don't push (override)
524
- vx -v 2.0.0-beta.1 -m "beta release"
552
+ vx -sv 2.0.0-beta.1 -m "beta release"
525
553
  vx -m "test" -d
526
554
  vx -m "fix: bug" -a # Interactive prompt for staging
527
555
  vx -m "fix: bug" -a tracked # Stage tracked files only
@@ -530,10 +558,75 @@ Examples:
530
558
  vx -m "fix: bug" -a p # Patch mode
531
559
  vx -m "fix: bug" -q # Quiet mode (minimal output)
532
560
  vx -m "feat: new feature" --publish # Bump, push and trigger npm publish
561
+ vx -m "fix: bug" -r # Generate release notes in release-notes/
533
562
  vx # Interactive mode
534
563
  `);
535
- process.exit(0);
536
564
  }
537
565
 
538
- // Run main function
539
- main();
566
+ // =============================================================================
567
+ // Main
568
+ // =============================================================================
569
+
570
+ async function main() {
571
+ try {
572
+ handleQuickFlags();
573
+
574
+ // Git repo check
575
+ if (!fs.existsSync('.git')) {
576
+ logError('āŒ Not a git repository. Run `git init` first.');
577
+ process.exit(1);
578
+ }
579
+
580
+ const opts = parseArgs();
581
+
582
+ // Interactive mode if no message provided
583
+ if (!opts.message) {
584
+ await runInteractiveMode(opts);
585
+ }
586
+
587
+ // Auto-detect version type from commit message
588
+ if (!opts.customVersion && !getFlag('--type', '-t')) {
589
+ opts.type = detectVersionType(opts.message, opts.type);
590
+ }
591
+
592
+ // Validate version type
593
+ if (!opts.customVersion && !['patch', 'minor', 'major'].includes(opts.type)) {
594
+ logError('Error: Version type must be patch, minor, or major');
595
+ process.exit(1);
596
+ }
597
+
598
+ // Release notes context prompt
599
+ let releaseNotesContext = '';
600
+ if (!opts.generateReleaseNotes && opts.publishToNpm) {
601
+ opts.generateReleaseNotes = true;
602
+ if (!quietMode) {
603
+ log('\nšŸ“‹ Release notes required for --publish.', 'yellow');
604
+ releaseNotesContext = await prompt(' Add context (press Enter to skip): ');
605
+ if (releaseNotesContext) log('');
606
+ }
607
+ } else if (opts.generateReleaseNotes && !quietMode) {
608
+ releaseNotesContext = await prompt('\nšŸ“‹ Add context to release notes (press Enter to skip): ');
609
+ if (releaseNotesContext) log('');
610
+ }
611
+
612
+ const branch = await runPreflightChecks(opts);
613
+
614
+ if (opts.dryRun) runDryRun(opts);
615
+
616
+ if (opts.addMode) stageFiles(opts.addMode);
617
+
618
+ const { oldVersion, newVersion, packageJson } = bumpVersion(opts);
619
+
620
+ if (opts.generateChangelog) generateChangelog(newVersion, opts.message);
621
+ if (opts.generateReleaseNotes) generateReleaseNotes(newVersion, opts.message, releaseNotesContext, packageJson);
622
+ if (opts.push) pushToRemote(opts, newVersion);
623
+
624
+ printSummary(opts, oldVersion, newVersion, branch);
625
+
626
+ } catch (error) {
627
+ logError('\nāŒ Error: ' + error.message);
628
+ process.exit(1);
629
+ }
630
+ }
631
+
632
+ main();