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:
@@ -24,6 +24,32 @@ var configCmd = &cobra.Command{
|
|||||||
cfg.Username = prompt(reader, fmt.Sprintf("Username [%s]", cfg.Username), cfg.Username)
|
cfg.Username = prompt(reader, fmt.Sprintf("Username [%s]", cfg.Username), cfg.Username)
|
||||||
cfg.Token = prompt(reader, fmt.Sprintf("API Token [%s]", maskToken(cfg.Token)), cfg.Token)
|
cfg.Token = prompt(reader, fmt.Sprintf("API Token [%s]", maskToken(cfg.Token)), cfg.Token)
|
||||||
|
|
||||||
|
fmt.Println("\n--- Homelab / beepc ---")
|
||||||
|
cfg.SSHHost = prompt(reader, fmt.Sprintf("SSH host (e.g. beepc) [%s]", cfg.SSHHost), cfg.SSHHost)
|
||||||
|
cfg.SSHUser = prompt(reader, fmt.Sprintf("SSH user [%s]", cfg.SSHUser), cfg.SSHUser)
|
||||||
|
cfg.ProjectsDir = promptDefault(reader, fmt.Sprintf("Projects dir on host [%s]", cfg.ProjectsDir), cfg.ProjectsDir, "~/projects")
|
||||||
|
cfg.HomelabDir = promptDefault(reader, fmt.Sprintf("Homelab repo dir on host [%s]", cfg.HomelabDir), cfg.HomelabDir, "~/homelab")
|
||||||
|
proxyDefault := cfg.ProxyHost
|
||||||
|
if proxyDefault == "" {
|
||||||
|
proxyDefault = cfg.SSHHost
|
||||||
|
}
|
||||||
|
cfg.ProxyHost = promptDefault(reader, fmt.Sprintf("Proxy host for Caddy (IP/hostname VPS uses to reach beepc) [%s]", proxyDefault), cfg.ProxyHost, proxyDefault)
|
||||||
|
|
||||||
|
fmt.Println("\n--- Caddy ---")
|
||||||
|
cfg.CaddyConfDir = promptDefault(reader, fmt.Sprintf("Caddy conf.d dir [%s]", cfg.CaddyConfDir), cfg.CaddyConfDir, "~/caddy/conf.d")
|
||||||
|
cfg.CaddyReloadCmd = promptDefault(reader, fmt.Sprintf("Caddy reload command [%s]", cfg.CaddyReloadCmd), cfg.CaddyReloadCmd, "sudo caddy reload")
|
||||||
|
cfg.Domain = promptDefault(reader, fmt.Sprintf("Base domain [%s]", cfg.Domain), cfg.Domain, "sketchferret.com")
|
||||||
|
|
||||||
|
fmt.Println("\n--- Port assignment ---")
|
||||||
|
portStr := ""
|
||||||
|
if cfg.PortStart != 0 {
|
||||||
|
portStr = fmt.Sprintf("%d", cfg.PortStart)
|
||||||
|
}
|
||||||
|
portInput := promptDefault(reader, fmt.Sprintf("Port range start [%s]", portStr), portStr, "8200")
|
||||||
|
if p, err := fmt.Sscanf(portInput, "%d", &cfg.PortStart); p == 0 || err != nil {
|
||||||
|
cfg.PortStart = 8200
|
||||||
|
}
|
||||||
|
|
||||||
if err := saveConfig(cfg); err != nil {
|
if err := saveConfig(cfg); err != nil {
|
||||||
return fmt.Errorf("saving config: %w", err)
|
return fmt.Errorf("saving config: %w", err)
|
||||||
}
|
}
|
||||||
@@ -42,6 +68,15 @@ func prompt(reader *bufio.Reader, label, fallback string) string {
|
|||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// promptDefault returns fallback if input is empty, otherwise defaultVal if fallback is also empty.
|
||||||
|
func promptDefault(reader *bufio.Reader, label, fallback, defaultVal string) string {
|
||||||
|
result := prompt(reader, label, fallback)
|
||||||
|
if result == "" {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func maskToken(t string) string {
|
func maskToken(t string) string {
|
||||||
if len(t) < 4 {
|
if len(t) < 4 {
|
||||||
return "****"
|
return "****"
|
||||||
|
|||||||
217
cmd/newproject.go
Normal file
217
cmd/newproject.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gogitea "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
npType string
|
||||||
|
npPrivate bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var newprojectCmd = &cobra.Command{
|
||||||
|
Use: "newproject <name>",
|
||||||
|
Short: "Full project scaffold: local + Gitea + beepc clone + homelab service + Caddy subdomain",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runNewProject,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
newprojectCmd.Flags().StringVarP(&npType, "type", "t", "", "Stack type: node, python, go, static (required)")
|
||||||
|
newprojectCmd.Flags().BoolVarP(&npPrivate, "private", "p", true, "Make the Gitea repo private")
|
||||||
|
newprojectCmd.Flags().StringVarP(&repoDesc, "desc", "d", "", "Repo description")
|
||||||
|
rootCmd.AddCommand(newprojectCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNewProject(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if npType == "" {
|
||||||
|
return fmt.Errorf("--type/-t is required: node, python, go, static")
|
||||||
|
}
|
||||||
|
switch npType {
|
||||||
|
case "node", "python", "go", "static":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown type %q — valid: node, python, go, static", npType)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg.SSHHost == "" || cfg.SSHUser == "" {
|
||||||
|
return fmt.Errorf("homelab SSH not configured — run 'gtea config' to set ssh_host and ssh_user")
|
||||||
|
}
|
||||||
|
|
||||||
|
step := func(n int, msg string) { fmt.Printf("[%d/6] %s\n", n, msg) }
|
||||||
|
fmt.Printf("==> Scaffolding %q (%s)\n\n", name, npType)
|
||||||
|
|
||||||
|
// 1. Local directory + git init
|
||||||
|
step(1, "Creating local project directory...")
|
||||||
|
if err := os.MkdirAll(name, 0755); err != nil {
|
||||||
|
return fmt.Errorf("creating folder: %w", err)
|
||||||
|
}
|
||||||
|
if err := runGit(name, "init", "-b", "main"); err != nil {
|
||||||
|
if err2 := runGit(name, "init"); err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Starter files
|
||||||
|
step(2, "Writing starter files...")
|
||||||
|
if err := writeStarterFiles(name, npType); err != nil {
|
||||||
|
return fmt.Errorf("writing starter files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Gitea repo
|
||||||
|
step(3, "Creating Gitea repo...")
|
||||||
|
client, err := gogitea.NewClient(cfg.URL, gogitea.SetToken(cfg.Token))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to Gitea: %w", err)
|
||||||
|
}
|
||||||
|
repo, _, err := client.CreateRepo(gogitea.CreateRepoOption{
|
||||||
|
Name: name,
|
||||||
|
Description: repoDesc,
|
||||||
|
Private: npPrivate,
|
||||||
|
AutoInit: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating Gitea repo: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" →", repo.HTMLURL)
|
||||||
|
|
||||||
|
// 4. Initial commit and push
|
||||||
|
step(4, "Initial commit and push...")
|
||||||
|
remote := authCloneURL(cfg, repo.CloneURL)
|
||||||
|
if err := runGit(name, "remote", "add", "origin", remote); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runGit(name, "add", "."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runGit(name, "commit", "-m", "Initial commit"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runGit(name, "push", "-u", "origin", "main"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve config defaults
|
||||||
|
projectsDir := orDefault(cfg.ProjectsDir, "~/projects")
|
||||||
|
homelabDir := orDefault(cfg.HomelabDir, "~/homelab")
|
||||||
|
domain := orDefault(cfg.Domain, "sketchferret.com")
|
||||||
|
caddyDir := orDefault(cfg.CaddyConfDir, "~/caddy/conf.d")
|
||||||
|
caddyReload := orDefault(cfg.CaddyReloadCmd, "sudo caddy reload")
|
||||||
|
proxyHost := orDefault(cfg.ProxyHost, cfg.SSHHost)
|
||||||
|
portStart := cfg.PortStart
|
||||||
|
if portStart == 0 {
|
||||||
|
portStart = 8200
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. beepc: clone + homelab service
|
||||||
|
step(5, fmt.Sprintf("Setting up beepc (%s@%s)...", cfg.SSHUser, cfg.SSHHost))
|
||||||
|
|
||||||
|
// Auto-assign port by counting existing service dirs
|
||||||
|
out, _ := sshRun(cfg.SSHUser, cfg.SSHHost,
|
||||||
|
fmt.Sprintf("ls %s/services/ 2>/dev/null | wc -l", homelabDir))
|
||||||
|
count, _ := strconv.Atoi(strings.TrimSpace(out))
|
||||||
|
port := portStart + count
|
||||||
|
|
||||||
|
// Clone to ~/projects/<name>
|
||||||
|
remotePath := projectsDir + "/" + name
|
||||||
|
cloneURL := authCloneURL(cfg, repo.CloneURL)
|
||||||
|
if _, err := sshRun(cfg.SSHUser, cfg.SSHHost,
|
||||||
|
fmt.Sprintf("mkdir -p %s && git clone %s %s", projectsDir, cloneURL, remotePath)); err != nil {
|
||||||
|
return fmt.Errorf("cloning on beepc: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Cloned → %s\n", remotePath)
|
||||||
|
|
||||||
|
// Homelab service
|
||||||
|
serviceDir := homelabDir + "/services/" + name
|
||||||
|
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||||
|
serviceDir+"/docker-compose.yml",
|
||||||
|
dockerComposeTemplate(name, npType, port)); err != nil {
|
||||||
|
return fmt.Errorf("writing docker-compose.yml: %w", err)
|
||||||
|
}
|
||||||
|
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||||
|
serviceDir+"/.env.example",
|
||||||
|
dotEnvExample(name, npType, port)); err != nil {
|
||||||
|
return fmt.Errorf("writing .env.example: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Homelab service → %s\n", serviceDir)
|
||||||
|
|
||||||
|
// 6. Caddy
|
||||||
|
step(6, "Adding Caddy config...")
|
||||||
|
subdomain := name + "." + domain
|
||||||
|
if err := sshWriteFile(cfg.SSHUser, cfg.SSHHost,
|
||||||
|
caddyDir+"/"+name+".caddy",
|
||||||
|
caddySnippet(subdomain, proxyHost, port)); err != nil {
|
||||||
|
return fmt.Errorf("writing Caddy snippet: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := sshRun(cfg.SSHUser, cfg.SSHHost, caddyReload); err != nil {
|
||||||
|
fmt.Printf(" Warning: Caddy reload failed — reload manually when ready\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" https://%s → localhost:%d\n", subdomain, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(`
|
||||||
|
Done!
|
||||||
|
Local: ./%s
|
||||||
|
Gitea: %s
|
||||||
|
beepc: %s
|
||||||
|
Service: %s
|
||||||
|
URL: https://%s
|
||||||
|
Port: %d
|
||||||
|
`, name, repo.HTMLURL, remotePath, serviceDir, subdomain, port)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStarterFiles(dir, stackType string) error {
|
||||||
|
files := map[string]string{
|
||||||
|
".gitignore": gitignoreFor(stackType),
|
||||||
|
"README.md": "# " + dir + "\n",
|
||||||
|
}
|
||||||
|
switch stackType {
|
||||||
|
case "node":
|
||||||
|
files["package.json"] = nodePackageJSON(dir)
|
||||||
|
files["tsconfig.json"] = tsconfigJSON()
|
||||||
|
files["src/index.ts"] = nodeIndexTS()
|
||||||
|
files["Dockerfile"] = nodeDockerfile()
|
||||||
|
case "python":
|
||||||
|
files["requirements.txt"] = "# Add dependencies here\n"
|
||||||
|
files["app.py"] = pythonApp()
|
||||||
|
files["Dockerfile"] = pythonDockerfile()
|
||||||
|
case "go":
|
||||||
|
files["go.mod"] = goMod(dir)
|
||||||
|
files["main.go"] = goMain()
|
||||||
|
files["Dockerfile"] = goDockerfile()
|
||||||
|
case "static":
|
||||||
|
files["index.html"] = staticHTML(dir)
|
||||||
|
files["Dockerfile"] = staticDockerfile()
|
||||||
|
}
|
||||||
|
for relPath, content := range files {
|
||||||
|
absPath := filepath.Join(dir, relPath)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func orDefault(s, def string) string {
|
||||||
|
if s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
15
cmd/root.go
15
cmd/root.go
@@ -17,6 +17,21 @@ type Config struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
|
||||||
|
// Homelab / beepc
|
||||||
|
SSHHost string `json:"ssh_host"`
|
||||||
|
SSHUser string `json:"ssh_user"`
|
||||||
|
ProjectsDir string `json:"projects_dir"`
|
||||||
|
HomelabDir string `json:"homelab_dir"`
|
||||||
|
ProxyHost string `json:"proxy_host"` // IP/hostname Caddy on VPS uses to reach beepc
|
||||||
|
|
||||||
|
// Caddy reverse proxy
|
||||||
|
CaddyConfDir string `json:"caddy_conf_dir"`
|
||||||
|
CaddyReloadCmd string `json:"caddy_reload_cmd"`
|
||||||
|
|
||||||
|
// Project defaults
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
PortStart int `json:"port_range_start"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
|||||||
37
cmd/ssh.go
Normal file
37
cmd/ssh.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sshRun executes a shell command on the remote host, returning combined output.
|
||||||
|
// Uses the system ssh binary so existing keys and ~/.ssh/config are honored.
|
||||||
|
func sshRun(user, host, command string) (string, error) {
|
||||||
|
c := exec.Command("ssh", "-o", "BatchMode=yes", user+"@"+host, command)
|
||||||
|
out, err := c.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
trimmed := strings.TrimSpace(string(out))
|
||||||
|
if trimmed != "" {
|
||||||
|
return string(out), fmt.Errorf("ssh %s@%s: %s: %w", user, host, trimmed, err)
|
||||||
|
}
|
||||||
|
return string(out), fmt.Errorf("ssh %s@%s: command failed: %w", user, host, err)
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshWriteFile writes content to a remote file, creating parent directories.
|
||||||
|
// Content is streamed via stdin to avoid shell escaping issues.
|
||||||
|
func sshWriteFile(user, host, remotePath, content string) error {
|
||||||
|
dir := path.Dir(remotePath)
|
||||||
|
cmd := fmt.Sprintf("mkdir -p %s && cat > %s", dir, remotePath)
|
||||||
|
c := exec.Command("ssh", "-o", "BatchMode=yes", user+"@"+host, cmd)
|
||||||
|
c.Stdin = strings.NewReader(content)
|
||||||
|
if out, err := c.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("writing %s on %s@%s: %s: %w",
|
||||||
|
remotePath, user, host, strings.TrimSpace(string(out)), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
226
cmd/templates.go
Normal file
226
cmd/templates.go
Normal 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
|
||||||
|
`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user