Initial commit

This commit is contained in:
2025-08-05 22:41:36 +10:00
commit 7a6b6b8ae7
14 changed files with 1243 additions and 0 deletions

80
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}