New `newproject` command with gum-powered terminal UI: - Project name and description input - Project type selection (Python, TypeScript, generic) - MCP selection with multi-select - Git init and Gitea repo creation options - Spec-kit and Docker support options - Generates customized CLAUDE.md, README, .gitignore - Creates .claude/settings.json with MCP config Run `newproject` from any terminal to launch wizard. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
573 lines
14 KiB
Bash
Executable File
573 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# newproject - Interactive project scaffolding tool
|
|
# Uses gum for beautiful terminal UI
|
|
#
|
|
|
|
set -e
|
|
|
|
# Colors
|
|
CYAN='\033[0;36m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Config
|
|
PROJECTS_DIR="$HOME/Projects"
|
|
TEMPLATE_REPO="git@git.htsn.io:hutson/template-project.git"
|
|
|
|
# Source central config if available
|
|
[ -f ~/.secrets ] && source ~/.secrets
|
|
[ -f ~/.hosts ] && source ~/.hosts
|
|
|
|
#######################################
|
|
# Header
|
|
#######################################
|
|
clear
|
|
gum style \
|
|
--foreground 212 --border-foreground 212 --border double \
|
|
--align center --width 60 --margin "1 2" --padding "1 2" \
|
|
'🚀 New Project Wizard' '' 'Create a new project with Claude Code'
|
|
|
|
#######################################
|
|
# Project Name
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Enter your project name (lowercase, hyphens ok):"
|
|
PROJECT_NAME=$(gum input --placeholder "my-awesome-project" --width 40)
|
|
|
|
if [ -z "$PROJECT_NAME" ]; then
|
|
gum style --foreground 196 "❌ Project name is required"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate name
|
|
if [[ ! "$PROJECT_NAME" =~ ^[a-z0-9-]+$ ]]; then
|
|
gum style --foreground 196 "❌ Project name must be lowercase alphanumeric with hyphens only"
|
|
exit 1
|
|
fi
|
|
|
|
PROJECT_PATH="$PROJECTS_DIR/$PROJECT_NAME"
|
|
|
|
if [ -d "$PROJECT_PATH" ]; then
|
|
gum style --foreground 196 "❌ Project already exists at $PROJECT_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
#######################################
|
|
# Project Type
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Select project type:"
|
|
PROJECT_TYPE=$(gum choose \
|
|
"python-fastapi" \
|
|
"python-generic" \
|
|
"typescript-react" \
|
|
"typescript-node" \
|
|
"generic" \
|
|
--header "Project Type")
|
|
|
|
#######################################
|
|
# Project Description
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Brief project description:"
|
|
PROJECT_DESC=$(gum input --placeholder "A brief description of what this project does" --width 60)
|
|
|
|
#######################################
|
|
# MCP Selection
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Select MCPs to enable (space to select, enter to confirm):"
|
|
|
|
# Define available MCPs with descriptions
|
|
MCPS=$(gum choose --no-limit \
|
|
"exa (web search, research)" \
|
|
"Ref (documentation lookup)" \
|
|
"ticktick (task management)" \
|
|
"beeper (messaging)" \
|
|
"airtable (database)" \
|
|
"claude-in-chrome (browser automation)" \
|
|
"shopping (Amazon)" \
|
|
"proton-mail (email)" \
|
|
"weather (forecasts)" \
|
|
--header "MCPs (space=select, enter=confirm)" \
|
|
--selected "exa (web search, research),Ref (documentation lookup)")
|
|
|
|
#######################################
|
|
# Git Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Git repository options:"
|
|
|
|
GIT_INIT=$(gum choose "Yes" "No" --header "Initialize git repository?")
|
|
|
|
GITEA_CREATE="No"
|
|
if [ "$GIT_INIT" = "Yes" ]; then
|
|
GITEA_CREATE=$(gum choose "Yes" "No" --header "Create repo on Gitea (git.htsn.io)?")
|
|
fi
|
|
|
|
#######################################
|
|
# Additional Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Additional options:"
|
|
|
|
ADDITIONAL=$(gum choose --no-limit \
|
|
"spec-kit (spec-driven development)" \
|
|
"pre-commit hooks" \
|
|
"GitHub Actions CI" \
|
|
"Docker support" \
|
|
--header "Additional features (space=select)")
|
|
|
|
#######################################
|
|
# Confirm
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 212 --border-foreground 212 --border rounded \
|
|
--align left --width 60 --margin "1 0" --padding "1 2" \
|
|
"📋 Project Summary" \
|
|
"" \
|
|
"Name: $PROJECT_NAME" \
|
|
"Type: $PROJECT_TYPE" \
|
|
"Path: $PROJECT_PATH" \
|
|
"Description: $PROJECT_DESC" \
|
|
"" \
|
|
"MCPs: $(echo "$MCPS" | tr '\n' ', ' | sed 's/, $//')" \
|
|
"" \
|
|
"Git: $GIT_INIT | Gitea: $GITEA_CREATE" \
|
|
"Extras: $(echo "$ADDITIONAL" | tr '\n' ', ' | sed 's/, $//')"
|
|
|
|
echo ""
|
|
CONFIRM=$(gum choose "Create Project" "Cancel" --header "Proceed?")
|
|
|
|
if [ "$CONFIRM" = "Cancel" ]; then
|
|
gum style --foreground 196 "❌ Cancelled"
|
|
exit 0
|
|
fi
|
|
|
|
#######################################
|
|
# Create Project
|
|
#######################################
|
|
echo ""
|
|
gum spin --spinner dot --title "Creating project directory..." -- sleep 0.5
|
|
mkdir -p "$PROJECT_PATH"
|
|
|
|
#######################################
|
|
# Generate .claude/settings.json (MCP config)
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating MCP configuration..." -- sleep 0.3
|
|
|
|
mkdir -p "$PROJECT_PATH/.claude"
|
|
|
|
# Build disabled MCPs list
|
|
ALL_MCPS=("exa" "Ref" "ticktick" "beeper" "airtable" "claude-in-chrome" "shopping" "proton-mail" "weather")
|
|
ENABLED_MCPS=""
|
|
|
|
# Parse selected MCPs
|
|
for mcp in "${ALL_MCPS[@]}"; do
|
|
if echo "$MCPS" | grep -qi "^$mcp "; then
|
|
ENABLED_MCPS="$ENABLED_MCPS $mcp"
|
|
fi
|
|
done
|
|
|
|
# Generate settings.json with disabled MCPs
|
|
cat > "$PROJECT_PATH/.claude/settings.json" << 'SETTINGS_EOF'
|
|
{
|
|
"mcpServers": {
|
|
SETTINGS_EOF
|
|
|
|
first=true
|
|
for mcp in "${ALL_MCPS[@]}"; do
|
|
if ! echo "$MCPS" | grep -qi "^$mcp "; then
|
|
if [ "$first" = true ]; then
|
|
first=false
|
|
else
|
|
echo "," >> "$PROJECT_PATH/.claude/settings.json"
|
|
fi
|
|
printf ' "%s": { "disabled": true }' "$mcp" >> "$PROJECT_PATH/.claude/settings.json"
|
|
fi
|
|
done
|
|
|
|
cat >> "$PROJECT_PATH/.claude/settings.json" << 'SETTINGS_EOF'
|
|
|
|
}
|
|
}
|
|
SETTINGS_EOF
|
|
|
|
#######################################
|
|
# Generate CLAUDE.md
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating CLAUDE.md..." -- sleep 0.3
|
|
|
|
# Determine type-specific content
|
|
case "$PROJECT_TYPE" in
|
|
python-fastapi)
|
|
TECH_STACK="Python 3.12+, FastAPI, SQLModel, Pydantic, Alembic"
|
|
TYPE_CHECK_CMD="mypy --strict src/"
|
|
LINT_CMD="ruff check src/ && black src/"
|
|
TEST_CMD="pytest"
|
|
;;
|
|
python-generic)
|
|
TECH_STACK="Python 3.12+"
|
|
TYPE_CHECK_CMD="mypy --strict src/"
|
|
LINT_CMD="ruff check src/ && black src/"
|
|
TEST_CMD="pytest"
|
|
;;
|
|
typescript-react)
|
|
TECH_STACK="TypeScript, React 18, Bun, TailwindCSS"
|
|
TYPE_CHECK_CMD="bun run tsc --noEmit"
|
|
LINT_CMD="bun run lint"
|
|
TEST_CMD="bun test"
|
|
;;
|
|
typescript-node)
|
|
TECH_STACK="TypeScript, Node.js/Bun"
|
|
TYPE_CHECK_CMD="bun run tsc --noEmit"
|
|
LINT_CMD="bun run lint"
|
|
TEST_CMD="bun test"
|
|
;;
|
|
generic)
|
|
TECH_STACK="[Customize]"
|
|
TYPE_CHECK_CMD="# Add type check command"
|
|
LINT_CMD="# Add lint command"
|
|
TEST_CMD="# Add test command"
|
|
;;
|
|
esac
|
|
|
|
# Check if spec-kit selected
|
|
USE_SPECKIT="No"
|
|
if echo "$ADDITIONAL" | grep -q "spec-kit"; then
|
|
USE_SPECKIT="Yes"
|
|
fi
|
|
|
|
# Generate CLAUDE.md
|
|
cat > "$PROJECT_PATH/CLAUDE.md" << CLAUDE_EOF
|
|
# $PROJECT_NAME - Claude Code Guidelines
|
|
|
|
## Project Overview
|
|
|
|
$PROJECT_DESC
|
|
|
|
**Tech Stack**: $TECH_STACK
|
|
|
|
**Key Directories**:
|
|
- \`src/\` - Main source code
|
|
- \`tests/\` - Test files
|
|
- \`docs/\` - Documentation
|
|
|
|
---
|
|
|
|
## Type Safety (Strict Enforcement)
|
|
|
|
**Type safety is non-negotiable. All code must pass strict type checking.**
|
|
|
|
\`\`\`bash
|
|
# Required before every commit
|
|
$TYPE_CHECK_CMD
|
|
\`\`\`
|
|
|
|
- Complete type hints on ALL functions (params + return types)
|
|
- No \`any\` types (TypeScript) or \`# type: ignore\` without justification
|
|
- Money/prices: Always \`Decimal\`, never \`float\`
|
|
|
|
---
|
|
|
|
## Required Tool Usage
|
|
|
|
### 1. Documentation Lookup (Priority Order)
|
|
|
|
\`\`\`
|
|
1. Ref Tools (private docs) → mcp__Ref__ref_search_documentation(query="topic ref_src=private")
|
|
2. Ref Tools (public docs) → mcp__Ref__ref_search_documentation(query="topic")
|
|
3. Exa Tools → mcp__exa__get_code_context_exa(query="...") (last resort)
|
|
\`\`\`
|
|
|
|
### 2. Code Review
|
|
|
|
Run after completing significant features:
|
|
\`\`\`
|
|
/code-review:code-review
|
|
\`\`\`
|
|
|
|
### 3. Git Commits
|
|
|
|
\`\`\`
|
|
/commit-commands:commit
|
|
/commit-commands:commit-push-pr # For complete workflow
|
|
\`\`\`
|
|
|
|
### 4. Frontend Design (if applicable)
|
|
|
|
\`\`\`
|
|
/frontend-design:frontend-design
|
|
\`\`\`
|
|
|
|
---
|
|
|
|
## Pre-Commit Checklist
|
|
|
|
\`\`\`bash
|
|
# Type safety
|
|
$TYPE_CHECK_CMD
|
|
|
|
# Linting
|
|
$LINT_CMD
|
|
|
|
# Tests
|
|
$TEST_CMD
|
|
|
|
# Code review (for significant changes)
|
|
/code-review:code-review
|
|
\`\`\`
|
|
|
|
---
|
|
|
|
CLAUDE_EOF
|
|
|
|
# Add spec-kit section if selected
|
|
if [ "$USE_SPECKIT" = "Yes" ]; then
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << 'SPECKIT_EOF'
|
|
## Spec-Driven Development
|
|
|
|
**All features and significant changes should follow spec-driven development.**
|
|
|
|
```
|
|
1. /speckit.specify → Define requirements (what & why)
|
|
2. /speckit.plan → Specify technology and architecture
|
|
3. /speckit.tasks → Break plan into actionable tasks
|
|
4. /speckit.implement → Build features according to plan
|
|
5. /speckit.clarify → Resolve ambiguities as needed
|
|
```
|
|
|
|
---
|
|
|
|
SPECKIT_EOF
|
|
fi
|
|
|
|
# Add MCP configuration section
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << MCP_EOF
|
|
## MCP Configuration
|
|
|
|
**Enabled MCPs for this project:**
|
|
$(echo "$MCPS" | sed 's/^/- /')
|
|
|
|
**Disabled MCPs** are configured in \`.claude/settings.json\`. To re-enable, remove from the disabled list or use \`/config\` → MCP Servers.
|
|
|
|
---
|
|
|
|
## Central Configuration Reference
|
|
|
|
This project can access Hutson's central homelab configuration files. These are synced across all machines via Syncthing.
|
|
|
|
| File | Purpose | Usage |
|
|
|------|---------|-------|
|
|
| \`~/.secrets\` | API keys, tokens, credentials | \`source ~/.secrets\` then use \`\$VAR_NAME\` |
|
|
| \`~/.hosts\` | IPs, hostnames, service URLs | \`source ~/.hosts\` then use \`\$IP_*\` or \`\$HOST_*\` |
|
|
| \`~/.ssh/config\` | SSH aliases for all homelab hosts | \`ssh pve\`, \`ssh truenas\`, \`ssh docker-host\`, etc. |
|
|
|
|
**When adding new credentials or hosts:**
|
|
1. Add to the central files (\`~/.secrets\` or \`~/.hosts\`)
|
|
2. Files sync via Syncthing to all machines
|
|
3. Never commit secrets to git - use environment variables
|
|
|
|
---
|
|
|
|
## Project-Specific Notes
|
|
|
|
[Add project-specific patterns, gotchas, and conventions here]
|
|
MCP_EOF
|
|
|
|
#######################################
|
|
# Generate README.md
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating README.md..." -- sleep 0.2
|
|
|
|
cat > "$PROJECT_PATH/README.md" << README_EOF
|
|
# $PROJECT_NAME
|
|
|
|
$PROJECT_DESC
|
|
|
|
## Quick Start
|
|
|
|
\`\`\`bash
|
|
# Install dependencies
|
|
# [Add install command]
|
|
|
|
# Run development server
|
|
# [Add dev command]
|
|
|
|
# Run tests
|
|
$TEST_CMD
|
|
\`\`\`
|
|
|
|
## Development
|
|
|
|
See [CLAUDE.md](CLAUDE.md) for development guidelines and conventions.
|
|
README_EOF
|
|
|
|
#######################################
|
|
# Generate .gitignore
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating .gitignore..." -- sleep 0.2
|
|
|
|
cat > "$PROJECT_PATH/.gitignore" << 'GITIGNORE_EOF'
|
|
# Dependencies
|
|
node_modules/
|
|
.venv/
|
|
venv/
|
|
__pycache__/
|
|
*.pyc
|
|
|
|
# Build outputs
|
|
dist/
|
|
build/
|
|
.next/
|
|
*.egg-info/
|
|
|
|
# Environment
|
|
.env
|
|
.env.local
|
|
.env.*.local
|
|
|
|
# IDE
|
|
.idea/
|
|
.vscode/
|
|
*.swp
|
|
*.swo
|
|
|
|
# OS
|
|
.DS_Store
|
|
Thumbs.db
|
|
|
|
# Testing
|
|
coverage/
|
|
.coverage
|
|
htmlcov/
|
|
.pytest_cache/
|
|
|
|
# Logs
|
|
*.log
|
|
logs/
|
|
GITIGNORE_EOF
|
|
|
|
#######################################
|
|
# Git Init
|
|
#######################################
|
|
if [ "$GIT_INIT" = "Yes" ]; then
|
|
gum spin --spinner dot --title "Initializing git repository..." -- \
|
|
git -C "$PROJECT_PATH" init -q
|
|
|
|
gum spin --spinner dot --title "Creating initial commit..." -- \
|
|
bash -c "cd '$PROJECT_PATH' && git add -A && git commit -q -m 'Initial project setup
|
|
|
|
Generated with newproject wizard.
|
|
|
|
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>'"
|
|
fi
|
|
|
|
#######################################
|
|
# Gitea Repo
|
|
#######################################
|
|
if [ "$GITEA_CREATE" = "Yes" ] && [ -n "$GITEA_TOKEN" ]; then
|
|
gum spin --spinner dot --title "Creating Gitea repository..." -- sleep 0.5
|
|
|
|
# Create repo via API
|
|
RESPONSE=$(curl -s -X POST "https://git.htsn.io/api/v1/user/repos" \
|
|
-H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"name\": \"$PROJECT_NAME\",
|
|
\"description\": \"$PROJECT_DESC\",
|
|
\"private\": false,
|
|
\"auto_init\": false
|
|
}" 2>/dev/null)
|
|
|
|
if echo "$RESPONSE" | grep -q "\"name\":\"$PROJECT_NAME\""; then
|
|
# Add remote and push
|
|
git -C "$PROJECT_PATH" remote add origin "git@git.htsn.io:hutson/$PROJECT_NAME.git" 2>/dev/null || true
|
|
gum spin --spinner dot --title "Pushing to Gitea..." -- \
|
|
git -C "$PROJECT_PATH" push -u origin main -q 2>/dev/null || true
|
|
GITEA_URL="https://git.htsn.io/hutson/$PROJECT_NAME"
|
|
else
|
|
GITEA_URL="(failed to create)"
|
|
fi
|
|
elif [ "$GITEA_CREATE" = "Yes" ]; then
|
|
GITEA_URL="(GITEA_TOKEN not set in ~/.secrets)"
|
|
fi
|
|
|
|
#######################################
|
|
# Spec-kit init
|
|
#######################################
|
|
if [ "$USE_SPECKIT" = "Yes" ]; then
|
|
gum spin --spinner dot --title "Initializing spec-kit structure..." -- sleep 0.3
|
|
mkdir -p "$PROJECT_PATH/specs"
|
|
mkdir -p "$PROJECT_PATH/plans"
|
|
mkdir -p "$PROJECT_PATH/tasks"
|
|
touch "$PROJECT_PATH/specs/.gitkeep"
|
|
touch "$PROJECT_PATH/plans/.gitkeep"
|
|
touch "$PROJECT_PATH/tasks/.gitkeep"
|
|
fi
|
|
|
|
#######################################
|
|
# Docker support
|
|
#######################################
|
|
if echo "$ADDITIONAL" | grep -q "Docker support"; then
|
|
gum spin --spinner dot --title "Adding Docker support..." -- sleep 0.3
|
|
|
|
cat > "$PROJECT_PATH/Dockerfile" << 'DOCKER_EOF'
|
|
# Customize this Dockerfile for your project
|
|
FROM python:3.12-slim
|
|
|
|
WORKDIR /app
|
|
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
COPY . .
|
|
|
|
CMD ["python", "-m", "src.main"]
|
|
DOCKER_EOF
|
|
|
|
cat > "$PROJECT_PATH/docker-compose.yml" << 'COMPOSE_EOF'
|
|
version: '3.8'
|
|
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "8000:8000"
|
|
environment:
|
|
- ENV=development
|
|
volumes:
|
|
- .:/app
|
|
COMPOSE_EOF
|
|
fi
|
|
|
|
#######################################
|
|
# Success!
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 82 --border-foreground 82 --border double \
|
|
--align center --width 60 --margin "1 2" --padding "1 2" \
|
|
'✅ Project Created Successfully!' \
|
|
'' \
|
|
"Path: $PROJECT_PATH"
|
|
|
|
echo ""
|
|
gum style --foreground 245 "Next steps:"
|
|
echo ""
|
|
echo " cd $PROJECT_PATH"
|
|
if [ "$USE_SPECKIT" = "Yes" ]; then
|
|
echo " /speckit.specify # Define requirements"
|
|
else
|
|
echo " # Start coding!"
|
|
fi
|
|
|
|
if [ -n "$GITEA_URL" ] && [ "$GITEA_URL" != "(failed to create)" ] && [ "$GITEA_URL" != "(GITEA_TOKEN not set in ~/.secrets)" ]; then
|
|
echo ""
|
|
echo " Gitea: $GITEA_URL"
|
|
fi
|
|
|
|
echo ""
|