gent-cli 2.1.0 → 5.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 +28 -42
- package/package.json +1 -1
- package/src/commands/add.js +102 -23
- package/src/commands/clone.js +137 -86
- package/src/commands/commit.js +89 -81
- package/src/commands/diff.js +257 -0
- package/src/commands/init.js +5 -36
- package/src/commands/log.js +57 -13
- package/src/commands/merge.js +245 -0
- package/src/commands/pull.js +176 -86
- package/src/commands/push.js +172 -79
- package/src/commands/remote.js +97 -113
- package/src/commands/reset.js +149 -0
- package/src/commands/rm.js +85 -0
- package/src/commands/show.js +167 -0
- package/src/commands/stash.js +255 -0
- package/src/commands/status.js +80 -46
- package/src/commands/tag.js +146 -0
- package/src/index.js +108 -57
- package/src/utils/constants.js +10 -26
- package/src/utils/diff-engine.js +236 -0
- package/src/utils/fileSystem.js +8 -60
- package/src/utils/hash-engine.js +337 -0
- package/src/utils/merge-engine.js +379 -0
- package/src/utils/object-store.js +54 -0
- package/src/commands/create.js +0 -121
- package/src/commands/list.js +0 -67
- package/src/services/repo-service.js +0 -291
- package/src/utils/cloud-sync.js +0 -323
- package/src/utils/diff.js +0 -121
package/README.md
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> A modern, Git-like version control CLI with built-in cloud authentication and global user identity management.
|
|
4
4
|
|
|
5
|
-
Gent is a lightweight version control system that feels exactly like Git but handles user identity automatically through the cloud. No more configuring
|
|
5
|
+
Gent is a lightweight version control system that feels exactly like Git but handles user identity automatically through the cloud. No more configuring user.name and user.email for every repository!
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Cloud Authentication**: Login once, work everywhere. Your identity follows you across projects.
|
|
10
10
|
- **Git-like Experience**: Familiar commands (init, add, commit, status, log, branch, checkout).
|
|
11
|
-
- **
|
|
12
|
-
- **Zero Configuration**: `gent init` is silent and auto-detects your authenticated user profile.
|
|
11
|
+
- **Zero Configuration**: gent init is silent and auto-detects your authenticated user profile.
|
|
13
12
|
- **Global Identity**: Commits are automatically authored with your cloud profile.
|
|
14
13
|
- **Secure**: Tokens stored securely in your home directory.
|
|
15
14
|
|
|
@@ -49,10 +48,12 @@ gent whoami
|
|
|
49
48
|
gent logout
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
##
|
|
51
|
+
## Usage
|
|
53
52
|
|
|
54
53
|
### 1. Initialize a Repository
|
|
55
54
|
|
|
55
|
+
Just like Git, gent init is silent and sets up a new repository in your current directory. It automatically uses your logged-in identity for configuration.
|
|
56
|
+
|
|
56
57
|
```bash
|
|
57
58
|
gent init
|
|
58
59
|
# Output: Initialized empty Gent repository in /path/to/project
|
|
@@ -60,12 +61,16 @@ gent init
|
|
|
60
61
|
|
|
61
62
|
### 2. Check Status
|
|
62
63
|
|
|
64
|
+
See which files are modified or untracked.
|
|
65
|
+
|
|
63
66
|
```bash
|
|
64
67
|
gent status
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
### 3. Stage Files
|
|
68
71
|
|
|
72
|
+
Add files to the staging area.
|
|
73
|
+
|
|
69
74
|
```bash
|
|
70
75
|
gent add filename.js
|
|
71
76
|
# or add all files
|
|
@@ -74,74 +79,55 @@ gent add .
|
|
|
74
79
|
|
|
75
80
|
### 4. Commit Changes
|
|
76
81
|
|
|
82
|
+
Create a commit. Gent automatically fetches your name and email from your global login session.
|
|
83
|
+
|
|
77
84
|
```bash
|
|
78
85
|
gent commit -m "Initial commit"
|
|
79
86
|
# Output: [main a1b2c3d] Initial commit
|
|
80
87
|
# Author: Your Name <your.email@example.com>
|
|
81
88
|
```
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
### 1. Create a Cloud Repository
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# Create and link a local repo
|
|
89
|
-
gent create my-repo --init-local
|
|
90
|
-
|
|
91
|
-
# Or initialize with cloud directly
|
|
92
|
-
gent init --cloud
|
|
93
|
-
```
|
|
90
|
+
### 5. View History
|
|
94
91
|
|
|
95
|
-
|
|
92
|
+
See your commit history.
|
|
96
93
|
|
|
97
94
|
```bash
|
|
98
|
-
gent
|
|
99
|
-
# or
|
|
100
|
-
gent
|
|
95
|
+
gent log
|
|
96
|
+
# or compact view
|
|
97
|
+
gent log --oneline
|
|
101
98
|
```
|
|
102
99
|
|
|
103
|
-
|
|
100
|
+
## Branching
|
|
104
101
|
|
|
105
|
-
|
|
106
|
-
gent clone <owner_id>/<repo_name>
|
|
107
|
-
# Example: gent clone 1/my-repo
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### 4. Push Changes
|
|
102
|
+
Manage branches just like you're used to.
|
|
111
103
|
|
|
112
104
|
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
gent commit -m "Update README" --push
|
|
116
|
-
```
|
|
105
|
+
# Create and switch to a new branch
|
|
106
|
+
gent checkout -b feature-login
|
|
117
107
|
|
|
118
|
-
|
|
108
|
+
# List branches
|
|
109
|
+
gent branch
|
|
119
110
|
|
|
120
|
-
|
|
121
|
-
gent
|
|
122
|
-
```
|
|
111
|
+
# Switch back to main
|
|
112
|
+
gent checkout main
|
|
123
113
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
gent remote add origin <owner_id>/<repo_name>
|
|
128
|
-
gent remote -v
|
|
114
|
+
# Delete a branch
|
|
115
|
+
gent branch -d feature-login
|
|
129
116
|
```
|
|
130
117
|
|
|
131
118
|
## Repository Structure
|
|
132
119
|
|
|
133
|
-
Gent creates a
|
|
120
|
+
Gent creates a .gent directory in your project root:
|
|
134
121
|
|
|
135
122
|
```
|
|
136
123
|
.gent/
|
|
137
124
|
├── config.json # Project configuration
|
|
138
125
|
├── objects/ # Stored file contents
|
|
139
126
|
├── refs/ # Branch pointers
|
|
140
|
-
├── remote.json # Remote configuration
|
|
141
127
|
└── HEAD # Current branch reference
|
|
142
128
|
```
|
|
143
129
|
|
|
144
|
-
Your authentication tokens are stored globally in
|
|
130
|
+
Your authentication tokens are stored globally in ~/.gent/auth.json.
|
|
145
131
|
|
|
146
132
|
## Contributing
|
|
147
133
|
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Add Command - Add file contents to the staging area
|
|
3
|
-
* Stages files
|
|
3
|
+
* Stages files with content snapshots (blobs) and diff stats
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs').promises;
|
|
6
7
|
const path = require('path');
|
|
7
8
|
const chalk = require('chalk');
|
|
8
9
|
const ora = require('ora');
|
|
9
10
|
const { getGentPath, readJSON, writeJSON, pathExists, getAllFiles, getIgnorePatterns } = require('../utils/fileSystem');
|
|
10
|
-
const { STAGING_FILE } = require('../utils/constants');
|
|
11
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
12
|
+
const { storeBlob, hashBlob, isBinaryBuffer, snapshotFile } = require('../utils/hash-engine');
|
|
13
|
+
const { diffText, formatUnifiedDiff } = require('../utils/diff-engine');
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Add files to staging area
|
|
@@ -24,7 +27,19 @@ async function add(files, options) {
|
|
|
24
27
|
|
|
25
28
|
// Read current staging area
|
|
26
29
|
const staging = await readJSON(stagingPath);
|
|
27
|
-
const
|
|
30
|
+
const stagedEntries = staging.entries || [];
|
|
31
|
+
const stagedMap = new Map(stagedEntries.map(e => [e.path, e]));
|
|
32
|
+
|
|
33
|
+
// Get last commit tree for diff comparison
|
|
34
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
35
|
+
const lastCommitHash = repository.branches[repository.currentBranch] || null;
|
|
36
|
+
const lastCommit = lastCommitHash
|
|
37
|
+
? (repository.commits || []).find(c => c.hash === lastCommitHash)
|
|
38
|
+
: null;
|
|
39
|
+
const lastTreeMap = new Map(
|
|
40
|
+
(lastCommit && lastCommit.tree ? lastCommit.tree : (lastCommit ? lastCommit.files : []))
|
|
41
|
+
.map(f => [f.path || f.name, f.hash])
|
|
42
|
+
);
|
|
28
43
|
|
|
29
44
|
let filesToAdd = [];
|
|
30
45
|
|
|
@@ -35,49 +50,113 @@ async function add(files, options) {
|
|
|
35
50
|
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
36
51
|
filesToAdd = allFiles.map(f => path.relative(cwd, f));
|
|
37
52
|
} else {
|
|
38
|
-
// Add specified files
|
|
39
53
|
for (const file of files) {
|
|
40
54
|
const filePath = path.resolve(cwd, file);
|
|
41
|
-
|
|
42
55
|
if (!await pathExists(filePath)) {
|
|
43
56
|
spinner.warn(chalk.yellow(`Warning: File not found: ${file}`));
|
|
44
57
|
continue;
|
|
45
58
|
}
|
|
46
|
-
|
|
47
|
-
const relativePath = path.relative(cwd, filePath);
|
|
48
|
-
filesToAdd.push(relativePath);
|
|
59
|
+
filesToAdd.push(path.relative(cwd, filePath));
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
//
|
|
63
|
+
// Snapshot each file → store blob, compute diff
|
|
53
64
|
let addedCount = 0;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
let totalInsertions = 0;
|
|
66
|
+
let totalDeletions = 0;
|
|
67
|
+
const diffSummaries = [];
|
|
68
|
+
|
|
69
|
+
for (const relPath of filesToAdd) {
|
|
70
|
+
const fullPath = path.join(cwd, relPath);
|
|
71
|
+
const content = await fs.readFile(fullPath);
|
|
72
|
+
|
|
73
|
+
// Skip binary files for diff (still store blob)
|
|
74
|
+
const binary = isBinaryBuffer(content);
|
|
75
|
+
const blobHash = await storeBlob(gentPath, content);
|
|
76
|
+
|
|
77
|
+
// Check if changed vs last commit
|
|
78
|
+
const prevHash = lastTreeMap.get(relPath);
|
|
79
|
+
if (prevHash === blobHash && stagedMap.has(relPath)) {
|
|
80
|
+
continue; // unchanged, already staged
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Determine change status
|
|
84
|
+
let status = 'added';
|
|
85
|
+
let stats = { insertions: 0, deletions: 0 };
|
|
86
|
+
|
|
87
|
+
if (prevHash && prevHash !== blobHash && !binary) {
|
|
88
|
+
status = 'modified';
|
|
89
|
+
try {
|
|
90
|
+
const { readBlobAsString } = require('../utils/hash-engine');
|
|
91
|
+
const oldContent = await readBlobAsString(gentPath, prevHash);
|
|
92
|
+
const diff = diffText(oldContent, content.toString('utf-8'));
|
|
93
|
+
stats = { insertions: diff.stats.insertions, deletions: diff.stats.deletions };
|
|
94
|
+
} catch {
|
|
95
|
+
// Old blob may not exist yet (first time adding objects)
|
|
96
|
+
}
|
|
97
|
+
} else if (!prevHash) {
|
|
98
|
+
status = 'added';
|
|
99
|
+
stats.insertions = content.toString('utf-8').split('\n').length;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
totalInsertions += stats.insertions;
|
|
103
|
+
totalDeletions += stats.deletions;
|
|
104
|
+
|
|
105
|
+
// Update staging entry
|
|
106
|
+
stagedMap.set(relPath, {
|
|
107
|
+
path: relPath,
|
|
108
|
+
hash: blobHash,
|
|
109
|
+
status,
|
|
110
|
+
binary,
|
|
111
|
+
stats
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
diffSummaries.push({ path: relPath, status, stats, binary });
|
|
115
|
+
addedCount++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect deleted files (tracked in last commit but gone from disk)
|
|
119
|
+
for (const [trackedPath, trackedHash] of lastTreeMap) {
|
|
120
|
+
const fullPath = path.join(cwd, trackedPath);
|
|
121
|
+
if (!await pathExists(fullPath) && !stagedMap.has(trackedPath)) {
|
|
122
|
+
stagedMap.set(trackedPath, {
|
|
123
|
+
path: trackedPath,
|
|
124
|
+
hash: null,
|
|
125
|
+
status: 'deleted',
|
|
126
|
+
binary: false,
|
|
127
|
+
stats: { insertions: 0, deletions: 0 }
|
|
128
|
+
});
|
|
129
|
+
diffSummaries.push({ path: trackedPath, status: 'deleted', stats: { insertions: 0, deletions: 0 }, binary: false });
|
|
57
130
|
addedCount++;
|
|
58
131
|
}
|
|
59
132
|
}
|
|
60
133
|
|
|
61
|
-
// Save staging area
|
|
62
|
-
staging.
|
|
134
|
+
// Save staging area (new format with entries)
|
|
135
|
+
staging.entries = Array.from(stagedMap.values());
|
|
136
|
+
staging.files = staging.entries.map(e => e.path); // backward compat
|
|
63
137
|
await writeJSON(stagingPath, staging);
|
|
64
138
|
|
|
65
|
-
spinner.succeed(chalk.green(
|
|
139
|
+
spinner.succeed(chalk.green(`Added ${addedCount} file(s) to staging area`));
|
|
66
140
|
|
|
67
|
-
if (
|
|
68
|
-
console.log(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
if (diffSummaries.length > 0) {
|
|
142
|
+
console.log('');
|
|
143
|
+
for (const d of diffSummaries) {
|
|
144
|
+
const statusIcon = d.status === 'added' ? chalk.green('+ new')
|
|
145
|
+
: d.status === 'deleted' ? chalk.red('- del')
|
|
146
|
+
: chalk.yellow('~ mod');
|
|
147
|
+
const statsStr = d.binary ? chalk.gray('(binary)')
|
|
148
|
+
: chalk.green(`+${d.stats.insertions}`) + ' ' + chalk.red(`-${d.stats.deletions}`);
|
|
149
|
+
console.log(` ${statusIcon} ${d.path} ${statsStr}`);
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.gray(`\n Total: `) + chalk.green(`+${totalInsertions}`) + ' ' + chalk.red(`-${totalDeletions}`));
|
|
152
|
+
console.log(chalk.cyan('\nUse "gent commit" to record your changes'));
|
|
73
153
|
}
|
|
74
154
|
|
|
75
155
|
} catch (error) {
|
|
76
156
|
spinner.fail(chalk.red('Failed to add files'));
|
|
77
|
-
|
|
78
157
|
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
79
158
|
console.error(chalk.red('\nError: Not a gent repository'));
|
|
80
|
-
console.log(chalk.yellow('
|
|
159
|
+
console.log(chalk.yellow('Run "gent init" to initialize a repository'));
|
|
81
160
|
} else {
|
|
82
161
|
console.error(chalk.red('\nError:'), error.message);
|
|
83
162
|
}
|
package/src/commands/clone.js
CHANGED
|
@@ -1,131 +1,182 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Clone Command - Clone a remote repository to local filesystem
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Download an entire repository (commits + objects) from a remote server
|
|
8
|
+
* and set up a local working copy. Like `git clone`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent clone <url> → Clone into folder named after repo
|
|
12
|
+
* gent clone <url> <directory> → Clone into specific directory
|
|
13
|
+
*
|
|
14
|
+
* ALGORITHM:
|
|
15
|
+
* 1. GET <url>/clone/ → receives full repo (commits, objects, config)
|
|
16
|
+
* 2. Create .gent/ directory structure
|
|
17
|
+
* 3. Store all blob objects in local object store
|
|
18
|
+
* 4. Write commits.json with full history
|
|
19
|
+
* 5. Checkout HEAD (restore working tree from latest commit)
|
|
20
|
+
* 6. Configure remote "origin" pointing to <url>
|
|
21
|
+
*
|
|
22
|
+
* BACKEND EXPECTATIONS:
|
|
23
|
+
* GET /api/repos/:id/clone/
|
|
24
|
+
* Returns:
|
|
25
|
+
* {
|
|
26
|
+
* name: "repo-name",
|
|
27
|
+
* commits: [...],
|
|
28
|
+
* objects: [ { hash, type, data: "<base64>" } ],
|
|
29
|
+
* branches: { "main": "<hash>", ... },
|
|
30
|
+
* currentBranch: "main",
|
|
31
|
+
* tags: { ... }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* ============================================================================
|
|
4
35
|
*/
|
|
5
36
|
|
|
6
37
|
const fs = require('fs').promises;
|
|
38
|
+
const path = require('path');
|
|
7
39
|
const chalk = require('chalk');
|
|
8
40
|
const ora = require('ora');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const repoService = require('../services/repo-service');
|
|
11
|
-
const authStorage = require('../utils/auth-storage');
|
|
12
41
|
const { ensureDir, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
13
42
|
const { GENT_DIR, CONFIG_FILE, STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
14
|
-
const
|
|
43
|
+
const apiClient = require('../utils/api-client');
|
|
44
|
+
const { storeBlob, readBlobAsString } = require('../utils/hash-engine');
|
|
15
45
|
|
|
16
46
|
/**
|
|
17
|
-
* Clone
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {Object} options
|
|
47
|
+
* Clone remote repository
|
|
48
|
+
* @param {String} url - Remote repository URL
|
|
49
|
+
* @param {String} directory - Optional target directory
|
|
50
|
+
* @param {Object} options
|
|
21
51
|
*/
|
|
22
|
-
async function clone(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
console.error(chalk.red('Error: You must be logged in to clone a repository'));
|
|
28
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Parse repository URL
|
|
33
|
-
if (!repoUrl) {
|
|
34
|
-
console.error(chalk.red('Error: Repository URL is required'));
|
|
35
|
-
console.log(chalk.yellow('Usage:'), chalk.cyan('gent clone <owner_id>/<repo_name> [directory]'));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const parts = repoUrl.split('/');
|
|
40
|
-
if (parts.length !== 2) {
|
|
41
|
-
console.error(chalk.red('Error: Invalid repository URL format'));
|
|
42
|
-
console.log(chalk.yellow('Expected:'), chalk.cyan('<owner_id>/<repo_name>'));
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
52
|
+
async function clone(url, directory, options) {
|
|
53
|
+
if (!url) {
|
|
54
|
+
console.error(chalk.red('Usage: gent clone <url> [directory]'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
const repoName = parts[1];
|
|
58
|
+
const spinner = ora(`Cloning from ${url}...`).start();
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
60
|
+
try {
|
|
61
|
+
// Fetch full repo from remote
|
|
62
|
+
spinner.text = 'Downloading repository data...';
|
|
63
|
+
const response = await apiClient.get(`${url}/clone/`);
|
|
53
64
|
|
|
54
|
-
|
|
65
|
+
const repoName = response.name || 'gent-repo';
|
|
55
66
|
const targetDir = directory || repoName;
|
|
56
67
|
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
57
68
|
|
|
58
|
-
// Check if directory already exists
|
|
59
69
|
if (await pathExists(targetPath)) {
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
const items = await fs.readdir(targetPath);
|
|
71
|
+
if (items.length > 0) {
|
|
72
|
+
spinner.fail(chalk.red(`Directory '${targetDir}' is not empty`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
console.log(chalk.cyan(`Cloning ${ownerId}/${repoName} into '${targetDir}'...\n`));
|
|
65
|
-
|
|
66
|
-
// Fetch repository metadata
|
|
67
|
-
const spinner = ora('Fetching repository...').start();
|
|
68
|
-
const repository = await repoService.getRepository(ownerId, repoName);
|
|
69
|
-
spinner.succeed('Repository fetched');
|
|
70
|
-
|
|
71
77
|
// Create directory structure
|
|
72
|
-
|
|
78
|
+
spinner.text = 'Setting up repository...';
|
|
73
79
|
const gentPath = path.join(targetPath, GENT_DIR);
|
|
74
80
|
await ensureDir(gentPath);
|
|
75
81
|
await ensureDir(path.join(gentPath, 'objects'));
|
|
76
82
|
await ensureDir(path.join(gentPath, 'refs', 'heads'));
|
|
77
83
|
await ensureDir(path.join(gentPath, 'refs', 'tags'));
|
|
78
84
|
|
|
79
|
-
//
|
|
85
|
+
// Store blob objects
|
|
86
|
+
const objects = response.objects || [];
|
|
87
|
+
spinner.text = `Storing ${objects.length} object(s)...`;
|
|
88
|
+
|
|
89
|
+
for (const obj of objects) {
|
|
90
|
+
if (obj.type === 'blob' && obj.data) {
|
|
91
|
+
const buf = Buffer.from(obj.data, 'base64');
|
|
92
|
+
await storeBlob(gentPath, buf);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Write commits.json
|
|
97
|
+
const repoData = {
|
|
98
|
+
commits: response.commits || [],
|
|
99
|
+
branches: response.branches || { main: null },
|
|
100
|
+
currentBranch: response.currentBranch || 'main',
|
|
101
|
+
tags: response.tags || {}
|
|
102
|
+
};
|
|
103
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repoData);
|
|
104
|
+
|
|
105
|
+
// Write config with remote
|
|
80
106
|
const config = {
|
|
81
|
-
user: {
|
|
82
|
-
name: user.first_name && user.last_name ? `${user.first_name} ${user.last_name}` : '',
|
|
83
|
-
email: user.email
|
|
84
|
-
},
|
|
107
|
+
user: { name: '', email: '' },
|
|
85
108
|
repository: {
|
|
86
|
-
name:
|
|
87
|
-
description:
|
|
109
|
+
name: repoName,
|
|
110
|
+
description: response.description || '',
|
|
88
111
|
created: new Date().toISOString()
|
|
89
|
-
}
|
|
112
|
+
},
|
|
113
|
+
remotes: {
|
|
114
|
+
origin: { url }
|
|
115
|
+
},
|
|
116
|
+
remoteRefs: {}
|
|
90
117
|
};
|
|
118
|
+
|
|
119
|
+
// Set remote ref to head
|
|
120
|
+
const headHash = repoData.branches[repoData.currentBranch];
|
|
121
|
+
if (headHash) {
|
|
122
|
+
config.remoteRefs[`origin/${repoData.currentBranch}`] = headHash;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
92
126
|
|
|
93
|
-
//
|
|
94
|
-
await writeJSON(path.join(gentPath, STAGING_FILE), { files: [] });
|
|
95
|
-
await writeJSON(path.join(gentPath, COMMITS_FILE), {
|
|
96
|
-
commits: [],
|
|
97
|
-
branches: { [repository.default_branch]: null },
|
|
98
|
-
currentBranch: repository.default_branch
|
|
99
|
-
});
|
|
127
|
+
// Write staging.json
|
|
128
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), { entries: [], files: [] });
|
|
100
129
|
|
|
101
|
-
//
|
|
130
|
+
// Write HEAD file
|
|
102
131
|
await fs.writeFile(
|
|
103
132
|
path.join(gentPath, 'HEAD'),
|
|
104
|
-
`ref: refs/heads/${
|
|
133
|
+
`ref: refs/heads/${repoData.currentBranch}\n`
|
|
105
134
|
);
|
|
106
135
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
// Create .gentignore
|
|
137
|
+
const ignorePath = path.join(targetPath, '.gentignore');
|
|
138
|
+
await fs.writeFile(ignorePath, `# Gent ignore\nnode_modules/\n.DS_Store\n*.log\n.env\n.gent/\n`);
|
|
139
|
+
|
|
140
|
+
// Checkout working tree from HEAD commit
|
|
141
|
+
if (headHash) {
|
|
142
|
+
const headCommit = repoData.commits.find(c => c.hash === headHash);
|
|
143
|
+
if (headCommit) {
|
|
144
|
+
const tree = headCommit.tree || (headCommit.files || []).map(f => ({
|
|
145
|
+
name: f.path || f.name, hash: f.hash
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
spinner.text = 'Checking out files...';
|
|
149
|
+
let fileCount = 0;
|
|
150
|
+
for (const entry of tree) {
|
|
151
|
+
try {
|
|
152
|
+
const content = await readBlobAsString(gentPath, entry.hash);
|
|
153
|
+
const fullPath = path.join(targetPath, entry.name || entry.path);
|
|
154
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
155
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
156
|
+
fileCount++;
|
|
157
|
+
} catch {
|
|
158
|
+
// Blob missing
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
spinner.succeed(chalk.green(`Cloned into '${targetDir}'`));
|
|
163
|
+
console.log(chalk.gray(` ${repoData.commits.length} commit(s), ${objects.length} object(s), ${fileCount} file(s)`));
|
|
164
|
+
} else {
|
|
165
|
+
spinner.succeed(chalk.green(`Cloned into '${targetDir}' (empty)`));
|
|
117
166
|
}
|
|
118
|
-
|
|
167
|
+
} else {
|
|
168
|
+
spinner.succeed(chalk.green(`Cloned into '${targetDir}' (no commits)`));
|
|
119
169
|
}
|
|
120
170
|
|
|
121
|
-
console.log(chalk.green(`\n✓ Successfully cloned into '${targetDir}'`));
|
|
122
|
-
console.log(chalk.yellow('\nNext steps:'));
|
|
123
|
-
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
124
|
-
console.log(chalk.cyan(' gent status'), chalk.gray('- Check repository status'));
|
|
125
|
-
|
|
126
171
|
} catch (error) {
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
spinner.fail(chalk.red('Clone failed'));
|
|
173
|
+
if (error.response?.status === 404) {
|
|
174
|
+
console.error(chalk.red('Repository not found'));
|
|
175
|
+
} else if (error.response?.data?.message) {
|
|
176
|
+
console.error(chalk.red(error.response.data.message));
|
|
177
|
+
} else {
|
|
178
|
+
console.error(chalk.red('Error:'), error.message);
|
|
179
|
+
}
|
|
129
180
|
process.exit(1);
|
|
130
181
|
}
|
|
131
182
|
}
|