Initial commit
This commit is contained in:
80
.gitignore
vendored
Normal file
80
.gitignore
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
163
README.md
Normal file
163
README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Gitea Creator
|
||||
|
||||
A command line tool to create repositories on Gitea servers with automatic git setup.
|
||||
|
||||
## Features
|
||||
|
||||
- Create repositories on Gitea servers via API
|
||||
- Interactive prompts for git initialization and setup
|
||||
- Automatic remote configuration
|
||||
- Optional automatic pushing to the new repository
|
||||
- Support for public/private repository creation
|
||||
- Comprehensive .gitignore creation for Node.js projects
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Local Development
|
||||
1. Clone this repository
|
||||
2. Install dependencies: `npm install`
|
||||
3. Build the project: `npm run build`
|
||||
4. The CLI will be available at `./dist/index.js`
|
||||
|
||||
### Option 2: Global Installation (Development)
|
||||
1. Clone this repository
|
||||
2. Install dependencies: `npm install`
|
||||
3. Build the project: `npm run build`
|
||||
4. Link globally: `npm link`
|
||||
5. Use `gitea-creator` command anywhere
|
||||
|
||||
### Option 3: Global Installation (from npm)
|
||||
```bash
|
||||
# If published to npm
|
||||
npm install -g gitea-creator
|
||||
|
||||
# Use anywhere
|
||||
gitea-creator my-repo --public
|
||||
```
|
||||
|
||||
### Option 4: Direct Usage
|
||||
```bash
|
||||
# Run directly without global installation
|
||||
node dist/index.js <repository-name> [options]
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### For Local Development
|
||||
Create a `.env` file (copy from `env.example`):
|
||||
|
||||
```bash
|
||||
# Copy the example file
|
||||
cp env.example .env
|
||||
|
||||
# Edit the .env file with your values
|
||||
# GITEA_TOKEN=your_gitea_access_token
|
||||
# GITEA_API_URL=https://your-gitea-server.com/api/v1
|
||||
```
|
||||
|
||||
### For Global Installation
|
||||
When installed globally, the tool supports multiple configuration methods:
|
||||
|
||||
#### Option 1: System Environment Variables (Recommended)
|
||||
```bash
|
||||
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
|
||||
export GITEA_TOKEN=your_gitea_access_token
|
||||
export GITEA_API_URL=https://your-gitea-server.com/api/v1
|
||||
|
||||
# Reload your shell
|
||||
source ~/.zshrc # or ~/.bashrc
|
||||
```
|
||||
|
||||
#### Option 2: Global Configuration File
|
||||
```bash
|
||||
# Create global config directory
|
||||
mkdir -p ~/.gitea-creator
|
||||
|
||||
# Create .env file
|
||||
echo "GITEA_TOKEN=your_token_here" > ~/.gitea-creator/.env
|
||||
echo "GITEA_API_URL=https://your-gitea-server.com/api/v1" >> ~/.gitea-creator/.env
|
||||
```
|
||||
|
||||
#### Option 3: View Configuration Instructions
|
||||
```bash
|
||||
# Get setup instructions anytime
|
||||
gitea-creator --config
|
||||
# or short form
|
||||
gitea-creator -c
|
||||
```
|
||||
|
||||
### Getting a Gitea Access Token
|
||||
1. Log into your Gitea server
|
||||
2. Go to Settings → Applications
|
||||
3. Click "Manage Access Tokens"
|
||||
4. Generate a new token with repository permissions
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
gitea-creator <repository-name> [options]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `-p, --public`: Create a public repository (non-private)
|
||||
- `-d, --description <description>`: Add a description to the repository
|
||||
- `-h, --help`: Display help information
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Create a private repository
|
||||
gitea-creator my-new-repo
|
||||
|
||||
# Create a public repository with description
|
||||
gitea-creator my-public-repo --public --description "My awesome project"
|
||||
|
||||
# Short form
|
||||
gitea-creator my-repo -p -d "Short description"
|
||||
|
||||
# Get configuration help
|
||||
gitea-creator --config
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
When you run the tool, it will:
|
||||
|
||||
1. **Create Repository**: Create the repository on your Gitea server with the specified options
|
||||
2. **Display URL**: Show the remote clone URL for the new repository
|
||||
3. **Git Initialization**: If no git repository exists, ask if you want to initialize one
|
||||
- Creates `.gitignore` with Node.js patterns if initializing
|
||||
4. **Remote Setup**: Ask if you want to set the new repository as the remote origin (defaults to yes)
|
||||
5. **Pushing**: Ask if you want to push the current repository (defaults to yes)
|
||||
- Automatically handles initial commits if there are uncommitted changes
|
||||
- Renames `master` branch to `main` if needed
|
||||
|
||||
### Interactive Prompts
|
||||
All prompts default to "yes" - you can just press Enter to accept the defaults:
|
||||
- Initialize git repository? (if none exists)
|
||||
- Set remote origin?
|
||||
- Push to remote?
|
||||
- Commit changes first? (if there are uncommitted changes)
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the project
|
||||
npm run build
|
||||
|
||||
# Run in development mode
|
||||
npm run dev
|
||||
|
||||
# Test the CLI
|
||||
node dist/index.js my-test-repo --public
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- Git (for git operations)
|
||||
- Valid Gitea server access token
|
||||
13
env.example
Normal file
13
env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Gitea Creator Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Your Gitea personal access token
|
||||
# You can create one in your Gitea settings under "Applications" -> "Manage Access Tokens"
|
||||
GITEA_TOKEN=26e3b54850d1b433117c1494853ffcfab501c53b
|
||||
|
||||
# Your Gitea server API URL (include /api/v1 at the end)
|
||||
# Examples:
|
||||
# GITEA_API_URL=https://git.harvmaster.com/api/v1
|
||||
# GITEA_API_URL=https://gitea.mycompany.com/api/v1
|
||||
# GITEA_API_URL=https://codeberg.org/api/v1
|
||||
GITEA_API_URL=https://git.harvmaster.com/api/v1
|
||||
104
package-lock.json
generated
Normal file
104
package-lock.json
generated
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "gitea-creator",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitea-creator",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"prompts": "^2.4.2"
|
||||
},
|
||||
"bin": {
|
||||
"gitea-creator": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
|
||||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prompts": {
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz",
|
||||
"integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"kleur": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kleur": "^3.0.3",
|
||||
"sisteransi": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "gitea-creator",
|
||||
"version": "1.0.0",
|
||||
"description": "CLI tool to create repositories on Gitea servers",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"gitea-creator": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc && node dist/index.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"README.md",
|
||||
"env.example"
|
||||
],
|
||||
"keywords": ["gitea", "git", "cli", "repository"],
|
||||
"author": "harvmaster",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"prompts": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
80
src/cli.ts
Normal file
80
src/cli.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Command } from 'commander';
|
||||
import { GlobalConfig } from './global-config.js';
|
||||
|
||||
/**
|
||||
* Command line interface options for creating a Gitea repository
|
||||
*/
|
||||
export interface CliOptions {
|
||||
/** Repository name (required) */
|
||||
name: string;
|
||||
/** Repository description */
|
||||
description?: string;
|
||||
/** Whether the repository should be public (non-private) */
|
||||
public?: boolean;
|
||||
/** Show configuration instructions */
|
||||
config?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI service for parsing command line arguments
|
||||
*/
|
||||
export class CliService {
|
||||
private readonly program: Command;
|
||||
|
||||
constructor() {
|
||||
this.program = new Command();
|
||||
this.setupCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the command line interface with all options and arguments
|
||||
*/
|
||||
private setupCommand(): void {
|
||||
this.program
|
||||
.name('gitea-creator')
|
||||
.description('CLI tool to create repositories on Gitea servers')
|
||||
.version('1.0.0')
|
||||
.argument('<name>', 'Name of the repository to create')
|
||||
.option('-p, --public', 'Create a public repository (non-private)', false)
|
||||
.option('-d, --description <description>', 'Repository description')
|
||||
.option('-c, --config', 'Show configuration setup instructions')
|
||||
.helpOption('-h, --help', 'Display help for command');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments and return options
|
||||
* @param argv - Command line arguments (defaults to process.argv)
|
||||
* @returns Parsed CLI options
|
||||
*/
|
||||
parse(argv?: string[]): CliOptions {
|
||||
const parsed = this.program.parse(argv);
|
||||
const options = parsed.opts();
|
||||
const [name] = parsed.args;
|
||||
|
||||
// Handle config option
|
||||
if (options.config) {
|
||||
console.log(GlobalConfig.getSetupInstructions());
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
console.error('❌ Error: Repository name is required');
|
||||
this.program.help();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
description: options.description,
|
||||
public: options.public,
|
||||
config: options.config,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help information
|
||||
*/
|
||||
help(): void {
|
||||
this.program.help();
|
||||
}
|
||||
}
|
||||
35
src/config.ts
Normal file
35
src/config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type ConfigRaw = {
|
||||
GITEA_TOKEN: string;
|
||||
GITEA_API_URL: string;
|
||||
}
|
||||
|
||||
import { GlobalConfig } from './global-config.js';
|
||||
|
||||
export class Config {
|
||||
static from(config: Record<string, string>) {
|
||||
const token = config.GITEA_TOKEN;
|
||||
if (!token) {
|
||||
const instructions = GlobalConfig.getSetupInstructions();
|
||||
throw new Error(`GITEA_TOKEN is not set.\n${instructions}`);
|
||||
}
|
||||
|
||||
const apiUrl = config.GITEA_API_URL;
|
||||
if (!apiUrl) {
|
||||
const instructions = GlobalConfig.getSetupInstructions();
|
||||
throw new Error(`GITEA_API_URL is not set.\n${instructions}`);
|
||||
}
|
||||
|
||||
return new Config({
|
||||
GITEA_TOKEN: token,
|
||||
GITEA_API_URL: apiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
readonly token: string;
|
||||
readonly apiUrl: string;
|
||||
|
||||
constructor(config: ConfigRaw) {
|
||||
this.token = config.GITEA_TOKEN;
|
||||
this.apiUrl = config.GITEA_API_URL;
|
||||
}
|
||||
}
|
||||
28
src/env.ts
Normal file
28
src/env.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { GlobalConfig } from './global-config.js';
|
||||
|
||||
/**
|
||||
* Environment variables configuration
|
||||
* Supports local .env, global config, and system environment variables
|
||||
*/
|
||||
function getConfig(): Record<string, string> {
|
||||
// For local development, try to read local .env first
|
||||
try {
|
||||
// This will only work if we're in a project directory with .env
|
||||
const localEnv = {
|
||||
GITEA_TOKEN: process.env.GITEA_TOKEN || '',
|
||||
GITEA_API_URL: process.env.GITEA_API_URL || '',
|
||||
};
|
||||
|
||||
// If we have local env vars, use them
|
||||
if (localEnv.GITEA_TOKEN && localEnv.GITEA_API_URL) {
|
||||
return localEnv;
|
||||
}
|
||||
} catch {
|
||||
// Ignore local env errors
|
||||
}
|
||||
|
||||
// Fall back to global configuration
|
||||
return GlobalConfig.getConfig();
|
||||
}
|
||||
|
||||
export const env = getConfig();
|
||||
215
src/git.ts
Normal file
215
src/git.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { existsSync } from 'fs';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Git service for handling git operations
|
||||
*/
|
||||
export class GitService {
|
||||
/**
|
||||
* Check if the current directory is a git repository
|
||||
* @returns true if git is initialized, false otherwise
|
||||
*/
|
||||
async isGitInitialized(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync('git status');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new git repository
|
||||
*/
|
||||
async initializeGit(): Promise<void> {
|
||||
try {
|
||||
await execAsync('git init');
|
||||
console.log('✅ Git repository initialized');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize git: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a .gitignore file with common Node.js patterns
|
||||
*/
|
||||
async createGitignore(): Promise<void> {
|
||||
const gitignoreContent = `# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
`;
|
||||
|
||||
try {
|
||||
await writeFile('.gitignore', gitignoreContent);
|
||||
console.log('✅ .gitignore file created');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create .gitignore: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a remote origin to the git repository
|
||||
* @param remoteUrl - The remote URL to add
|
||||
*/
|
||||
async addRemote(remoteUrl: string): Promise<void> {
|
||||
try {
|
||||
// Check if origin already exists
|
||||
try {
|
||||
await execAsync('git remote get-url origin');
|
||||
// If we get here, origin exists, so update it
|
||||
await execAsync(`git remote set-url origin ${remoteUrl}`);
|
||||
console.log('✅ Updated remote origin');
|
||||
} catch {
|
||||
// Origin doesn't exist, add it
|
||||
await execAsync(`git remote add origin ${remoteUrl}`);
|
||||
console.log('✅ Added remote origin');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add remote: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any commits in the repository
|
||||
* @returns true if there are commits, false otherwise
|
||||
*/
|
||||
async hasCommits(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync('git log --oneline -1');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any staged or unstaged changes
|
||||
* @returns true if there are changes, false otherwise
|
||||
*/
|
||||
async hasChanges(): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain');
|
||||
return stdout.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all files and commit them
|
||||
* @param message - Commit message
|
||||
*/
|
||||
async addAndCommit(message: string = 'Initial commit'): Promise<void> {
|
||||
try {
|
||||
await execAsync('git add .');
|
||||
await execAsync(`git commit -m "${message}"`);
|
||||
console.log('✅ Files committed');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to commit: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push to the remote repository
|
||||
* @param branch - Branch to push (defaults to current branch)
|
||||
*/
|
||||
async push(branch: string = 'main'): Promise<void> {
|
||||
try {
|
||||
// First, check if we're on the main/master branch and rename if needed
|
||||
const { stdout: currentBranch } = await execAsync('git branch --show-current');
|
||||
const current = currentBranch.trim();
|
||||
|
||||
if (current === 'master' && branch === 'main') {
|
||||
await execAsync('git branch -m master main');
|
||||
console.log('✅ Renamed branch from master to main');
|
||||
}
|
||||
|
||||
await execAsync(`git push -u origin ${branch}`);
|
||||
console.log('✅ Pushed to remote repository');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to push: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/global-config.ts
Normal file
127
src/global-config.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Global configuration manager for the CLI tool
|
||||
* Supports both environment variables and global config files
|
||||
*/
|
||||
export class GlobalConfig {
|
||||
private static readonly CONFIG_DIR = path.join(homedir(), '.gitea-creator');
|
||||
private static readonly CONFIG_FILE = path.join(this.CONFIG_DIR, 'config');
|
||||
private static readonly ENV_FILE = path.join(this.CONFIG_DIR, '.env');
|
||||
|
||||
/**
|
||||
* Get configuration from multiple sources in priority order:
|
||||
* 1. Environment variables
|
||||
* 2. Global .env file (~/.gitea-creator/.env)
|
||||
* 3. Global config file (~/.gitea-creator/config)
|
||||
* @returns Configuration object
|
||||
*/
|
||||
static getConfig(): Record<string, string> {
|
||||
const config: Record<string, string> = {};
|
||||
|
||||
// Try global config file first (lowest priority)
|
||||
try {
|
||||
if (existsSync(this.CONFIG_FILE)) {
|
||||
const configContent = readFileSync(this.CONFIG_FILE, 'utf-8');
|
||||
this.parseConfigContent(configContent, config);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore config file errors
|
||||
}
|
||||
|
||||
// Try global .env file (medium priority)
|
||||
try {
|
||||
if (existsSync(this.ENV_FILE)) {
|
||||
const envContent = readFileSync(this.ENV_FILE, 'utf-8');
|
||||
this.parseEnvContent(envContent, config);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore .env file errors
|
||||
}
|
||||
|
||||
// Environment variables (highest priority)
|
||||
if (process.env.GITEA_TOKEN) {
|
||||
config.GITEA_TOKEN = process.env.GITEA_TOKEN;
|
||||
}
|
||||
if (process.env.GITEA_API_URL) {
|
||||
config.GITEA_API_URL = process.env.GITEA_API_URL;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse configuration file content
|
||||
* Format: KEY=value (one per line)
|
||||
*/
|
||||
private static parseConfigContent(content: string, config: Record<string, string>): void {
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
config[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse .env file content
|
||||
* Format: KEY=value (supports quotes)
|
||||
*/
|
||||
private static parseEnvContent(content: string, config: Record<string, string>): void {
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
let value = valueParts.join('=').trim();
|
||||
// Remove quotes if present
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
config[key.trim()] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path where global config should be stored
|
||||
*/
|
||||
static getConfigPath(): string {
|
||||
return this.CONFIG_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instructions for setting up global configuration
|
||||
*/
|
||||
static getSetupInstructions(): string {
|
||||
return `
|
||||
Global Configuration Setup:
|
||||
|
||||
Option 1: Environment Variables (Recommended)
|
||||
Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):
|
||||
export GITEA_TOKEN=your_token_here
|
||||
export GITEA_API_URL=https://your-gitea-server.com/api/v1
|
||||
|
||||
Option 2: Global Config File
|
||||
Create: ${this.CONFIG_DIR}/.env
|
||||
Content:
|
||||
GITEA_TOKEN=your_token_here
|
||||
GITEA_API_URL=https://your-gitea-server.com/api/v1
|
||||
|
||||
Option 3: Simple Config File
|
||||
Create: ${this.CONFIG_DIR}/config
|
||||
Content:
|
||||
GITEA_TOKEN=your_token_here
|
||||
GITEA_API_URL=https://your-gitea-server.com/api/v1
|
||||
`;
|
||||
}
|
||||
}
|
||||
174
src/index.ts
Normal file
174
src/index.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Repo, type InitializationOptions, type GiteaRepository } from "./repo.js";
|
||||
import { Config } from "./config.js";
|
||||
import { env } from "./env.js";
|
||||
import { CliService } from "./cli.js";
|
||||
import { GitService } from "./git.js";
|
||||
import { PromptService } from "./prompts.js";
|
||||
|
||||
/**
|
||||
* Main application class that orchestrates the CLI workflow
|
||||
*/
|
||||
export class App {
|
||||
private config?: Config;
|
||||
private readonly gitService: GitService;
|
||||
private readonly promptService: PromptService;
|
||||
|
||||
constructor() {
|
||||
this.gitService = new GitService();
|
||||
this.promptService = new PromptService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize configuration (lazy loading)
|
||||
*/
|
||||
private initConfig(): void {
|
||||
if (!this.config) {
|
||||
this.config = Config.from(env);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a repository on Gitea with the given options
|
||||
* @param options - Repository creation options
|
||||
* @returns Repository data from Gitea API
|
||||
*/
|
||||
async createRepo(options: InitializationOptions): Promise<GiteaRepository> {
|
||||
this.initConfig();
|
||||
const repo = await Repo.create(this.config!, options);
|
||||
return repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main application workflow
|
||||
*/
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
// Parse command line arguments
|
||||
const cliService = new CliService();
|
||||
const options = cliService.parse();
|
||||
|
||||
console.log(`🚀 Creating repository: ${options.name}`);
|
||||
if (options.description) {
|
||||
console.log(`📝 Description: ${options.description}`);
|
||||
}
|
||||
console.log(`🔒 Visibility: ${options.public ? 'Public' : 'Private'}`);
|
||||
console.log('');
|
||||
|
||||
// Create repository on Gitea
|
||||
const repoData = await this.createRepo({
|
||||
name: options.name,
|
||||
description: options.description || '',
|
||||
private: !options.public, // CLI uses --public flag, but API expects private boolean
|
||||
});
|
||||
|
||||
console.log('✅ Repository created successfully!');
|
||||
console.log(`🌐 Remote URL: ${repoData.clone_url}`);
|
||||
console.log('');
|
||||
|
||||
// Handle git operations
|
||||
await this.handleGitOperations(repoData.clone_url);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle git-related operations (initialization, remote setup, pushing)
|
||||
* @param remoteUrl - The remote repository URL
|
||||
*/
|
||||
private async handleGitOperations(remoteUrl: string): Promise<void> {
|
||||
const isGitRepo = await this.gitService.isGitInitialized();
|
||||
|
||||
// If no git repo, ask if user wants to initialize one
|
||||
if (!isGitRepo) {
|
||||
const shouldInit = await this.promptService.askInitializeGit();
|
||||
if (!shouldInit) {
|
||||
console.log('ℹ️ Skipping git setup');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.gitService.initializeGit();
|
||||
await this.gitService.createGitignore();
|
||||
}
|
||||
|
||||
// Ask if user wants to set remote
|
||||
const shouldSetRemote = await this.promptService.askSetRemote(remoteUrl);
|
||||
if (shouldSetRemote) {
|
||||
await this.gitService.addRemote(remoteUrl);
|
||||
}
|
||||
|
||||
// Ask if user wants to push
|
||||
const shouldPush = await this.promptService.askPushRepo();
|
||||
if (!shouldPush) {
|
||||
console.log('ℹ️ Skipping push to remote');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have commits
|
||||
const hasCommits = await this.gitService.hasCommits();
|
||||
if (!hasCommits) {
|
||||
// Check if we have changes to commit
|
||||
const hasChanges = await this.gitService.hasChanges();
|
||||
if (hasChanges) {
|
||||
const shouldCommit = await this.promptService.askCommitChanges();
|
||||
if (shouldCommit) {
|
||||
const message = await this.promptService.askCommitMessage();
|
||||
await this.gitService.addAndCommit(message);
|
||||
} else {
|
||||
console.log('ℹ️ Skipping commit and push (no commits to push)');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No changes to commit and push');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Push to remote
|
||||
await this.gitService.push();
|
||||
console.log('🎉 All done!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for the CLI application
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
// Set up prompt cancellation handler
|
||||
const promptService = new PromptService();
|
||||
process.on('SIGINT', () => promptService.onCancel());
|
||||
|
||||
// Check if user just wants help or config
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
||||
const cliService = new CliService();
|
||||
cliService.help();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle config option early (before requiring repository name)
|
||||
if (args.includes('--config') || args.includes('-c')) {
|
||||
const { GlobalConfig } = await import('./global-config.js');
|
||||
console.log(GlobalConfig.getSetupInstructions());
|
||||
return;
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
await app.run();
|
||||
}
|
||||
|
||||
// Run the application - handles both direct execution and npm global installation
|
||||
// Check if this file is being executed as the main module
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
|
||||
process.argv[1]?.includes('gitea-creator');
|
||||
|
||||
if (isMainModule) {
|
||||
main().catch((error) => {
|
||||
console.error('❌ Unexpected error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
90
src/prompts.ts
Normal file
90
src/prompts.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import prompts from 'prompts';
|
||||
|
||||
/**
|
||||
* Interactive prompt service for user input
|
||||
*/
|
||||
export class PromptService {
|
||||
/**
|
||||
* Ask user if they want to initialize git
|
||||
* @returns true if user wants to initialize git, false otherwise
|
||||
*/
|
||||
async askInitializeGit(): Promise<boolean> {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'initGit',
|
||||
message: 'No git repository found. Would you like to initialize one?',
|
||||
initial: true,
|
||||
});
|
||||
|
||||
return response.initGit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if they want to set the repository as the remote origin
|
||||
* @param remoteUrl - The remote URL that will be set
|
||||
* @returns true if user wants to set remote, false otherwise
|
||||
*/
|
||||
async askSetRemote(remoteUrl: string): Promise<boolean> {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'setRemote',
|
||||
message: `Set ${remoteUrl} as the remote origin?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
return response.setRemote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if they want to push the current repository
|
||||
* @returns true if user wants to push, false otherwise
|
||||
*/
|
||||
async askPushRepo(): Promise<boolean> {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'pushRepo',
|
||||
message: 'Push the current repository to the remote?',
|
||||
initial: true,
|
||||
});
|
||||
|
||||
return response.pushRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if they want to add and commit current changes before pushing
|
||||
* @returns true if user wants to commit changes, false otherwise
|
||||
*/
|
||||
async askCommitChanges(): Promise<boolean> {
|
||||
const response = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'commitChanges',
|
||||
message: 'You have uncommitted changes. Would you like to commit them first?',
|
||||
initial: true,
|
||||
});
|
||||
|
||||
return response.commitChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for a commit message
|
||||
* @returns the commit message
|
||||
*/
|
||||
async askCommitMessage(): Promise<string> {
|
||||
const response = await prompts({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Enter commit message:',
|
||||
initial: 'Initial commit',
|
||||
});
|
||||
|
||||
return response.message || 'Initial commit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case when prompts are cancelled (Ctrl+C)
|
||||
*/
|
||||
onCancel() {
|
||||
console.log('\n❌ Operation cancelled by user');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
60
src/repo.ts
Normal file
60
src/repo.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Config } from "./config.js";
|
||||
|
||||
export type InitializationOptions = {
|
||||
name: string;
|
||||
description: string;
|
||||
private: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gitea repository response type
|
||||
*/
|
||||
export type GiteaRepository = {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
private: boolean;
|
||||
clone_url: string;
|
||||
ssh_url: string;
|
||||
html_url: string;
|
||||
owner: {
|
||||
login: string;
|
||||
full_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new repository on Gitea
|
||||
*/
|
||||
export class Repo {
|
||||
static async create(config: Config, options: Partial<InitializationOptions> = {}): Promise<GiteaRepository> {
|
||||
const { name, description, private: isPrivate } = options;
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/user/repos`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `token ${config.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
private: isPrivate,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}`;
|
||||
try {
|
||||
const error = await response.json() as { message?: string };
|
||||
errorMessage = error.message || errorMessage;
|
||||
} catch {
|
||||
// Ignore JSON parsing errors, use status code
|
||||
}
|
||||
throw new Error(`Failed to create repository (${response.status}): ${errorMessage}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<GiteaRepository>;
|
||||
}
|
||||
}
|
||||
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"lib": ["esnext"],
|
||||
"types": ["node"],
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user