gent-cli 1.6.0 → 1.8.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 +42 -28
- package/package.json +1 -1
- package/src/commands/clone.js +133 -0
- package/src/commands/commit.js +50 -7
- package/src/commands/create.js +118 -0
- package/src/commands/init.js +33 -5
- package/src/commands/list.js +67 -0
- package/src/commands/log.js +4 -4
- package/src/commands/pull.js +123 -0
- package/src/commands/push.js +112 -0
- package/src/commands/remote.js +133 -0
- package/src/index.js +49 -0
- package/src/services/repo-service.js +283 -0
- package/src/utils/cloud-sync.js +322 -0
- package/src/utils/constants.js +28 -1
- package/src/utils/diff.js +121 -0
- package/src/utils/fileSystem.js +47 -3
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
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 user.name and user.email for every repository!
|
|
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
|
-
- **
|
|
11
|
+
- **Cloud Synchronization**: Push and pull repositories to/from the cloud.
|
|
12
|
+
- **Zero Configuration**: `gent init` is silent and auto-detects your authenticated user profile.
|
|
12
13
|
- **Global Identity**: Commits are automatically authored with your cloud profile.
|
|
13
14
|
- **Secure**: Tokens stored securely in your home directory.
|
|
14
15
|
|
|
@@ -48,12 +49,10 @@ gent whoami
|
|
|
48
49
|
gent logout
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
## Usage
|
|
52
|
+
## Local Usage
|
|
52
53
|
|
|
53
54
|
### 1. Initialize a Repository
|
|
54
55
|
|
|
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
|
-
|
|
57
56
|
```bash
|
|
58
57
|
gent init
|
|
59
58
|
# Output: Initialized empty Gent repository in /path/to/project
|
|
@@ -61,16 +60,12 @@ gent init
|
|
|
61
60
|
|
|
62
61
|
### 2. Check Status
|
|
63
62
|
|
|
64
|
-
See which files are modified or untracked.
|
|
65
|
-
|
|
66
63
|
```bash
|
|
67
64
|
gent status
|
|
68
65
|
```
|
|
69
66
|
|
|
70
67
|
### 3. Stage Files
|
|
71
68
|
|
|
72
|
-
Add files to the staging area.
|
|
73
|
-
|
|
74
69
|
```bash
|
|
75
70
|
gent add filename.js
|
|
76
71
|
# or add all files
|
|
@@ -79,55 +74,74 @@ gent add .
|
|
|
79
74
|
|
|
80
75
|
### 4. Commit Changes
|
|
81
76
|
|
|
82
|
-
Create a commit. Gent automatically fetches your name and email from your global login session.
|
|
83
|
-
|
|
84
77
|
```bash
|
|
85
78
|
gent commit -m "Initial commit"
|
|
86
79
|
# Output: [main a1b2c3d] Initial commit
|
|
87
80
|
# Author: Your Name <your.email@example.com>
|
|
88
81
|
```
|
|
89
82
|
|
|
90
|
-
|
|
83
|
+
## Cloud Features
|
|
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
|
+
```
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
### 2. List Your Repositories
|
|
93
96
|
|
|
94
97
|
```bash
|
|
95
|
-
gent
|
|
96
|
-
# or
|
|
97
|
-
gent
|
|
98
|
+
gent list
|
|
99
|
+
# or
|
|
100
|
+
gent ls
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
### 3. Clone a Repository
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
```bash
|
|
106
|
+
gent clone <owner_id>/<repo_name>
|
|
107
|
+
# Example: gent clone 1/my-repo
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 4. Push Changes
|
|
103
111
|
|
|
104
112
|
```bash
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
gent push
|
|
114
|
+
# or commit and push in one go
|
|
115
|
+
gent commit -m "Update README" --push
|
|
116
|
+
```
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
gent branch
|
|
118
|
+
### 5. Pull Changes
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
gent
|
|
120
|
+
```bash
|
|
121
|
+
gent pull
|
|
122
|
+
```
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
### 6. Manage Remotes
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gent remote add origin <owner_id>/<repo_name>
|
|
128
|
+
gent remote -v
|
|
116
129
|
```
|
|
117
130
|
|
|
118
131
|
## Repository Structure
|
|
119
132
|
|
|
120
|
-
Gent creates a
|
|
133
|
+
Gent creates a `.gent` directory in your project root:
|
|
121
134
|
|
|
122
135
|
```
|
|
123
136
|
.gent/
|
|
124
137
|
├── config.json # Project configuration
|
|
125
138
|
├── objects/ # Stored file contents
|
|
126
139
|
├── refs/ # Branch pointers
|
|
140
|
+
├── remote.json # Remote configuration
|
|
127
141
|
└── HEAD # Current branch reference
|
|
128
142
|
```
|
|
129
143
|
|
|
130
|
-
Your authentication tokens are stored globally in
|
|
144
|
+
Your authentication tokens are stored globally in `~/.gent/auth.json`.
|
|
131
145
|
|
|
132
146
|
## Contributing
|
|
133
147
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone Command - Clone a cloud repository to local
|
|
3
|
+
* Downloads repository data and sets up local working copy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const repoService = require('../services/repo-service');
|
|
11
|
+
const authStorage = require('../utils/auth-storage');
|
|
12
|
+
const { ensureDir, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
13
|
+
const { GENT_DIR, CONFIG_FILE, STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
14
|
+
const { addRemote, syncCommitsFromCloud } = require('../utils/cloud-sync');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clone a cloud repository
|
|
18
|
+
* @param {string} repoUrl - Repository URL (owner_id/repo_name)
|
|
19
|
+
* @param {string} directory - Target directory (optional)
|
|
20
|
+
* @param {Object} options - Command options
|
|
21
|
+
*/
|
|
22
|
+
async function clone(repoUrl, directory, options) {
|
|
23
|
+
try {
|
|
24
|
+
// Check authentication
|
|
25
|
+
const user = await authStorage.getUser();
|
|
26
|
+
if (!user) {
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
const ownerId = parseInt(parts[0]);
|
|
47
|
+
const repoName = parts[1];
|
|
48
|
+
|
|
49
|
+
if (isNaN(ownerId)) {
|
|
50
|
+
console.error(chalk.red('Error: Owner ID must be a number'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine target directory
|
|
55
|
+
const targetDir = directory || repoName;
|
|
56
|
+
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
57
|
+
|
|
58
|
+
// Check if directory already exists
|
|
59
|
+
if (await pathExists(targetPath)) {
|
|
60
|
+
console.error(chalk.red(`Error: Directory '${targetDir}' already exists`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
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
|
+
// Create directory structure
|
|
72
|
+
await ensureDir(targetPath);
|
|
73
|
+
const gentPath = path.join(targetPath, GENT_DIR);
|
|
74
|
+
await ensureDir(gentPath);
|
|
75
|
+
await ensureDir(path.join(gentPath, 'objects'));
|
|
76
|
+
await ensureDir(path.join(gentPath, 'refs', 'heads'));
|
|
77
|
+
await ensureDir(path.join(gentPath, 'refs', 'tags'));
|
|
78
|
+
|
|
79
|
+
// Create configuration
|
|
80
|
+
const config = {
|
|
81
|
+
user: {
|
|
82
|
+
name: user.first_name && user.last_name ? `${user.first_name} ${user.last_name}` : '',
|
|
83
|
+
email: user.email
|
|
84
|
+
},
|
|
85
|
+
repository: {
|
|
86
|
+
name: repository.name,
|
|
87
|
+
description: repository.description,
|
|
88
|
+
created: new Date().toISOString()
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
92
|
+
|
|
93
|
+
// Create initial files
|
|
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
|
+
});
|
|
100
|
+
|
|
101
|
+
// Create HEAD file
|
|
102
|
+
await fs.writeFile(
|
|
103
|
+
path.join(gentPath, 'HEAD'),
|
|
104
|
+
`ref: refs/heads/${repository.default_branch}\n`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Add remote
|
|
108
|
+
await addRemote('origin', ownerId, repoName, targetPath);
|
|
109
|
+
|
|
110
|
+
// Fetch commits, branches, and files
|
|
111
|
+
try {
|
|
112
|
+
await syncCommitsFromCloud(ownerId, repoName, targetPath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Ignore if repository has no commits yet
|
|
115
|
+
if (!error.message.includes('404')) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk.yellow('Note: Repository has no commits yet'));
|
|
119
|
+
}
|
|
120
|
+
|
|
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
|
+
} catch (error) {
|
|
127
|
+
console.error(chalk.red('Failed to clone repository'));
|
|
128
|
+
console.error(chalk.red('Error:'), error.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = clone;
|
package/src/commands/commit.js
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const fs = require('fs').promises;
|
|
7
8
|
const chalk = require('chalk');
|
|
8
9
|
const inquirer = require('inquirer');
|
|
9
10
|
const ora = require('ora');
|
|
10
|
-
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
11
|
+
const { getGentPath, readJSON, writeJSON, saveBlob, getBlob } = require('../utils/fileSystem');
|
|
11
12
|
const authStorage = require('../utils/auth-storage');
|
|
12
13
|
const { STAGING_FILE, COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
13
14
|
const { generateCommitHash, getFileHash } = require('../utils/helpers');
|
|
15
|
+
const { computeDiff } = require('../utils/diff');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Create a new commit
|
|
@@ -80,31 +82,66 @@ async function commit(options) {
|
|
|
80
82
|
|
|
81
83
|
// Create commit object
|
|
82
84
|
const commit = {
|
|
83
|
-
|
|
85
|
+
sha: generateCommitHash(),
|
|
84
86
|
message: message,
|
|
85
87
|
author: {
|
|
86
88
|
name: authorName,
|
|
87
89
|
email: authorEmail
|
|
88
90
|
},
|
|
89
91
|
timestamp: new Date().toISOString(),
|
|
90
|
-
parent: repository.branches[repository.currentBranch]
|
|
92
|
+
parent: repository.branches[repository.currentBranch] ? [repository.branches[repository.currentBranch]] : [],
|
|
91
93
|
files: []
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
//
|
|
96
|
+
// Create a map of parent files for quick lookup
|
|
97
|
+
const parentFilesMap = new Map();
|
|
98
|
+
if (commit.parent && commit.parent.length > 0) {
|
|
99
|
+
const parentSha = commit.parent[0];
|
|
100
|
+
const parentCommit = repository.commits.find(c => c.sha === parentSha);
|
|
101
|
+
if (parentCommit) {
|
|
102
|
+
parentCommit.files.forEach(f => parentFilesMap.set(f.path, f.hash));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Hash staged files and compute diffs
|
|
95
107
|
for (const file of stagedFiles) {
|
|
96
108
|
const filePath = path.join(cwd, file);
|
|
109
|
+
let content;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// If file is deleted or unreadable, we handle it as empty content or handle deletion if we supported it
|
|
115
|
+
// For now assuming staged files exist
|
|
116
|
+
content = '';
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
const hash = await getFileHash(filePath);
|
|
120
|
+
|
|
121
|
+
// Save the blob for future diffs
|
|
122
|
+
await saveBlob(content, hash);
|
|
123
|
+
|
|
124
|
+
// Get previous content
|
|
125
|
+
let oldContent = '';
|
|
126
|
+
if (parentFilesMap.has(file)) {
|
|
127
|
+
const oldHash = parentFilesMap.get(file);
|
|
128
|
+
oldContent = await getBlob(oldHash);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compute Myers diff
|
|
132
|
+
const changes = computeDiff(oldContent, content);
|
|
133
|
+
|
|
98
134
|
commit.files.push({
|
|
99
135
|
path: file,
|
|
100
|
-
hash: hash
|
|
136
|
+
hash: hash,
|
|
137
|
+
diff: changes
|
|
101
138
|
});
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
// Add commit to repository
|
|
105
142
|
repository.commits = repository.commits || [];
|
|
106
143
|
repository.commits.push(commit);
|
|
107
|
-
repository.branches[repository.currentBranch] = commit.
|
|
144
|
+
repository.branches[repository.currentBranch] = commit.sha;
|
|
108
145
|
|
|
109
146
|
// Save repository and clear staging
|
|
110
147
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
@@ -114,11 +151,17 @@ async function commit(options) {
|
|
|
114
151
|
spinner.succeed(chalk.green('✓ Changes committed successfully!'));
|
|
115
152
|
|
|
116
153
|
// Display commit info
|
|
117
|
-
console.log(chalk.cyan(`\n[${repository.currentBranch} ${commit.
|
|
154
|
+
console.log(chalk.cyan(`\n[${repository.currentBranch} ${commit.sha.substring(0, 7)}] ${message}`));
|
|
118
155
|
console.log(chalk.gray(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
119
156
|
console.log(chalk.gray(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
120
157
|
console.log(chalk.gray(`\n${commit.files.length} file(s) changed`));
|
|
121
158
|
|
|
159
|
+
// Handle --push option
|
|
160
|
+
if (options.push) {
|
|
161
|
+
const push = require('./push');
|
|
162
|
+
await push('origin', repository.currentBranch, {});
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
} catch (error) {
|
|
123
166
|
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
124
167
|
console.error(chalk.red('Error: Not a gent repository'));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Command - Create a new cloud repository
|
|
3
|
+
* Creates a repository on the cloud and optionally initializes locally
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const repoService = require('../services/repo-service');
|
|
11
|
+
const authStorage = require('../utils/auth-storage');
|
|
12
|
+
const { ensureDir, writeJSON } = require('../utils/fileSystem');
|
|
13
|
+
const { GENT_DIR } = require('../utils/constants');
|
|
14
|
+
const { addRemote } = require('../utils/cloud-sync');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a new repository on the cloud
|
|
18
|
+
* @param {string} repoName - Repository name
|
|
19
|
+
* @param {Object} options - Command options
|
|
20
|
+
*/
|
|
21
|
+
async function create(repoName, options) {
|
|
22
|
+
try {
|
|
23
|
+
// Check authentication
|
|
24
|
+
const user = await authStorage.getUser();
|
|
25
|
+
if (!user) {
|
|
26
|
+
console.error(chalk.red('Error: You must be logged in to create a repository'));
|
|
27
|
+
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate repository name
|
|
32
|
+
if (!repoName) {
|
|
33
|
+
console.error(chalk.red('Error: Repository name is required'));
|
|
34
|
+
console.log(chalk.yellow('Usage:'), chalk.cyan('gent create <repo-name>'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Prompt for additional details if not provided
|
|
39
|
+
let description = options.description || '';
|
|
40
|
+
let isPrivate = options.private || false;
|
|
41
|
+
|
|
42
|
+
if (!options.yes) {
|
|
43
|
+
const answers = await inquirer.prompt([
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'description',
|
|
47
|
+
message: 'Repository description (optional):',
|
|
48
|
+
default: description
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'confirm',
|
|
52
|
+
name: 'isPrivate',
|
|
53
|
+
message: 'Make repository private?',
|
|
54
|
+
default: isPrivate
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'confirm',
|
|
58
|
+
name: 'initLocal',
|
|
59
|
+
message: 'Initialize local repository and link remote?',
|
|
60
|
+
default: true
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
description = answers.description;
|
|
65
|
+
isPrivate = answers.isPrivate;
|
|
66
|
+
options.initLocal = answers.initLocal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create repository
|
|
70
|
+
const spinner = ora('Creating repository...').start();
|
|
71
|
+
|
|
72
|
+
const repository = await repoService.createRepository(
|
|
73
|
+
repoName,
|
|
74
|
+
description,
|
|
75
|
+
isPrivate,
|
|
76
|
+
'main'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
spinner.succeed(chalk.green(`Repository created: ${repository.name}`));
|
|
80
|
+
|
|
81
|
+
console.log(chalk.cyan('\nRepository Details:'));
|
|
82
|
+
console.log(chalk.gray(' Name:'), repository.name);
|
|
83
|
+
console.log(chalk.gray(' Owner:'), repository.owner_email);
|
|
84
|
+
console.log(chalk.gray(' Private:'), repository.is_private ? 'Yes' : 'No');
|
|
85
|
+
console.log(chalk.gray(' Description:'), repository.description || '(none)');
|
|
86
|
+
console.log(chalk.gray(' Created:'), new Date(repository.created_at).toLocaleString());
|
|
87
|
+
|
|
88
|
+
// Initialize local repository if requested
|
|
89
|
+
if (options.initLocal) {
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const gentPath = path.join(cwd, GENT_DIR);
|
|
92
|
+
|
|
93
|
+
// Create .gent directory
|
|
94
|
+
await ensureDir(gentPath);
|
|
95
|
+
await ensureDir(path.join(gentPath, 'objects'));
|
|
96
|
+
await ensureDir(path.join(gentPath, 'refs', 'heads'));
|
|
97
|
+
|
|
98
|
+
// Add remote
|
|
99
|
+
await addRemote('origin', repository.owner_id, repository.name, cwd);
|
|
100
|
+
|
|
101
|
+
console.log(chalk.green('\n✓ Local repository initialized and remote added'));
|
|
102
|
+
console.log(chalk.yellow('\nNext steps:'));
|
|
103
|
+
console.log(chalk.cyan(' gent add <files>'), chalk.gray('- Add files to staging'));
|
|
104
|
+
console.log(chalk.cyan(' gent commit -m "message"'), chalk.gray('- Commit changes'));
|
|
105
|
+
console.log(chalk.cyan(' gent push'), chalk.gray('- Push to cloud'));
|
|
106
|
+
} else {
|
|
107
|
+
console.log(chalk.yellow('\nTo clone this repository:'));
|
|
108
|
+
console.log(chalk.cyan(` gent clone ${repository.owner_id}/${repository.name}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(chalk.red('Failed to create repository'));
|
|
113
|
+
console.error(chalk.red('Error:'), error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = create;
|
package/src/commands/init.js
CHANGED
|
@@ -28,9 +28,10 @@ async function init(options) {
|
|
|
28
28
|
// Get authenticated user profile if available
|
|
29
29
|
let defaultName = '';
|
|
30
30
|
let defaultEmail = '';
|
|
31
|
+
let user = null;
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
+
user = await authStorage.getUser();
|
|
34
35
|
if (user) {
|
|
35
36
|
if (user.first_name || user.last_name) {
|
|
36
37
|
defaultName = [user.first_name, user.last_name].filter(Boolean).join(' ');
|
|
@@ -62,12 +63,8 @@ async function init(options) {
|
|
|
62
63
|
await ensureDir(path.join(gentPath, 'refs', 'tags'));
|
|
63
64
|
|
|
64
65
|
// Create/Update configuration
|
|
65
|
-
// Only write config if it doesn't exist OR if we have valid user info to update
|
|
66
66
|
const configPath = path.join(gentPath, CONFIG_FILE);
|
|
67
67
|
if (!(await pathExists(configPath)) || (defaultName && defaultEmail)) {
|
|
68
|
-
// If re-init, we might want to preserve existing config unless we have better info?
|
|
69
|
-
// Git re-init doesn't overwrite config usually.
|
|
70
|
-
// But for now, let's write ensuring we have a config file.
|
|
71
68
|
if (!isReinit || !(await pathExists(configPath))) {
|
|
72
69
|
await writeJSON(configPath, config);
|
|
73
70
|
}
|
|
@@ -109,6 +106,37 @@ node_modules/
|
|
|
109
106
|
console.log(chalk.gray(`Initialized empty Gent repository in ${gentPath}`));
|
|
110
107
|
}
|
|
111
108
|
|
|
109
|
+
// Handle --cloud option
|
|
110
|
+
if (options.cloud) {
|
|
111
|
+
if (!user) {
|
|
112
|
+
console.log(chalk.yellow('\nWarning: Skipping cloud repository creation (not logged in)'));
|
|
113
|
+
console.log(chalk.gray('Run "gent login" then "gent create <name> --init-local"'));
|
|
114
|
+
} else {
|
|
115
|
+
try {
|
|
116
|
+
const repoService = require('../services/repo-service');
|
|
117
|
+
const { addRemote } = require('../utils/cloud-sync');
|
|
118
|
+
const ora = require('ora');
|
|
119
|
+
|
|
120
|
+
const spinner = ora('Creating cloud repository...').start();
|
|
121
|
+
const repoName = path.basename(cwd);
|
|
122
|
+
|
|
123
|
+
const repository = await repoService.createRepository(
|
|
124
|
+
repoName,
|
|
125
|
+
options.description || 'A gent repository',
|
|
126
|
+
options.private || false,
|
|
127
|
+
'main'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await addRemote('origin', repository.owner_id, repository.name, cwd);
|
|
131
|
+
spinner.succeed(chalk.green(`Created cloud repository: ${repository.owner_id}/${repository.name}`));
|
|
132
|
+
console.log(chalk.gray(`Remote 'origin' added`));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.log(chalk.red(`\nFailed to create cloud repository: ${error.message}`));
|
|
135
|
+
console.log(chalk.gray('You can create it later with "gent create <name>"'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
} catch (error) {
|
|
113
141
|
console.error(chalk.red('Failed to initialize repository'));
|
|
114
142
|
console.error(chalk.red('Error:'), error.message);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Command - List all cloud repositories
|
|
3
|
+
* Displays all repositories owned by the authenticated user
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const repoService = require('../services/repo-service');
|
|
9
|
+
const authStorage = require('../utils/auth-storage');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format date for display
|
|
13
|
+
* @param {string} dateString - ISO date string
|
|
14
|
+
* @returns {string} Formatted date
|
|
15
|
+
*/
|
|
16
|
+
function formatDate(dateString) {
|
|
17
|
+
const date = new Date(dateString);
|
|
18
|
+
return date.toLocaleDateString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* List all repositories owned by the authenticated user
|
|
23
|
+
* @param {Object} options - Command options
|
|
24
|
+
*/
|
|
25
|
+
async function list(options) {
|
|
26
|
+
try {
|
|
27
|
+
// Check authentication
|
|
28
|
+
const user = await authStorage.getUser();
|
|
29
|
+
if (!user) {
|
|
30
|
+
console.error(chalk.red('Error: You must be logged in to list repositories'));
|
|
31
|
+
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fetch repositories
|
|
36
|
+
const spinner = ora('Fetching repositories...').start();
|
|
37
|
+
const repositories = await repoService.listRepositories();
|
|
38
|
+
spinner.stop();
|
|
39
|
+
|
|
40
|
+
if (repositories.length === 0) {
|
|
41
|
+
console.log(chalk.yellow('No repositories found'));
|
|
42
|
+
console.log(chalk.gray('Create a new repository with:'), chalk.cyan('gent create <repo-name>'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(chalk.cyan(`\nYour Repositories (${repositories.length}):\n`));
|
|
47
|
+
|
|
48
|
+
repositories.forEach(repo => {
|
|
49
|
+
const privacy = repo.is_private ? chalk.red('🔒 Private') : chalk.green('🌐 Public');
|
|
50
|
+
console.log(chalk.bold(repo.name), privacy);
|
|
51
|
+
if (repo.description) {
|
|
52
|
+
console.log(chalk.gray(` ${repo.description}`));
|
|
53
|
+
}
|
|
54
|
+
console.log(chalk.gray(` Owner: ${repo.owner_email}`));
|
|
55
|
+
console.log(chalk.gray(` Created: ${formatDate(repo.created_at)}`));
|
|
56
|
+
console.log(chalk.gray(` Default branch: ${repo.default_branch}`));
|
|
57
|
+
console.log(chalk.gray(` Clone: gent clone ${repo.owner_id}/${repo.name}\n`));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(chalk.red('Failed to list repositories'));
|
|
62
|
+
console.error(chalk.red('Error:'), error.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = list;
|
package/src/commands/log.js
CHANGED
|
@@ -55,10 +55,10 @@ function displayDetailedLog(commits, currentCommitHash, currentBranch) {
|
|
|
55
55
|
console.log(chalk.bold.cyan(`\nCommit History (${currentBranch} branch):\n`));
|
|
56
56
|
|
|
57
57
|
commits.forEach((commit, index) => {
|
|
58
|
-
const isHead = commit.
|
|
58
|
+
const isHead = commit.sha === currentCommitHash;
|
|
59
59
|
const headLabel = isHead ? chalk.yellow.bold(' (HEAD)') : '';
|
|
60
60
|
|
|
61
|
-
console.log(chalk.yellow(`commit ${commit.
|
|
61
|
+
console.log(chalk.yellow(`commit ${commit.sha}`) + headLabel);
|
|
62
62
|
console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
63
63
|
console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
64
64
|
console.log(chalk.gray(` (${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`));
|
|
@@ -79,9 +79,9 @@ function displayDetailedLog(commits, currentCommitHash, currentBranch) {
|
|
|
79
79
|
*/
|
|
80
80
|
function displayOnelineLog(commits, currentCommitHash) {
|
|
81
81
|
commits.forEach(commit => {
|
|
82
|
-
const isHead = commit.
|
|
82
|
+
const isHead = commit.sha === currentCommitHash;
|
|
83
83
|
const headLabel = isHead ? chalk.yellow(' (HEAD)') : '';
|
|
84
|
-
const shortHash = chalk.yellow(commit.
|
|
84
|
+
const shortHash = chalk.yellow(commit.sha.substring(0, 7));
|
|
85
85
|
const message = chalk.white(commit.message);
|
|
86
86
|
const timeAgo = chalk.gray(`(${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`);
|
|
87
87
|
|