installer: env and app manager
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
new file mode 100644
index 0000000..4b66fa5
--- /dev/null
+++ b/core/installer/app_manager.go
@@ -0,0 +1,88 @@
+package installer
+
+import (
+	"io/fs"
+
+	"golang.org/x/exp/slices"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/util"
+)
+
+const kustomizationFileName = "kustomization.yaml"
+
+type AppRepository interface {
+	Find(name string) (*App, error)
+}
+
+type AppManager struct {
+	fs       billy.Filesystem
+	config   Config
+	appRepo  AppRepository
+	rootKust *Kustomization
+}
+
+func NewAppManager(fs billy.Filesystem, config Config, appRepo AppRepository) (*AppManager, error) {
+	rootKustF, err := fs.Open(kustomizationFileName)
+	if err != nil {
+		return nil, err
+	}
+	defer rootKustF.Close()
+	rootKust, err := ReadKustomization(rootKustF)
+	if err != nil {
+		return nil, err
+	}
+	return &AppManager{
+		fs,
+		config,
+		appRepo,
+		rootKust,
+	}, nil
+}
+
+func (m *AppManager) Install(name string) error {
+	app, err := m.appRepo.Find(name)
+	if err != nil {
+		return nil
+	}
+	if err := util.RemoveAll(m.fs, name); err != nil {
+		return err
+	}
+	if err := m.fs.MkdirAll(name, fs.ModePerm); err != nil {
+		return nil
+	}
+	appRoot, err := m.fs.Chroot(name)
+	if err != nil {
+		return err
+	}
+	appKust := NewKustomization()
+	for _, t := range app.Templates {
+		out, err := appRoot.Create(t.Name())
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		if err := t.Execute(out, m.config); err != nil {
+			return err
+		}
+		appKust.Resources = append(appKust.Resources, t.Name())
+	}
+	appKustF, err := appRoot.Create(kustomizationFileName)
+	if err != nil {
+		return err
+	}
+	defer appKustF.Close()
+	if err := appKust.Write(appKustF); err != nil {
+		return err
+	}
+	if slices.Contains(m.rootKust.Resources, name) {
+		return nil
+	}
+	m.rootKust.Resources = append(m.rootKust.Resources, name)
+	rootKustF, err := m.fs.Create(kustomizationFileName)
+	if err != nil {
+		return err
+	}
+	defer rootKustF.Close()
+	return m.rootKust.Write(rootKustF)
+}