Add interactive project scaffolding wizard

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>
This commit is contained in:
Hutson Cappelmann
2026-01-26 10:30:50 -05:00
parent 8e55393191
commit 98235b12b8

572
scripts/newproject Executable file
View File

@@ -0,0 +1,572 @@
#!/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 ""