installer: fully automate initial bootstrap and env creation
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index a1d86d2..97bf9b6 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -1,21 +1,34 @@
 package installer
 
 import (
+	"errors"
+	"fmt"
 	"io"
 	"io/fs"
+	"net"
+	"path"
 	"path/filepath"
 	"time"
 
+	"github.com/go-git/go-billy/v5/util"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
+	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"golang.org/x/crypto/ssh"
+	"sigs.k8s.io/yaml"
 )
 
 type RepoIO interface {
+	Fetch() error
 	ReadKustomization(path string) (*Kustomization, error)
 	WriteKustomization(path string, kust Kustomization) error
+	WriteYaml(path string, data any) error
 	CommitAndPush(message string) error
+	Reader(path string) (io.ReadCloser, error)
 	Writer(path string) (io.WriteCloser, error)
+	CreateDir(path string) error
+	RemoveDir(path string) error
+	InstallApp(app App, path string, values map[string]any) error
 }
 
 type repoIO struct {
@@ -30,12 +43,20 @@
 	}
 }
 
-func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
-	wt, err := r.repo.Worktree()
-	if err != nil {
-		return nil, err
+func (r *repoIO) Fetch() error {
+	err := r.repo.Fetch(&git.FetchOptions{
+		RemoteName: "origin",
+		Auth:       auth(r.signer),
+		Force:      true,
+	})
+	if err == nil || err == git.NoErrAlreadyUpToDate {
+		return nil
 	}
-	inp, err := wt.Filesystem.Open(path)
+	return err
+}
+
+func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
+	inp, err := r.Reader(path)
 	if err != nil {
 		return nil, err
 	}
@@ -43,6 +64,14 @@
 	return ReadKustomization(inp)
 }
 
+func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return nil, err
+	}
+	return wt.Filesystem.Open(path)
+}
+
 func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
 	wt, err := r.repo.Worktree()
 	if err != nil {
@@ -62,6 +91,21 @@
 	return kust.Write(out)
 }
 
+func (r *repoIO) WriteYaml(path string, data any) error {
+	out, err := r.Writer(path)
+	if err != nil {
+		return err
+	}
+	serialized, err := yaml.Marshal(data)
+	if err != nil {
+		return err
+	}
+	if _, err := out.Write(serialized); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (r *repoIO) CommitAndPush(message string) error {
 	wt, err := r.repo.Worktree()
 	if err != nil {
@@ -83,3 +127,80 @@
 		Auth:       auth(r.signer),
 	})
 }
+
+func (r *repoIO) CreateDir(path string) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	return wt.Filesystem.MkdirAll(path, fs.ModePerm)
+}
+
+func (r *repoIO) RemoveDir(path string) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	err = util.RemoveAll(wt.Filesystem, path)
+	if err == nil || errors.Is(err, fs.ErrNotExist) {
+		return nil
+	}
+	return err
+}
+
+func (r *repoIO) InstallApp(app App, root string, values map[string]any) error {
+	{
+		appsKustPath := path.Join(root, "kustomization.yaml")
+		appsKust, err := r.ReadKustomization(appsKustPath)
+		if err != nil {
+			return err
+		}
+		appsKust.AddResources(app.Name)
+		if err := r.WriteKustomization(appsKustPath, *appsKust); err != nil {
+			return err
+		}
+	}
+	appRootDir := path.Join(root, app.Name)
+	{
+		if err := r.RemoveDir(appRootDir); err != nil {
+			return err
+		}
+		if err := r.CreateDir(appRootDir); err != nil {
+			return err
+		}
+		if err := r.WriteYaml(path.Join(appRootDir, configFileName), values); err != nil {
+			return err
+		}
+	}
+	{
+		appKust := NewKustomization()
+		for _, t := range app.Templates {
+			appKust.AddResources(t.Name())
+			out, err := r.Writer(path.Join(appRootDir, t.Name()))
+			if err != nil {
+				return err
+			}
+			defer out.Close()
+			if err := t.Execute(out, values); err != nil {
+				return err
+			}
+		}
+		if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
+			return err
+		}
+	}
+	return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
+}
+
+func auth(signer ssh.Signer) *gitssh.PublicKeys {
+	return &gitssh.PublicKeys{
+		Signer: signer,
+		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
+			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+				// TODO(giolekva): verify server public key
+				// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
+				return nil
+			},
+		},
+	}
+}