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
package/src/commands/pull.js
CHANGED
|
@@ -11,23 +11,13 @@
|
|
|
11
11
|
* gent pull → Pull from origin/current-branch
|
|
12
12
|
* gent pull <remote> <branch> → Pull specific remote/branch
|
|
13
13
|
*
|
|
14
|
-
* ALGORITHM:
|
|
15
|
-
* 1. GET /
|
|
16
|
-
* 2.
|
|
17
|
-
* 3.
|
|
18
|
-
* 4.
|
|
19
|
-
* 5.
|
|
20
|
-
* 6. If fast-forward:
|
|
21
|
-
*
|
|
22
|
-
* BACKEND EXPECTATIONS:
|
|
23
|
-
* GET /api/repos/:id/pull/?branch=main&since=abc1234
|
|
24
|
-
* Returns:
|
|
25
|
-
* {
|
|
26
|
-
* branch: "main",
|
|
27
|
-
* commits: [...],
|
|
28
|
-
* objects: [ { hash, type, data: "<base64>" } ],
|
|
29
|
-
* head: "<remoteHeadHash>"
|
|
30
|
-
* }
|
|
14
|
+
* ALGORITHM (client-side, no /pull/ endpoint):
|
|
15
|
+
* 1. GET .../branches/{branch}/ → get remote branch head SHA
|
|
16
|
+
* 2. GET .../commits/ → list all remote commits
|
|
17
|
+
* 3. Diff local vs remote commits, find new ones
|
|
18
|
+
* 4. For each new commit, fetch tree + blobs via individual endpoints
|
|
19
|
+
* 5. Store objects locally
|
|
20
|
+
* 6. If diverged: run 3-way merge. If fast-forward: advance pointer
|
|
31
21
|
*
|
|
32
22
|
* ============================================================================
|
|
33
23
|
*/
|
|
@@ -37,10 +27,10 @@ const path = require('path');
|
|
|
37
27
|
const chalk = require('chalk');
|
|
38
28
|
const ora = require('ora');
|
|
39
29
|
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
40
|
-
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
30
|
+
const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
|
|
41
31
|
const apiClient = require('../utils/api-client');
|
|
42
32
|
const authStorage = require('../utils/auth-storage');
|
|
43
|
-
const { storeBlob, objectExists } = require('../utils/hash-engine');
|
|
33
|
+
const { storeBlob, objectExists, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
|
|
44
34
|
const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
|
|
45
35
|
const { generateCommitHash } = require('../utils/helpers');
|
|
46
36
|
|
|
@@ -72,57 +62,136 @@ async function pull(remoteName, branchName, options) {
|
|
|
72
62
|
return;
|
|
73
63
|
}
|
|
74
64
|
|
|
65
|
+
const repoInfo = parseRemoteUrl(remoteConfig.url);
|
|
66
|
+
if (!repoInfo) {
|
|
67
|
+
spinner.fail(chalk.red('Invalid remote URL format'));
|
|
68
|
+
console.log(chalk.yellow('Expected: /api/repos/{owner_id}/{repo_name}'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
75
72
|
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
76
73
|
const branch = branchName || repository.currentBranch;
|
|
77
74
|
const localHead = repository.branches[branch] || null;
|
|
78
75
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
// 1. Get remote branch info to find remote HEAD
|
|
77
|
+
spinner.text = `Fetching branch info for ${branch}...`;
|
|
78
|
+
let remoteHead;
|
|
79
|
+
try {
|
|
80
|
+
const branchInfo = await apiClient.get(
|
|
81
|
+
buildRepoUrl(API_ENDPOINTS.REPO_BRANCH_DETAIL, { ...repoInfo, branch_name: branch })
|
|
82
|
+
);
|
|
83
|
+
remoteHead = branchInfo.commit_sha;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error.response?.status === 404) {
|
|
86
|
+
spinner.succeed(chalk.green('Remote branch not found — nothing to pull'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
if (!remoteHead || remoteHead === localHead) {
|
|
93
|
+
spinner.succeed(chalk.green('Already up-to-date'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
// 2. Fetch all remote commits
|
|
98
|
+
spinner.text = `Fetching commits...`;
|
|
99
|
+
const remoteCommits = await apiClient.get(
|
|
100
|
+
buildRepoUrl(API_ENDPOINTS.REPO_COMMITS, repoInfo)
|
|
88
101
|
);
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
103
|
+
// 3. Find commits we don't have locally
|
|
104
|
+
const localCommitSet = new Set((repository.commits || []).map(c => c.hash || c.sha));
|
|
105
|
+
const newRemoteCommits = remoteCommits.filter(c => !localCommitSet.has(c.sha));
|
|
93
106
|
|
|
94
|
-
if (
|
|
107
|
+
if (newRemoteCommits.length === 0) {
|
|
108
|
+
// We have all commits but pointer is different — update ref
|
|
109
|
+
config.remoteRefs = config.remoteRefs || {};
|
|
110
|
+
config.remoteRefs[`${remote}/${branch}`] = remoteHead;
|
|
111
|
+
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
95
112
|
spinner.succeed(chalk.green('Already up-to-date'));
|
|
96
113
|
return;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
//
|
|
100
|
-
spinner.text = `
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
// 4. For each new commit, fetch tree and blobs
|
|
117
|
+
spinner.text = `Fetching ${newRemoteCommits.length} new commit(s) + objects...`;
|
|
118
|
+
|
|
119
|
+
const fetchedCommits = [];
|
|
120
|
+
for (const commit of newRemoteCommits) {
|
|
121
|
+
// Fetch tree for this commit
|
|
122
|
+
let treeEntries = [];
|
|
123
|
+
if (commit.tree_sha) {
|
|
124
|
+
try {
|
|
125
|
+
const tree = await apiClient.get(
|
|
126
|
+
buildRepoUrl(API_ENDPOINTS.REPO_TREE_DETAIL, { ...repoInfo, sha: commit.tree_sha })
|
|
127
|
+
);
|
|
128
|
+
treeEntries = tree.entries || [];
|
|
129
|
+
} catch {
|
|
130
|
+
// Tree may not be available
|
|
131
|
+
}
|
|
105
132
|
}
|
|
106
|
-
}
|
|
107
133
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
// Fetch and store blobs
|
|
135
|
+
for (const entry of treeEntries) {
|
|
136
|
+
if (entry.type === 'blob' && entry.sha) {
|
|
137
|
+
if (!(await objectExists(gentPath, entry.sha))) {
|
|
138
|
+
try {
|
|
139
|
+
const blob = await apiClient.get(
|
|
140
|
+
buildRepoUrl(API_ENDPOINTS.REPO_BLOB_DETAIL, { ...repoInfo, sha: entry.sha })
|
|
141
|
+
);
|
|
142
|
+
if (blob.content) {
|
|
143
|
+
const buf = decodeRemoteBlobContent(blob.content, entry.sha);
|
|
144
|
+
await storeBlob(gentPath, buf);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Blob fetch failed, continue
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Convert remote commit format to local format
|
|
154
|
+
fetchedCommits.push({
|
|
155
|
+
hash: commit.sha,
|
|
156
|
+
message: commit.message,
|
|
157
|
+
author: { name: commit.author_name, email: commit.author_email },
|
|
158
|
+
timestamp: commit.committed_at,
|
|
159
|
+
parent: commit.parent_shas && commit.parent_shas[0] || null,
|
|
160
|
+
mergeParent: commit.parent_shas && commit.parent_shas[1] || null,
|
|
161
|
+
treeHash: commit.tree_sha,
|
|
162
|
+
tree: treeEntries.map(e => ({
|
|
163
|
+
mode: e.mode || '100644',
|
|
164
|
+
name: e.name,
|
|
165
|
+
hash: e.sha,
|
|
166
|
+
type: e.type || 'blob'
|
|
167
|
+
})),
|
|
168
|
+
files: treeEntries.map(e => ({ path: e.name, hash: e.sha })),
|
|
169
|
+
stats: {}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
111
172
|
|
|
112
|
-
// Add new commits (dedup)
|
|
173
|
+
// 5. Add new commits to local store (dedup)
|
|
113
174
|
let newCount = 0;
|
|
114
|
-
|
|
175
|
+
const commitSet = new Set((repository.commits || []).map(c => c.hash));
|
|
176
|
+
for (const commit of fetchedCommits) {
|
|
115
177
|
if (!commitSet.has(commit.hash)) {
|
|
178
|
+
repository.commits = repository.commits || [];
|
|
116
179
|
repository.commits.push(commit);
|
|
117
180
|
commitSet.add(commit.hash);
|
|
118
181
|
newCount++;
|
|
119
182
|
}
|
|
120
183
|
}
|
|
121
184
|
|
|
185
|
+
// 6. Merge strategy
|
|
186
|
+
config.remoteRefs = config.remoteRefs || {};
|
|
187
|
+
|
|
122
188
|
if (!localHead || isAncestor(repository.commits, localHead, remoteHead)) {
|
|
123
189
|
// Fast-forward
|
|
190
|
+
const previousTree = localHead ? getCommitTree(repository.commits, localHead) : [];
|
|
191
|
+
const nextTree = getCommitTree(repository.commits, remoteHead);
|
|
124
192
|
repository.branches[branch] = remoteHead;
|
|
125
193
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
194
|
+
await checkoutTree(gentPath, process.cwd(), previousTree, nextTree);
|
|
126
195
|
|
|
127
196
|
config.remoteRefs[`${remote}/${branch}`] = remoteHead;
|
|
128
197
|
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
@@ -168,6 +237,9 @@ async function pull(remoteName, branchName, options) {
|
|
|
168
237
|
repository.commits.push(mergeCommit);
|
|
169
238
|
repository.branches[branch] = mergeCommit.hash;
|
|
170
239
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
240
|
+
if (!mergeResult.hasConflicts) {
|
|
241
|
+
await checkoutTree(gentPath, process.cwd(), oursTree, mergeResult.mergedEntries);
|
|
242
|
+
}
|
|
171
243
|
|
|
172
244
|
config.remoteRefs[`${remote}/${branch}`] = remoteHead;
|
|
173
245
|
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
@@ -187,8 +259,8 @@ async function pull(remoteName, branchName, options) {
|
|
|
187
259
|
spinner.fail(chalk.red('Pull failed'));
|
|
188
260
|
if (error.response?.status === 401) {
|
|
189
261
|
console.error(chalk.red('Authentication failed — run "gent login"'));
|
|
190
|
-
} else if (error.response?.data
|
|
191
|
-
console.error(chalk.red(error.response.data
|
|
262
|
+
} else if (error.response?.data) {
|
|
263
|
+
console.error(chalk.red(JSON.stringify(error.response.data)));
|
|
192
264
|
} else {
|
|
193
265
|
console.error(chalk.red('Error:'), error.message);
|
|
194
266
|
}
|
|
@@ -210,4 +282,40 @@ function isAncestor(commits, hashA, hashB) {
|
|
|
210
282
|
return false;
|
|
211
283
|
}
|
|
212
284
|
|
|
285
|
+
function getCommitTree(commits, hash) {
|
|
286
|
+
const commit = commits.find(c => c.hash === hash);
|
|
287
|
+
if (!commit) return [];
|
|
288
|
+
return commit.tree || (commit.files || []).map(f => ({
|
|
289
|
+
mode: '100644',
|
|
290
|
+
name: f.path || f.name,
|
|
291
|
+
hash: f.hash,
|
|
292
|
+
type: 'blob'
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function checkoutTree(gentPath, cwd, previousTree, nextTree) {
|
|
297
|
+
const nextPaths = new Set(nextTree.map(e => e.name || e.path));
|
|
298
|
+
|
|
299
|
+
for (const entry of previousTree) {
|
|
300
|
+
const relPath = entry.name || entry.path;
|
|
301
|
+
if (!relPath || nextPaths.has(relPath)) continue;
|
|
302
|
+
try {
|
|
303
|
+
await fs.unlink(path.join(cwd, relPath));
|
|
304
|
+
} catch {
|
|
305
|
+
// File already absent.
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const entry of nextTree) {
|
|
310
|
+
if (entry.type && entry.type !== 'blob') continue;
|
|
311
|
+
const relPath = entry.name || entry.path;
|
|
312
|
+
if (!relPath || !entry.hash) continue;
|
|
313
|
+
|
|
314
|
+
const content = await readBlobAsString(gentPath, entry.hash);
|
|
315
|
+
const fullPath = path.join(cwd, relPath);
|
|
316
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
317
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
213
321
|
module.exports = pull;
|
package/src/commands/push.js
CHANGED
|
@@ -14,28 +14,22 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ALGORITHM:
|
|
16
16
|
* 1. Read local commits since last known remote HEAD
|
|
17
|
-
* 2.
|
|
18
|
-
* 3. POST packfile
|
|
19
|
-
* 4. Remote updates branch pointer
|
|
17
|
+
* 2. Build proper pack: collect tree objects + blob objects
|
|
18
|
+
* 3. POST packfile to /api/repos/{owner_id}/{repo_name}/push/
|
|
19
|
+
* 4. Remote updates branch pointer via branch_updates
|
|
20
20
|
*
|
|
21
|
-
* DATA FORMAT SENT TO BACKEND:
|
|
22
|
-
* POST /api/repos
|
|
21
|
+
* DATA FORMAT SENT TO BACKEND (PushPackRequest):
|
|
22
|
+
* POST /api/repos/{owner_id}/{repo_name}/push/
|
|
23
23
|
* {
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* pack: {
|
|
25
|
+
* commits: [{ sha, message, tree_sha, parent_shas, author_name, author_email, committed_at }],
|
|
26
|
+
* trees: [{ sha, entries: [{ type, mode, name, sha }] }],
|
|
27
|
+
* blobs: [{ sha, size, content, encoding }]
|
|
28
|
+
* },
|
|
29
|
+
* branch_updates: [{ name, commit_sha }],
|
|
28
30
|
* tags: { "v1.0": { hash, message, ... } }
|
|
29
31
|
* }
|
|
30
32
|
*
|
|
31
|
-
* BACKEND EXPECTATIONS:
|
|
32
|
-
* - Validate auth (JWT Bearer token)
|
|
33
|
-
* - Verify fast-forward (reject non-ff unless force=true)
|
|
34
|
-
* - Store blob objects in backend object store
|
|
35
|
-
* - Append commits to branch history
|
|
36
|
-
* - Update branch refs
|
|
37
|
-
* - Return { success, ref, hash }
|
|
38
|
-
*
|
|
39
33
|
* ============================================================================
|
|
40
34
|
*/
|
|
41
35
|
|
|
@@ -44,10 +38,10 @@ const path = require('path');
|
|
|
44
38
|
const chalk = require('chalk');
|
|
45
39
|
const ora = require('ora');
|
|
46
40
|
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
47
|
-
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
41
|
+
const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
|
|
48
42
|
const apiClient = require('../utils/api-client');
|
|
49
43
|
const authStorage = require('../utils/auth-storage');
|
|
50
|
-
const { readBlob, objectExists } = require('../utils/hash-engine');
|
|
44
|
+
const { readBlob, readTree, objectExists, readBlobAsString } = require('../utils/hash-engine');
|
|
51
45
|
|
|
52
46
|
/**
|
|
53
47
|
* Push commits to remote
|
|
@@ -81,6 +75,15 @@ async function push(remoteName, branchName, options) {
|
|
|
81
75
|
return;
|
|
82
76
|
}
|
|
83
77
|
|
|
78
|
+
// Parse remote URL to get owner_id and repo_name
|
|
79
|
+
const repoInfo = parseRemoteUrl(remoteConfig.url);
|
|
80
|
+
if (!repoInfo) {
|
|
81
|
+
spinner.fail(chalk.red('Invalid remote URL format'));
|
|
82
|
+
console.log(chalk.yellow('Expected: /api/repos/{owner_id}/{repo_name}'));
|
|
83
|
+
console.log(chalk.yellow('Use "gent remote set-url origin <url>" to fix'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
84
87
|
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
85
88
|
const branch = branchName || repository.currentBranch;
|
|
86
89
|
const localHead = repository.branches[branch];
|
|
@@ -103,26 +106,71 @@ async function push(remoteName, branchName, options) {
|
|
|
103
106
|
|
|
104
107
|
spinner.text = `Pushing ${commitsToPush.length} commit(s) to ${remote}/${branch}...`;
|
|
105
108
|
|
|
106
|
-
// Collect all blob
|
|
107
|
-
const
|
|
109
|
+
// Collect all tree and blob objects from commits
|
|
110
|
+
const treeShas = new Set();
|
|
111
|
+
const blobShas = new Set();
|
|
112
|
+
|
|
108
113
|
for (const commit of commitsToPush) {
|
|
114
|
+
// Collect tree SHA
|
|
115
|
+
if (commit.treeHash) {
|
|
116
|
+
treeShas.add(commit.treeHash);
|
|
117
|
+
}
|
|
118
|
+
// Collect blob hashes from tree entries
|
|
109
119
|
const tree = commit.tree || commit.files || [];
|
|
110
120
|
for (const entry of tree) {
|
|
111
|
-
|
|
112
|
-
if (h) blobHashes.add(h);
|
|
121
|
+
if (entry.hash) blobShas.add(entry.hash);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
118
|
-
for (const
|
|
125
|
+
// Build tree objects for the pack
|
|
126
|
+
const packTrees = [];
|
|
127
|
+
for (const treeSha of treeShas) {
|
|
128
|
+
try {
|
|
129
|
+
if (await objectExists(gentPath, treeSha)) {
|
|
130
|
+
const entries = await readTree(gentPath, treeSha);
|
|
131
|
+
packTrees.push({
|
|
132
|
+
sha: treeSha,
|
|
133
|
+
entries: entries.map(e => ({
|
|
134
|
+
type: e.type || 'blob',
|
|
135
|
+
mode: e.mode || '100644',
|
|
136
|
+
name: e.name,
|
|
137
|
+
sha: e.hash
|
|
138
|
+
}))
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// If tree can't be read from object store, build from commit data
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If no tree objects from object store, build from commit tree data
|
|
147
|
+
if (packTrees.length === 0) {
|
|
148
|
+
for (const commit of commitsToPush) {
|
|
149
|
+
if (commit.treeHash && commit.tree) {
|
|
150
|
+
packTrees.push({
|
|
151
|
+
sha: commit.treeHash,
|
|
152
|
+
entries: commit.tree.map(e => ({
|
|
153
|
+
type: e.type || 'blob',
|
|
154
|
+
mode: e.mode || '100644',
|
|
155
|
+
name: e.name || e.path,
|
|
156
|
+
sha: e.hash
|
|
157
|
+
}))
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build blob objects for the pack
|
|
164
|
+
const packBlobs = [];
|
|
165
|
+
for (const hash of blobShas) {
|
|
119
166
|
try {
|
|
120
167
|
if (await objectExists(gentPath, hash)) {
|
|
121
168
|
const data = await readBlob(gentPath, hash);
|
|
122
|
-
|
|
123
|
-
hash,
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
packBlobs.push({
|
|
170
|
+
sha: hash,
|
|
171
|
+
size: data.length,
|
|
172
|
+
content: data.toString('base64'),
|
|
173
|
+
encoding: 'base64'
|
|
126
174
|
});
|
|
127
175
|
}
|
|
128
176
|
} catch {
|
|
@@ -130,31 +178,34 @@ async function push(remoteName, branchName, options) {
|
|
|
130
178
|
}
|
|
131
179
|
}
|
|
132
180
|
|
|
133
|
-
// Build
|
|
181
|
+
// Build commits for the pack
|
|
182
|
+
const packCommits = commitsToPush.map(c => ({
|
|
183
|
+
sha: c.hash,
|
|
184
|
+
message: c.message,
|
|
185
|
+
tree_sha: c.treeHash || '',
|
|
186
|
+
parent_shas: [c.parent, c.mergeParent].filter(Boolean),
|
|
187
|
+
author_name: typeof c.author === 'object' ? (c.author.name || 'Unknown') : (c.author || 'Unknown'),
|
|
188
|
+
author_email: typeof c.author === 'object' ? (c.author.email || '') : '',
|
|
189
|
+
committed_at: c.timestamp || new Date().toISOString()
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
// Build push payload matching PushPackRequest schema
|
|
134
193
|
const payload = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
treeHash: c.treeHash || null,
|
|
145
|
-
tree: c.tree || null,
|
|
146
|
-
files: c.files || [],
|
|
147
|
-
stats: c.stats || {}
|
|
148
|
-
})),
|
|
149
|
-
objects,
|
|
194
|
+
pack: {
|
|
195
|
+
commits: packCommits,
|
|
196
|
+
trees: packTrees,
|
|
197
|
+
blobs: packBlobs
|
|
198
|
+
},
|
|
199
|
+
branch_updates: [{
|
|
200
|
+
name: branch,
|
|
201
|
+
commit_sha: localHead
|
|
202
|
+
}],
|
|
150
203
|
tags: repository.tags || {}
|
|
151
204
|
};
|
|
152
205
|
|
|
153
206
|
// Send to backend
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
payload
|
|
157
|
-
);
|
|
207
|
+
const pushUrl = buildRepoUrl(API_ENDPOINTS.REPO_PUSH, repoInfo);
|
|
208
|
+
const response = await apiClient.post(pushUrl, payload);
|
|
158
209
|
|
|
159
210
|
// Update remote ref
|
|
160
211
|
config.remoteRefs[`${remote}/${branch}`] = localHead;
|
|
@@ -162,7 +213,7 @@ async function push(remoteName, branchName, options) {
|
|
|
162
213
|
|
|
163
214
|
spinner.succeed(chalk.green(`Pushed ${commitsToPush.length} commit(s) to ${remote}/${branch}`));
|
|
164
215
|
console.log(chalk.gray(` ${localHead.substring(0, 7)} → ${remote}/${branch}`));
|
|
165
|
-
console.log(chalk.gray(` ${
|
|
216
|
+
console.log(chalk.gray(` ${packBlobs.length} blob(s), ${packTrees.length} tree(s) transferred`));
|
|
166
217
|
|
|
167
218
|
} catch (error) {
|
|
168
219
|
spinner.fail(chalk.red('Push failed'));
|
|
@@ -172,8 +223,10 @@ async function push(remoteName, branchName, options) {
|
|
|
172
223
|
console.log(chalk.yellow('Run "gent pull" first, or use "gent push --force"'));
|
|
173
224
|
} else if (error.response?.status === 401) {
|
|
174
225
|
console.error(chalk.red('Authentication failed — run "gent login"'));
|
|
175
|
-
} else if (error.response?.
|
|
176
|
-
console.error(chalk.red(
|
|
226
|
+
} else if (error.response?.status === 403) {
|
|
227
|
+
console.error(chalk.red('Permission denied — only repo owner can push'));
|
|
228
|
+
} else if (error.response?.data) {
|
|
229
|
+
console.error(chalk.red(JSON.stringify(error.response.data, null, 2)));
|
|
177
230
|
} else {
|
|
178
231
|
console.error(chalk.red('Error:'), error.message);
|
|
179
232
|
}
|
package/src/commands/register.js
CHANGED
|
@@ -17,12 +17,19 @@ async function register(options) {
|
|
|
17
17
|
console.log(chalk.cyan('\n🚀 Create your Gent account\n'));
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
+
let email = options.email;
|
|
21
|
+
let password = options.password;
|
|
22
|
+
let passwordConfirm = options.passwordConfirm;
|
|
23
|
+
let firstName = options.firstName;
|
|
24
|
+
let lastName = options.lastName;
|
|
25
|
+
|
|
20
26
|
// Prompt for user information
|
|
21
27
|
const answers = await inquirer.prompt([
|
|
22
28
|
{
|
|
23
29
|
type: 'input',
|
|
24
30
|
name: 'email',
|
|
25
31
|
message: 'Email address:',
|
|
32
|
+
when: !email,
|
|
26
33
|
validate: (input) => {
|
|
27
34
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
28
35
|
return emailRegex.test(input) || 'Please enter a valid email address';
|
|
@@ -33,6 +40,7 @@ async function register(options) {
|
|
|
33
40
|
name: 'password',
|
|
34
41
|
message: 'Password:',
|
|
35
42
|
mask: '*',
|
|
43
|
+
when: !password,
|
|
36
44
|
validate: (input) => {
|
|
37
45
|
if (input.length < 8) {
|
|
38
46
|
return 'Password must be at least 8 characters long';
|
|
@@ -45,33 +53,46 @@ async function register(options) {
|
|
|
45
53
|
name: 'passwordConfirm',
|
|
46
54
|
message: 'Confirm password:',
|
|
47
55
|
mask: '*',
|
|
56
|
+
when: !passwordConfirm,
|
|
48
57
|
validate: (input, answers) => {
|
|
49
|
-
return input === answers.password || 'Passwords do not match';
|
|
58
|
+
return input === (password || answers.password) || 'Passwords do not match';
|
|
50
59
|
}
|
|
51
60
|
},
|
|
52
61
|
{
|
|
53
62
|
type: 'input',
|
|
54
63
|
name: 'firstName',
|
|
55
64
|
message: 'First name:',
|
|
65
|
+
when: firstName === undefined,
|
|
56
66
|
default: ''
|
|
57
67
|
},
|
|
58
68
|
{
|
|
59
69
|
type: 'input',
|
|
60
70
|
name: 'lastName',
|
|
61
71
|
message: 'Last name:',
|
|
72
|
+
when: lastName === undefined,
|
|
62
73
|
default: ''
|
|
63
74
|
}
|
|
64
75
|
]);
|
|
65
76
|
|
|
77
|
+
email = email || answers.email;
|
|
78
|
+
password = password || answers.password;
|
|
79
|
+
passwordConfirm = passwordConfirm || answers.passwordConfirm;
|
|
80
|
+
firstName = firstName !== undefined ? firstName : answers.firstName;
|
|
81
|
+
lastName = lastName !== undefined ? lastName : answers.lastName;
|
|
82
|
+
|
|
83
|
+
if (!email || !password || !passwordConfirm) {
|
|
84
|
+
throw new Error('Email, password, and password confirmation are required');
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
const spinner = ora('Creating your account...').start();
|
|
67
88
|
|
|
68
89
|
// Register user
|
|
69
90
|
const user = await authService.register(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
email,
|
|
92
|
+
password,
|
|
93
|
+
passwordConfirm,
|
|
94
|
+
firstName || '',
|
|
95
|
+
lastName || ''
|
|
75
96
|
);
|
|
76
97
|
|
|
77
98
|
spinner.succeed(chalk.green('✓ Account created successfully!'));
|
package/src/commands/remote.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
const path = require('path');
|
|
30
30
|
const chalk = require('chalk');
|
|
31
31
|
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
32
|
-
const { CONFIG_FILE } = require('../utils/constants');
|
|
32
|
+
const { CONFIG_FILE, parseRemoteUrl } = require('../utils/constants');
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Manage remotes
|
|
@@ -55,6 +55,13 @@ async function remote(subcommand, args, options) {
|
|
|
55
55
|
console.error(chalk.red(`Remote '${name}' already exists`));
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
+
// Validate URL format
|
|
59
|
+
if (!parseRemoteUrl(url)) {
|
|
60
|
+
console.error(chalk.red('Invalid remote URL format'));
|
|
61
|
+
console.log(chalk.yellow('Expected: /api/repos/{owner_id}/{repo_name}'));
|
|
62
|
+
console.log(chalk.yellow('Example: /api/repos/1/my-project'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
58
65
|
config.remotes[name] = { url };
|
|
59
66
|
await writeJSON(configPath, config);
|
|
60
67
|
console.log(chalk.green(`Added remote '${name}' → ${url}`));
|
|
@@ -85,6 +92,12 @@ async function remote(subcommand, args, options) {
|
|
|
85
92
|
console.error(chalk.red(`Remote '${name}' not found`));
|
|
86
93
|
return;
|
|
87
94
|
}
|
|
95
|
+
// Validate URL format
|
|
96
|
+
if (!parseRemoteUrl(url)) {
|
|
97
|
+
console.error(chalk.red('Invalid remote URL format'));
|
|
98
|
+
console.log(chalk.yellow('Expected: /api/repos/{owner_id}/{repo_name}'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
88
101
|
config.remotes[name].url = url;
|
|
89
102
|
await writeJSON(configPath, config);
|
|
90
103
|
console.log(chalk.green(`Updated '${name}' → ${url}`));
|