- 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
227 lines
5.0 KiB
Go
227 lines
5.0 KiB
Go
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
|
|
`
|
|
}
|