appmanager: replace svelte implementation with go based one
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index b01cd82..0582594 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -31,9 +31,10 @@
 	Writer(path string) (io.WriteCloser, error)
 	CreateDir(path string) error
 	RemoveDir(path string) error
-	InstallApp(app App, path string, values map[string]any) error
-	FindAllInstances(root string, name string) ([]AppConfig, error)
-	FindInstance(root string, name string) (AppConfig, error)
+	InstallApp(app App, path string, values map[string]any, derived Derived) error
+	RemoveApp(path string) error
+	FindAllInstances(root string, appId string) ([]AppConfig, error)
+	FindInstance(root string, id string) (AppConfig, error)
 }
 
 type repoIO struct {
@@ -181,12 +182,24 @@
 	return err
 }
 
-type AppConfig struct {
-	Id     string         `json:"id"`
-	Config map[string]any `json:"config"`
+type Release struct {
+	Namespace string `json:"Namespace"`
 }
 
-func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any) error {
+type Derived struct {
+	Release Release        `json:"Release"`
+	Global  Values         `json:"Global"`
+	Values  map[string]any `json:"Values"`
+}
+
+type AppConfig struct {
+	Id      string         `json:"id"`
+	AppId   string         `json:"appId"`
+	Config  map[string]any `json:"config"`
+	Derived Derived        `json:"derived"`
+}
+
+func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
 	if !filepath.IsAbs(appRootDir) {
 		return fmt.Errorf("Expected absolute path: %s", appRootDir)
 	}
@@ -217,8 +230,9 @@
 			return err
 		}
 		cfg := AppConfig{
-			Id:     app.Name,
-			Config: values,
+			AppId:   app.Name,
+			Config:  values,
+			Derived: derived,
 		}
 		if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
 			return err
@@ -233,7 +247,7 @@
 				return err
 			}
 			defer out.Close()
-			if err := t.Execute(out, values); err != nil {
+			if err := t.Execute(out, derived); err != nil {
 				return err
 			}
 		}
@@ -244,6 +258,19 @@
 	return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
 }
 
+func (r *repoIO) RemoveApp(appRootDir string) error {
+	r.RemoveDir(appRootDir)
+	parent, child := filepath.Split(appRootDir)
+	kustPath := filepath.Join(parent, "kustomization.yaml")
+	kust, err := r.ReadKustomization(kustPath)
+	if err != nil {
+		return err
+	}
+	kust.RemoveResources(child)
+	r.WriteKustomization(kustPath, *kust)
+	return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
+}
+
 func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
 	if !filepath.IsAbs(root) {
 		return nil, fmt.Errorf("Expected absolute path: %s", root)
@@ -258,14 +285,15 @@
 		if err != nil {
 			return nil, err
 		}
-		if cfg.Id == name {
+		cfg.Id = app
+		if cfg.AppId == name {
 			ret = append(ret, cfg)
 		}
 	}
 	return ret, nil
 }
 
-func (r *repoIO) FindInstance(root string, name string) (AppConfig, error) {
+func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
 	if !filepath.IsAbs(root) {
 		return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
 	}
@@ -274,8 +302,13 @@
 		return AppConfig{}, err
 	}
 	for _, app := range kust.Resources {
-		if app == name {
-			return r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
+		if app == id {
+			cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
+			if err != nil {
+				return AppConfig{}, err
+			}
+			cfg.Id = id
+			return cfg, nil
 		}
 	}
 	return AppConfig{}, nil
@@ -301,3 +334,71 @@
 		return yaml.UnmarshalStrict(contents, o)
 	}
 }
+
+func deriveValues(values any, schema map[string]any, networks []Network) (map[string]any, error) {
+	ret := make(map[string]any)
+	for k, v := range values.(map[string]any) { // TODO(giolekva): validate
+		def, err := fieldSchema(schema, k)
+		if err != nil {
+			return nil, err
+		}
+		t, ok := def["type"]
+		if !ok {
+			return nil, fmt.Errorf("Found field with undefined type: %s", k)
+		}
+		if t == "string" {
+			role, ok := def["role"]
+			if ok && role == "network" {
+				n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
+				if err != nil {
+					return nil, err
+				}
+				ret[k] = n
+			} else {
+				ret[k] = v
+			}
+		} else {
+			ret[k], err = deriveValues(v, def, networks)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	return ret, nil
+}
+
+func findNetwork(networks []Network, name string) (Network, error) {
+	for _, n := range networks {
+		if n.Name == name {
+			return n, nil
+		}
+	}
+	return Network{}, fmt.Errorf("Network not found: %s", name)
+}
+
+func fieldSchema(schema map[string]any, key string) (map[string]any, error) {
+	properties, ok := schema["properties"]
+	if !ok {
+		return nil, fmt.Errorf("Properties not found")
+	}
+	propMap, ok := properties.(map[string]any)
+	if !ok {
+		return nil, fmt.Errorf("Expected properties to be map")
+	}
+	def, ok := propMap[key]
+	if !ok {
+		return nil, fmt.Errorf("Unknown field: %s", key)
+	}
+	ret, ok := def.(map[string]any)
+	if !ok {
+		return nil, fmt.Errorf("Invalid schema")
+	}
+	return ret, nil
+}
+
+type Network struct {
+	Name              string
+	IngressClass      string
+	CertificateIssuer string
+	Domain            string
+}