Add newproject command and fix template bugs

- Add newproject, ssh, templates commands for full project scaffolding
- Add homelab SSH, Caddy, port, and proxy_host fields to Config
- Fix dotEnvExample: use container-internal port (3000/8000/8080) not host port
- Fix caddySnippet: use configurable proxy_host instead of localhost
- Fix dockerComposeTemplate: add build: path so image-less deploys work
- Commit gtea.exe binary
This commit is contained in:
2026-05-29 12:37:14 -04:00
parent b8c6e129e8
commit e88f33071e
6 changed files with 530 additions and 0 deletions

226
cmd/templates.go Normal file
View File

@@ -0,0 +1,226 @@
package cmd
import "fmt"
func gitignoreFor(stackType string) string {
base := ".env\n.env.*\n!.env.example\n"
switch stackType {
case "node":
return base + "node_modules/\ndist/\n.next/\n*.log\n"
case "python":
return base + "__pycache__/\n*.pyc\n.venv/\nvenv/\n*.egg-info/\ndist/\nbuild/\n"
case "go":
return base + "*.exe\n*.test\n*.out\n/vendor/\n"
case "static":
return base + "dist/\n.cache/\n"
}
return base
}
func dockerComposeTemplate(name, stackType string, port int) string {
projectsDir := "~/projects"
var svc string
switch stackType {
case "node":
svc = fmt.Sprintf(
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
" build: %s/%s\n"+
" restart: unless-stopped\n ports:\n - \"%d:3000\"\n env_file: .env\n",
name, projectsDir, name, port)
case "python":
svc = fmt.Sprintf(
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
" build: %s/%s\n"+
" restart: unless-stopped\n ports:\n - \"%d:8000\"\n env_file: .env\n",
name, projectsDir, name, port)
case "go":
svc = fmt.Sprintf(
" app:\n image: ${IMAGE:-ghcr.io/sketchferret/%s:latest}\n"+
" build: %s/%s\n"+
" restart: unless-stopped\n ports:\n - \"%d:8080\"\n env_file: .env\n",
name, projectsDir, name, port)
case "static":
svc = fmt.Sprintf(
" web:\n image: nginx:alpine\n restart: unless-stopped\n"+
" ports:\n - \"%d:80\"\n volumes:\n - ./dist:/usr/share/nginx/html:ro\n",
port)
}
return "services:\n" + svc
}
func dotEnvExample(name, stackType string, port int) string {
_ = port // host port is set by compose; container uses its own internal port
header := fmt.Sprintf("# %s environment\n# Copy to .env and fill in values\n\n", name)
switch stackType {
case "node":
return header + "PORT=3000\n# DATABASE_URL=\n# SECRET_KEY=\n"
case "python":
return header + "PORT=8000\n# DATABASE_URL=\n# SECRET_KEY=\n"
case "go":
return header + "PORT=8080\n# DATABASE_URL=\n"
}
return header
}
func caddySnippet(subdomain, proxyHost string, port int) string {
return fmt.Sprintf("%s {\n\treverse_proxy %s:%d\n}\n", subdomain, proxyHost, port)
}
// --- local starter files ---
func nodePackageJSON(name string) string {
return fmt.Sprintf(`{
"name": "%s",
"version": "0.1.0",
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {},
"devDependencies": {
"typescript": "^5.0.0",
"ts-node": "^10.0.0",
"@types/node": "^20.0.0"
}
}
`, name)
}
func tsconfigJSON() string {
return `{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true
}
}
`
}
func nodeIndexTS() string {
// Template literal backtick handled via concatenation
return "import http from 'http';\n\n" +
"const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;\n\n" +
"const server = http.createServer((_req, res) => {\n" +
" res.writeHead(200, { 'Content-Type': 'text/plain' });\n" +
" res.end('Hello World\\n');\n" +
"});\n\n" +
"server.listen(port, () => {\n" +
" console.log(`Server running on port ${port}`);\n" +
"});\n"
}
func nodeDockerfile() string {
return `FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
`
}
func pythonApp() string {
return `from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello World"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
`
}
func pythonDockerfile() string {
return `FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
`
}
func goMod(name string) string {
return fmt.Sprintf("module %s\n\ngo 1.22\n", name)
}
func goMain() string {
return `package main
import (
"fmt"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World")
})
fmt.Println("Listening on :" + port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
`
}
func goDockerfile() string {
return `FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o app .
FROM scratch
COPY --from=builder /app/app /app
EXPOSE 8080
CMD ["/app"]
`
}
func staticHTML(name string) string {
return fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s</title>
</head>
<body>
<h1>%s</h1>
</body>
</html>
`, name, name)
}
func staticDockerfile() string {
return `FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
`
}