Git Repository

Stackyard

Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.

Projektseite ↗
HTTPShttps://zanvex.de/git/stackyard.git

cmd/stackyard/main.go

Zum Verzeichnis
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"time"

	"stackyard/internal/config"
	"stackyard/internal/git"
	"stackyard/internal/project"
	"stackyard/internal/scanner"
	"stackyard/internal/storage"
	"stackyard/internal/todo"
	"stackyard/internal/web"
)

func main() {
	serve := flag.NewFlagSet("serve", flag.ExitOnError)
	serveConfigPath := serve.String("config", "", "Path to config file")

	scan := flag.NewFlagSet("scan", flag.ExitOnError)
	scanConfigPath := scan.String("config", "", "Path to config file")

	if len(os.Args) < 2 {
		fmt.Println("usage: stackyard <serve|scan> [options]")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "serve":
		_ = serve.Parse(os.Args[2:])
		if err := runServe(*serveConfigPath); err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}
	case "scan":
		_ = scan.Parse(os.Args[2:])
		if err := runScan(*scanConfigPath); err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}
	default:
		fmt.Printf("unknown command: %s\n", os.Args[1])
		os.Exit(1)
	}
}

func runServe(configPath string) error {
	cfg, err := config.Load(configPath)
	if err != nil {
		return err
	}
	store, err := storage.Open(cfg.Database.Path)
	if err != nil {
		return err
	}
	defer store.Close()

	webServer := web.NewServer(cfg, store)
	defer webServer.Close()
	server := &http.Server{
		Addr:              cfg.Server.Listen,
		Handler:           webServer.Handler(),
		ReadHeaderTimeout: 5 * time.Second,
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = server.Shutdown(shutdownCtx)
	}()

	fmt.Printf("Stackyard listening on http://%s\n", cfg.Server.Listen)
	err = server.ListenAndServe()
	if errors.Is(err, http.ErrServerClosed) {
		return nil
	}
	return err
}

func runScan(configPath string) error {
	cfg, err := config.Load(configPath)
	if err != nil {
		return err
	}
	store, err := storage.Open(cfg.Database.Path)
	if err != nil {
		return err
	}
	defer store.Close()

	sc := scanner.New(cfg.Scanner)
	candidates, err := sc.Scan(cfg.Roots)
	if err != nil {
		return err
	}

	gitClient := git.NewClient()
	projects := make([]project.Project, 0, len(candidates))
	for _, candidate := range candidates {
		p := project.Project{
			ID:            project.IDFromPath(candidate.Path),
			Name:          candidate.Name,
			RootName:      candidate.RootName,
			Path:          candidate.Path,
			PrimaryType:   candidate.PrimaryType,
			State:         project.StateActive,
			LastModified:  candidate.LastModified,
			LastScannedAt: time.Now(),
		}

		status, commit, err := gitClient.Inspect(context.Background(), candidate.Path)
		if err != nil {
			fmt.Fprintf(os.Stderr, "stackyard: git inspect skipped for %s: %v\n", candidate.Path, err)
		} else {
			p.Git.IsRepo = status.IsRepo
			p.Git.Branch = status.Branch
			p.Git.Dirty = status.Dirty
			p.Git.Ahead = status.Ahead
			p.Git.Behind = status.Behind
			if commit != nil {
				p.Git.LastCommitHash = commit.Hash
				p.Git.LastCommitSubject = commit.Subject
				p.Git.LastCommitAt = commit.Committed
			}
		}
		todoPath, exists, err := todo.Resolve(candidate.Path)
		if err != nil {
			fmt.Fprintf(os.Stderr, "stackyard: todo resolve skipped for %s: %v\n", candidate.Path, err)
		} else if exists {
			p.TodoPath = todoPath
		}
		projects = append(projects, p)

		fmt.Printf("%s [%s]\n", candidate.Name, candidate.PrimaryType)
		fmt.Printf("  path: %s\n", candidate.Path)
		if status.IsRepo {
			fmt.Printf("  git: branch=%s dirty=%t ahead=%d behind=%d\n", status.Branch, status.Dirty, status.Ahead, status.Behind)
		} else {
			fmt.Println("  git: no repository")
		}
		if commit != nil {
			fmt.Printf("  last commit: %s (%s)\n", commit.Subject, commit.Committed.Format(time.RFC3339))
		}
		if exists {
			fmt.Printf("  todo: %s\n", todoPath)
		} else {
			fmt.Printf("  todo: missing (%s)\n", todoPath)
		}
	}

	return store.UpsertProjects(context.Background(), projects)
}