development-hub/bin/new-project

596 lines
18 KiB
Bash
Executable File

#!/bin/bash
#
# new-project - Create a new project in Rob's development ecosystem
#
# This script automates:
# - Creating local project directory
# - Creating Gitea repository via API
# - Setting up documentation symlinks
# - Generating project files from templates
# - Updating build-public-docs.sh
#
# Usage:
# new-project myproject --title "My Project" --tagline "Short description"
# new-project myproject # Interactive mode
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HUB_ROOT="$(dirname "$SCRIPT_DIR")"
CONFIG_DIR="$HOME/.config/development-hub"
SETTINGS_FILE="$CONFIG_DIR/settings.json"
TOKEN_FILE="$CONFIG_DIR/gitea-token"
TEMPLATES_DIR="$HUB_ROOT/templates"
# Load settings from JSON file if it exists
load_settings() {
if [[ -f "$SETTINGS_FILE" ]]; then
# Use python to parse JSON (more portable than jq)
PROJECTS_ROOT=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('default_project_path', '$HOME/PycharmProjects'))" 2>/dev/null || echo "$HOME/PycharmProjects")
GIT_HOST_TYPE=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_type', ''))" 2>/dev/null || echo "")
GIT_HOST_URL=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_url', ''))" 2>/dev/null || echo "")
GIT_HOST_OWNER=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_owner', ''))" 2>/dev/null || echo "")
GIT_HOST_TOKEN=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_token', ''))" 2>/dev/null || echo "")
else
PROJECTS_ROOT="${PROJECTS_ROOT:-$HOME/PycharmProjects}"
GIT_HOST_TYPE=""
GIT_HOST_URL=""
GIT_HOST_OWNER=""
GIT_HOST_TOKEN=""
fi
# Allow environment variables to override
PROJECTS_ROOT="${PROJECTS_ROOT_OVERRIDE:-$PROJECTS_ROOT}"
GIT_HOST_URL="${GITEA_URL:-$GIT_HOST_URL}"
GIT_HOST_OWNER="${GITEA_OWNER:-$GIT_HOST_OWNER}"
GIT_HOST_TOKEN="${GITEA_TOKEN:-$GIT_HOST_TOKEN}"
PROJECT_DOCS_ROOT="$PROJECTS_ROOT/project-docs"
}
load_settings
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
# Parse arguments
PROJECT_NAME=""
PROJECT_TITLE=""
PROJECT_TAGLINE=""
DRY_RUN=false
SKIP_GITEA=false
DEPLOY_DOCS=false
show_help() {
cat << EOF
Usage: $(basename "$0") <project-name> [options]
Create a new project in the development ecosystem.
Arguments:
project-name Name for the project (lowercase, alphanumeric, hyphens)
Options:
--title "Title" Display title (default: derived from name)
--tagline "..." Short description (prompted if not provided)
--deploy Build and deploy public docs to Gitea Pages
--dry-run Show what would happen without doing it
--skip-gitea Skip Gitea repo creation (for offline use)
--help, -h Show this help message
Examples:
$(basename "$0") my-tool --title "My Tool" --tagline "A useful tool"
$(basename "$0") my-tool # Interactive mode
After creation, your project will be at:
~/PycharmProjects/<project-name>/
With documentation at:
~/PycharmProjects/project-docs/docs/projects/<project-name>/
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--title)
PROJECT_TITLE="$2"
shift 2
;;
--tagline)
PROJECT_TAGLINE="$2"
shift 2
;;
--deploy)
DEPLOY_DOCS=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--skip-gitea)
SKIP_GITEA=true
shift
;;
--help|-h)
show_help
exit 0
;;
-*)
log_error "Unknown option: $1"
show_help
exit 1
;;
*)
if [[ -z "$PROJECT_NAME" ]]; then
PROJECT_NAME="$1"
else
log_error "Unexpected argument: $1"
exit 1
fi
shift
;;
esac
done
}
validate_name() {
local name="$1"
if [[ -z "$name" ]]; then
log_error "Project name is required"
show_help
exit 1
fi
# Check format: lowercase, alphanumeric, hyphens
if [[ ! "$name" =~ ^[a-z][a-z0-9-]*$ ]]; then
log_error "Project name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
exit 1
fi
# Check if project already exists
if [[ -d "$PROJECTS_ROOT/$name" ]]; then
log_error "Project directory already exists: $PROJECTS_ROOT/$name"
exit 1
fi
# Check if docs already exist
if [[ -d "$PROJECT_DOCS_ROOT/docs/projects/$name" ]]; then
log_error "Documentation directory already exists: $PROJECT_DOCS_ROOT/docs/projects/$name"
exit 1
fi
}
# Derive title from name if not provided
derive_title() {
local name="$1"
# Convert hyphens to spaces and capitalize each word
echo "$name" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1'
}
prompt_for_missing() {
if [[ -z "$PROJECT_TITLE" ]]; then
local default_title
default_title=$(derive_title "$PROJECT_NAME")
read -rp "Project title [$default_title]: " PROJECT_TITLE
PROJECT_TITLE="${PROJECT_TITLE:-$default_title}"
fi
if [[ -z "$PROJECT_TAGLINE" ]]; then
read -rp "Project tagline (short description): " PROJECT_TAGLINE
if [[ -z "$PROJECT_TAGLINE" ]]; then
log_error "Tagline is required"
exit 1
fi
fi
}
load_git_token() {
# Already loaded from settings?
if [[ -n "$GIT_HOST_TOKEN" ]]; then
log_info "Using token from settings"
return 0
fi
# Check legacy token file
if [[ -f "$TOKEN_FILE" ]]; then
GIT_HOST_TOKEN=$(cat "$TOKEN_FILE")
log_info "Loaded token from $TOKEN_FILE"
return 0
fi
# Check if git hosting is configured
if [[ -z "$GIT_HOST_URL" ]] || [[ -z "$GIT_HOST_OWNER" ]]; then
log_error "Git hosting not configured"
echo ""
echo "Please configure git hosting in Development Hub:"
echo " 1. Run 'development-hub'"
echo " 2. Go to Settings"
echo " 3. Configure Git Hosting section"
echo ""
log_info "Or use --skip-gitea to skip repository creation"
exit 1
fi
# Prompt user to create token
log_warn "No API token found"
echo ""
echo "To create a token:"
if [[ "$GIT_HOST_TYPE" == "github" ]]; then
echo " 1. Go to https://github.com/settings/tokens"
echo " 2. Generate a new token with 'repo' scope"
elif [[ "$GIT_HOST_TYPE" == "gitlab" ]]; then
echo " 1. Go to $GIT_HOST_URL/-/profile/personal_access_tokens"
echo " 2. Generate a new token with 'api' scope"
else
echo " 1. Go to $GIT_HOST_URL/user/settings/applications"
echo " 2. Generate a new token with 'repo' scope"
fi
echo " 3. Copy the token"
echo ""
read -rsp "Paste your API token: " GIT_HOST_TOKEN
echo ""
if [[ -z "$GIT_HOST_TOKEN" ]]; then
log_error "Token is required for API access"
log_info "Use --skip-gitea to skip repository creation"
exit 1
fi
# Save token for future use
mkdir -p "$CONFIG_DIR"
echo "$GIT_HOST_TOKEN" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
log_success "Token saved to $TOKEN_FILE"
}
create_git_repo() {
local name="$1"
local description="$2"
log_step "Creating repository: $name on $GIT_HOST_TYPE"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would create repo: $GIT_HOST_URL/$GIT_HOST_OWNER/$name"
return 0
fi
local response http_code body
if [[ "$GIT_HOST_TYPE" == "github" ]]; then
# GitHub API
response=$(curl -s -w "\n%{http_code}" -X POST "https://api.github.com/user/repos" \
-H "Authorization: token $GIT_HOST_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$name\",
\"description\": \"$description\",
\"private\": false,
\"auto_init\": false
}")
elif [[ "$GIT_HOST_TYPE" == "gitlab" ]]; then
# GitLab API
response=$(curl -s -w "\n%{http_code}" -X POST "$GIT_HOST_URL/api/v4/projects" \
-H "PRIVATE-TOKEN: $GIT_HOST_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$name\",
\"description\": \"$description\",
\"visibility\": \"public\",
\"initialize_with_readme\": false
}")
else
# Gitea API (default)
response=$(curl -s -w "\n%{http_code}" -X POST "$GIT_HOST_URL/api/v1/user/repos" \
-H "Authorization: token $GIT_HOST_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$name\",
\"description\": \"$description\",
\"private\": false,
\"auto_init\": false
}")
fi
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [[ "$http_code" == "201" ]]; then
log_success "Created repository: $GIT_HOST_URL/$GIT_HOST_OWNER/$name"
elif [[ "$http_code" == "409" ]] || [[ "$http_code" == "422" ]]; then
log_warn "Repository already exists"
else
log_error "Failed to create repository (HTTP $http_code)"
echo "$body" | head -5
exit 1
fi
}
create_local_project() {
local name="$1"
local project_dir="$PROJECTS_ROOT/$name"
log_step "Creating local project: $project_dir"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would create: $project_dir"
return 0
fi
mkdir -p "$project_dir"
cd "$project_dir"
git init --quiet
git remote add origin "$GIT_HOST_URL/$GIT_HOST_OWNER/$name.git"
log_success "Created local project with git"
}
apply_template() {
local template="$1"
local output="$2"
local name="$3"
local title="$4"
local tagline="$5"
local year
year=$(date +%Y)
local date
date=$(date +%Y-%m-%d)
sed -e "s|{{PROJECT_NAME}}|$name|g" \
-e "s|{{PROJECT_TITLE}}|$title|g" \
-e "s|{{PROJECT_TAGLINE}}|$tagline|g" \
-e "s|{{YEAR}}|$year|g" \
-e "s|{{DATE}}|$date|g" \
-e "s|{{GITEA_URL}}|$GIT_HOST_URL|g" \
-e "s|{{GITEA_OWNER}}|$GIT_HOST_OWNER|g" \
"$template" > "$output"
}
generate_project_files() {
local name="$1"
local title="$2"
local tagline="$3"
local project_dir="$PROJECTS_ROOT/$name"
log_step "Generating project files from templates"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would generate files in $project_dir"
return 0
fi
# Generate each file from template
apply_template "$TEMPLATES_DIR/gitignore.template" "$project_dir/.gitignore" "$name" "$title" "$tagline"
apply_template "$TEMPLATES_DIR/CLAUDE.md.template" "$project_dir/CLAUDE.md" "$name" "$title" "$tagline"
apply_template "$TEMPLATES_DIR/README.md.template" "$project_dir/README.md" "$name" "$title" "$tagline"
apply_template "$TEMPLATES_DIR/pyproject.toml.template" "$project_dir/pyproject.toml" "$name" "$title" "$tagline"
log_success "Generated project files"
}
setup_documentation() {
local name="$1"
local title="$2"
local tagline="$3"
local docs_dir="$PROJECT_DOCS_ROOT/docs/projects/$name"
local project_dir="$PROJECTS_ROOT/$name"
log_step "Setting up documentation"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would create: $docs_dir"
log_info "[DRY RUN] Would create symlink: $project_dir/docs"
return 0
fi
# Create docs directory
mkdir -p "$docs_dir"
# Generate documentation files
apply_template "$TEMPLATES_DIR/overview.md.template" "$docs_dir/overview.md" "$name" "$title" "$tagline"
apply_template "$TEMPLATES_DIR/updating-documentation.md.template" "$docs_dir/updating-documentation.md" "$name" "$title" "$tagline"
# Create symlink
ln -s "../project-docs/docs/projects/$name" "$project_dir/docs"
log_success "Created documentation with symlink"
}
update_sidebar() {
local name="$1"
local title="$2"
local sidebar_file="$PROJECT_DOCS_ROOT/sidebars.ts"
log_step "Updating sidebars.ts"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would add '$title' to sidebar"
return 0
fi
# Check if already exists
if grep -q "projects/$name/overview" "$sidebar_file"; then
log_warn "Project already in sidebar"
return 0
fi
# Create the new sidebar entry
local sidebar_entry=" {
type: 'category',
label: '$title',
collapsed: true,
items: [
'projects/$name/overview',
'projects/$name/updating-documentation',
],
},"
# Insert before "Goals & Roadmap" category
# Use perl for multi-line insertion (more reliable than sed)
perl -i -p0e "s|(\\s+\\{\\s+type: 'category',\\s+label: 'Goals & Roadmap',)|$sidebar_entry\n\$1|s" "$sidebar_file"
log_success "Added project to sidebar"
}
update_build_script() {
local name="$1"
local title="$2"
local tagline="$3"
local build_script="$PROJECT_DOCS_ROOT/scripts/build-public-docs.sh"
log_step "Updating build-public-docs.sh"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would add to PROJECT_CONFIG:"
log_info " PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GIT_HOST_OWNER|$name|$name\""
return 0
fi
# Find the last PROJECT_CONFIG line and add after it
local config_line="PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GIT_HOST_OWNER|$name|$name\""
# Check if already exists
if grep -q "PROJECT_CONFIG\[\"$name\"\]" "$build_script"; then
log_warn "Project already in build script"
return 0
fi
# Add after the last PROJECT_CONFIG line
sed -i "/^PROJECT_CONFIG\[\"ramble\"\]/a $config_line" "$build_script"
log_success "Added project to build script"
}
initial_commit() {
local name="$1"
local project_dir="$PROJECTS_ROOT/$name"
log_step "Creating initial commit"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would commit and push"
return 0
fi
cd "$project_dir"
git add .
git commit -m "Initial project setup
Created by development-hub/new-project script"
if [[ "$SKIP_GITEA" != true ]]; then
git push -u origin main 2>/dev/null || git push -u origin master 2>/dev/null || {
log_warn "Could not push to remote. You may need to push manually."
}
fi
log_success "Created initial commit"
}
deploy_public_docs() {
local name="$1"
local build_script="$PROJECT_DOCS_ROOT/scripts/build-public-docs.sh"
log_step "Building and deploying public documentation"
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would run: $build_script $name --deploy"
return 0
fi
if [[ ! -x "$build_script" ]]; then
log_error "Build script not found: $build_script"
return 1
fi
"$build_script" "$name" --deploy
log_success "Public docs deployed"
}
print_summary() {
local name="$1"
local title="$2"
local deployed="$3"
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Project '$title' created successfully!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Locations:"
echo " Local: $PROJECTS_ROOT/$name/"
echo " Remote: $GIT_HOST_URL/$GIT_HOST_OWNER/$name"
echo " Docs: $PROJECT_DOCS_ROOT/docs/projects/$name/"
echo ""
echo "Next steps:"
echo " 1. cd $PROJECTS_ROOT/$name"
echo " 2. Start developing!"
echo ""
if [[ "$deployed" == true ]]; then
echo "Public docs deployed."
else
echo "To publish documentation:"
echo " $PROJECT_DOCS_ROOT/scripts/build-public-docs.sh $name --deploy"
fi
echo ""
}
main() {
parse_args "$@"
validate_name "$PROJECT_NAME"
prompt_for_missing
echo ""
log_info "Creating project: $PROJECT_NAME"
log_info "Title: $PROJECT_TITLE"
log_info "Tagline: $PROJECT_TAGLINE"
echo ""
if [[ "$DRY_RUN" == true ]]; then
log_warn "DRY RUN - No changes will be made"
echo ""
fi
# Load token and create repo (unless skipping)
if [[ "$SKIP_GITEA" != true ]]; then
load_git_token
create_git_repo "$PROJECT_NAME" "$PROJECT_TAGLINE"
fi
create_local_project "$PROJECT_NAME"
generate_project_files "$PROJECT_NAME" "$PROJECT_TITLE" "$PROJECT_TAGLINE"
setup_documentation "$PROJECT_NAME" "$PROJECT_TITLE" "$PROJECT_TAGLINE"
update_sidebar "$PROJECT_NAME" "$PROJECT_TITLE"
update_build_script "$PROJECT_NAME" "$PROJECT_TITLE" "$PROJECT_TAGLINE"
initial_commit "$PROJECT_NAME"
# Deploy public docs if requested
if [[ "$DEPLOY_DOCS" == true ]]; then
deploy_public_docs "$PROJECT_NAME"
fi
if [[ "$DRY_RUN" != true ]]; then
print_summary "$PROJECT_NAME" "$PROJECT_TITLE" "$DEPLOY_DOCS"
fi
}
main "$@"