gent-cli 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Repos Command - List and create remote repositories
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+ const path = require('path');
8
+ const { API_ENDPOINTS } = require('../utils/constants');
9
+ const apiClient = require('../utils/api-client');
10
+ const authStorage = require('../utils/auth-storage');
11
+
12
+ /**
13
+ * List or create remote repositories
14
+ * @param {Object} options
15
+ */
16
+ async function repos(options) {
17
+ try {
18
+ const isAuth = await authStorage.isAuthenticated();
19
+ if (!isAuth) {
20
+ console.error(chalk.red('Not authenticated'));
21
+ console.log(chalk.yellow('Run "gent login" first'));
22
+ return;
23
+ }
24
+
25
+ if (options.create) {
26
+ await createRepo(options);
27
+ return;
28
+ }
29
+
30
+ await listRepos();
31
+
32
+ } catch (error) {
33
+ if (error.response?.status === 401) {
34
+ console.error(chalk.red('Authentication failed — run "gent login"'));
35
+ } else if (error.response?.data) {
36
+ console.error(chalk.red(JSON.stringify(error.response.data)));
37
+ } else {
38
+ console.error(chalk.red('Error:'), error.message);
39
+ }
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * List all user repositories
46
+ */
47
+ async function listRepos() {
48
+ const spinner = ora('Fetching repositories...').start();
49
+
50
+ const data = await apiClient.get(API_ENDPOINTS.REPOS);
51
+
52
+ spinner.stop();
53
+
54
+ if (!data || data.length === 0) {
55
+ console.log(chalk.gray('No repositories found'));
56
+ console.log(chalk.yellow('Use "gent repos --create <name>" to create one'));
57
+ return;
58
+ }
59
+
60
+ console.log(chalk.bold.cyan('\nRepositories:\n'));
61
+
62
+ for (const repo of data) {
63
+ const visibility = repo.is_private ? chalk.red('private') : chalk.green('public');
64
+ const desc = repo.description ? chalk.gray(` — ${repo.description}`) : '';
65
+ const url = chalk.gray(` /api/repos/${repo.owner_id}/${repo.name}`);
66
+ console.log(` ${chalk.white.bold(repo.name)} [${visibility}]${desc}`);
67
+ console.log(` ${url}`);
68
+ }
69
+
70
+ console.log();
71
+ }
72
+
73
+ /**
74
+ * Create a new remote repository
75
+ */
76
+ async function createRepo(options) {
77
+ const name = options.create;
78
+ if (typeof name !== 'string' || !name) {
79
+ console.error(chalk.red('Usage: gent repos --create <name>'));
80
+ return;
81
+ }
82
+
83
+ const spinner = ora(`Creating repository '${name}'...`).start();
84
+
85
+ const payload = {
86
+ name,
87
+ description: options.description || '',
88
+ is_private: !!options.private,
89
+ };
90
+
91
+ if (options.defaultBranch) {
92
+ payload.default_branch = options.defaultBranch;
93
+ }
94
+
95
+ const data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
96
+ const repo = data.repository || data;
97
+
98
+ spinner.succeed(chalk.green(`Created repository '${repo.name}'`));
99
+ console.log(chalk.gray(` URL: /api/repos/${repo.owner_id}/${repo.name}`));
100
+ console.log(chalk.gray(` Use "gent remote add origin /api/repos/${repo.owner_id}/${repo.name}" to link`));
101
+ }
102
+
103
+ module.exports = repos;
@@ -28,7 +28,8 @@
28
28
  const path = require('path');
29
29
  const chalk = require('chalk');
30
30
  const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
31
- const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
31
+ const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
32
+ const apiClient = require('../utils/api-client');
32
33
  const authStorage = require('../utils/auth-storage');
33
34
 
34
35
  /**
@@ -104,10 +105,12 @@ async function createTag(name, repository, gentPath, options) {
104
105
  const tagObj = { hash: commitHash };
105
106
 
106
107
  // Annotated tag
108
+ let taggerName = '';
109
+ let taggerEmail = '';
107
110
  if (options.message) {
108
111
  const config = await readJSON(path.join(gentPath, CONFIG_FILE));
109
- let taggerName = config.user.name;
110
- let taggerEmail = config.user.email;
112
+ taggerName = config.user.name;
113
+ taggerEmail = config.user.email;
111
114
 
112
115
  if (!taggerName || !taggerEmail) {
113
116
  const user = await authStorage.getUser();
@@ -127,6 +130,9 @@ async function createTag(name, repository, gentPath, options) {
127
130
  await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
128
131
 
129
132
  console.log(chalk.green(`Created tag '${name}' → ${commitHash.substring(0, 7)}`));
133
+
134
+ // Sync to remote
135
+ await syncTagCreate(name, tagObj, taggerName, taggerEmail, gentPath);
130
136
  }
131
137
 
132
138
  /**
@@ -141,6 +147,69 @@ async function deleteTag(name, repository, gentPath) {
141
147
  delete repository.tags[name];
142
148
  await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
143
149
  console.log(chalk.green(`Deleted tag '${name}'`));
150
+
151
+ // Sync deletion to remote
152
+ await syncTagDelete(name, gentPath);
153
+ }
154
+
155
+ /**
156
+ * Sync tag creation to remote API
157
+ */
158
+ async function syncTagCreate(name, tagObj, taggerName, taggerEmail, gentPath) {
159
+ try {
160
+ const isAuth = await authStorage.isAuthenticated();
161
+ if (!isAuth) return;
162
+
163
+ const config = await readJSON(path.join(gentPath, CONFIG_FILE));
164
+ const remoteConfig = config.remotes && config.remotes.origin;
165
+ if (!remoteConfig) return;
166
+
167
+ const repoInfo = parseRemoteUrl(remoteConfig.url);
168
+ if (!repoInfo) return;
169
+
170
+ const payload = {
171
+ name,
172
+ commit_sha: tagObj.hash,
173
+ message: tagObj.message || '',
174
+ annotated: !!tagObj.annotated,
175
+ };
176
+
177
+ if (taggerName) payload.tagger_name = taggerName;
178
+ if (taggerEmail) payload.tagger_email = taggerEmail;
179
+
180
+ const url = buildRepoUrl(API_ENDPOINTS.REPO_TAGS_CREATE, repoInfo);
181
+ await apiClient.post(url, payload);
182
+ console.log(chalk.gray(` ↑ Synced to remote`));
183
+ } catch (error) {
184
+ if (error.response?.status === 400) {
185
+ console.log(chalk.gray(` ⚠ Remote sync skipped (tag may already exist remotely)`));
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Sync tag deletion to remote API
192
+ */
193
+ async function syncTagDelete(name, gentPath) {
194
+ try {
195
+ const isAuth = await authStorage.isAuthenticated();
196
+ if (!isAuth) return;
197
+
198
+ const config = await readJSON(path.join(gentPath, CONFIG_FILE));
199
+ const remoteConfig = config.remotes && config.remotes.origin;
200
+ if (!remoteConfig) return;
201
+
202
+ const repoInfo = parseRemoteUrl(remoteConfig.url);
203
+ if (!repoInfo) return;
204
+
205
+ const url = buildRepoUrl(API_ENDPOINTS.REPO_TAG_DETAIL, { ...repoInfo, tag_name: name });
206
+ await apiClient.delete(url);
207
+ console.log(chalk.gray(` ↑ Deleted from remote`));
208
+ } catch (error) {
209
+ if (error.response?.status === 404) {
210
+ // Tag didn't exist remotely
211
+ }
212
+ }
144
213
  }
145
214
 
146
215
  module.exports = tag;
package/src/index.js CHANGED
@@ -41,6 +41,7 @@ const stashCommand = require('./commands/stash');
41
41
  const remoteCommand = require('./commands/remote');
42
42
  const pushCommand = require('./commands/push');
43
43
  const pullCommand = require('./commands/pull');
44
+ const reposCommand = require('./commands/repos');
44
45
 
45
46
  // Import auth commands
46
47
  const registerCommand = require('./commands/register');
@@ -52,7 +53,7 @@ const whoamiCommand = require('./commands/whoami');
52
53
  program
53
54
  .name('gent')
54
55
  .description(chalk.cyan('Gent - A Git-like version control CLI with cloud backend'))
55
- .version(packageJson.version, '-v, --version', 'Output the current version');
56
+ .version(packageJson.version, '-V, --version', 'Output the current version');
56
57
 
57
58
  // ─── Repository Setup ───────────────────────────────────
58
59
 
@@ -60,6 +61,7 @@ program
60
61
  .command('init')
61
62
  .description('Initialize a new gent repository')
62
63
  .option('-y, --yes', 'Skip prompts and use defaults')
64
+ .option('--remote [name]', 'Create a remote repository on the backend')
63
65
  .action(initCommand);
64
66
 
65
67
  program
@@ -168,6 +170,15 @@ program
168
170
  .option('-v, --verbose', 'Show remote URLs')
169
171
  .action(remoteCommand);
170
172
 
173
+ program
174
+ .command('repos')
175
+ .description('List or create remote repositories')
176
+ .option('--create <name>', 'Create a new remote repository')
177
+ .option('--description <text>', 'Repository description (with --create)')
178
+ .option('--private', 'Make repository private (with --create)')
179
+ .option('--default-branch <name>', 'Default branch name (with --create)')
180
+ .action(reposCommand);
181
+
171
182
  program
172
183
  .command('push [remote] [branch]')
173
184
  .description('Push local commits to remote')
@@ -184,6 +195,11 @@ program
184
195
  program
185
196
  .command('register')
186
197
  .description('Create a new user account')
198
+ .option('-e, --email <email>', 'Email address')
199
+ .option('-p, --password <password>', 'Password')
200
+ .option('--password-confirm <password>', 'Password confirmation')
201
+ .option('--first-name <name>', 'First name')
202
+ .option('--last-name <name>', 'Last name')
187
203
  .action(registerCommand);
188
204
 
189
205
  program
@@ -226,7 +242,7 @@ try {
226
242
  program.outputHelp();
227
243
  }
228
244
  } catch (err) {
229
- if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed') {
245
+ if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed' && err.code !== 'commander.version') {
230
246
  console.error(chalk.red('Error:'), err.message);
231
247
  process.exit(1);
232
248
  }
@@ -170,10 +170,23 @@ async function del(url, config = {}) {
170
170
  return response.data;
171
171
  }
172
172
 
173
+ /**
174
+ * Make PATCH request
175
+ * @param {string} url - Endpoint URL
176
+ * @param {Object} data - Request payload
177
+ * @param {Object} config - Axios config
178
+ * @returns {Promise} Response data
179
+ */
180
+ async function patch(url, data = {}, config = {}) {
181
+ const response = await apiClient.patch(url, data, config);
182
+ return response.data;
183
+ }
184
+
173
185
  module.exports = {
174
186
  get,
175
187
  post,
176
188
  put,
177
189
  delete: del,
190
+ patch,
178
191
  apiClient // Export raw client if needed
179
192
  };
@@ -15,22 +15,67 @@ module.exports = {
15
15
  // API Configuration
16
16
  API_BASE_URL: 'https://gent-api.onrender.com',
17
17
  API_ENDPOINTS: {
18
+ // Auth
18
19
  LOGIN: '/api/auth/login/',
19
20
  REGISTER: '/api/auth/register/',
20
21
  LOGOUT: '/api/auth/logout/',
21
22
  REFRESH: '/api/auth/token/refresh/',
22
23
  PROFILE: '/api/auth/profile/',
23
24
 
24
- // Repository endpoints (used by push/pull/clone)
25
- // Base: /api/repos/:id/
25
+ // Repository management
26
26
  REPOS: '/api/repos/',
27
- REPO_PUSH: '/push/', // POST - upload commits + objects
28
- REPO_PULL: '/pull/', // GET - download commits + objects since hash
29
- REPO_CLONE: '/clone/', // GET - full repo download
30
- REPO_REFS: '/refs/', // GET - list remote branch refs
31
- REPO_TAGS: '/tags/', // GET/POST/DELETE - tag management
32
- REPO_MERGE: '/merge/', // POST - server-side merge request
33
- REPO_COMMITS: '/commits/', // GET - commit history
27
+ REPOS_CREATE: '/api/repos/create/',
28
+ // Template: /api/repos/{owner_id}/{repo_name}/
29
+ REPO_DETAIL: '/api/repos/{owner_id}/{repo_name}/',
30
+ REPO_DELETE: '/api/repos/{owner_id}/{repo_name}/delete/',
31
+
32
+ // Push
33
+ REPO_PUSH: '/api/repos/{owner_id}/{repo_name}/push/',
34
+
35
+ // Branches
36
+ REPO_BRANCHES: '/api/repos/{owner_id}/{repo_name}/branches/',
37
+ REPO_BRANCHES_CREATE: '/api/repos/{owner_id}/{repo_name}/branches/create/',
38
+ REPO_BRANCH_DETAIL: '/api/repos/{owner_id}/{repo_name}/branches/{branch_name}/',
39
+
40
+ // Tags
41
+ REPO_TAGS: '/api/repos/{owner_id}/{repo_name}/tags/',
42
+ REPO_TAGS_CREATE: '/api/repos/{owner_id}/{repo_name}/tags/create/',
43
+ REPO_TAG_DETAIL: '/api/repos/{owner_id}/{repo_name}/tags/{tag_name}/',
44
+
45
+ // Commits
46
+ REPO_COMMITS: '/api/repos/{owner_id}/{repo_name}/commits/',
47
+ REPO_COMMIT_DETAIL: '/api/repos/{owner_id}/{repo_name}/commits/{sha}/',
48
+
49
+ // Objects (trees & blobs)
50
+ REPO_TREE_CREATE: '/api/repos/{owner_id}/{repo_name}/tree/create/',
51
+ REPO_TREE_DETAIL: '/api/repos/{owner_id}/{repo_name}/tree/{sha}/',
52
+ REPO_BLOB_CREATE: '/api/repos/{owner_id}/{repo_name}/blob/create/',
53
+ REPO_BLOB_DETAIL: '/api/repos/{owner_id}/{repo_name}/blob/{sha}/',
54
+ },
55
+
56
+ /**
57
+ * Build a repo-scoped API path by replacing {owner_id} and {repo_name} tokens.
58
+ * @param {string} template - Endpoint template from API_ENDPOINTS
59
+ * @param {object} params - { owner_id, repo_name, branch_name?, tag_name?, sha? }
60
+ * @returns {string}
61
+ */
62
+ buildRepoUrl(template, params) {
63
+ let url = template;
64
+ for (const [key, value] of Object.entries(params)) {
65
+ url = url.replace(`{${key}}`, encodeURIComponent(value));
66
+ }
67
+ return url;
68
+ },
69
+
70
+ /**
71
+ * Parse a remote URL like /api/repos/{owner_id}/{repo_name} into { owner_id, repo_name }.
72
+ * @param {string} url - Remote URL stored in config
73
+ * @returns {{ owner_id: string, repo_name: string } | null}
74
+ */
75
+ parseRemoteUrl(url) {
76
+ const match = url.match(/\/api\/repos\/(\d+)\/([^/]+)\/?$/);
77
+ if (!match) return null;
78
+ return { owner_id: match[1], repo_name: match[2] };
34
79
  },
35
80
 
36
81
  // Default ignore patterns
@@ -137,6 +137,29 @@ function hashBlob(content) {
137
137
  return hashObject('blob', content);
138
138
  }
139
139
 
140
+ /**
141
+ * Decode blob content returned by the backend.
142
+ * Current Django endpoints return raw UTF-8 content, while older/planned pull
143
+ * responses may return base64. Prefer the representation whose blob hash
144
+ * matches the expected object SHA.
145
+ * @param {String} content
146
+ * @param {String} expectedHash
147
+ * @returns {Buffer}
148
+ */
149
+ function decodeRemoteBlobContent(content, expectedHash) {
150
+ const raw = Buffer.from(content, 'utf-8');
151
+ if (!expectedHash || hashBlob(raw) === expectedHash) {
152
+ return raw;
153
+ }
154
+
155
+ const decoded = Buffer.from(content, 'base64');
156
+ if (hashBlob(decoded) === expectedHash) {
157
+ return decoded;
158
+ }
159
+
160
+ return raw;
161
+ }
162
+
140
163
  /**
141
164
  * Hash a tree structure.
142
165
  * @param {Array<{mode: String, name: String, hash: String, type: String}>} entries
@@ -324,6 +347,7 @@ module.exports = {
324
347
  hashObject,
325
348
  hashBlob,
326
349
  hashTree,
350
+ decodeRemoteBlobContent,
327
351
  objectExists,
328
352
  storeBlob,
329
353
  readBlob,