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.
- package/README.md +505 -172
- package/package.json +3 -2
- package/src/commands/branch.js +66 -2
- package/src/commands/clone.js +159 -49
- package/src/commands/init.js +50 -1
- package/src/commands/pull.js +152 -44
- package/src/commands/push.js +106 -53
- package/src/commands/register.js +27 -6
- package/src/commands/remote.js +14 -1
- package/src/commands/repos.js +103 -0
- package/src/commands/tag.js +72 -3
- package/src/index.js +18 -2
- package/src/utils/api-client.js +13 -0
- package/src/utils/constants.js +54 -9
- package/src/utils/hash-engine.js +24 -0
|
@@ -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;
|
package/src/commands/tag.js
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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, '-
|
|
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
|
}
|
package/src/utils/api-client.js
CHANGED
|
@@ -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
|
};
|
package/src/utils/constants.js
CHANGED
|
@@ -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
|
|
25
|
-
// Base: /api/repos/:id/
|
|
25
|
+
// Repository management
|
|
26
26
|
REPOS: '/api/repos/',
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
package/src/utils/hash-engine.js
CHANGED
|
@@ -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,
|