Files
template-project/scripts/newproject
Hutson Cappelmann 295b6703b2 Add TickTick and Obsidian integrations to wizard
TickTick integration:
- Creates setup tasks in TickTick Inbox when selected
- Tasks: Set up dev env, Review brief, Write first test, Implement MVP
- Uses existing PA Python CLI integration

Obsidian integration:
- Creates project note in ~/Notes/Projects/
- Includes quick links, project brief, tech stack
- Auto-opens note in Obsidian after creation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 17:23:37 -05:00

1168 lines
31 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")
#######################################
# 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"
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
#######################################
# 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 ""