env-manager: ui polish (#127)

* env-manager: migrate to pico 2.0.6

* env: option to hide children from ui

* introduce template hierarchy

* style: improve menu styling

* env: reorganize tasks, pull before install

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/core/installer/tasks/activate.go b/core/installer/tasks/activate.go
index 0980dee..1333f28 100644
--- a/core/installer/tasks/activate.go
+++ b/core/installer/tasks/activate.go
@@ -13,15 +13,10 @@
 //go:embed env-tmpl
 var filesTmpls embed.FS
 
-type activateEnvTask struct {
-	basicTask
-	env Env
-	st  *state
-}
-
 func NewActivateEnvTask(env Env, st *state) Task {
 	return newSequentialParentTask(
-		fmt.Sprintf("Activate new %s instance", env.PCloudEnvName),
+		"Activate GitOps",
+		false,
 		AddNewEnvTask(env, st),
 		// TODO(gio): sync dodo-flux
 	)
diff --git a/core/installer/tasks/dns.go b/core/installer/tasks/dns.go
index 0880782..25af486 100644
--- a/core/installer/tasks/dns.go
+++ b/core/installer/tasks/dns.go
@@ -16,7 +16,8 @@
 
 func SetupZoneTask(env Env, ingressIP net.IP, st *state) Task {
 	return newSequentialParentTask(
-		fmt.Sprintf("Setup DNS zone records for %s", env.Domain),
+		"Configure DNS",
+		true,
 		CreateZoneRecords(env.Domain, st.publicIPs, ingressIP, env, st),
 		WaitToPropagate(env.Domain, st.publicIPs),
 	)
@@ -29,7 +30,7 @@
 	env Env,
 	st *state,
 ) Task {
-	t := newLeafTask("Configure DNS", func() error {
+	t := newLeafTask("Generate and publish DNS records", func() error {
 		repo, err := st.ssClient.GetRepo("config")
 		if err != nil {
 			return err
@@ -90,12 +91,17 @@
 			}); err != nil {
 				return err
 			}
-			rootKust := installer.NewKustomization()
-			rootKust.AddResources("dns-zone.yaml")
-			if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+			rootKust, err := r.ReadKustomization("kustomization.yaml")
+			if err != nil {
 				return err
 			}
-			r.CommitAndPush("configure dns zone")
+			rootKust.AddResources("dns-zone.yaml")
+			if err := r.WriteKustomization("kustomization.yaml", *rootKust); err != nil {
+				return err
+			}
+			if err := r.CommitAndPush("configure dns zone"); err != nil {
+				return err
+			}
 		}
 		return nil
 	})
@@ -106,7 +112,7 @@
 	name string,
 	expected []net.IP,
 ) Task {
-	t := newLeafTask("Propagate DNS records", func() error {
+	t := newLeafTask("Wait to propagate", func() error {
 		ctx := context.TODO()
 		gotExpectedIPs := func(actual []net.IP) bool {
 			for _, a := range actual {
diff --git a/core/installer/tasks/env.go b/core/installer/tasks/env.go
index eb537da..80289c4 100644
--- a/core/installer/tasks/env.go
+++ b/core/installer/tasks/env.go
@@ -52,14 +52,10 @@
 	}
 	t := newSequentialParentTask(
 		"Create env",
-		append(
-			[]Task{
-				SetupConfigRepoTask(env, &st),
-				NewActivateEnvTask(env, &st),
-				SetupZoneTask(env, startIP, &st),
-			},
-			SetupInfra(env, startIP, &st)...,
-		)...,
+		true,
+		SetupConfigRepoTask(env, &st),
+		SetupZoneTask(env, startIP, &st),
+		SetupInfra(env, startIP, &st),
 	)
 	rctx, done := context.WithCancel(context.Background())
 	t.OnDone(func(_ error) {
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 0216f84..0685a6d 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -13,8 +13,8 @@
 
 var initGroups = []string{"admin"}
 
-func SetupInfra(env Env, startIP net.IP, st *state) []Task {
-	t := newLeafTask("Create client", func() error {
+func CreateRepoClient(env Env, st *state) Task {
+	t := newLeafTask("Create repo client", func() error {
 		repo, err := st.ssClient.GetRepo("config")
 		if err != nil {
 			return err
@@ -30,25 +30,25 @@
 		st.emptySuffixGen = installer.NewEmptySuffixGenerator()
 		return nil
 	})
-	return []Task{
-		CommitEnvironmentConfiguration(env, st),
-		ConfigureFirstAccount(env, st),
-		&t,
-		newConcurrentParentTask(
-			"Core services",
-			SetupNetwork(env, startIP, st),
-			SetupCertificateIssuers(env, st),
-			SetupAuth(env, st),
-			SetupGroupMemberships(env, st),
-			SetupHeadscale(env, startIP, st),
-			SetupWelcome(env, st),
-			SetupAppStore(env, st),
-		),
-	}
+	return &t
+}
+
+func SetupInfra(env Env, startIP net.IP, st *state) Task {
+	return newConcurrentParentTask(
+		"Setup core services",
+		true,
+		SetupNetwork(env, startIP, st),
+		SetupCertificateIssuers(env, st),
+		SetupAuth(env, st),
+		SetupGroupMemberships(env, st),
+		SetupHeadscale(env, startIP, st),
+		SetupWelcome(env, st),
+		SetupAppStore(env, st),
+	)
 }
 
 func CommitEnvironmentConfiguration(env Env, st *state) Task {
-	t := newLeafTask("Configure environment infrastructure", func() error {
+	t := newLeafTask("commit config", func() error {
 		repo, err := st.ssClient.GetRepo("config")
 		if err != nil {
 			return err
@@ -129,7 +129,7 @@
 }
 
 func SetupNetwork(env Env, startIP net.IP, st *state) Task {
-	t := newLeafTask("Setup network", func() error {
+	t := newLeafTask("Setup private and public networks", func() error {
 		startAddr, err := netip.ParseAddr(startIP.String())
 		if err != nil {
 			return err
@@ -232,7 +232,7 @@
 		}
 		return nil
 	})
-	return newSequentialParentTask("Configure TLS certificate issuers", &pub, &priv)
+	return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
 }
 
 func SetupAuth(env Env, st *state) Task {
@@ -250,6 +250,7 @@
 	})
 	return newSequentialParentTask(
 		"Authentication services",
+		false,
 		&t,
 		waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
 	)
@@ -269,7 +270,8 @@
 		return nil
 	})
 	return newSequentialParentTask(
-		"Group Membership",
+		"Group membership",
+		false,
 		&t,
 		waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
 	)
@@ -290,7 +292,8 @@
 		return nil
 	})
 	return newSequentialParentTask(
-		"Headscale service",
+		"Setup mesh VPN",
+		false,
 		&t,
 		waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
 	)
@@ -323,6 +326,7 @@
 	})
 	return newSequentialParentTask(
 		"Welcome service",
+		false,
 		&t,
 		waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
 	)
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index f898a1f..e6dd47e 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -11,10 +11,23 @@
 
 func SetupConfigRepoTask(env Env, st *state) Task {
 	return newSequentialParentTask(
-		"Configuration repository",
-		NewCreateConfigRepoTask(env, st),
-		CreateGitClientTask(env, st),
+		"Configure Git repository for new environment",
+		true,
+		newSequentialParentTask(
+			"Start up Git server",
+			false,
+			NewCreateConfigRepoTask(env, st),
+			CreateGitClientTask(env, st),
+		),
 		NewInitConfigRepoTask(env, st),
+		NewActivateEnvTask(env, st),
+		newSequentialParentTask(
+			"Create initial commit",
+			false,
+			CreateRepoClient(env, st),
+			CommitEnvironmentConfiguration(env, st),
+			ConfigureFirstAccount(env, st),
+		),
 	)
 }
 
@@ -85,7 +98,7 @@
 }
 
 func NewInitConfigRepoTask(env Env, st *state) Task {
-	t := newLeafTask("Create Git repository for environment configuration", func() error {
+	t := newLeafTask("Configure access control lists", func() error {
 		st.fluxUserName = fmt.Sprintf("flux-%s", env.Name)
 		keys, err := installer.NewSSHKeyPair(st.fluxUserName)
 		if err != nil {
@@ -100,7 +113,23 @@
 			return err
 		}
 		repoIO := installer.NewRepoIO(repo, st.ssClient.Signer)
-		if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s PCloud environment", env.Name), "readme"); err != nil {
+		if err := func() error {
+			w, err := repoIO.Writer("README.md")
+			if err != nil {
+				return err
+			}
+			defer w.Close()
+			if _, err := fmt.Fprintf(w, "# %s PCloud environment", env.Name); err != nil {
+				return err
+			}
+			return nil
+		}(); err != nil {
+			return err
+		}
+		if err := repoIO.WriteKustomization("kustomization.yaml", installer.NewKustomization()); err != nil {
+			return err
+		}
+		if err := repoIO.CommitAndPush("init"); err != nil {
 			return err
 		}
 		if err := st.ssClient.AddUser(st.fluxUserName, keys.AuthorizedKey()); err != nil {
diff --git a/core/installer/tasks/tasks.go b/core/installer/tasks/tasks.go
index 75e8a68..6d0ec01 100644
--- a/core/installer/tasks/tasks.go
+++ b/core/installer/tasks/tasks.go
@@ -86,25 +86,31 @@
 
 type parentTask struct {
 	leafTask
-	subtasks []Task
+	subtasks     []Task
+	showChildren bool
 }
 
-func newParentTask(title string, start func() error, subtasks ...Task) parentTask {
+func newParentTask(title string, showChildren bool, start func() error, subtasks ...Task) parentTask {
 	return parentTask{
-		leafTask: newLeafTask(title, start),
-		subtasks: subtasks,
+		leafTask:     newLeafTask(title, start),
+		subtasks:     subtasks,
+		showChildren: showChildren,
 	}
 }
 
 func (t *parentTask) Subtasks() []Task {
-	return t.subtasks
+	if t.showChildren {
+		return t.subtasks
+	} else {
+		return make([]Task, 0)
+	}
 }
 
 type sequentialParentTask struct {
 	parentTask
 }
 
-func newSequentialParentTask(title string, subtasks ...Task) *sequentialParentTask {
+func newSequentialParentTask(title string, showChildren bool, subtasks ...Task) *sequentialParentTask {
 	start := func() error {
 		errCh := make(chan error)
 		for i := range subtasks[:len(subtasks)-1] {
@@ -124,7 +130,7 @@
 		return <-errCh
 	}
 	return &sequentialParentTask{
-		parentTask: newParentTask(title, start, subtasks...),
+		parentTask: newParentTask(title, showChildren, start, subtasks...),
 	}
 }
 
@@ -132,7 +138,7 @@
 	parentTask
 }
 
-func newConcurrentParentTask(title string, subtasks ...Task) *concurrentParentTask {
+func newConcurrentParentTask(title string, showChildren bool, subtasks ...Task) *concurrentParentTask {
 	start := func() error {
 		errCh := make(chan error)
 		for i := range subtasks {
@@ -151,6 +157,6 @@
 		return nil
 	}
 	return &concurrentParentTask{
-		parentTask: newParentTask(title, start, subtasks...),
+		parentTask: newParentTask(title, showChildren, start, subtasks...),
 	}
 }
diff --git a/core/installer/tasks/tasks_test.go b/core/installer/tasks/tasks_test.go
index 7aa78f3..194be44 100644
--- a/core/installer/tasks/tasks_test.go
+++ b/core/installer/tasks/tasks_test.go
@@ -27,7 +27,7 @@
 	two := newLeafTask("two", func() error {
 		return nil
 	})
-	l := newSequentialParentTask("parent", &one, &two)
+	l := newSequentialParentTask("parent", true, &one, &two)
 	done := make(chan error)
 	l.OnDone(func(err error) {
 		done <- err
@@ -46,7 +46,7 @@
 	two := newLeafTask("two", func() error {
 		return nil
 	})
-	l := newSequentialParentTask("parent", &one, &two)
+	l := newSequentialParentTask("parent", true, &one, &two)
 	done := make(chan error)
 	l.OnDone(func(err error) {
 		done <- err
@@ -67,7 +67,7 @@
 		fmt.Println("two")
 		return fmt.Errorf("two")
 	})
-	l := newSequentialParentTask("parent", &one, &two)
+	l := newSequentialParentTask("parent", true, &one, &two)
 	done := make(chan error)
 	l.OnDone(func(err error) {
 		done <- err