Initial commit: unitdore scaffold + syncup/edit commands

- config: load/save/add unit, Unit struct
- runtime: podman + docker discovery and exists check
- cmd: syncup (discover + reconcile), edit, cobra root
- PLAN.md: full project plan
This commit is contained in:
2026-04-03 14:39:09 +02:00
commit aa7d85822c
12 changed files with 633 additions and 0 deletions

73
runtime/docker.go Normal file
View File

@@ -0,0 +1,73 @@
package runtime
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
)
type Docker struct{}
func (d *Docker) Name() string { return "docker" }
type dockerContainer struct {
Names string `json:"Names"`
Image string `json:"Image"`
Command string `json:"Command"`
State string `json:"State"`
}
func (d *Docker) ListRunning() ([]Container, error) {
bin, err := exec.LookPath("docker")
if err != nil {
return nil, nil // docker not installed — not an error
}
out, err := exec.Command(bin, "ps", "--format", "json").Output()
if err != nil {
return nil, fmt.Errorf("docker ps: %w", err)
}
// Docker outputs one JSON object per line (not a JSON array)
var containers []Container
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
var c dockerContainer
if err := json.Unmarshal([]byte(line), &c); err != nil {
continue
}
name := strings.TrimPrefix(c.Names, "/")
if name == "" {
continue
}
containers = append(containers, Container{
Name: name,
Image: c.Image,
Command: c.Command,
Runtime: "docker",
})
}
return containers, nil
}
func (d *Docker) Exists(name string) (bool, error) {
bin, err := exec.LookPath("docker")
if err != nil {
return false, nil
}
out, err := exec.Command(bin, "ps", "-a", "--format", "{{.Names}}").Output()
if err != nil {
return false, fmt.Errorf("docker ps -a: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.TrimSpace(line) == name {
return true, nil
}
}
return false, nil
}

73
runtime/podman.go Normal file
View File

@@ -0,0 +1,73 @@
package runtime
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
)
type Podman struct{}
func (p *Podman) Name() string { return "podman" }
type podmanContainer struct {
Names []string `json:"Names"`
Image string `json:"Image"`
Command string `json:"Command"`
State string `json:"State"`
}
func (p *Podman) ListRunning() ([]Container, error) {
bin, err := exec.LookPath("podman")
if err != nil {
return nil, nil // podman not installed — not an error
}
out, err := exec.Command(bin, "ps", "--format", "json").Output()
if err != nil {
return nil, fmt.Errorf("podman ps: %w", err)
}
var raw []podmanContainer
if err := json.Unmarshal(out, &raw); err != nil {
return nil, fmt.Errorf("parsing podman output: %w", err)
}
var containers []Container
for _, c := range raw {
name := ""
if len(c.Names) > 0 {
name = c.Names[0]
}
if name == "" {
continue
}
containers = append(containers, Container{
Name: name,
Image: c.Image,
Command: c.Command,
Runtime: "podman",
})
}
return containers, nil
}
func (p *Podman) Exists(name string) (bool, error) {
bin, err := exec.LookPath("podman")
if err != nil {
return false, nil
}
out, err := exec.Command(bin, "ps", "-a", "--format", "{{.Names}}").Output()
if err != nil {
return false, fmt.Errorf("podman ps -a: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.TrimSpace(line) == name {
return true, nil
}
}
return false, nil
}

33
runtime/runtime.go Normal file
View File

@@ -0,0 +1,33 @@
package runtime
// Container represents a discovered running container.
type Container struct {
Name string
Image string
Command string
Runtime string // "podman" or "docker"
}
// Runtime is the interface all container runtimes must implement.
type Runtime interface {
Name() string
ListRunning() ([]Container, error)
Exists(name string) (bool, error)
}
// Available returns all supported runtimes (callers skip those with no binary).
func Available() []Runtime {
return []Runtime{&Podman{}, &Docker{}}
}
// Get returns a runtime by name ("podman" or "docker").
func Get(name string) Runtime {
switch name {
case "podman":
return &Podman{}
case "docker":
return &Docker{}
default:
return nil
}
}