@gitset-dev/cli 1.0.0 → 1.2.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 +191 -1
  2. package/package.json +1 -1
  3. package/src/index.js +371 -97
package/README.md CHANGED
@@ -1 +1,191 @@
1
- # gitset-cli
1
+ <div align="center">
2
+ <img src="https://github.com/imprvhub/gitset/blob/main/public/favicon-192.png" alt="GitSet CLI" />
3
+ <br>
4
+ <a href="https://badge.fury.io/js/@gitset-dev%2Fcli">
5
+ <img src="https://img.shields.io/npm/v/@gitset-dev/cli?color=%237BFEF5" alt="npm version" />
6
+ </a>
7
+ <a href="https://opensource.org/licenses/MPL-2.0">
8
+ <img src="https://img.shields.io/badge/License-MPL_2.0-%237BFEF5" alt="License: MPL 2.0" />
9
+ </a>
10
+ <br>
11
+ <br>
12
+ <h3>
13
+ GitSet CLI - AI-Driven Commit Message Generation
14
+ </h3>
15
+ </div>
16
+
17
+ ## Overview
18
+
19
+ GitSet CLI is an integral component of the GitSet.dev ecosystem, designed to enhance Git workflow automation through AI-driven commit message generation. By leveraging Google's Gemini Pro AI technology, it provides intelligent analysis of staged changes to generate contextually appropriate commit messages, supporting both semantic and personalized formatting styles.
20
+
21
+ ## Key Capabilities
22
+
23
+ The GitSet CLI enhances repository management through:
24
+
25
+ - **AI-Powered Analysis**: Utilizes advanced AI processing to analyze staged changes and generate contextually appropriate commit messages
26
+ - **Semantic Versioning Support**: Implements conventional commit standards for maintaining structured version control
27
+ - **Style Adaptation**: Analyzes existing commit patterns to match personal or team commit message conventions
28
+ - **Efficient Processing**: Provides rapid analysis and suggestion generation while maintaining minimal resource utilization
29
+ - **Cross-Platform Architecture**: Ensures consistent operation across various operating systems and environments
30
+ - **License Management**: Flexible licensing system with both free and pro tiers for different usage needs
31
+ - **Usage Tracking**: Built-in monitoring of API usage and license status
32
+
33
+ ## System Requirements
34
+
35
+ - Node.js Runtime Environment (Version 18.0.0 or higher)
36
+ - Git (Installed and configured)
37
+ - Active internet connection for AI processing
38
+
39
+ ## Installation Process
40
+
41
+ Install the GitSet CLI globally via npm:
42
+
43
+ ```bash
44
+ npm install -g @gitset-dev/cli
45
+ ```
46
+
47
+ ## Implementation Guide
48
+
49
+ ### Basic Usage
50
+
51
+ 1. Stage your modifications:
52
+ ```bash
53
+ git add .
54
+ ```
55
+
56
+ 2. Generate commit message suggestions:
57
+ ```bash
58
+ # Semantic versioning format (default)
59
+ gitset suggest
60
+
61
+ # Custom formatting style
62
+ gitset suggest --mode custom
63
+ ```
64
+
65
+ 3. Implement the generated message:
66
+ ```bash
67
+ git commit -m "generated_message"
68
+ ```
69
+
70
+ ### License Management
71
+
72
+ Activate your GitSet license:
73
+ ```bash
74
+ # Activate with license key
75
+ gitset activate <license-key>
76
+
77
+ # Check license status and usage
78
+ gitset status
79
+
80
+ # Remove current license
81
+ gitset deactivate
82
+ ```
83
+
84
+ ### Operational Modes
85
+
86
+ #### Semantic Mode (Default Implementation)
87
+ Implements conventional commit standards to generate structured, semantic commit messages. This mode is optimized for maintaining consistent and professional Git history in enterprise environments.
88
+
89
+ Example output:
90
+ ```bash
91
+ $ gitset suggest
92
+ ✨ Generated Suggestion:
93
+ ------------------------
94
+ feat: Implement JWT authentication system
95
+
96
+ - Add token generation and validation mechanisms
97
+ - Integrate login and registration endpoints
98
+ - Configure route protection middleware
99
+ ```
100
+
101
+ #### Custom Mode
102
+ Analyzes existing commit patterns to generate messages that align with established conventions:
103
+
104
+ - Evaluates recent commit history (default: 20 commits) for pattern recognition
105
+ - Adapts to existing formatting conventions and structural patterns
106
+ - Maintains sequential naming conventions if detected
107
+ - Preserves emoji usage patterns and placement
108
+ - Replicates capitalization and punctuation styles
109
+ - Balances descriptive content with stylistic consistency
110
+
111
+ Example of style adaptation:
112
+ ```bash
113
+ # Given existing commit pattern:
114
+ FEATURE_123: Enhanced login interface 🚀
115
+ FEATURE_124: Updated navigation system ✨
116
+ FEATURE_125: Resolved routing conflicts 🔧
117
+
118
+ # Generated suggestion maintains consistency:
119
+ FEATURE_126: Implemented user preferences 🎯
120
+ ```
121
+
122
+ ## Configuration Reference
123
+
124
+ ### Command Structure
125
+
126
+ Available commands:
127
+ - `gitset suggest` - Initiates commit message generation based on staged changes
128
+ - `gitset activate` - Activates GitSet with a license key
129
+ - `gitset status` - Checks current license status and usage
130
+ - `gitset deactivate` - Removes current license configuration
131
+ - `gitset help` - Displays detailed usage information
132
+
133
+ ### Available Parameters
134
+
135
+ For suggest command:
136
+ - `--mode <mode>` - Specifies generation mode ('semantic' or 'custom')
137
+ - `--commit-count <count>` - Defines number of commits to analyze (default: 20)
138
+ - `--version` - Displays CLI version information
139
+ - `--help` - Provides command usage information
140
+
141
+ ## Plans and Pricing
142
+
143
+ - **Basic (Free)**
144
+ - 10 requests per month
145
+ - Basic commit message generation
146
+ - Semantic and custom modes support
147
+
148
+ - **Pro**
149
+ - Unlimited requests
150
+ - Advanced features and priority support
151
+ - Visit https://gitset.dev/pricing for details
152
+
153
+ ## Development Contribution
154
+
155
+ We welcome contributions to enhance the GitSet CLI. Please follow these steps:
156
+
157
+ 1. Fork the repository
158
+ 2. Create a feature branch:
159
+ ```bash
160
+ git checkout -b feature/enhancement-description
161
+ ```
162
+ 3. Implement modifications:
163
+ ```bash
164
+ git commit -m 'feat: Add enhancement description'
165
+ ```
166
+ 4. Push changes:
167
+ ```bash
168
+ git push origin feature/enhancement-description
169
+ ```
170
+ 5. Submit a Pull Request
171
+
172
+ ## License Information
173
+
174
+ This project operates under the Mozilla Public License 2.0 - refer to [LICENSE.md](LICENSE.md) for detailed terms.
175
+
176
+ ## Support Channels
177
+
178
+ - Technical Support: support@gitset.dev
179
+ - Contact Form: https://gitset.dev/contact
180
+ - Issue Tracking: https://github.com/gitset-dev/gitset-cli/issues
181
+ - Account Management: https://gitset.dev/account
182
+
183
+ ## Acknowledgments
184
+
185
+ - Contributors who have helped improve this tool
186
+ - Commander.js for CLI framework support
187
+ - Google's Gemini Pro for AI capabilities
188
+
189
+ ---
190
+
191
+ Part of the [GitSet.dev](https://gitset.dev) ecosystem - Smart AI Documentation & Version Control for GitHub Repositories.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitset-dev/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Generate semantic commit messages using AI-driven analysis of staged code changes.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -1,11 +1,31 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { exec } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
  import fetch from 'node-fetch';
5
5
  import { program } from 'commander';
6
6
  import path from 'path';
7
+ import os from 'os';
8
+ import fs from 'fs/promises';
9
+ import crypto from 'crypto';
7
10
 
8
11
  const execAsync = promisify(exec);
12
+ const API_URL = 'https://gitset-commit-messages.vercel.app';
13
+
14
+ const CONFIG_LOCATIONS = {
15
+ global: {
16
+ dir: path.join(os.homedir(), '.config', 'gitset'),
17
+ file: 'credentials.json'
18
+ },
19
+ local: {
20
+ dir: path.join(process.cwd(), '.gitset'),
21
+ file: 'credentials.json'
22
+ },
23
+ system: {
24
+ darwin: path.join(os.homedir(), 'Library', 'Application Support', 'GitSet'),
25
+ win32: path.join(process.env.APPDATA || '', 'GitSet'),
26
+ linux: path.join(os.homedir(), '.local', 'share', 'gitset')
27
+ }
28
+ };
9
29
 
10
30
  const colors = {
11
31
  reset: '\x1b[0m',
@@ -27,60 +47,115 @@ function log(step, message, isError = false) {
27
47
  console.log(`${prefix} ${colors.reset}[${timestamp}] ${stepColor}${step}${colors.reset}: ${message}`);
28
48
  }
29
49
 
30
- async function getGitDiff(file) {
50
+ function getConfigPath() {
51
+ const localPath = path.join(CONFIG_LOCATIONS.local.dir, CONFIG_LOCATIONS.local.file);
31
52
  try {
32
- log('Diff', `Getting differences for ${file}...`);
33
- const { stdout: lastCommit } = await execAsync('git rev-parse HEAD');
34
-
35
- let previousContent = '';
36
- try {
37
- const { stdout } = await execAsync(`git show ${lastCommit.trim()}:${file}`);
38
- const lineCount = stdout.split('\n').length;
39
- if (lineCount > MAX_LINES) {
40
- log('Diff', `Skipping ${file}: exceeds ${MAX_LINES} lines (has ${lineCount} lines)`, true);
41
- return null;
42
- }
43
- previousContent = stdout;
44
- log('Diff', `✓ Retrieved previous content (${stdout.length} bytes, ${lineCount} lines)`);
45
- } catch (e) {
46
- log('Diff', `New file detected: ${file}`);
53
+ if (fs.existsSync(localPath)) {
54
+ return localPath;
47
55
  }
56
+ } catch (error) {}
48
57
 
49
- const { stdout: currentContent } = await execAsync(`git show :${file}`);
50
- const currentLineCount = currentContent.split('\n').length;
51
- if (currentLineCount > MAX_LINES) {
52
- log('Diff', `Skipping ${file}: exceeds ${MAX_LINES} lines (has ${currentLineCount} lines)`, true);
53
- return null;
58
+ const globalPath = path.join(CONFIG_LOCATIONS.global.dir, CONFIG_LOCATIONS.global.file);
59
+ try {
60
+ if (fs.existsSync(globalPath)) {
61
+ return globalPath;
54
62
  }
55
- log('Diff', `✓ Retrieved current content (${currentContent.length} bytes, ${currentLineCount} lines)`);
63
+ } catch (error) {}
56
64
 
57
- return {
58
- previous: previousContent,
59
- current: currentContent
60
- };
65
+ const systemDir = CONFIG_LOCATIONS.system[process.platform] || CONFIG_LOCATIONS.system.linux;
66
+ return path.join(systemDir, CONFIG_LOCATIONS.local.file);
67
+ }
68
+
69
+ async function ensureConfigDir() {
70
+ const configPath = getConfigPath();
71
+ const configDir = path.dirname(configPath);
72
+ try {
73
+ await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
61
74
  } catch (error) {
62
- log('Diff', `Error processing ${file}: ${error.message}`, true);
63
- return null;
75
+ if (error.code !== 'EEXIST') {
76
+ throw error;
77
+ }
64
78
  }
79
+ return configPath;
65
80
  }
66
81
 
67
- async function getRepoInfo() {
82
+ async function saveConfig(config) {
83
+ const configPath = await ensureConfigDir();
84
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
85
+ }
86
+
87
+ async function loadConfig() {
88
+ const storedConfig = {
89
+ licenseKey: null,
90
+ lastValidation: null,
91
+ installationId: null
92
+ };
93
+
68
94
  try {
69
- log('Repo', 'Retrieving repository information...');
70
- const { stdout: remoteUrl } = await execAsync('git config --get remote.origin.url');
71
- const repoPath = remoteUrl.trim()
72
- .replace('git@github.com:', '')
73
- .replace('https://github.com/', '')
74
- .replace('.git', '');
75
- log('Repo', `✓ Repository identified: ${repoPath}`);
76
- return repoPath;
95
+ const configPath = getConfigPath();
96
+ const configData = await fs.readFile(configPath, 'utf8');
97
+ const parsedConfig = JSON.parse(configData);
98
+
99
+ if (!parsedConfig.installationId) {
100
+ parsedConfig.installationId = crypto.randomUUID();
101
+ await saveConfig(parsedConfig);
102
+ }
103
+
104
+ return parsedConfig;
77
105
  } catch (error) {
78
- log('Repo', `Error: ${error.message}`, true);
79
- return '';
106
+ if (error.code === 'ENOENT') {
107
+
108
+ storedConfig.installationId = crypto.randomUUID();
109
+ await saveConfig(storedConfig);
110
+ return storedConfig;
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ async function makeApiRequest(endpoint, data) {
117
+ const config = await loadConfig();
118
+ const headers = {
119
+ 'Content-Type': 'application/json'
120
+ };
121
+
122
+ if (config.licenseKey) {
123
+ headers['X-License-Key'] = config.licenseKey;
124
+ }
125
+
126
+ if (!config.installationId) {
127
+ config.installationId = crypto.randomUUID();
128
+ await saveConfig(config);
80
129
  }
130
+
131
+ headers['X-Installation-Id'] = config.installationId;
132
+
133
+ const response = await fetch(`${API_URL}${endpoint}`, {
134
+ method: 'POST',
135
+ headers,
136
+ body: JSON.stringify(data)
137
+ });
138
+
139
+ if (!response.ok) {
140
+ const errorData = await response.json();
141
+
142
+ if (response.status === 429 && errorData.status === 'quota_exceeded') {
143
+ log('Quota', 'Monthly request limit reached:', true);
144
+ log('Info', errorData.message);
145
+ log('Info', `Next reset date: ${new Date(errorData.next_reset_date).toLocaleDateString()}`);
146
+ log('Info', `Remaining days: ${errorData.days_until_reset}`);
147
+ log('Upgrade', 'To remove limits, activate a pro license:');
148
+ log('Command', ' gitset activate <license-key>');
149
+ log('Purchase', 'Visit https://gitset.dev/pricing to get a license');
150
+ process.exit(1);
151
+ }
152
+
153
+ throw new Error(errorData.message || 'Failed to make request');
154
+ }
155
+
156
+ return response.json();
81
157
  }
82
158
 
83
- // Files that should typically be ignored
84
159
  const IGNORED_FILES = [
85
160
  'package-lock.json',
86
161
  'yarn.lock',
@@ -96,8 +171,6 @@ const IGNORED_FILES = [
96
171
  'coverage'
97
172
  ];
98
173
 
99
- const MAX_LINES = 3000;
100
-
101
174
  function shouldIgnoreFile(filePath) {
102
175
  return IGNORED_FILES.some(ignored =>
103
176
  filePath === ignored ||
@@ -108,9 +181,49 @@ function shouldIgnoreFile(filePath) {
108
181
  );
109
182
  }
110
183
 
184
+ async function getLastCommits(count = 20) {
185
+ try {
186
+ log('History', `Fetching last ${count} commits...`);
187
+ const { stdout } = await execAsync(`git log -${count} --pretty=format:"%s"`);
188
+ const commits = stdout.split('\n');
189
+ log('History', `✓ Retrieved ${commits.length} commits`);
190
+ return commits;
191
+ } catch (error) {
192
+ log('History', `Error fetching commit history: ${error.message}`, true);
193
+ return [];
194
+ }
195
+ }
196
+
197
+ async function getGitDiff(file) {
198
+ try {
199
+ log('Diff', `Getting differences for ${file}...`);
200
+ const { stdout: lastCommit } = await execAsync('git rev-parse HEAD');
201
+
202
+ let previousContent = '';
203
+ try {
204
+ const { stdout } = await execAsync(`git show ${lastCommit.trim()}:${file}`);
205
+ previousContent = stdout;
206
+ log('Diff', `✓ Retrieved previous content`);
207
+ } catch (e) {
208
+ log('Diff', `New file detected: ${file}`);
209
+ }
210
+
211
+ const { stdout: currentContent } = await execAsync(`git show :${file}`);
212
+ log('Diff', `✓ Retrieved current content`);
213
+
214
+ return {
215
+ previous: previousContent,
216
+ current: currentContent
217
+ };
218
+ } catch (error) {
219
+ log('Diff', `Error processing ${file}: ${error.message}`, true);
220
+ return null;
221
+ }
222
+ }
223
+
111
224
  async function getStagedFiles() {
112
225
  try {
113
- log('Git', 'Looking for staged files...');
226
+ log('Git', 'Retrieving staged files...');
114
227
  const { stdout } = await execAsync('git diff --cached --name-status');
115
228
  const files = stdout.split('\n')
116
229
  .filter(line => line)
@@ -129,9 +242,26 @@ async function getStagedFiles() {
129
242
  }
130
243
  }
131
244
 
132
- async function generateCommitMessage() {
245
+ async function getRepoInfo() {
246
+ try {
247
+ log('Repo', 'Retrieving repository information...');
248
+ const { stdout: remoteUrl } = await execAsync('git config --get remote.origin.url');
249
+ const repoPath = remoteUrl.trim()
250
+ .replace('git@github.com:', '')
251
+ .replace('https://github.com/', '')
252
+ .replace('.git', '');
253
+ log('Repo', `✓ Repository identified: ${repoPath}`);
254
+ return repoPath;
255
+ } catch (error) {
256
+ log('Repo', `Error: ${error.message}`, true);
257
+ return '';
258
+ }
259
+ }
260
+
261
+ async function generateCommitMessage(options) {
133
262
  try {
134
263
  log('Start', 'Starting commit message generation process...');
264
+ const { mode = 'semantic', commitCount = 20 } = options;
135
265
 
136
266
  const files = await getStagedFiles();
137
267
  if (files.length === 0) {
@@ -149,71 +279,52 @@ async function generateCommitMessage() {
149
279
  console.log(` ${statusText}${colors.reset}: ${file}`);
150
280
  });
151
281
 
152
- const fileChanges = (await Promise.all(
153
- files.map(async ({ status, file }) => {
154
- const diffResult = await getGitDiff(file);
155
- if (!diffResult) return null;
156
-
157
- const { previous, current } = diffResult;
158
- const extension = path.extname(file).toLowerCase();
159
-
160
- return {
161
- name: path.basename(file),
162
- path: file,
163
- changeType: status === 'A' ? 'added' : status === 'M' ? 'modified' : 'deleted',
164
- contentType: ['.jpg', '.png', '.gif', '.pdf'].includes(extension) ? 'binary' : 'text',
165
- changes: {
166
- before: previous,
167
- after: current
168
- }
169
- };
170
- })
171
- )).filter(change => change !== null);
172
-
173
- const repoName = await getRepoInfo();
174
- log('API', 'Processing diffs with AI...');
175
- log('NOTE', `The following commit message is a general reference and may not be fully accurate. If it doesn't meet your expectations, try again or suggest improvements here: https://gitset.dev/contact`)
176
- const response = await fetch('https://gitset-commit-messages.vercel.app/generate-commit-message', {
177
- method: 'POST',
178
- headers: { 'Content-Type': 'application/json' },
179
- body: JSON.stringify({
180
- repo_name: repoName,
181
- file_changes: fileChanges
182
- })
183
- });
282
+ const fileChanges = [];
283
+ for (const { status, file } of files) {
284
+ const diffResult = await getGitDiff(file);
285
+ if (!diffResult) continue;
184
286
 
185
- if (!response.ok) {
186
- const errorData = await response.text();
187
- let errorMessage;
188
-
189
- try {
190
- const parsedError = JSON.parse(errorData);
191
- if (response.status === 500 && parsedError.message?.includes('429')) {
192
- errorMessage = `${colors.yellow}The API rate limit has been reached. Please try again in a few minutes.${colors.reset}`;
193
- } else {
194
- errorMessage = `Server error (${response.status}): ${parsedError.message || errorData}`;
287
+ fileChanges.push({
288
+ name: path.basename(file),
289
+ path: file,
290
+ changeType: status === 'A' ? 'added' : status === 'M' ? 'modified' : 'deleted',
291
+ contentType: 'text',
292
+ changes: {
293
+ before: diffResult.previous,
294
+ after: diffResult.current
195
295
  }
196
- } catch {
197
- errorMessage = `Server error (${response.status}): ${errorData}`;
198
- }
199
-
200
- throw new Error(errorMessage);
296
+ });
201
297
  }
202
298
 
203
- const { commit_message } = await response.json();
299
+ const repoName = await getRepoInfo();
300
+ const commitHistory = mode === 'custom' ? await getLastCommits(parseInt(commitCount)) : [];
301
+
302
+ log('API', `Processing diffs with AI using ${mode} mode...`);
303
+
304
+ const response = await makeApiRequest('/generate-commit-message', {
305
+ repo_name: repoName,
306
+ file_changes: fileChanges,
307
+ mode: mode,
308
+ commit_history: commitHistory
309
+ });
310
+
204
311
  log('Success', '✨ Commit message generated\n');
312
+
205
313
  function formatCommitMessage(message) {
206
314
  const [title, ...descriptionParts] = message.split('\n');
207
315
  const description = descriptionParts.join('\n');
208
316
  return `${colors.bright}${colors.customCyan}${title}${colors.reset}${description ? `\n${colors.darkCyan}${description}${colors.reset}` : ''}`;
209
317
  }
210
318
 
211
- console.log(`${colors.bright}📝 Suggested message:${colors.reset}`);
319
+ console.log(`${colors.bright}📝 Suggested message (${mode} mode):${colors.reset}`);
212
320
  console.log(`${colors.yellow}------------------${colors.reset}`);
213
- console.log(formatCommitMessage(commit_message));
321
+ console.log(formatCommitMessage(response.commit_message));
214
322
 
215
323
  } catch (error) {
216
324
  log('Error', error.message, true);
325
+ if (error.message.includes('permission denied')) {
326
+ log('Help', 'Make sure you have the necessary permissions and are in a git repository.');
327
+ }
217
328
  process.exit(1);
218
329
  }
219
330
  }
@@ -221,11 +332,174 @@ async function generateCommitMessage() {
221
332
  program
222
333
  .name('gitset')
223
334
  .description('Smart AI Docs & Versioning for GitHub Repositories.')
224
- .version('1.0.0');
335
+ .version('1.1.0');
225
336
 
226
337
  program
338
+ .command('activate')
339
+ .description('Activate GitSet with a license key')
340
+ .argument('[licenseKey]', 'Your GitSet license key')
341
+ .action(async (licenseKey) => {
342
+ try {
343
+ if (!licenseKey) {
344
+ const config = await loadConfig();
345
+ if (config.licenseKey) {
346
+ log('License', 'Current license status:');
347
+ const status = await makeApiRequest('/validate-license', { licenseKey: config.licenseKey });
348
+ log('Status', `Plan: ${status.data.product_name}`);
349
+ log('Status', `Valid until: ${new Date(status.data.renews_at).toLocaleDateString()}`);
350
+ } else {
351
+ log('License', 'No license key configured', true);
352
+ log('Info', 'You can use GitSet with limited features (10 requests/month)');
353
+ log('Info', 'To unlock unlimited usage, activate a pro license:');
354
+ log('Command', ' gitset activate <license-key>');
355
+ log('Purchase', 'Visit https://gitset.dev/pricing to get a license');
356
+ }
357
+ return;
358
+ }
359
+
360
+ const response = await makeApiRequest('/validate-license', { licenseKey });
361
+ const config = await loadConfig();
362
+ await saveConfig({
363
+ ...config, // Mantiene el installationId existente
364
+ licenseKey,
365
+ lastValidation: new Date().toISOString(),
366
+ subscriptionData: response.data
367
+ });
368
+ log('Success', '✨ License activated successfully!');
369
+ log('Plan', response.data.product_name);
370
+ log('Info', 'You now have unlimited access to all features');
371
+
372
+ } catch (error) {
373
+ log('Error', error.message, true);
374
+ process.exit(1);
375
+ }
376
+ });
377
+
378
+ program
227
379
  .command('suggest')
228
- .description('Generate semantic commit messages using AI-driven analysis of staged code changes.')
380
+ .description('Generate commit messages using AI-driven analysis of staged code changes.')
381
+ .option('-m, --mode <mode>', 'Commit message mode (semantic or custom)', 'semantic')
382
+ .option('-c, --commit-count <count>', 'Number of previous commits to analyze for custom mode', '20')
229
383
  .action(generateCommitMessage);
230
384
 
385
+ program
386
+ .command('status')
387
+ .description('Check your GitSet license status and usage')
388
+ .action(async () => {
389
+ try {
390
+ const config = await loadConfig();
391
+
392
+ if (!config.licenseKey) {
393
+ log('License', 'No license key configured');
394
+ log('Info', 'You are using GitSet with limited features (10 requests/month)');
395
+ log('Info', 'To unlock unlimited usage, activate a pro license:');
396
+ log('Command', ' gitset activate <license-key>');
397
+ log('Purchase', 'Visit https://gitset.dev/pricing to get a license');
398
+ return;
399
+ }
400
+
401
+ const response = await makeApiRequest('/validate-license', { licenseKey: config.licenseKey });
402
+
403
+ log('License', 'License is active and valid ✨');
404
+ log('Plan', response.data.product_name);
405
+ log('Status', response.data.status);
406
+ if (response.data.renews_at) {
407
+ log('Renewal', `Next renewal: ${new Date(response.data.renews_at).toLocaleDateString()}`);
408
+ }
409
+
410
+ if (!response.data.product_name.toLowerCase().includes('pro')) {
411
+ log('Usage', 'Request limits (Basic Plan):');
412
+ const usageResponse = await makeApiRequest('/usage-info', { licenseKey: config.licenseKey });
413
+ log('Info', `Used ${usageResponse.current_usage} of ${usageResponse.limit} monthly requests`);
414
+ log('Info', `Reset date: ${new Date(usageResponse.next_reset_date).toLocaleDateString()}`);
415
+ } else {
416
+ log('Usage', 'Unlimited requests (Pro Plan)');
417
+ }
418
+
419
+ } catch (error) {
420
+ log('Error', error.message, true);
421
+ if (error.message.includes('validation')) {
422
+ log('Help', 'Your license might have expired or been deactivated');
423
+ log('Info', 'You can check your subscription status at https://gitset.dev/account');
424
+ }
425
+ }
426
+ });
427
+
428
+ program
429
+ .command('deactivate')
430
+ .description('Deactivate and remove current license key')
431
+ .action(async () => {
432
+ try {
433
+ const config = await loadConfig();
434
+
435
+ if (!config.licenseKey) {
436
+ log('Info', 'No active license found');
437
+ return;
438
+ }
439
+
440
+ await saveConfig({
441
+ ...config,
442
+ licenseKey: null,
443
+ lastValidation: null,
444
+ subscriptionData: null,
445
+ });
446
+ log('Success', 'License deactivated successfully');
447
+ log('Info', 'Switched to basic plan (10 requests/month)');
448
+ log('Info', 'You can reactivate your license anytime with:');
449
+ log('Command', ' gitset activate <license-key>');
450
+
451
+ } catch (error) {
452
+ log('Error', error.message, true);
453
+ process.exit(1);
454
+ }
455
+ });
456
+
457
+ program
458
+ .command('help')
459
+ .description('Show detailed help and usage information')
460
+ .action(() => {
461
+ console.log(`
462
+ ${colors.bright}GitSet CLI - Smart AI Commit Messages${colors.reset}
463
+
464
+ ${colors.cyan}Usage:${colors.reset}
465
+ gitset suggest Generate a commit message for staged changes
466
+ gitset activate <key> Activate GitSet with a license key
467
+ gitset status Check license status and usage
468
+ gitset deactivate Remove current license key
469
+ gitset help Show this help message
470
+
471
+ ${colors.cyan}Options for 'suggest':${colors.reset}
472
+ -m, --mode <mode> Commit message mode (semantic or custom)
473
+ -c, --commit-count <n> Number of commits to analyze in custom mode
474
+
475
+ ${colors.cyan}Plans:${colors.reset}
476
+ Basic (Free) 10 requests per month
477
+ Pro Unlimited requests
478
+
479
+ ${colors.cyan}Examples:${colors.reset}
480
+ $ gitset suggest Generate semantic commit message
481
+ $ gitset suggest -m custom Generate message matching your style
482
+ $ gitset activate ABC123... Activate pro license
483
+ $ gitset status Check current license status
484
+
485
+ ${colors.cyan}Learn more:${colors.reset}
486
+ Visit https://gitset.dev for documentation and pricing
487
+ Report issues at https://github.com/gitset/cli/issues
488
+ `);
489
+ });
490
+
491
+ program.exitOverride((err) => {
492
+ if (err.code === 'commander.help') {
493
+ process.exit(0);
494
+ }
495
+
496
+ log('Error', err.message, true);
497
+
498
+ if (err.code === 'commander.unknownCommand') {
499
+ log('Help', 'Run "gitset help" to see available commands');
500
+ }
501
+
502
+ process.exit(1);
503
+ });
504
+
231
505
  program.parse();