- New prompt: "Open project in VS Code after creation?" - Opens project folder in VS Code using `code` command - Shown in summary alongside other integrations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1187 lines
32 KiB
Bash
Executable File
1187 lines
32 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# newproject - Interactive project scaffolding wizard
|
|
# Uses gum for beautiful terminal UI
|
|
#
|
|
|
|
set -e
|
|
|
|
# Config
|
|
PROJECTS_DIR="$HOME/Projects"
|
|
STIGNORE_FILE="$PROJECTS_DIR/.stignore"
|
|
|
|
# Source central config if available
|
|
[ -f ~/.secrets ] && source ~/.secrets
|
|
[ -f ~/.hosts ] && source ~/.hosts
|
|
|
|
#######################################
|
|
# Check/Install gum
|
|
#######################################
|
|
if ! command -v gum &> /dev/null; then
|
|
echo "Installing gum (terminal UI toolkit)..."
|
|
if command -v brew &> /dev/null; then
|
|
brew install gum
|
|
else
|
|
echo "❌ Please install gum: https://github.com/charmbracelet/gum"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# 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 Brief (Multi-line)
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Describe your project (Ctrl+D or Esc when done):"
|
|
gum style --foreground 240 --italic "Include goals, features, tech preferences, constraints..."
|
|
echo ""
|
|
PROJECT_BRIEF=$(gum write --placeholder "Describe your project in detail..." --width 70 --height 8)
|
|
|
|
# Extract first line as short description
|
|
PROJECT_DESC=$(echo "$PROJECT_BRIEF" | head -1)
|
|
|
|
#######################################
|
|
# MCP Selection
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Select MCPs to enable (space to select, enter to confirm):"
|
|
|
|
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 Remote Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Git repository options:"
|
|
|
|
GIT_INIT=$(gum choose "Yes" "No" --header "Initialize git repository?")
|
|
|
|
GIT_REMOTE="None"
|
|
if [ "$GIT_INIT" = "Yes" ]; then
|
|
GIT_REMOTE=$(gum choose \
|
|
"Gitea (git.htsn.io) - Private" \
|
|
"GitHub (github.com) - Public" \
|
|
"Both (Gitea + GitHub)" \
|
|
"None (local only)" \
|
|
--header "Where to push?")
|
|
fi
|
|
|
|
#######################################
|
|
# Syncthing Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Syncthing sync options:"
|
|
|
|
SYNCTHING_OPT=$(gum choose \
|
|
"Exclude from sync (recommended for git repos)" \
|
|
"Include in sync" \
|
|
--header "Sync this project via Syncthing?")
|
|
|
|
#######################################
|
|
# Network/Traefik Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Does this project need web access (subdomain routing)?"
|
|
|
|
NETWORK_ACCESS=$(gum choose \
|
|
"No web access needed" \
|
|
"Local network only (10.10.10.x)" \
|
|
"Local + Tailscale (remote access)" \
|
|
"Public Internet (via Cloudflare)" \
|
|
--header "Network access level")
|
|
|
|
SUBDOMAIN=""
|
|
DEPLOY_TARGET=""
|
|
if [ "$NETWORK_ACCESS" != "No web access needed" ]; then
|
|
echo ""
|
|
gum style --foreground 245 "Subdomain for this project:"
|
|
SUBDOMAIN=$(gum input --placeholder "$PROJECT_NAME" --value "$PROJECT_NAME" --width 30)
|
|
SUBDOMAIN="${SUBDOMAIN}.htsn.io"
|
|
|
|
echo ""
|
|
gum style --foreground 245 "Deployment target:"
|
|
DEPLOY_TARGET=$(gum choose \
|
|
"docker-host (10.10.10.206)" \
|
|
"trading-vm (10.10.10.221)" \
|
|
"saltbox (10.10.10.100)" \
|
|
"Other/Manual" \
|
|
--header "Where will this deploy?")
|
|
fi
|
|
|
|
#######################################
|
|
# Database Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Database requirements:"
|
|
|
|
DATABASE=$(gum choose \
|
|
"None" \
|
|
"SQLite (local file)" \
|
|
"PostgreSQL" \
|
|
"TimescaleDB (time-series)" \
|
|
"Redis (cache/queue)" \
|
|
"PostgreSQL + Redis" \
|
|
--header "Database")
|
|
|
|
#######################################
|
|
# Claude-mem Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Enable claude-mem (persistent memory across sessions)?"
|
|
|
|
CLAUDE_MEM=$(gum choose \
|
|
"Yes - Enable claude-mem" \
|
|
"No - Disable for this project" \
|
|
--header "Claude-mem")
|
|
|
|
#######################################
|
|
# Additional Options
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Additional options:"
|
|
|
|
ADDITIONAL=$(gum choose --no-limit \
|
|
"spec-kit (spec-driven development)" \
|
|
"Docker support" \
|
|
"pre-commit hooks" \
|
|
"GitHub Actions CI" \
|
|
".env.example (from ~/.secrets)" \
|
|
--header "Additional features (space=select)")
|
|
|
|
#######################################
|
|
# License
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "License:"
|
|
|
|
LICENSE=$(gum choose \
|
|
"MIT" \
|
|
"Apache 2.0" \
|
|
"Proprietary (no license file)" \
|
|
"None" \
|
|
--header "License type")
|
|
|
|
#######################################
|
|
# TickTick Integration
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Create setup tasks in TickTick?"
|
|
|
|
TICKTICK_TASKS=$(gum choose \
|
|
"Yes - Create project setup tasks" \
|
|
"No" \
|
|
--header "TickTick integration")
|
|
|
|
#######################################
|
|
# Open in Obsidian
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Create project note in Obsidian?"
|
|
|
|
OBSIDIAN_NOTE=$(gum choose \
|
|
"Yes - Create note in ~/Notes/Projects/" \
|
|
"No" \
|
|
--header "Obsidian integration")
|
|
|
|
#######################################
|
|
# Open in VS Code
|
|
#######################################
|
|
echo ""
|
|
gum style --foreground 245 "Open project in VS Code after creation?"
|
|
|
|
OPEN_VSCODE=$(gum choose \
|
|
"Yes" \
|
|
"No" \
|
|
--header "Open in VS Code")
|
|
|
|
#######################################
|
|
# Confirm
|
|
#######################################
|
|
echo ""
|
|
|
|
# Build summary
|
|
SUMMARY="📋 Project Summary
|
|
|
|
Name: $PROJECT_NAME
|
|
Type: $PROJECT_TYPE
|
|
Path: $PROJECT_PATH
|
|
|
|
MCPs: $(echo "$MCPS" | tr '\n' ', ' | sed 's/, $//')
|
|
|
|
Git: $GIT_INIT | Remote: $GIT_REMOTE
|
|
Syncthing: $SYNCTHING_OPT
|
|
Claude-mem: $CLAUDE_MEM
|
|
Database: $DATABASE
|
|
License: $LICENSE
|
|
TickTick: $TICKTICK_TASKS | Obsidian: $OBSIDIAN_NOTE | VS Code: $OPEN_VSCODE"
|
|
|
|
if [ -n "$SUBDOMAIN" ]; then
|
|
SUMMARY="$SUMMARY
|
|
Network: $NETWORK_ACCESS
|
|
Subdomain: $SUBDOMAIN
|
|
Deploy to: $DEPLOY_TARGET"
|
|
fi
|
|
|
|
gum style --foreground 212 --border-foreground 212 --border rounded \
|
|
--align left --width 65 --margin "1 0" --padding "1 2" \
|
|
"$SUMMARY"
|
|
|
|
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"
|
|
mkdir -p "$PROJECT_PATH/src"
|
|
mkdir -p "$PROJECT_PATH/tests"
|
|
mkdir -p "$PROJECT_PATH/docs"
|
|
|
|
#######################################
|
|
# Save Project Brief
|
|
#######################################
|
|
if [ -n "$PROJECT_BRIEF" ]; then
|
|
gum spin --spinner dot --title "Saving project brief..." -- sleep 0.2
|
|
cat > "$PROJECT_PATH/docs/PROJECT_BRIEF.md" << BRIEF_EOF
|
|
# $PROJECT_NAME - Project Brief
|
|
|
|
$PROJECT_BRIEF
|
|
|
|
---
|
|
|
|
*Generated by newproject wizard on $(date '+%Y-%m-%d')*
|
|
BRIEF_EOF
|
|
fi
|
|
|
|
#######################################
|
|
# Generate .claude/settings.json
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating MCP configuration..." -- sleep 0.3
|
|
|
|
mkdir -p "$PROJECT_PATH/.claude"
|
|
|
|
ALL_MCPS=("exa" "Ref" "ticktick" "beeper" "airtable" "claude-in-chrome" "shopping" "proton-mail" "weather")
|
|
|
|
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
|
|
|
|
# Add claude-mem configuration if disabled
|
|
if [[ "$CLAUDE_MEM" == *"No"* ]]; then
|
|
cat >> "$PROJECT_PATH/.claude/settings.json" << 'CLAUDEMEM_EOF'
|
|
,
|
|
"plugins": {
|
|
"claude-mem": {
|
|
"disabled": true
|
|
}
|
|
}
|
|
CLAUDEMEM_EOF
|
|
fi
|
|
|
|
cat >> "$PROJECT_PATH/.claude/settings.json" << 'SETTINGS_CLOSE_EOF'
|
|
}
|
|
SETTINGS_CLOSE_EOF
|
|
|
|
#######################################
|
|
# Generate CLAUDE.md
|
|
#######################################
|
|
gum spin --spinner dot --title "Generating CLAUDE.md..." -- sleep 0.3
|
|
|
|
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"
|
|
INSTALL_CMD="pip install -e '.[dev]'"
|
|
DEV_CMD="uvicorn src.main:app --reload"
|
|
;;
|
|
python-generic)
|
|
TECH_STACK="Python 3.12+"
|
|
TYPE_CHECK_CMD="mypy --strict src/"
|
|
LINT_CMD="ruff check src/ && black src/"
|
|
TEST_CMD="pytest"
|
|
INSTALL_CMD="pip install -e '.[dev]'"
|
|
DEV_CMD="python -m src.main"
|
|
;;
|
|
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"
|
|
INSTALL_CMD="bun install"
|
|
DEV_CMD="bun run dev"
|
|
;;
|
|
typescript-node)
|
|
TECH_STACK="TypeScript, Node.js/Bun"
|
|
TYPE_CHECK_CMD="bun run tsc --noEmit"
|
|
LINT_CMD="bun run lint"
|
|
TEST_CMD="bun test"
|
|
INSTALL_CMD="bun install"
|
|
DEV_CMD="bun run dev"
|
|
;;
|
|
generic)
|
|
TECH_STACK="[Customize]"
|
|
TYPE_CHECK_CMD="# Add type check command"
|
|
LINT_CMD="# Add lint command"
|
|
TEST_CMD="# Add test command"
|
|
INSTALL_CMD="# Add install command"
|
|
DEV_CMD="# Add dev command"
|
|
;;
|
|
esac
|
|
|
|
USE_SPECKIT="No"
|
|
if echo "$ADDITIONAL" | grep -q "spec-kit"; then
|
|
USE_SPECKIT="Yes"
|
|
fi
|
|
|
|
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 (see \`docs/PROJECT_BRIEF.md\` for full context)
|
|
|
|
---
|
|
|
|
## 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_CHECK_CMD
|
|
$LINT_CMD
|
|
$TEST_CMD
|
|
\`\`\`
|
|
|
|
---
|
|
|
|
CLAUDE_EOF
|
|
|
|
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
|
|
|
|
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\`.
|
|
MCP_EOF
|
|
|
|
# Add claude-mem section
|
|
if [[ "$CLAUDE_MEM" == *"Yes"* ]]; then
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << 'CLAUDEMEM_DOC_EOF'
|
|
|
|
**Claude-mem:** Enabled - sessions are recorded for persistent memory.
|
|
- Dashboard: http://localhost:37777
|
|
- Search past work: `/CLAUDE` or ask about previous sessions
|
|
CLAUDEMEM_DOC_EOF
|
|
else
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << 'CLAUDEMEM_DOC_EOF'
|
|
|
|
**Claude-mem:** Disabled for this project.
|
|
CLAUDEMEM_DOC_EOF
|
|
fi
|
|
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << 'MCP_CLOSE_EOF'
|
|
|
|
---
|
|
|
|
## Central Configuration Reference
|
|
|
|
| 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
|
|
MCP_CLOSE_EOF
|
|
|
|
# Add deployment info if applicable
|
|
if [ -n "$SUBDOMAIN" ]; then
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << DEPLOY_EOF
|
|
|
|
---
|
|
|
|
## Deployment
|
|
|
|
| Setting | Value |
|
|
|---------|-------|
|
|
| **Subdomain** | https://$SUBDOMAIN |
|
|
| **Deploy Target** | $DEPLOY_TARGET |
|
|
| **Network Access** | $NETWORK_ACCESS |
|
|
|
|
**Deploy command:**
|
|
\`\`\`bash
|
|
./scripts/deploy.sh
|
|
\`\`\`
|
|
DEPLOY_EOF
|
|
fi
|
|
|
|
cat >> "$PROJECT_PATH/CLAUDE.md" << 'NOTES_EOF'
|
|
|
|
---
|
|
|
|
## Project-Specific Notes
|
|
|
|
[Add project-specific patterns, gotchas, and conventions here]
|
|
NOTES_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
|
|
$INSTALL_CMD
|
|
|
|
# Run development server
|
|
$DEV_CMD
|
|
|
|
# Run tests
|
|
$TEST_CMD
|
|
\`\`\`
|
|
|
|
## Development
|
|
|
|
See [CLAUDE.md](CLAUDE.md) for development guidelines and conventions.
|
|
See [docs/PROJECT_BRIEF.md](docs/PROJECT_BRIEF.md) for full project context.
|
|
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
|
|
|
|
#######################################
|
|
# Generate .env.example
|
|
#######################################
|
|
if echo "$ADDITIONAL" | grep -q ".env.example"; then
|
|
gum spin --spinner dot --title "Generating .env.example..." -- sleep 0.3
|
|
|
|
cat > "$PROJECT_PATH/.env.example" << 'ENVEX_EOF'
|
|
# Copy this to .env and fill in values
|
|
# Most values available in ~/.secrets
|
|
|
|
# Application
|
|
ENV=development
|
|
LOG_LEVEL=INFO
|
|
|
|
ENVEX_EOF
|
|
|
|
# Add relevant env vars based on selections
|
|
if echo "$MCPS" | grep -qi "exa"; then
|
|
echo "# Exa (web search)" >> "$PROJECT_PATH/.env.example"
|
|
echo "EXA_API_KEY=\${EXA_API_KEY}" >> "$PROJECT_PATH/.env.example"
|
|
echo "" >> "$PROJECT_PATH/.env.example"
|
|
fi
|
|
|
|
if [ "$DATABASE" = "PostgreSQL" ] || [ "$DATABASE" = "PostgreSQL + Redis" ] || [ "$DATABASE" = "TimescaleDB (time-series)" ]; then
|
|
echo "# Database" >> "$PROJECT_PATH/.env.example"
|
|
echo "DATABASE_URL=postgresql://user:password@localhost:5432/$PROJECT_NAME" >> "$PROJECT_PATH/.env.example"
|
|
echo "" >> "$PROJECT_PATH/.env.example"
|
|
fi
|
|
|
|
if [ "$DATABASE" = "Redis (cache/queue)" ] || [ "$DATABASE" = "PostgreSQL + Redis" ]; then
|
|
echo "# Redis" >> "$PROJECT_PATH/.env.example"
|
|
echo "REDIS_URL=redis://localhost:6379" >> "$PROJECT_PATH/.env.example"
|
|
echo "" >> "$PROJECT_PATH/.env.example"
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# License
|
|
#######################################
|
|
if [ "$LICENSE" = "MIT" ]; then
|
|
gum spin --spinner dot --title "Adding MIT license..." -- sleep 0.2
|
|
cat > "$PROJECT_PATH/LICENSE" << 'MIT_EOF'
|
|
MIT License
|
|
|
|
Copyright (c) 2026 Hutson
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
MIT_EOF
|
|
elif [ "$LICENSE" = "Apache 2.0" ]; then
|
|
gum spin --spinner dot --title "Adding Apache 2.0 license..." -- sleep 0.2
|
|
cat > "$PROJECT_PATH/LICENSE" << 'APACHE_EOF'
|
|
Apache License
|
|
Version 2.0, January 2004
|
|
http://www.apache.org/licenses/
|
|
|
|
Copyright 2026 Hutson
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
APACHE_EOF
|
|
fi
|
|
|
|
#######################################
|
|
# Docker support
|
|
#######################################
|
|
if echo "$ADDITIONAL" | grep -q "Docker support"; then
|
|
gum spin --spinner dot --title "Adding Docker support..." -- sleep 0.3
|
|
|
|
if [[ "$PROJECT_TYPE" == python* ]]; then
|
|
cat > "$PROJECT_PATH/Dockerfile" << 'DOCKER_EOF'
|
|
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
|
|
else
|
|
cat > "$PROJECT_PATH/Dockerfile" << 'DOCKER_EOF'
|
|
FROM oven/bun:1
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package.json bun.lockb ./
|
|
RUN bun install --frozen-lockfile
|
|
|
|
COPY . .
|
|
|
|
CMD ["bun", "run", "start"]
|
|
DOCKER_EOF
|
|
fi
|
|
|
|
# docker-compose with database if selected
|
|
cat > "$PROJECT_PATH/docker-compose.yml" << COMPOSE_EOF
|
|
version: '3.8'
|
|
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "8000:8000"
|
|
environment:
|
|
- ENV=development
|
|
volumes:
|
|
- .:/app
|
|
COMPOSE_EOF
|
|
|
|
if [ "$DATABASE" = "PostgreSQL" ] || [ "$DATABASE" = "PostgreSQL + Redis" ]; then
|
|
cat >> "$PROJECT_PATH/docker-compose.yml" << 'COMPOSE_PG_EOF'
|
|
depends_on:
|
|
- postgres
|
|
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: app
|
|
POSTGRES_PASSWORD: changeme
|
|
POSTGRES_DB: app
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
ports:
|
|
- "5432:5432"
|
|
|
|
volumes:
|
|
postgres_data:
|
|
COMPOSE_PG_EOF
|
|
fi
|
|
|
|
if [ "$DATABASE" = "Redis (cache/queue)" ] || [ "$DATABASE" = "PostgreSQL + Redis" ]; then
|
|
cat >> "$PROJECT_PATH/docker-compose.yml" << 'COMPOSE_REDIS_EOF'
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "6379:6379"
|
|
COMPOSE_REDIS_EOF
|
|
fi
|
|
|
|
if [ "$DATABASE" = "TimescaleDB (time-series)" ]; then
|
|
cat >> "$PROJECT_PATH/docker-compose.yml" << 'COMPOSE_TS_EOF'
|
|
depends_on:
|
|
- timescaledb
|
|
|
|
timescaledb:
|
|
image: timescale/timescaledb:latest-pg16
|
|
environment:
|
|
POSTGRES_USER: app
|
|
POSTGRES_PASSWORD: changeme
|
|
POSTGRES_DB: app
|
|
volumes:
|
|
- timescale_data:/var/lib/postgresql/data
|
|
ports:
|
|
- "5432:5432"
|
|
|
|
volumes:
|
|
timescale_data:
|
|
COMPOSE_TS_EOF
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# Spec-kit structure
|
|
#######################################
|
|
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
|
|
|
|
#######################################
|
|
# Deploy script (if web access needed)
|
|
#######################################
|
|
if [ -n "$SUBDOMAIN" ]; then
|
|
gum spin --spinner dot --title "Creating deploy script..." -- sleep 0.3
|
|
mkdir -p "$PROJECT_PATH/scripts"
|
|
|
|
# Extract deploy host
|
|
case "$DEPLOY_TARGET" in
|
|
"docker-host"*) DEPLOY_HOST="docker-host" ;;
|
|
"trading-vm"*) DEPLOY_HOST="trading-vm" ;;
|
|
"saltbox"*) DEPLOY_HOST="saltbox" ;;
|
|
*) DEPLOY_HOST="docker-host" ;;
|
|
esac
|
|
|
|
cat > "$PROJECT_PATH/scripts/deploy.sh" << DEPLOY_EOF
|
|
#!/usr/bin/env bash
|
|
# Deploy $PROJECT_NAME to $DEPLOY_HOST
|
|
|
|
set -e
|
|
|
|
DEPLOY_HOST="$DEPLOY_HOST"
|
|
PROJECT_NAME="$PROJECT_NAME"
|
|
SUBDOMAIN="$SUBDOMAIN"
|
|
|
|
echo "🚀 Deploying \$PROJECT_NAME to \$DEPLOY_HOST..."
|
|
|
|
# Build and push
|
|
docker build -t \$PROJECT_NAME:latest .
|
|
|
|
# Copy to remote (or use registry)
|
|
# docker save \$PROJECT_NAME:latest | ssh \$DEPLOY_HOST docker load
|
|
|
|
echo "✅ Deployed to https://\$SUBDOMAIN"
|
|
DEPLOY_EOF
|
|
chmod +x "$PROJECT_PATH/scripts/deploy.sh"
|
|
|
|
# Generate Traefik config stub
|
|
mkdir -p "$PROJECT_PATH/deploy"
|
|
cat > "$PROJECT_PATH/deploy/traefik.yml" << TRAEFIK_EOF
|
|
# Traefik dynamic configuration for $PROJECT_NAME
|
|
# Copy to /opt/traefik/config/$PROJECT_NAME.yml on traefik host
|
|
|
|
http:
|
|
routers:
|
|
$PROJECT_NAME:
|
|
rule: "Host(\`$SUBDOMAIN\`)"
|
|
service: $PROJECT_NAME
|
|
entryPoints:
|
|
- websecure
|
|
tls:
|
|
certResolver: letsencrypt
|
|
|
|
services:
|
|
$PROJECT_NAME:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://${DEPLOY_HOST}:8000"
|
|
TRAEFIK_EOF
|
|
|
|
# Note about network access in docs
|
|
cat > "$PROJECT_PATH/docs/DEPLOYMENT.md" << NETDOC_EOF
|
|
# Deployment Guide
|
|
|
|
## Network Access: $NETWORK_ACCESS
|
|
|
|
**Subdomain:** https://$SUBDOMAIN
|
|
**Deploy Target:** $DEPLOY_TARGET
|
|
|
|
### Setup Steps
|
|
|
|
1. **Deploy the application:**
|
|
\`\`\`bash
|
|
./scripts/deploy.sh
|
|
\`\`\`
|
|
|
|
2. **Configure Traefik:**
|
|
Copy \`deploy/traefik.yml\` to the Traefik config directory:
|
|
\`\`\`bash
|
|
scp deploy/traefik.yml traefik:/opt/traefik/config/$PROJECT_NAME.yml
|
|
\`\`\`
|
|
|
|
NETDOC_EOF
|
|
|
|
if [ "$NETWORK_ACCESS" = "Public Internet (via Cloudflare)" ]; then
|
|
cat >> "$PROJECT_PATH/docs/DEPLOYMENT.md" << 'CFDOC_EOF'
|
|
3. **Add Cloudflare DNS:**
|
|
- Add A record pointing to your public IP
|
|
- Or use Cloudflare Tunnel for added security
|
|
|
|
4. **Verify SSL:**
|
|
Traefik will auto-provision Let's Encrypt certificate
|
|
CFDOC_EOF
|
|
elif [ "$NETWORK_ACCESS" = "Local + Tailscale (remote access)" ]; then
|
|
cat >> "$PROJECT_PATH/docs/DEPLOYMENT.md" << 'TSDOC_EOF'
|
|
3. **Tailscale Access:**
|
|
- Ensure deploy target is on Tailscale network
|
|
- Access via Tailscale IP or MagicDNS name
|
|
|
|
4. **No public DNS needed** - access via Tailscale only
|
|
TSDOC_EOF
|
|
else
|
|
cat >> "$PROJECT_PATH/docs/DEPLOYMENT.md" << 'LOCDOC_EOF'
|
|
3. **Local Access Only:**
|
|
- Add entry to local DNS (Pi-hole) or /etc/hosts
|
|
- Access only from 10.10.10.x network
|
|
LOCDOC_EOF
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# Syncthing Configuration
|
|
#######################################
|
|
if [ "$SYNCTHING_OPT" = "Exclude from sync (recommended for git repos)" ]; then
|
|
gum spin --spinner dot --title "Adding to Syncthing ignore list..." -- sleep 0.3
|
|
|
|
if [ -f "$STIGNORE_FILE" ]; then
|
|
# Check if already in ignore file
|
|
if ! grep -q "^$PROJECT_NAME$" "$STIGNORE_FILE" 2>/dev/null; then
|
|
echo "$PROJECT_NAME" >> "$STIGNORE_FILE"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# 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
|
|
|
|
#######################################
|
|
# Git Remotes
|
|
#######################################
|
|
GITEA_URL=""
|
|
GITHUB_URL=""
|
|
|
|
if [[ "$GIT_REMOTE" == *"Gitea"* ]] && [ -n "$GITEA_TOKEN" ]; then
|
|
gum spin --spinner dot --title "Creating Gitea repository..." -- sleep 0.5
|
|
|
|
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\": true,
|
|
\"auto_init\": false
|
|
}" 2>/dev/null)
|
|
|
|
if echo "$RESPONSE" | grep -q "\"name\":\"$PROJECT_NAME\""; then
|
|
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"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$GIT_REMOTE" == *"GitHub"* ]]; then
|
|
if command -v gh &> /dev/null; then
|
|
gum spin --spinner dot --title "Creating GitHub repository..." -- sleep 0.5
|
|
|
|
gh repo create "$PROJECT_NAME" --public --source="$PROJECT_PATH" --push 2>/dev/null || true
|
|
GITHUB_URL="https://github.com/$(gh api user -q .login)/$PROJECT_NAME"
|
|
|
|
# If both remotes, rename
|
|
if [ -n "$GITEA_URL" ]; then
|
|
git -C "$PROJECT_PATH" remote rename origin gitea 2>/dev/null || true
|
|
git -C "$PROJECT_PATH" remote add github "git@github.com:$(gh api user -q .login)/$PROJECT_NAME.git" 2>/dev/null || true
|
|
fi
|
|
else
|
|
GITHUB_URL="(gh CLI not installed - create manually)"
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# TickTick Tasks
|
|
#######################################
|
|
if [[ "$TICKTICK_TASKS" == *"Yes"* ]]; then
|
|
PA_DIR="$HOME/Projects/personal-assistant"
|
|
if [ -d "$PA_DIR" ] && [ -f "$PA_DIR/venv/bin/python" ]; then
|
|
gum spin --spinner dot --title "Creating TickTick tasks..." -- sleep 0.5
|
|
|
|
# Create project setup tasks
|
|
(
|
|
cd "$PA_DIR"
|
|
source venv/bin/activate
|
|
|
|
# Create tasks for the new project
|
|
python -m src.integrations.ticktick.cli add "[$PROJECT_NAME] Set up development environment" --project "Inbox" 2>/dev/null || true
|
|
python -m src.integrations.ticktick.cli add "[$PROJECT_NAME] Review PROJECT_BRIEF.md and refine requirements" --project "Inbox" 2>/dev/null || true
|
|
python -m src.integrations.ticktick.cli add "[$PROJECT_NAME] Write first test" --project "Inbox" 2>/dev/null || true
|
|
python -m src.integrations.ticktick.cli add "[$PROJECT_NAME] Implement MVP" --project "Inbox" 2>/dev/null || true
|
|
) 2>/dev/null
|
|
|
|
TICKTICK_CREATED="Yes"
|
|
else
|
|
TICKTICK_CREATED="(PA not found)"
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# Obsidian Note
|
|
#######################################
|
|
OBSIDIAN_NOTE_PATH=""
|
|
if [[ "$OBSIDIAN_NOTE" == *"Yes"* ]]; then
|
|
gum spin --spinner dot --title "Creating Obsidian note..." -- sleep 0.3
|
|
|
|
NOTES_DIR="$HOME/Notes/Projects"
|
|
mkdir -p "$NOTES_DIR"
|
|
|
|
OBSIDIAN_NOTE_PATH="$NOTES_DIR/$PROJECT_NAME.md"
|
|
|
|
cat > "$OBSIDIAN_NOTE_PATH" << OBSIDIAN_EOF
|
|
# $PROJECT_NAME
|
|
|
|
$PROJECT_DESC
|
|
|
|
## Quick Links
|
|
|
|
- **Project**: [Open in Finder](file://$PROJECT_PATH)
|
|
- **CLAUDE.md**: [Open](file://$PROJECT_PATH/CLAUDE.md)
|
|
- **Project Brief**: [Open](file://$PROJECT_PATH/docs/PROJECT_BRIEF.md)
|
|
OBSIDIAN_EOF
|
|
|
|
# Add repo links if available
|
|
if [ -n "$GITEA_URL" ] && [ "$GITEA_URL" != "(failed)" ]; then
|
|
echo "- **Gitea**: [$PROJECT_NAME]($GITEA_URL)" >> "$OBSIDIAN_NOTE_PATH"
|
|
fi
|
|
if [ -n "$GITHUB_URL" ] && [[ "$GITHUB_URL" != *"not installed"* ]]; then
|
|
echo "- **GitHub**: [$PROJECT_NAME]($GITHUB_URL)" >> "$OBSIDIAN_NOTE_PATH"
|
|
fi
|
|
if [ -n "$SUBDOMAIN" ]; then
|
|
echo "- **URL**: https://$SUBDOMAIN" >> "$OBSIDIAN_NOTE_PATH"
|
|
fi
|
|
|
|
cat >> "$OBSIDIAN_NOTE_PATH" << OBSIDIAN_EOF2
|
|
|
|
## Project Brief
|
|
|
|
$PROJECT_BRIEF
|
|
|
|
## Tech Stack
|
|
|
|
- **Type**: $PROJECT_TYPE
|
|
- **Database**: $DATABASE
|
|
- **License**: $LICENSE
|
|
|
|
## Notes
|
|
|
|
<!-- Add your project notes here -->
|
|
|
|
---
|
|
|
|
*Created $(date '+%Y-%m-%d') via newproject wizard*
|
|
OBSIDIAN_EOF2
|
|
|
|
# Open in Obsidian
|
|
open "obsidian://open?vault=Notes&file=Projects/$PROJECT_NAME" 2>/dev/null || true
|
|
fi
|
|
|
|
#######################################
|
|
# Open in VS Code
|
|
#######################################
|
|
if [ "$OPEN_VSCODE" = "Yes" ]; then
|
|
gum spin --spinner dot --title "Opening in VS Code..." -- sleep 0.3
|
|
code "$PROJECT_PATH" 2>/dev/null || true
|
|
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 " # Review docs/PROJECT_BRIEF.md"
|
|
echo " # Start coding!"
|
|
fi
|
|
|
|
if [ -n "$GITEA_URL" ] && [ "$GITEA_URL" != "(failed)" ]; then
|
|
echo ""
|
|
echo " Gitea: $GITEA_URL"
|
|
fi
|
|
|
|
if [ -n "$GITHUB_URL" ] && [[ "$GITHUB_URL" != *"not installed"* ]]; then
|
|
echo ""
|
|
echo " GitHub: $GITHUB_URL"
|
|
fi
|
|
|
|
if [ -n "$SUBDOMAIN" ]; then
|
|
echo ""
|
|
echo " Subdomain: https://$SUBDOMAIN (configure Traefik)"
|
|
fi
|
|
|
|
if [ -n "$TICKTICK_CREATED" ] && [ "$TICKTICK_CREATED" = "Yes" ]; then
|
|
echo ""
|
|
echo " ✅ TickTick tasks created (check Inbox)"
|
|
fi
|
|
|
|
if [ -n "$OBSIDIAN_NOTE_PATH" ]; then
|
|
echo ""
|
|
echo " 📝 Obsidian note: ~/Notes/Projects/$PROJECT_NAME.md"
|
|
fi
|
|
|
|
echo ""
|