gent-cli 1.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 +316 -0
- package/commands-reference.js +116 -0
- package/package.json +43 -0
- package/src/commands/add.js +88 -0
- package/src/commands/branch.js +115 -0
- package/src/commands/checkout.js +74 -0
- package/src/commands/commit.js +108 -0
- package/src/commands/init.js +140 -0
- package/src/commands/log.js +92 -0
- package/src/commands/status.js +167 -0
- package/src/index.js +104 -0
- package/src/utils/constants.js +41 -0
- package/src/utils/fileSystem.js +188 -0
- package/src/utils/helpers.js +130 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout Command - Switch branches
|
|
3
|
+
* Changes the current working branch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
9
|
+
const { COMMITS_FILE } = require('../utils/constants');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Switch to a different branch
|
|
13
|
+
* @param {String} branch - Branch name
|
|
14
|
+
* @param {Object} options - Command options
|
|
15
|
+
*/
|
|
16
|
+
async function checkout(branch, options) {
|
|
17
|
+
try {
|
|
18
|
+
const gentPath = await getGentPath();
|
|
19
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
20
|
+
|
|
21
|
+
const branches = repository.branches || {};
|
|
22
|
+
|
|
23
|
+
// Create new branch if -b flag is used
|
|
24
|
+
if (options.create) {
|
|
25
|
+
if (branches.hasOwnProperty(branch)) {
|
|
26
|
+
console.error(chalk.red(`Error: Branch '${branch}' already exists`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create and switch to new branch
|
|
31
|
+
const currentCommit = branches[repository.currentBranch] || null;
|
|
32
|
+
branches[branch] = currentCommit;
|
|
33
|
+
repository.branches = branches;
|
|
34
|
+
repository.currentBranch = branch;
|
|
35
|
+
|
|
36
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green(`✓ Created and switched to branch '${branch}'`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Switch to existing branch
|
|
43
|
+
if (!branches.hasOwnProperty(branch)) {
|
|
44
|
+
console.error(chalk.red(`Error: Branch '${branch}' not found`));
|
|
45
|
+
console.log(chalk.yellow(`\nℹ Use "gent branch" to see available branches`));
|
|
46
|
+
console.log(chalk.yellow(`ℹ Use "gent checkout -b ${branch}" to create a new branch`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (branch === repository.currentBranch) {
|
|
51
|
+
console.log(chalk.yellow(`Already on branch '${branch}'`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
repository.currentBranch = branch;
|
|
56
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
57
|
+
|
|
58
|
+
const commitHash = branches[branch];
|
|
59
|
+
const commitInfo = commitHash ? chalk.gray(` at ${commitHash.substring(0, 7)}`) : '';
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green(`✓ Switched to branch '${branch}'${commitInfo}`));
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
65
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
66
|
+
console.log(chalk.yellow('\nℹ Run "gent init" to initialize a repository'));
|
|
67
|
+
} else {
|
|
68
|
+
console.error(chalk.red('Error:'), error.message);
|
|
69
|
+
}
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = checkout;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Command - Record changes to the repository
|
|
3
|
+
* Creates a new commit with staged files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const ora = require('ora');
|
|
10
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
11
|
+
const { STAGING_FILE, COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
12
|
+
const { generateCommitHash, getFileHash } = require('../utils/helpers');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a new commit
|
|
16
|
+
* @param {Object} options - Command options
|
|
17
|
+
*/
|
|
18
|
+
async function commit(options) {
|
|
19
|
+
try {
|
|
20
|
+
const gentPath = await getGentPath();
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
|
|
23
|
+
// Read staging area
|
|
24
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
25
|
+
const stagedFiles = staging.files || [];
|
|
26
|
+
|
|
27
|
+
if (stagedFiles.length === 0) {
|
|
28
|
+
console.log(chalk.yellow('No changes added to commit'));
|
|
29
|
+
console.log(chalk.gray('Use "gent add <file>..." to stage files'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get commit message
|
|
34
|
+
let message = options.message;
|
|
35
|
+
|
|
36
|
+
if (!message) {
|
|
37
|
+
const answer = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'message',
|
|
41
|
+
message: 'Commit message:',
|
|
42
|
+
validate: (input) => {
|
|
43
|
+
return input.length > 0 || 'Commit message cannot be empty';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
message = answer.message;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const spinner = ora('Creating commit...').start();
|
|
51
|
+
|
|
52
|
+
// Read config and repository
|
|
53
|
+
const config = await readJSON(path.join(gentPath, CONFIG_FILE));
|
|
54
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
55
|
+
|
|
56
|
+
// Create commit object
|
|
57
|
+
const commit = {
|
|
58
|
+
hash: generateCommitHash(),
|
|
59
|
+
message: message,
|
|
60
|
+
author: {
|
|
61
|
+
name: config.user.name,
|
|
62
|
+
email: config.user.email
|
|
63
|
+
},
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
parent: repository.branches[repository.currentBranch] || null,
|
|
66
|
+
files: []
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Hash staged files
|
|
70
|
+
for (const file of stagedFiles) {
|
|
71
|
+
const filePath = path.join(cwd, file);
|
|
72
|
+
const hash = await getFileHash(filePath);
|
|
73
|
+
commit.files.push({
|
|
74
|
+
path: file,
|
|
75
|
+
hash: hash
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add commit to repository
|
|
80
|
+
repository.commits = repository.commits || [];
|
|
81
|
+
repository.commits.push(commit);
|
|
82
|
+
repository.branches[repository.currentBranch] = commit.hash;
|
|
83
|
+
|
|
84
|
+
// Save repository and clear staging
|
|
85
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
86
|
+
staging.files = [];
|
|
87
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
88
|
+
|
|
89
|
+
spinner.succeed(chalk.green('✓ Changes committed successfully!'));
|
|
90
|
+
|
|
91
|
+
// Display commit info
|
|
92
|
+
console.log(chalk.cyan(`\n[${repository.currentBranch} ${commit.hash.substring(0, 7)}] ${message}`));
|
|
93
|
+
console.log(chalk.gray(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
94
|
+
console.log(chalk.gray(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
95
|
+
console.log(chalk.gray(`\n${commit.files.length} file(s) changed`));
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
99
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
100
|
+
console.log(chalk.yellow('\nℹ Run "gent init" to initialize a repository'));
|
|
101
|
+
} else {
|
|
102
|
+
console.error(chalk.red('Error:'), error.message);
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = commit;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command - Initialize a new gent repository
|
|
3
|
+
* Creates .gent directory and necessary files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const ora = require('ora');
|
|
11
|
+
const boxen = require('boxen');
|
|
12
|
+
const { ensureDir, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
13
|
+
const { GENT_DIR, CONFIG_FILE, STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize a new gent repository
|
|
17
|
+
* @param {Object} options - Command options
|
|
18
|
+
*/
|
|
19
|
+
async function init(options) {
|
|
20
|
+
const spinner = ora('Initializing gent repository...').start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const gentPath = path.join(cwd, GENT_DIR);
|
|
25
|
+
|
|
26
|
+
// Check if already initialized
|
|
27
|
+
if (await pathExists(gentPath)) {
|
|
28
|
+
spinner.fail(chalk.red('Gent repository already exists!'));
|
|
29
|
+
console.log(chalk.yellow('\nℹ Use gent status to see the current state'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get user configuration if not using defaults
|
|
34
|
+
let config = {
|
|
35
|
+
user: {
|
|
36
|
+
name: 'Anonymous',
|
|
37
|
+
email: 'anonymous@example.com'
|
|
38
|
+
},
|
|
39
|
+
repository: {
|
|
40
|
+
name: path.basename(cwd),
|
|
41
|
+
description: 'A gent repository',
|
|
42
|
+
created: new Date().toISOString()
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!options.yes) {
|
|
47
|
+
spinner.stop();
|
|
48
|
+
|
|
49
|
+
const answers = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'input',
|
|
52
|
+
name: 'userName',
|
|
53
|
+
message: 'Enter your name:',
|
|
54
|
+
default: 'Anonymous'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'userEmail',
|
|
59
|
+
message: 'Enter your email:',
|
|
60
|
+
default: 'anonymous@example.com',
|
|
61
|
+
validate: (input) => {
|
|
62
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
63
|
+
return emailRegex.test(input) || 'Please enter a valid email';
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'input',
|
|
68
|
+
name: 'repoName',
|
|
69
|
+
message: 'Repository name:',
|
|
70
|
+
default: path.basename(cwd)
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'input',
|
|
74
|
+
name: 'repoDescription',
|
|
75
|
+
message: 'Repository description:',
|
|
76
|
+
default: 'A gent repository'
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
config.user.name = answers.userName;
|
|
81
|
+
config.user.email = answers.userEmail;
|
|
82
|
+
config.repository.name = answers.repoName;
|
|
83
|
+
config.repository.description = answers.repoDescription;
|
|
84
|
+
|
|
85
|
+
spinner.start('Creating repository structure...');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create directory structure
|
|
89
|
+
await ensureDir(gentPath);
|
|
90
|
+
await ensureDir(path.join(gentPath, 'objects'));
|
|
91
|
+
await ensureDir(path.join(gentPath, 'refs', 'heads'));
|
|
92
|
+
await ensureDir(path.join(gentPath, 'refs', 'tags'));
|
|
93
|
+
|
|
94
|
+
// Create initial files
|
|
95
|
+
await writeJSON(path.join(gentPath, CONFIG_FILE), config);
|
|
96
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), { files: [] });
|
|
97
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), { commits: [], branches: { main: null }, currentBranch: 'main' });
|
|
98
|
+
|
|
99
|
+
// Create HEAD file
|
|
100
|
+
await fs.writeFile(path.join(gentPath, 'HEAD'), 'ref: refs/heads/main\n');
|
|
101
|
+
|
|
102
|
+
// Create .gentignore
|
|
103
|
+
const gentignore = `# Gent ignore patterns
|
|
104
|
+
node_modules/
|
|
105
|
+
.DS_Store
|
|
106
|
+
*.log
|
|
107
|
+
.env
|
|
108
|
+
.gent/
|
|
109
|
+
`;
|
|
110
|
+
await fs.writeFile(path.join(cwd, '.gentignore'), gentignore);
|
|
111
|
+
|
|
112
|
+
spinner.succeed(chalk.green('✓ Gent repository initialized successfully!'));
|
|
113
|
+
|
|
114
|
+
// Display success message
|
|
115
|
+
const message = chalk.white(`
|
|
116
|
+
${chalk.bold('Repository:')} ${config.repository.name}
|
|
117
|
+
${chalk.bold('User:')} ${config.user.name} <${config.user.email}>
|
|
118
|
+
${chalk.bold('Branch:')} main
|
|
119
|
+
|
|
120
|
+
${chalk.cyan('Next steps:')}
|
|
121
|
+
${chalk.gray('•')} gent add <files> - Add files to staging
|
|
122
|
+
${chalk.gray('•')} gent commit - Commit your changes
|
|
123
|
+
${chalk.gray('•')} gent status - View repository status
|
|
124
|
+
`);
|
|
125
|
+
|
|
126
|
+
console.log(boxen(message, {
|
|
127
|
+
padding: 1,
|
|
128
|
+
margin: 1,
|
|
129
|
+
borderStyle: 'round',
|
|
130
|
+
borderColor: 'cyan'
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
spinner.fail(chalk.red('Failed to initialize repository'));
|
|
135
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = init;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Command - Show commit logs
|
|
3
|
+
* Displays commit history with details
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { formatDistanceToNow } = require('date-fns');
|
|
9
|
+
const { getGentPath, readJSON } = require('../utils/fileSystem');
|
|
10
|
+
const { COMMITS_FILE } = require('../utils/constants');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Show commit history
|
|
14
|
+
* @param {Object} options - Command options
|
|
15
|
+
*/
|
|
16
|
+
async function log(options) {
|
|
17
|
+
try {
|
|
18
|
+
const gentPath = await getGentPath();
|
|
19
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
20
|
+
|
|
21
|
+
const commits = repository.commits || [];
|
|
22
|
+
const currentBranch = repository.currentBranch || 'main';
|
|
23
|
+
|
|
24
|
+
if (commits.length === 0) {
|
|
25
|
+
console.log(chalk.yellow('No commits yet'));
|
|
26
|
+
console.log(chalk.gray('Use "gent commit" to create your first commit'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Limit number of commits to show
|
|
31
|
+
const limit = parseInt(options.number) || 10;
|
|
32
|
+
const commitsToShow = commits.slice(-limit).reverse();
|
|
33
|
+
|
|
34
|
+
if (options.oneline) {
|
|
35
|
+
displayOnelineLog(commitsToShow, repository.branches[currentBranch]);
|
|
36
|
+
} else {
|
|
37
|
+
displayDetailedLog(commitsToShow, repository.branches[currentBranch], currentBranch);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
42
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
43
|
+
console.log(chalk.yellow('\nℹ Run "gent init" to initialize a repository'));
|
|
44
|
+
} else {
|
|
45
|
+
console.error(chalk.red('Error:'), error.message);
|
|
46
|
+
}
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Display detailed commit log
|
|
53
|
+
*/
|
|
54
|
+
function displayDetailedLog(commits, currentCommitHash, currentBranch) {
|
|
55
|
+
console.log(chalk.bold.cyan(`\nCommit History (${currentBranch} branch):\n`));
|
|
56
|
+
|
|
57
|
+
commits.forEach((commit, index) => {
|
|
58
|
+
const isHead = commit.hash === currentCommitHash;
|
|
59
|
+
const headLabel = isHead ? chalk.yellow.bold(' (HEAD)') : '';
|
|
60
|
+
|
|
61
|
+
console.log(chalk.yellow(`commit ${commit.hash}`) + headLabel);
|
|
62
|
+
console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
63
|
+
console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
64
|
+
console.log(chalk.gray(` (${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`));
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.white(` ${commit.message}`));
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.gray(` ${commit.files.length} file(s) changed`));
|
|
69
|
+
|
|
70
|
+
if (index < commits.length - 1) {
|
|
71
|
+
console.log(chalk.gray(' │'));
|
|
72
|
+
}
|
|
73
|
+
console.log();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Display oneline commit log
|
|
79
|
+
*/
|
|
80
|
+
function displayOnelineLog(commits, currentCommitHash) {
|
|
81
|
+
commits.forEach(commit => {
|
|
82
|
+
const isHead = commit.hash === currentCommitHash;
|
|
83
|
+
const headLabel = isHead ? chalk.yellow(' (HEAD)') : '';
|
|
84
|
+
const shortHash = chalk.yellow(commit.hash.substring(0, 7));
|
|
85
|
+
const message = chalk.white(commit.message);
|
|
86
|
+
const timeAgo = chalk.gray(`(${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`);
|
|
87
|
+
|
|
88
|
+
console.log(`${shortHash}${headLabel} ${message} ${timeAgo}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = log;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Command - Show the working tree status
|
|
3
|
+
* Displays staged, modified, and untracked files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { getGentPath, readJSON, pathExists, getTrackedFiles, getIgnorePatterns } = require('../utils/fileSystem');
|
|
10
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
11
|
+
const { getFileHash, getAllFiles } = require('../utils/helpers');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Show repository status
|
|
15
|
+
* @param {Object} options - Command options
|
|
16
|
+
*/
|
|
17
|
+
async function status(options) {
|
|
18
|
+
try {
|
|
19
|
+
const gentPath = await getGentPath();
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
|
|
22
|
+
// Read staging area and commits
|
|
23
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
24
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
25
|
+
|
|
26
|
+
const stagedFiles = staging.files || [];
|
|
27
|
+
const currentBranch = repository.currentBranch || 'main';
|
|
28
|
+
const lastCommit = repository.branches[currentBranch];
|
|
29
|
+
|
|
30
|
+
// Get all files in the working directory
|
|
31
|
+
const ignorePatterns = await getIgnorePatterns(cwd);
|
|
32
|
+
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
33
|
+
|
|
34
|
+
// Get tracked files from last commit
|
|
35
|
+
const trackedFiles = await getTrackedFiles(gentPath, lastCommit);
|
|
36
|
+
|
|
37
|
+
// Categorize files
|
|
38
|
+
const stagedSet = new Set(stagedFiles);
|
|
39
|
+
const trackedSet = new Set(trackedFiles.map(f => f.path));
|
|
40
|
+
|
|
41
|
+
const modified = [];
|
|
42
|
+
const untracked = [];
|
|
43
|
+
const deleted = [];
|
|
44
|
+
|
|
45
|
+
// Check for modifications and untracked files
|
|
46
|
+
for (const file of allFiles) {
|
|
47
|
+
const relativePath = path.relative(cwd, file);
|
|
48
|
+
|
|
49
|
+
if (trackedSet.has(relativePath)) {
|
|
50
|
+
// Check if modified
|
|
51
|
+
const currentHash = await getFileHash(file);
|
|
52
|
+
const trackedFile = trackedFiles.find(f => f.path === relativePath);
|
|
53
|
+
|
|
54
|
+
if (trackedFile && currentHash !== trackedFile.hash && !stagedSet.has(relativePath)) {
|
|
55
|
+
modified.push(relativePath);
|
|
56
|
+
}
|
|
57
|
+
} else if (!stagedSet.has(relativePath)) {
|
|
58
|
+
untracked.push(relativePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for deleted files
|
|
63
|
+
for (const trackedFile of trackedFiles) {
|
|
64
|
+
const fullPath = path.join(cwd, trackedFile.path);
|
|
65
|
+
if (!await pathExists(fullPath) && !stagedSet.has(trackedFile.path)) {
|
|
66
|
+
deleted.push(trackedFile.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Display status
|
|
71
|
+
if (options.short) {
|
|
72
|
+
displayShortStatus(stagedFiles, modified, untracked, deleted);
|
|
73
|
+
} else {
|
|
74
|
+
displayDetailedStatus(currentBranch, stagedFiles, modified, untracked, deleted, lastCommit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
79
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
80
|
+
console.log(chalk.yellow('\nℹ Run "gent init" to initialize a repository'));
|
|
81
|
+
} else {
|
|
82
|
+
console.error(chalk.red('Error:'), error.message);
|
|
83
|
+
}
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Display detailed status output
|
|
90
|
+
*/
|
|
91
|
+
function displayDetailedStatus(branch, staged, modified, untracked, deleted, lastCommit) {
|
|
92
|
+
console.log(chalk.bold(`On branch ${chalk.cyan(branch)}`));
|
|
93
|
+
|
|
94
|
+
if (!lastCommit) {
|
|
95
|
+
console.log(chalk.gray('No commits yet\n'));
|
|
96
|
+
} else {
|
|
97
|
+
console.log(chalk.gray(`Last commit: ${lastCommit.substring(0, 7)}\n`));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Staged files
|
|
101
|
+
if (staged.length > 0) {
|
|
102
|
+
console.log(chalk.green.bold('Changes to be committed:'));
|
|
103
|
+
console.log(chalk.gray(' (use "gent reset <file>..." to unstage)\n'));
|
|
104
|
+
staged.forEach(file => {
|
|
105
|
+
console.log(chalk.green(`\t${file}`));
|
|
106
|
+
});
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Modified files
|
|
111
|
+
if (modified.length > 0) {
|
|
112
|
+
console.log(chalk.red.bold('Changes not staged for commit:'));
|
|
113
|
+
console.log(chalk.gray(' (use "gent add <file>..." to update what will be committed)\n'));
|
|
114
|
+
modified.forEach(file => {
|
|
115
|
+
console.log(chalk.red(`\tmodified: ${file}`));
|
|
116
|
+
});
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Deleted files
|
|
121
|
+
if (deleted.length > 0) {
|
|
122
|
+
deleted.forEach(file => {
|
|
123
|
+
console.log(chalk.red(`\tdeleted: ${file}`));
|
|
124
|
+
});
|
|
125
|
+
console.log();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Untracked files
|
|
129
|
+
if (untracked.length > 0) {
|
|
130
|
+
console.log(chalk.red.bold('Untracked files:'));
|
|
131
|
+
console.log(chalk.gray(' (use "gent add <file>..." to include in what will be committed)\n'));
|
|
132
|
+
untracked.forEach(file => {
|
|
133
|
+
console.log(chalk.red(`\t${file}`));
|
|
134
|
+
});
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Status summary
|
|
139
|
+
if (staged.length === 0 && modified.length === 0 && untracked.length === 0 && deleted.length === 0) {
|
|
140
|
+
console.log(chalk.green('✓ Working tree clean'));
|
|
141
|
+
} else if (staged.length === 0) {
|
|
142
|
+
console.log(chalk.yellow('No changes added to commit (use "gent add" to track files)'));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Display short status output
|
|
148
|
+
*/
|
|
149
|
+
function displayShortStatus(staged, modified, untracked, deleted) {
|
|
150
|
+
staged.forEach(file => {
|
|
151
|
+
console.log(chalk.green('A ') + file);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
modified.forEach(file => {
|
|
155
|
+
console.log(chalk.red(' M ') + file);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
deleted.forEach(file => {
|
|
159
|
+
console.log(chalk.red(' D ') + file);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
untracked.forEach(file => {
|
|
163
|
+
console.log(chalk.red('?? ') + file);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = status;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gent CLI - A Git-like version control system
|
|
5
|
+
* Main entry point for the CLI application
|
|
6
|
+
*
|
|
7
|
+
* @author Your Name
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { program } = require('commander');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const packageJson = require('../package.json');
|
|
14
|
+
|
|
15
|
+
// Import commands
|
|
16
|
+
const initCommand = require('./commands/init');
|
|
17
|
+
const statusCommand = require('./commands/status');
|
|
18
|
+
const addCommand = require('./commands/add');
|
|
19
|
+
const commitCommand = require('./commands/commit');
|
|
20
|
+
const logCommand = require('./commands/log');
|
|
21
|
+
const branchCommand = require('./commands/branch');
|
|
22
|
+
const checkoutCommand = require('./commands/checkout');
|
|
23
|
+
|
|
24
|
+
// Configure CLI
|
|
25
|
+
program
|
|
26
|
+
.name('gent')
|
|
27
|
+
.description(chalk.cyan('🚀 Gent - A Git-like version control CLI'))
|
|
28
|
+
.version(packageJson.version, '-v, --version', 'Output the current version');
|
|
29
|
+
|
|
30
|
+
// Register commands
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Initialize a new gent repository')
|
|
34
|
+
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
35
|
+
.action(initCommand);
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('status')
|
|
39
|
+
.description('Show the working tree status')
|
|
40
|
+
.option('-s, --short', 'Give the output in short format')
|
|
41
|
+
.action(statusCommand);
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('add <files...>')
|
|
45
|
+
.description('Add file contents to the staging area')
|
|
46
|
+
.option('-A, --all', 'Add all files')
|
|
47
|
+
.action(addCommand);
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('commit')
|
|
51
|
+
.description('Record changes to the repository')
|
|
52
|
+
.option('-m, --message <message>', 'Commit message')
|
|
53
|
+
.option('-a, --all', 'Automatically stage all modified files')
|
|
54
|
+
.action(commitCommand);
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('log')
|
|
58
|
+
.description('Show commit logs')
|
|
59
|
+
.option('-n, --number <count>', 'Limit the number of commits to show', '10')
|
|
60
|
+
.option('--oneline', 'Show each commit on a single line')
|
|
61
|
+
.action(logCommand);
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command('branch')
|
|
65
|
+
.description('List, create, or delete branches')
|
|
66
|
+
.argument('[name]', 'Branch name to create')
|
|
67
|
+
.option('-d, --delete <name>', 'Delete a branch')
|
|
68
|
+
.option('-a, --all', 'List all branches')
|
|
69
|
+
.action(branchCommand);
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command('checkout <branch>')
|
|
73
|
+
.description('Switch branches or restore working tree files')
|
|
74
|
+
.option('-b, --create', 'Create a new branch')
|
|
75
|
+
.action(checkoutCommand);
|
|
76
|
+
|
|
77
|
+
// Help command
|
|
78
|
+
program
|
|
79
|
+
.command('help [command]')
|
|
80
|
+
.description('Display help for a specific command')
|
|
81
|
+
.action((command) => {
|
|
82
|
+
if (command) {
|
|
83
|
+
program.commands.find(cmd => cmd.name() === command)?.help();
|
|
84
|
+
} else {
|
|
85
|
+
program.help();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Error handling
|
|
90
|
+
program.exitOverride();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
program.parse(process.argv);
|
|
94
|
+
|
|
95
|
+
// Show help if no command provided
|
|
96
|
+
if (!process.argv.slice(2).length) {
|
|
97
|
+
program.outputHelp();
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed') {
|
|
101
|
+
console.error(chalk.red('Error:'), err.message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|