gent-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.
package/QUICKSTART.md ADDED
@@ -0,0 +1,198 @@
1
+ # Quick Start Guide - Gent CLI
2
+
3
+ ## Installation & Setup
4
+
5
+ ```bash
6
+ # Navigate to the CLI directory
7
+ cd apps/Cli
8
+
9
+ # Install dependencies
10
+ npm install
11
+
12
+ # Test the CLI
13
+ node src/index.js --help
14
+ ```
15
+
16
+ ## Make it Globally Available (Optional)
17
+
18
+ ```bash
19
+ # Link the CLI globally
20
+ npm link
21
+
22
+ # Now you can use 'gent' from anywhere
23
+ gent --help
24
+ ```
25
+
26
+ ## Your First Repository
27
+
28
+ ### 1. Initialize
29
+ ```bash
30
+ # Create a new directory
31
+ mkdir my-project
32
+ cd my-project
33
+
34
+ # Initialize gent repository
35
+ gent init
36
+ ```
37
+
38
+ You'll be prompted for:
39
+ - Your name
40
+ - Your email
41
+ - Repository name
42
+ - Repository description
43
+
44
+ Or skip prompts with `-y`:
45
+ ```bash
46
+ gent init -y
47
+ ```
48
+
49
+ ### 2. Add Files
50
+ ```bash
51
+ # Create some files
52
+ echo "console.log('Hello');" > index.js
53
+
54
+ # Add to staging area
55
+ gent add index.js
56
+
57
+ # Or add all files
58
+ gent add .
59
+ ```
60
+
61
+ ### 3. Check Status
62
+ ```bash
63
+ gent status
64
+ ```
65
+
66
+ ### 4. Commit Changes
67
+ ```bash
68
+ # With message flag
69
+ gent commit -m "Initial commit"
70
+
71
+ # Or interactive
72
+ gent commit
73
+ ```
74
+
75
+ ### 5. View History
76
+ ```bash
77
+ # See all commits
78
+ gent log
79
+
80
+ # Compact view
81
+ gent log --oneline
82
+
83
+ # Limit commits shown
84
+ gent log -n 5
85
+ ```
86
+
87
+ ## Working with Branches
88
+
89
+ ### Create a Branch
90
+ ```bash
91
+ gent branch feature-name
92
+ ```
93
+
94
+ ### Switch to Branch
95
+ ```bash
96
+ gent checkout feature-name
97
+ ```
98
+
99
+ ### Create and Switch
100
+ ```bash
101
+ gent checkout -b new-feature
102
+ ```
103
+
104
+ ### List Branches
105
+ ```bash
106
+ gent branch
107
+ ```
108
+
109
+ ### Delete Branch
110
+ ```bash
111
+ gent branch -d old-feature
112
+ ```
113
+
114
+ ## Common Workflows
115
+
116
+ ### Feature Development
117
+ ```bash
118
+ # Start a new feature
119
+ gent checkout -b feature-login
120
+
121
+ # Make changes
122
+ echo "// Login code" > login.js
123
+ gent add login.js
124
+ gent commit -m "Add login feature"
125
+
126
+ # View your work
127
+ gent log
128
+
129
+ # Switch back to main
130
+ gent checkout main
131
+ ```
132
+
133
+ ### Quick Commit All
134
+ ```bash
135
+ # Stage and commit all changes
136
+ gent add .
137
+ gent commit -m "Update all files"
138
+ ```
139
+
140
+ ### Check What Changed
141
+ ```bash
142
+ # See status
143
+ gent status
144
+
145
+ # Short format
146
+ gent status -s
147
+ ```
148
+
149
+ ## Tips & Tricks
150
+
151
+ 1. **Use .gentignore** - Exclude files like `node_modules/`
152
+ 2. **Commit Often** - Small commits are easier to track
153
+ 3. **Descriptive Messages** - Write clear commit messages
154
+ 4. **Branch for Features** - Keep main branch stable
155
+ 5. **Check Status** - Always review before committing
156
+
157
+ ## Running the Demo
158
+
159
+ See the CLI in action:
160
+
161
+ ```bash
162
+ chmod +x demo.sh
163
+ ./demo.sh
164
+ ```
165
+
166
+ ## Troubleshooting
167
+
168
+ **Not a gent repository error?**
169
+ ```bash
170
+ # Make sure you initialized
171
+ gent init
172
+ ```
173
+
174
+ **No changes to commit?**
175
+ ```bash
176
+ # Add files first
177
+ gent add <files>
178
+ ```
179
+
180
+ **Command not found?**
181
+ ```bash
182
+ # Use node directly
183
+ node src/index.js <command>
184
+
185
+ # Or link globally
186
+ npm link
187
+ ```
188
+
189
+ ## Next Steps
190
+
191
+ - Read the full [README.md](README.md)
192
+ - Explore the [source code](src/)
193
+ - Try the [demo script](demo.sh)
194
+ - Build your own commands!
195
+
196
+ ---
197
+
198
+ **Happy coding with Gent! šŸš€**
package/package.json CHANGED
@@ -1,23 +1,29 @@
1
1
  {
2
2
  "name": "gent-cli",
3
- "version": "1.0.0",
4
- "description": "A Git-like version control CLI tool",
3
+ "version": "1.2.0",
4
+ "description": "A Git-like version control CLI tool with cloud authentication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "gent": "./src/index.js"
7
+ "gent": "src/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/index.js",
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
12
12
  "demo": "bash demo.sh",
13
13
  "link": "npm link",
14
- "unlink": "npm unlink"
14
+ "unlink": "npm unlink",
15
+ "prepublishOnly": "echo 'Ready to publish gent-cli!'"
15
16
  },
16
17
  "keywords": [
17
18
  "cli",
18
19
  "version-control",
19
20
  "git-like",
20
- "gent"
21
+ "gent",
22
+ "authentication",
23
+ "jwt",
24
+ "api",
25
+ "command-line",
26
+ "developer-tools"
21
27
  ],
22
28
  "author": "Abdalrahman Kanawati <kanawatiabdalrahman@gmail.com>",
23
29
  "license": "ISC",
@@ -30,14 +36,21 @@
30
36
  "url": "https://github.com/SaadShaya7/gent/issues"
31
37
  },
32
38
  "dependencies": {
33
- "commander": "^11.1.0",
39
+ "axios": "^1.6.0",
40
+ "boxen": "^5.1.2",
34
41
  "chalk": "^4.1.2",
42
+ "commander": "^11.1.0",
43
+ "crypto-js": "^4.2.0",
44
+ "date-fns": "^2.30.0",
35
45
  "inquirer": "^8.2.5",
36
- "ora": "^5.4.1",
37
- "boxen": "^5.1.2",
38
- "date-fns": "^2.30.0"
46
+ "ora": "^5.4.1"
39
47
  },
40
48
  "engines": {
41
49
  "node": ">=14.0.0"
42
- }
50
+ },
51
+ "files": [
52
+ "src/",
53
+ "README.md",
54
+ "QUICKSTART.md"
55
+ ]
43
56
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Login Command - Authenticate user
3
+ * Handles user login with email and password
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+ const boxen = require('boxen');
10
+ const authService = require('../services/auth-service');
11
+
12
+ /**
13
+ * Login user
14
+ * @param {Object} options - Command options
15
+ */
16
+ async function login(options) {
17
+ console.log(chalk.cyan('\nšŸ” Login to your Gent account\n'));
18
+
19
+ try {
20
+ let email = options.email;
21
+ let password = options.password;
22
+
23
+ // If credentials not provided via flags, prompt for them
24
+ if (!email || !password) {
25
+ const answers = await inquirer.prompt([
26
+ {
27
+ type: 'input',
28
+ name: 'email',
29
+ message: 'Email address:',
30
+ when: !email,
31
+ validate: (input) => {
32
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+ return emailRegex.test(input) || 'Please enter a valid email address';
34
+ }
35
+ },
36
+ {
37
+ type: 'password',
38
+ name: 'password',
39
+ message: 'Password:',
40
+ when: !password,
41
+ mask: '*'
42
+ }
43
+ ]);
44
+
45
+ email = email || answers.email;
46
+ password = password || answers.password;
47
+ }
48
+
49
+ const spinner = ora('Logging in...').start();
50
+
51
+ // Login user
52
+ const user = await authService.login(email, password);
53
+
54
+ spinner.succeed(chalk.green('āœ“ Login successful!'));
55
+
56
+ // Display welcome message
57
+ const message = chalk.white(`
58
+ ${chalk.bold('Welcome back!')}
59
+
60
+ ${chalk.bold('Email:')} ${user.email}
61
+ ${chalk.bold('Name:')} ${user.first_name || ''} ${user.last_name || ''}
62
+
63
+ ${chalk.cyan('You are now logged in!')}
64
+
65
+ ${chalk.bold('Commands:')}
66
+ ${chalk.gray('•')} gent whoami - View your profile
67
+ ${chalk.gray('•')} gent init - Initialize a repository
68
+ ${chalk.gray('•')} gent help - See all commands
69
+ `);
70
+
71
+ console.log(boxen(message, {
72
+ padding: 1,
73
+ margin: 1,
74
+ borderStyle: 'round',
75
+ borderColor: 'cyan'
76
+ }));
77
+
78
+ } catch (error) {
79
+ console.error(chalk.red('\nāœ— Login failed'));
80
+ console.error(chalk.red(`Error: ${error.message}\n`));
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ module.exports = login;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Logout Command - End user session
3
+ * Handles user logout and clearing authentication data
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const authService = require('../services/auth-service');
9
+ const authStorage = require('../utils/auth-storage');
10
+
11
+ /**
12
+ * Logout user
13
+ * @param {Object} options - Command options
14
+ */
15
+ async function logout(options) {
16
+ try {
17
+ // Check if user is authenticated
18
+ const isAuth = await authStorage.isAuthenticated();
19
+
20
+ if (!isAuth) {
21
+ console.log(chalk.yellow('\nℹ You are not logged in\n'));
22
+ return;
23
+ }
24
+
25
+ const spinner = ora('Logging out...').start();
26
+
27
+ // Logout user (calls API and clears local storage)
28
+ await authService.logout();
29
+
30
+ spinner.succeed(chalk.green('āœ“ Logged out successfully!'));
31
+ console.log(chalk.gray('Your session has been ended.\n'));
32
+
33
+ } catch (error) {
34
+ console.error(chalk.red('\nāœ— Logout failed'));
35
+ console.error(chalk.red(`Error: ${error.message}\n`));
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ module.exports = logout;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Register Command - Create a new user account
3
+ * Handles user registration with email and password
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+ const boxen = require('boxen');
10
+ const authService = require('../services/auth-service');
11
+
12
+ /**
13
+ * Register a new user
14
+ * @param {Object} options - Command options
15
+ */
16
+ async function register(options) {
17
+ console.log(chalk.cyan('\nšŸš€ Create your Gent account\n'));
18
+
19
+ try {
20
+ // Prompt for user information
21
+ const answers = await inquirer.prompt([
22
+ {
23
+ type: 'input',
24
+ name: 'email',
25
+ message: 'Email address:',
26
+ validate: (input) => {
27
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
28
+ return emailRegex.test(input) || 'Please enter a valid email address';
29
+ }
30
+ },
31
+ {
32
+ type: 'password',
33
+ name: 'password',
34
+ message: 'Password:',
35
+ mask: '*',
36
+ validate: (input) => {
37
+ if (input.length < 8) {
38
+ return 'Password must be at least 8 characters long';
39
+ }
40
+ return true;
41
+ }
42
+ },
43
+ {
44
+ type: 'password',
45
+ name: 'passwordConfirm',
46
+ message: 'Confirm password:',
47
+ mask: '*',
48
+ validate: (input, answers) => {
49
+ return input === answers.password || 'Passwords do not match';
50
+ }
51
+ },
52
+ {
53
+ type: 'input',
54
+ name: 'firstName',
55
+ message: 'First name:',
56
+ default: ''
57
+ },
58
+ {
59
+ type: 'input',
60
+ name: 'lastName',
61
+ message: 'Last name:',
62
+ default: ''
63
+ }
64
+ ]);
65
+
66
+ const spinner = ora('Creating your account...').start();
67
+
68
+ // Register user
69
+ const user = await authService.register(
70
+ answers.email,
71
+ answers.password,
72
+ answers.passwordConfirm,
73
+ answers.firstName,
74
+ answers.lastName
75
+ );
76
+
77
+ spinner.succeed(chalk.green('āœ“ Account created successfully!'));
78
+
79
+ // Display success message
80
+ const message = chalk.white(`
81
+ ${chalk.bold('Welcome to Gent!')}
82
+
83
+ ${chalk.bold('Email:')} ${user.email}
84
+ ${chalk.bold('Name:')} ${user.first_name || ''} ${user.last_name || ''}
85
+ ${chalk.bold('Account created:')} ${new Date(user.date_joined).toLocaleDateString()}
86
+
87
+ ${chalk.cyan('You are now logged in!')}
88
+
89
+ ${chalk.bold('Next steps:')}
90
+ ${chalk.gray('•')} gent init - Initialize a repository
91
+ ${chalk.gray('•')} gent whoami - View your profile
92
+ ${chalk.gray('•')} gent help - See all commands
93
+ `);
94
+
95
+ console.log(boxen(message, {
96
+ padding: 1,
97
+ margin: 1,
98
+ borderStyle: 'round',
99
+ borderColor: 'green'
100
+ }));
101
+
102
+ } catch (error) {
103
+ console.error(chalk.red('\nāœ— Registration failed'));
104
+ console.error(chalk.red(`Error: ${error.message}\n`));
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ module.exports = register;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Whoami Command - Display current user information
3
+ * Shows authenticated user profile
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const boxen = require('boxen');
8
+ const ora = require('ora');
9
+ const { formatDistanceToNow } = require('date-fns');
10
+ const authStorage = require('../utils/auth-storage');
11
+ const authService = require('../services/auth-service');
12
+
13
+ /**
14
+ * Display current user information
15
+ * @param {Object} options - Command options
16
+ */
17
+ async function whoami(options) {
18
+ try {
19
+ // Check if user is authenticated
20
+ const isAuth = await authStorage.isAuthenticated();
21
+
22
+ if (!isAuth) {
23
+ console.log(chalk.yellow('\nℹ You are not logged in'));
24
+ console.log(chalk.gray('Use "gent login" or "gent register" to authenticate\n'));
25
+ return;
26
+ }
27
+
28
+ const spinner = ora('Fetching user profile...').start();
29
+
30
+ // Get user profile from API (this will also test token validity)
31
+ const user = await authService.getProfile();
32
+
33
+ spinner.stop();
34
+
35
+ // Format date
36
+ const joinedDate = new Date(user.date_joined);
37
+ const joinedAgo = formatDistanceToNow(joinedDate, { addSuffix: true });
38
+
39
+ // Display user information
40
+ const message = chalk.white(`
41
+ ${chalk.bold.cyan('šŸ‘¤ User Profile')}
42
+
43
+ ${chalk.bold('Email:')} ${user.email}
44
+ ${chalk.bold('Name:')} ${user.first_name || 'N/A'} ${user.last_name || ''}
45
+ ${chalk.bold('Account ID:')} ${user.id}
46
+ ${chalk.bold('Joined:')} ${joinedDate.toLocaleDateString()} ${chalk.gray(`(${joinedAgo})`)}
47
+ ${chalk.bold('Status:')} ${user.is_active ? chalk.green('Active') : chalk.red('Inactive')}
48
+ `);
49
+
50
+ console.log(boxen(message, {
51
+ padding: 1,
52
+ margin: 1,
53
+ borderStyle: 'round',
54
+ borderColor: 'cyan'
55
+ }));
56
+
57
+ } catch (error) {
58
+ console.error(chalk.red('\nāœ— Failed to fetch user profile'));
59
+ console.error(chalk.red(`Error: ${error.message}\n`));
60
+
61
+ if (error.message.includes('login')) {
62
+ console.log(chalk.gray('Use "gent login" to authenticate\n'));
63
+ }
64
+
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ module.exports = whoami;
package/src/index.js CHANGED
@@ -21,6 +21,12 @@ const logCommand = require('./commands/log');
21
21
  const branchCommand = require('./commands/branch');
22
22
  const checkoutCommand = require('./commands/checkout');
23
23
 
24
+ // Import auth commands
25
+ const registerCommand = require('./commands/register');
26
+ const loginCommand = require('./commands/login');
27
+ const logoutCommand = require('./commands/logout');
28
+ const whoamiCommand = require('./commands/whoami');
29
+
24
30
  // Configure CLI
25
31
  program
26
32
  .name('gent')
@@ -74,6 +80,29 @@ program
74
80
  .option('-b, --create', 'Create a new branch')
75
81
  .action(checkoutCommand);
76
82
 
83
+ // Authentication commands
84
+ program
85
+ .command('register')
86
+ .description('Create a new user account')
87
+ .action(registerCommand);
88
+
89
+ program
90
+ .command('login')
91
+ .description('Login to your account')
92
+ .option('-e, --email <email>', 'Email address')
93
+ .option('-p, --password <password>', 'Password')
94
+ .action(loginCommand);
95
+
96
+ program
97
+ .command('logout')
98
+ .description('Logout from your account')
99
+ .action(logoutCommand);
100
+
101
+ program
102
+ .command('whoami')
103
+ .description('Display current user information')
104
+ .action(whoamiCommand);
105
+
77
106
  // Help command
78
107
  program
79
108
  .command('help [command]')
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Auth Service - Authentication operations
3
+ * Handles registration, login, logout, and token management
4
+ */
5
+
6
+ const apiClient = require('../utils/api-client');
7
+ const authStorage = require('../utils/auth-storage');
8
+ const { API_ENDPOINTS } = require('../utils/constants');
9
+
10
+ /**
11
+ * Register a new user
12
+ * @param {string} email - User email
13
+ * @param {string} password - User password
14
+ * @param {string} passwordConfirm - Password confirmation
15
+ * @param {string} firstName - User first name
16
+ * @param {string} lastName - User last name
17
+ * @returns {Promise<Object>} User data
18
+ */
19
+ async function register(email, password, passwordConfirm, firstName, lastName) {
20
+ try {
21
+ const payload = {
22
+ email,
23
+ password,
24
+ password_confirm: passwordConfirm,
25
+ first_name: firstName,
26
+ last_name: lastName
27
+ };
28
+
29
+ const response = await apiClient.post(API_ENDPOINTS.REGISTER, payload);
30
+
31
+ // API returns { message, user: {...}, tokens: { access, refresh } }
32
+ const { tokens, user } = response;
33
+
34
+ // Store tokens
35
+ await authStorage.saveTokens(tokens.access, tokens.refresh, user);
36
+
37
+ return user;
38
+ } catch (error) {
39
+ if (error.response?.data) {
40
+ // Extract API error messages
41
+ const errors = error.response.data;
42
+ const errorMessages = [];
43
+
44
+ for (const [field, messages] of Object.entries(errors)) {
45
+ if (Array.isArray(messages)) {
46
+ errorMessages.push(...messages);
47
+ } else {
48
+ errorMessages.push(messages);
49
+ }
50
+ }
51
+
52
+ throw new Error(errorMessages.join(', '));
53
+ }
54
+
55
+ throw new Error(error.message || 'Registration failed');
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Login user
61
+ * @param {string} email - User email
62
+ * @param {string} password - User password
63
+ * @returns {Promise<Object>} User data
64
+ */
65
+ async function login(email, password) {
66
+ try {
67
+ const payload = { email, password };
68
+
69
+ const response = await apiClient.post(API_ENDPOINTS.LOGIN, payload);
70
+
71
+ // API returns { message, user: {...}, tokens: { access, refresh } }
72
+ const { tokens, user } = response;
73
+
74
+ // Store tokens
75
+ await authStorage.saveTokens(tokens.access, tokens.refresh, user);
76
+
77
+ return user;
78
+ } catch (error) {
79
+ if (error.response?.status === 401) {
80
+ throw new Error('Invalid email or password');
81
+ }
82
+
83
+ if (error.response?.data) {
84
+ const errors = error.response.data;
85
+ const errorMessages = [];
86
+
87
+ for (const [field, messages] of Object.entries(errors)) {
88
+ if (Array.isArray(messages)) {
89
+ errorMessages.push(...messages);
90
+ } else {
91
+ errorMessages.push(messages);
92
+ }
93
+ }
94
+
95
+ throw new Error(errorMessages.join(', '));
96
+ }
97
+
98
+ throw new Error(error.message || 'Login failed');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Logout user
104
+ * @returns {Promise<void>}
105
+ */
106
+ async function logout() {
107
+ try {
108
+ const refreshToken = await authStorage.getRefreshToken();
109
+
110
+ if (refreshToken) {
111
+ // Call logout endpoint to blacklist refresh token
112
+ await apiClient.post(API_ENDPOINTS.LOGOUT, {
113
+ refresh: refreshToken
114
+ });
115
+ }
116
+ } catch (error) {
117
+ // Even if API call fails, clear local auth
118
+ console.error('Logout API call failed:', error.message);
119
+ } finally {
120
+ // Always clear local authentication
121
+ await authStorage.clearAuth();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Refresh access token
127
+ * @returns {Promise<string>} New access token
128
+ */
129
+ async function refreshToken() {
130
+ try {
131
+ const refreshToken = await authStorage.getRefreshToken();
132
+
133
+ if (!refreshToken) {
134
+ throw new Error('No refresh token available');
135
+ }
136
+
137
+ const response = await apiClient.post(API_ENDPOINTS.REFRESH, {
138
+ refresh: refreshToken
139
+ });
140
+
141
+ const { access } = response;
142
+
143
+ // Update only access token
144
+ await authStorage.updateAccessToken(access);
145
+
146
+ return access;
147
+ } catch (error) {
148
+ // If refresh fails, clear auth
149
+ await authStorage.clearAuth();
150
+ throw new Error('Session expired. Please login again.');
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get current user profile
156
+ * @returns {Promise<Object>} User profile data
157
+ */
158
+ async function getProfile() {
159
+ try {
160
+ const response = await apiClient.get(API_ENDPOINTS.PROFILE);
161
+ return response;
162
+ } catch (error) {
163
+ if (error.response?.status === 401) {
164
+ throw new Error('Not authenticated. Please login first.');
165
+ }
166
+
167
+ throw new Error(error.message || 'Failed to fetch profile');
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ register,
173
+ login,
174
+ logout,
175
+ refreshToken,
176
+ getProfile
177
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * API Client - HTTP client for making authenticated API requests
3
+ * Handles request/response interceptors and automatic token refresh
4
+ */
5
+
6
+ const axios = require('axios');
7
+ const { API_BASE_URL } = require('./constants');
8
+ const authStorage = require('./auth-storage');
9
+
10
+ // Create axios instance with base configuration
11
+ const apiClient = axios.create({
12
+ baseURL: API_BASE_URL,
13
+ headers: {
14
+ 'Content-Type': 'application/json'
15
+ },
16
+ timeout: 60000 // 60 seconds
17
+ });
18
+
19
+ // Track if we're currently refreshing token to avoid multiple refresh requests
20
+ let isRefreshing = false;
21
+ let failedRequestsQueue = [];
22
+
23
+ /**
24
+ * Process queued requests after token refresh
25
+ * @param {Error|null} error - Error if refresh failed
26
+ * @param {string|null} token - New access token if refresh succeeded
27
+ */
28
+ function processQueue(error, token = null) {
29
+ failedRequestsQueue.forEach(promise => {
30
+ if (error) {
31
+ promise.reject(error);
32
+ } else {
33
+ promise.resolve(token);
34
+ }
35
+ });
36
+
37
+ failedRequestsQueue = [];
38
+ }
39
+
40
+ // Request interceptor - Add JWT token to headers
41
+ apiClient.interceptors.request.use(
42
+ async (config) => {
43
+ const token = await authStorage.getAccessToken();
44
+
45
+ if (token) {
46
+ config.headers.Authorization = `Bearer ${token}`;
47
+ }
48
+
49
+ return config;
50
+ },
51
+ (error) => {
52
+ return Promise.reject(error);
53
+ }
54
+ );
55
+
56
+ // Response interceptor - Handle 401 errors and refresh token
57
+ apiClient.interceptors.response.use(
58
+ (response) => {
59
+ return response;
60
+ },
61
+ async (error) => {
62
+ const originalRequest = error.config;
63
+
64
+ // If error is 401 and we haven't tried to refresh yet
65
+ if (error.response?.status === 401 && !originalRequest._retry) {
66
+ if (isRefreshing) {
67
+ // If already refreshing, queue this request
68
+ return new Promise((resolve, reject) => {
69
+ failedRequestsQueue.push({ resolve, reject });
70
+ })
71
+ .then(token => {
72
+ originalRequest.headers.Authorization = `Bearer ${token}`;
73
+ return apiClient(originalRequest);
74
+ })
75
+ .catch(err => {
76
+ return Promise.reject(err);
77
+ });
78
+ }
79
+
80
+ originalRequest._retry = true;
81
+ isRefreshing = true;
82
+
83
+ try {
84
+ const refreshToken = await authStorage.getRefreshToken();
85
+
86
+ if (!refreshToken) {
87
+ // No refresh token, user needs to login again
88
+ await authStorage.clearAuth();
89
+ throw new Error('Session expired. Please login again.');
90
+ }
91
+
92
+ // Call refresh endpoint
93
+ const response = await axios.post(
94
+ `${API_BASE_URL}/api/auth/token/refresh/`,
95
+ { refresh: refreshToken }
96
+ );
97
+
98
+ const { access } = response.data;
99
+
100
+ // Update stored access token
101
+ await authStorage.updateAccessToken(access);
102
+
103
+ // Update authorization header
104
+ originalRequest.headers.Authorization = `Bearer ${access}`;
105
+
106
+ // Process queued requests
107
+ processQueue(null, access);
108
+
109
+ isRefreshing = false;
110
+
111
+ // Retry original request
112
+ return apiClient(originalRequest);
113
+
114
+ } catch (refreshError) {
115
+ // Refresh failed, clear auth and reject
116
+ processQueue(refreshError, null);
117
+ isRefreshing = false;
118
+ await authStorage.clearAuth();
119
+ return Promise.reject(refreshError);
120
+ }
121
+ }
122
+
123
+ return Promise.reject(error);
124
+ }
125
+ );
126
+
127
+ /**
128
+ * Make GET request
129
+ * @param {string} url - Endpoint URL
130
+ * @param {Object} config - Axios config
131
+ * @returns {Promise} Response data
132
+ */
133
+ async function get(url, config = {}) {
134
+ const response = await apiClient.get(url, config);
135
+ return response.data;
136
+ }
137
+
138
+ /**
139
+ * Make POST request
140
+ * @param {string} url - Endpoint URL
141
+ * @param {Object} data - Request payload
142
+ * @param {Object} config - Axios config
143
+ * @returns {Promise} Response data
144
+ */
145
+ async function post(url, data = {}, config = {}) {
146
+ const response = await apiClient.post(url, data, config);
147
+ return response.data;
148
+ }
149
+
150
+ /**
151
+ * Make PUT request
152
+ * @param {string} url - Endpoint URL
153
+ * @param {Object} data - Request payload
154
+ * @param {Object} config - Axios config
155
+ * @returns {Promise} Response data
156
+ */
157
+ async function put(url, data = {}, config = {}) {
158
+ const response = await apiClient.put(url, data, config);
159
+ return response.data;
160
+ }
161
+
162
+ /**
163
+ * Make DELETE request
164
+ * @param {string} url - Endpoint URL
165
+ * @param {Object} config - Axios config
166
+ * @returns {Promise} Response data
167
+ */
168
+ async function del(url, config = {}) {
169
+ const response = await apiClient.delete(url, config);
170
+ return response.data;
171
+ }
172
+
173
+ module.exports = {
174
+ get,
175
+ post,
176
+ put,
177
+ delete: del,
178
+ apiClient // Export raw client if needed
179
+ };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Auth Storage - Secure token and user data storage
3
+ * Handles encryption and persistence of authentication data
4
+ */
5
+
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+ const CryptoJS = require('crypto-js');
9
+ const { GENT_DIR, AUTH_FILE } = require('./constants');
10
+
11
+ // Simple encryption key (in production, use environment variable or OS keychain)
12
+ const ENCRYPTION_KEY = 'gent-cli-secret-key-v1';
13
+
14
+ /**
15
+ * Get the auth file path
16
+ * @returns {string} Path to auth.json file
17
+ */
18
+ function getAuthFilePath() {
19
+ return path.join(process.cwd(), GENT_DIR, AUTH_FILE);
20
+ }
21
+
22
+ /**
23
+ * Encrypt data
24
+ * @param {Object} data - Data to encrypt
25
+ * @returns {string} Encrypted string
26
+ */
27
+ function encrypt(data) {
28
+ const jsonString = JSON.stringify(data);
29
+ return CryptoJS.AES.encrypt(jsonString, ENCRYPTION_KEY).toString();
30
+ }
31
+
32
+ /**
33
+ * Decrypt data
34
+ * @param {string} encryptedData - Encrypted string
35
+ * @returns {Object} Decrypted data
36
+ */
37
+ function decrypt(encryptedData) {
38
+ const bytes = CryptoJS.AES.decrypt(encryptedData, ENCRYPTION_KEY);
39
+ const decryptedString = bytes.toString(CryptoJS.enc.Utf8);
40
+ return JSON.parse(decryptedString);
41
+ }
42
+
43
+ /**
44
+ * Save authentication tokens and user data
45
+ * @param {string} accessToken - JWT access token
46
+ * @param {string} refreshToken - JWT refresh token
47
+ * @param {Object} user - User profile data
48
+ */
49
+ async function saveTokens(accessToken, refreshToken, user) {
50
+ const authFilePath = getAuthFilePath();
51
+ const gentDir = path.join(process.cwd(), GENT_DIR);
52
+
53
+ const authData = {
54
+ accessToken,
55
+ refreshToken,
56
+ user,
57
+ timestamp: new Date().toISOString()
58
+ };
59
+
60
+ const encryptedData = encrypt(authData);
61
+
62
+ try {
63
+ // Ensure .gent directory exists
64
+ await fs.mkdir(gentDir, { recursive: true });
65
+ await fs.writeFile(authFilePath, JSON.stringify({ data: encryptedData }), 'utf8');
66
+ } catch (error) {
67
+ throw new Error(`Failed to save authentication data: ${error.message}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Read authentication data from file
73
+ * @returns {Object|null} Decrypted auth data or null if not found
74
+ */
75
+ async function readAuthData() {
76
+ const authFilePath = getAuthFilePath();
77
+
78
+ try {
79
+ const fileContent = await fs.readFile(authFilePath, 'utf8');
80
+ const { data } = JSON.parse(fileContent);
81
+ return decrypt(data);
82
+ } catch (error) {
83
+ // File doesn't exist or is corrupted
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get access token
90
+ * @returns {string|null} Access token or null
91
+ */
92
+ async function getAccessToken() {
93
+ const authData = await readAuthData();
94
+ return authData ? authData.accessToken : null;
95
+ }
96
+
97
+ /**
98
+ * Get refresh token
99
+ * @returns {string|null} Refresh token or null
100
+ */
101
+ async function getRefreshToken() {
102
+ const authData = await readAuthData();
103
+ return authData ? authData.refreshToken : null;
104
+ }
105
+
106
+ /**
107
+ * Get user profile
108
+ * @returns {Object|null} User data or null
109
+ */
110
+ async function getUser() {
111
+ const authData = await readAuthData();
112
+ return authData ? authData.user : null;
113
+ }
114
+
115
+ /**
116
+ * Check if user is authenticated
117
+ * @returns {boolean} True if authenticated
118
+ */
119
+ async function isAuthenticated() {
120
+ const authData = await readAuthData();
121
+ return authData !== null && authData.accessToken !== null;
122
+ }
123
+
124
+ /**
125
+ * Clear authentication data
126
+ */
127
+ async function clearAuth() {
128
+ const authFilePath = getAuthFilePath();
129
+
130
+ try {
131
+ await fs.unlink(authFilePath);
132
+ } catch (error) {
133
+ // File doesn't exist, nothing to clear
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Update only the access token (used after refresh)
139
+ * @param {string} newAccessToken - New access token
140
+ */
141
+ async function updateAccessToken(newAccessToken) {
142
+ const authData = await readAuthData();
143
+
144
+ if (!authData) {
145
+ throw new Error('No authentication data found');
146
+ }
147
+
148
+ authData.accessToken = newAccessToken;
149
+ authData.timestamp = new Date().toISOString();
150
+
151
+ const authFilePath = getAuthFilePath();
152
+ const gentDir = path.join(process.cwd(), GENT_DIR);
153
+ const encryptedData = encrypt(authData);
154
+
155
+ // Ensure .gent directory exists
156
+ await fs.mkdir(gentDir, { recursive: true });
157
+ await fs.writeFile(authFilePath, JSON.stringify({ data: encryptedData }), 'utf8');
158
+ }
159
+
160
+ module.exports = {
161
+ saveTokens,
162
+ getAccessToken,
163
+ getRefreshToken,
164
+ getUser,
165
+ isAuthenticated,
166
+ clearAuth,
167
+ updateAccessToken
168
+ };
@@ -10,6 +10,17 @@ module.exports = {
10
10
  STAGING_FILE: 'staging.json',
11
11
  COMMITS_FILE: 'commits.json',
12
12
  HEAD_FILE: 'HEAD',
13
+ AUTH_FILE: 'auth.json',
14
+
15
+ // API Configuration
16
+ API_BASE_URL: 'https://gent-api.onrender.com',
17
+ API_ENDPOINTS: {
18
+ LOGIN: '/api/auth/login/',
19
+ REGISTER: '/api/auth/register/',
20
+ LOGOUT: '/api/auth/logout/',
21
+ REFRESH: '/api/auth/token/refresh/',
22
+ PROFILE: '/api/auth/profile/'
23
+ },
13
24
 
14
25
  // Default ignore patterns
15
26
  DEFAULT_IGNORE_PATTERNS: [
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Gent CLI - Command Summary
5
- * Quick reference for all available commands
6
- */
7
-
8
- const chalk = require('chalk');
9
- const boxen = require('boxen');
10
-
11
- const commands = [
12
- {
13
- name: 'init',
14
- description: 'Initialize a new gent repository',
15
- usage: 'gent init [options]',
16
- options: ['-y, --yes Skip prompts and use defaults'],
17
- examples: ['gent init', 'gent init -y']
18
- },
19
- {
20
- name: 'status',
21
- description: 'Show the working tree status',
22
- usage: 'gent status [options]',
23
- options: ['-s, --short Give output in short format'],
24
- examples: ['gent status', 'gent status -s']
25
- },
26
- {
27
- name: 'add',
28
- description: 'Add file contents to the staging area',
29
- usage: 'gent add <files...> [options]',
30
- options: ['-A, --all Add all files'],
31
- examples: ['gent add file.js', 'gent add .', 'gent add --all']
32
- },
33
- {
34
- name: 'commit',
35
- description: 'Record changes to the repository',
36
- usage: 'gent commit [options]',
37
- options: [
38
- '-m, --message <message> Commit message',
39
- '-a, --all Auto stage modified files'
40
- ],
41
- examples: ['gent commit -m "Fix bug"', 'gent commit', 'gent commit -a -m "Update all"']
42
- },
43
- {
44
- name: 'log',
45
- description: 'Show commit logs',
46
- usage: 'gent log [options]',
47
- options: [
48
- '-n, --number <count> Limit commits (default: 10)',
49
- '--oneline Show each commit on one line'
50
- ],
51
- examples: ['gent log', 'gent log -n 5', 'gent log --oneline']
52
- },
53
- {
54
- name: 'branch',
55
- description: 'List, create, or delete branches',
56
- usage: 'gent branch [name] [options]',
57
- options: [
58
- '-d, --delete <name> Delete a branch',
59
- '-a, --all List all branches'
60
- ],
61
- examples: ['gent branch', 'gent branch feature-x', 'gent branch -d old-feature']
62
- },
63
- {
64
- name: 'checkout',
65
- description: 'Switch branches',
66
- usage: 'gent checkout <branch> [options]',
67
- options: ['-b, --create Create a new branch'],
68
- examples: ['gent checkout main', 'gent checkout -b new-feature']
69
- }
70
- ];
71
-
72
- console.log(chalk.bold.cyan('\nšŸš€ Gent CLI - Command Reference\n'));
73
-
74
- commands.forEach((cmd, index) => {
75
- console.log(chalk.yellow.bold(`${index + 1}. ${cmd.name.toUpperCase()}`));
76
- console.log(chalk.white(` ${cmd.description}`));
77
- console.log(chalk.gray(` Usage: ${cmd.usage}`));
78
-
79
- if (cmd.options.length > 0) {
80
- console.log(chalk.gray(' Options:'));
81
- cmd.options.forEach(opt => {
82
- console.log(chalk.gray(` ${opt}`));
83
- });
84
- }
85
-
86
- console.log(chalk.cyan(' Examples:'));
87
- cmd.examples.forEach(ex => {
88
- console.log(chalk.green(` $ ${ex}`));
89
- });
90
-
91
- console.log();
92
- });
93
-
94
- const tips = `
95
- ${chalk.bold('šŸ’” Quick Tips:')}
96
-
97
- ${chalk.cyan('•')} Always run ${chalk.yellow('gent init')} first in a new project
98
- ${chalk.cyan('•')} Use ${chalk.yellow('gent status')} to see what changed
99
- ${chalk.cyan('•')} Stage files with ${chalk.yellow('gent add')} before committing
100
- ${chalk.cyan('•')} Create branches for new features
101
- ${chalk.cyan('•')} Check ${chalk.yellow('gent log')} to view history
102
-
103
- ${chalk.bold('šŸ“š Documentation:')}
104
- ${chalk.gray('• README.md - Complete documentation')}
105
- ${chalk.gray('• QUICKSTART.md - Quick start guide')}
106
- ${chalk.gray('• demo.sh - Interactive demo')}
107
- `;
108
-
109
- console.log(boxen(tips, {
110
- padding: 1,
111
- margin: 1,
112
- borderStyle: 'round',
113
- borderColor: 'cyan'
114
- }));
115
-
116
- console.log(chalk.gray('Run'), chalk.yellow('gent --help'), chalk.gray('for more information\n'));