@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
819 lines (783 loc) • 25.2 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitIntegration = void 0;
exports.getGitIntegration = getGitIntegration;
exports.initializeGitRepository = initializeGitRepository;
exports.getGitStatus = getGitStatus;
const events_1 = require("events");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class GitIntegration extends events_1.EventEmitter {
constructor() {
super();
this.defaultGitFlow = {
masterBranch: 'main',
developBranch: 'develop',
featurePrefix: 'feature/',
bugfixPrefix: 'bugfix/',
releasePrefix: 'release/',
hotfixPrefix: 'hotfix/',
tagPrefix: 'v'
};
this.defaultGitignore = `# Dependencies
node_modules/
bower_components/
vendor/
# Build outputs
dist/
build/
out/
.next/
.nuxt/
.cache/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Test coverage
coverage/
.nyc_output/
*.lcov
# Temporary files
tmp/
temp/
.tmp/
.temp/
# Package manager files
.npm/
.yarn/
.pnpm/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Re-Shell specific
.re-shell/
*.re-shell.cache
.re-shell-backup/`;
}
async initializeRepository(projectPath, config = {}) {
this.emit('init:start', { projectPath, config });
const result = {
success: false,
repoPath: projectPath,
branch: config.defaultBranch || 'main',
errors: [],
warnings: []
};
try {
// Check if already a git repository
const isRepo = await this.isGitRepository(projectPath);
if (isRepo) {
result.warnings?.push('Directory is already a Git repository');
const status = await this.getStatus(projectPath);
result.branch = status.currentBranch;
result.success = true;
return result;
}
// Initialize repository
await this.runGitCommand('init', projectPath);
this.emit('init:initialized', projectPath);
// Set default branch name
if (config.defaultBranch && config.defaultBranch !== 'master') {
try {
await this.runGitCommand(`branch -M ${config.defaultBranch}`, projectPath);
result.branch = config.defaultBranch;
}
catch (error) {
result.warnings?.push(`Failed to rename default branch: ${error}`);
}
}
// Configure user if provided
if (config.userName || config.userEmail) {
await this.configureUser(projectPath, config.userName, config.userEmail);
}
// Create .gitignore
const gitignorePath = path.join(projectPath, '.gitignore');
if (!await fs.pathExists(gitignorePath)) {
const gitignoreContent = config.gitignoreTemplate || this.defaultGitignore;
await fs.writeFile(gitignorePath, gitignoreContent);
this.emit('init:gitignore_created', gitignorePath);
}
// Create README if it doesn't exist
const readmePath = path.join(projectPath, 'README.md');
if (!await fs.pathExists(readmePath)) {
const readmeContent = this.generateReadme(projectPath, config);
await fs.writeFile(readmePath, readmeContent);
this.emit('init:readme_created', readmePath);
}
// Setup git hooks if provided
if (config.hooks && config.hooks.length > 0) {
await this.setupHooks(projectPath, config.hooks);
}
// Create initial commit
const commitResult = await this.createInitialCommit(projectPath, config);
if (commitResult.success) {
result.initialCommit = commitResult.commitHash;
}
else {
result.errors?.push('Failed to create initial commit');
}
// Setup remote if provided
if (config.remoteUrl) {
try {
await this.addRemote(projectPath, 'origin', config.remoteUrl);
result.remote = config.remoteUrl;
this.emit('init:remote_added', config.remoteUrl);
}
catch (error) {
result.warnings?.push(`Failed to add remote: ${error.message}`);
}
}
result.success = true;
this.emit('init:complete', result);
return result;
}
catch (error) {
result.errors?.push(error.message);
this.emit('init:error', error);
return result;
}
}
async isGitRepository(projectPath) {
try {
await this.runGitCommand('rev-parse --git-dir', projectPath);
return true;
}
catch {
return false;
}
}
async getStatus(projectPath) {
const status = {
initialized: false,
hasRemote: false,
currentBranch: '',
clean: true,
ahead: 0,
behind: 0,
untracked: [],
modified: [],
staged: [],
conflicts: []
};
try {
// Check if initialized
status.initialized = await this.isGitRepository(projectPath);
if (!status.initialized)
return status;
// Get current branch
try {
status.currentBranch = (await this.runGitCommand('rev-parse --abbrev-ref HEAD', projectPath)).trim();
}
catch {
status.currentBranch = 'unknown';
}
// Check for remote
try {
const remotes = await this.runGitCommand('remote -v', projectPath);
status.hasRemote = remotes.trim().length > 0;
}
catch {
status.hasRemote = false;
}
// Get status porcelain
const porcelain = await this.runGitCommand('status --porcelain=v1', projectPath);
const lines = porcelain.trim().split('\n').filter(line => line);
for (const line of lines) {
const code = line.substring(0, 2);
const file = line.substring(3);
if (code === '??') {
status.untracked.push(file);
}
else if (code === 'UU' || code === 'AA' || code === 'DD') {
status.conflicts.push(file);
}
else {
if (code[0] !== ' ' && code[0] !== '?') {
status.staged.push(file);
}
if (code[1] !== ' ' && code[1] !== '?') {
status.modified.push(file);
}
}
}
status.clean = lines.length === 0;
// Get ahead/behind if has remote
if (status.hasRemote && status.currentBranch !== 'unknown') {
try {
const upstream = await this.runGitCommand(`rev-list --left-right --count @{upstream}...HEAD`, projectPath);
const [behind, ahead] = upstream.trim().split(/\s+/).map(n => parseInt(n, 10));
status.behind = behind || 0;
status.ahead = ahead || 0;
}
catch {
// No upstream branch
}
}
return status;
}
catch (error) {
this.emit('status:error', error);
return status;
}
}
async createBranch(projectPath, config) {
try {
// Validate branch name
if (!this.isValidBranchName(config.name)) {
throw new Error(`Invalid branch name: ${config.name}`);
}
// Determine base branch
const baseBranch = config.baseBranch || await this.getCurrentBranch(projectPath);
// Create branch
await this.runGitCommand(`checkout -b ${config.name} ${baseBranch}`, projectPath);
this.emit('branch:created', { name: config.name, base: baseBranch });
// Push to remote if requested
if (config.push) {
try {
await this.runGitCommand(`push -u origin ${config.name}`, projectPath);
this.emit('branch:pushed', config.name);
}
catch (error) {
this.emit('branch:push_failed', { branch: config.name, error });
}
}
return { success: true, branch: config.name };
}
catch (error) {
return { success: false, branch: '', error: error.message };
}
}
async createFeatureBranch(projectPath, featureName, gitFlow) {
const flow = { ...this.defaultGitFlow, ...gitFlow };
const branchName = `${flow.featurePrefix}${featureName}`;
return this.createBranch(projectPath, {
name: branchName,
type: 'feature',
baseBranch: flow.developBranch,
checkout: true
});
}
async commit(projectPath, options) {
try {
// Stage files
if (options.stage === 'all') {
await this.runGitCommand('add -A', projectPath);
}
else if (options.stage === 'modified') {
await this.runGitCommand('add -u', projectPath);
}
else if (options.stage === 'specific' && options.files) {
for (const file of options.files) {
await this.runGitCommand(`add "${file}"`, projectPath);
}
}
// Build commit command
let commitCmd = 'commit';
if (options.amend)
commitCmd += ' --amend';
if (options.noVerify)
commitCmd += ' --no-verify';
if (options.signoff)
commitCmd += ' --signoff';
// Add message
commitCmd += ` -m "${options.message.replace(/"/g, '\\"')}"`;
if (options.description) {
commitCmd += ` -m "${options.description.replace(/"/g, '\\"')}"`;
}
// Execute commit
const output = await this.runGitCommand(commitCmd, projectPath);
// Extract commit hash
const hashMatch = output.match(/\[[\w\s-]+\s+([a-f0-9]+)\]/);
const commitHash = hashMatch ? hashMatch[1] : undefined;
this.emit('commit:created', { hash: commitHash, message: options.message });
return { success: true, commitHash };
}
catch (error) {
return { success: false, error: error.message };
}
}
async createInitialCommit(projectPath, config) {
const message = config.commitMessage || 'Initial commit';
return this.commit(projectPath, {
message,
description: 'Project initialized with Re-Shell CLI',
stage: 'all',
signoff: config.signCommits
});
}
async setupGitFlow(projectPath, config) {
const flow = { ...this.defaultGitFlow, ...config };
const errors = [];
try {
// Ensure we're on the main branch
await this.runGitCommand(`checkout ${flow.masterBranch}`, projectPath);
// Create develop branch if it doesn't exist
try {
await this.runGitCommand(`checkout -b ${flow.developBranch}`, projectPath);
this.emit('gitflow:branch_created', flow.developBranch);
}
catch {
// Branch might already exist
await this.runGitCommand(`checkout ${flow.developBranch}`, projectPath);
}
// Push develop branch
try {
await this.runGitCommand(`push -u origin ${flow.developBranch}`, projectPath);
}
catch (error) {
errors.push(`Failed to push develop branch: ${error.message}`);
}
// Create .gitflow config file
const gitflowConfig = {
gitflow: {
branch: {
master: flow.masterBranch,
develop: flow.developBranch
},
prefix: {
feature: flow.featurePrefix,
bugfix: flow.bugfixPrefix,
release: flow.releasePrefix,
hotfix: flow.hotfixPrefix,
support: 'support/',
versiontag: flow.tagPrefix
}
}
};
const configPath = path.join(projectPath, '.gitflow');
await fs.writeJson(configPath, gitflowConfig, { spaces: 2 });
this.emit('gitflow:setup_complete', flow);
return { success: true, errors: errors.length > 0 ? errors : undefined };
}
catch (error) {
errors.push(error.message);
return { success: false, errors };
}
}
async addRemote(projectPath, name, url) {
await this.runGitCommand(`remote add ${name} ${url}`, projectPath);
}
async push(projectPath, options = {}) {
try {
const remote = options.remote || 'origin';
const branch = options.branch || await this.getCurrentBranch(projectPath);
let pushCmd = `push`;
if (options.force)
pushCmd += ' --force';
if (options.tags)
pushCmd += ' --tags';
if (options.setUpstream)
pushCmd += ' -u';
pushCmd += ` ${remote} ${branch}`;
await this.runGitCommand(pushCmd, projectPath);
this.emit('push:complete', { remote, branch });
return { success: true };
}
catch (error) {
return { success: false, error: error.message };
}
}
async pull(projectPath, options = {}) {
try {
const remote = options.remote || 'origin';
const branch = options.branch || await this.getCurrentBranch(projectPath);
let pullCmd = `pull`;
if (options.rebase)
pullCmd += ' --rebase';
if (options.noCommit)
pullCmd += ' --no-commit';
pullCmd += ` ${remote} ${branch}`;
await this.runGitCommand(pullCmd, projectPath);
this.emit('pull:complete', { remote, branch });
return { success: true };
}
catch (error) {
return { success: false, error: error.message };
}
}
async tag(projectPath, tagName, options = {}) {
try {
let tagCmd = 'tag';
if (options.annotated || options.message)
tagCmd += ' -a';
if (options.force)
tagCmd += ' -f';
if (options.sign)
tagCmd += ' -s';
if (options.message)
tagCmd += ` -m "${options.message.replace(/"/g, '\\"')}"`;
tagCmd += ` ${tagName}`;
await this.runGitCommand(tagCmd, projectPath);
this.emit('tag:created', tagName);
return { success: true };
}
catch (error) {
return { success: false, error: error.message };
}
}
async stash(projectPath, options = {}) {
try {
let stashCmd = 'stash push';
if (options.includeUntracked)
stashCmd += ' --include-untracked';
if (options.keepIndex)
stashCmd += ' --keep-index';
if (options.message)
stashCmd += ` -m "${options.message.replace(/"/g, '\\"')}"`;
await this.runGitCommand(stashCmd, projectPath);
this.emit('stash:created');
return { success: true };
}
catch (error) {
return { success: false, error: error.message };
}
}
async getLog(projectPath, options = {}) {
let logCmd = 'log';
if (options.limit)
logCmd += ` -n ${options.limit}`;
if (options.oneline)
logCmd += ' --oneline';
if (options.graph)
logCmd += ' --graph';
if (options.author)
logCmd += ` --author="${options.author}"`;
if (options.since)
logCmd += ` --since="${options.since}"`;
if (options.until)
logCmd += ` --until="${options.until}"`;
const output = await this.runGitCommand(logCmd, projectPath);
return output.trim().split('\n').filter(line => line);
}
async getDiff(projectPath, options = {}) {
let diffCmd = 'diff';
if (options.staged)
diffCmd += ' --staged';
if (options.nameOnly)
diffCmd += ' --name-only';
if (options.stat)
diffCmd += ' --stat';
if (options.commit)
diffCmd += ` ${options.commit}`;
return await this.runGitCommand(diffCmd, projectPath);
}
async setupHooks(projectPath, hooks) {
const hooksDir = path.join(projectPath, '.git', 'hooks');
await fs.ensureDir(hooksDir);
for (const hook of hooks) {
const hookPath = path.join(hooksDir, hook.name);
const hookContent = `#!/bin/sh
# ${hook.description || `${hook.name} hook`}
# Generated by Re-Shell CLI
${hook.script}
`;
await fs.writeFile(hookPath, hookContent);
await fs.chmod(hookPath, '755');
this.emit('hook:created', hook.name);
}
}
async configureUser(projectPath, userName, userEmail) {
if (userName) {
await this.runGitCommand(`config user.name "${userName}"`, projectPath);
}
if (userEmail) {
await this.runGitCommand(`config user.email "${userEmail}"`, projectPath);
}
}
async getConfig(projectPath, key, options = {}) {
try {
let cmd = `config`;
if (options.global)
cmd += ' --global';
cmd += ` --get ${key}`;
const value = await this.runGitCommand(cmd, projectPath);
return value.trim();
}
catch {
return null;
}
}
async setConfig(projectPath, key, value, options = {}) {
let cmd = `config`;
if (options.global)
cmd += ' --global';
cmd += ` ${key} "${value}"`;
await this.runGitCommand(cmd, projectPath);
}
async getCurrentBranch(projectPath) {
return (await this.runGitCommand('rev-parse --abbrev-ref HEAD', projectPath)).trim();
}
isValidBranchName(name) {
// Git branch name validation rules
const invalidPatterns = [
/^-/, // Cannot start with dash
/\.$/, // Cannot end with dot
/\.lock$/, // Cannot end with .lock
/[\s~^:?*\[\\]/, // Invalid characters
/\.\./, // Cannot contain ..
/@\{/, // Cannot contain @{
/\/\// // Cannot contain //
];
return !invalidPatterns.some(pattern => pattern.test(name));
}
generateReadme(projectPath, config) {
const projectName = path.basename(projectPath);
return `# ${projectName}
This project was initialized with [Re-Shell CLI](https://github.com/re-shell/cli).
## Getting Started
### Prerequisites
- Node.js >= 16.0.0
- npm, yarn, or pnpm
### Installation
\`\`\`bash
npm install
\`\`\`
### Development
\`\`\`bash
npm run dev
\`\`\`
### Building
\`\`\`bash
npm run build
\`\`\`
### Testing
\`\`\`bash
npm test
\`\`\`
## Project Structure
\`\`\`
${projectName}/
├── src/ # Source code
├── tests/ # Test files
├── dist/ # Build output
└── README.md # This file
\`\`\`
## Contributing
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
`;
}
async runGitCommand(command, cwd) {
try {
const { stdout, stderr } = await execAsync(`git ${command}`, {
cwd,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB
});
if (stderr && !stderr.includes('warning:')) {
this.emit('git:warning', stderr);
}
return stdout;
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error('Git is not installed or not in PATH');
}
throw new Error(error.stderr || error.message);
}
}
// Utility methods
async ensureGitInstalled() {
try {
(0, child_process_1.execSync)('git --version', { stdio: 'ignore' });
return true;
}
catch {
return false;
}
}
async getGitVersion() {
try {
const output = (0, child_process_1.execSync)('git --version', { encoding: 'utf8' });
const match = output.match(/(\d+\.\d+\.\d+)/);
return match ? match[1] : null;
}
catch {
return null;
}
}
generateGitignore(projectType) {
const templates = {
node: this.defaultGitignore,
python: `# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
pip-log.txt
pip-delete-this-directory.txt
.pytest_cache/
.coverage
.tox/
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db`,
java: `# Java
*.class
*.jar
*.war
*.ear
*.nar
hs_err_pid*
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Gradle
.gradle/
build/
!gradle-wrapper.jar
# IDE
.idea/
*.iml
*.iws
*.ipr
.project
.classpath
.settings/
bin/
# OS
.DS_Store
Thumbs.db`,
go: `# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/
go.sum
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db`
};
return templates[projectType] || this.defaultGitignore;
}
}
exports.GitIntegration = GitIntegration;
// Global instance
let globalGitIntegration = null;
function getGitIntegration() {
if (!globalGitIntegration) {
globalGitIntegration = new GitIntegration();
}
return globalGitIntegration;
}
async function initializeGitRepository(projectPath, config) {
const git = getGitIntegration();
return git.initializeRepository(projectPath, config);
}
async function getGitStatus(projectPath) {
const git = getGitIntegration();
return git.getStatus(projectPath);
}