From ea7c2b331889d6d7adb32eac00856abc2ce17671 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Tue, 5 Nov 2024 15:23:32 +0100
Subject: [PATCH 01/23] # This is a combination of 39 commits. # This is the
 1st commit message:

feat: add prompt-ui

# This is the commit message #2:

feat: configure and set algod data directory

# This is the commit message #3:

fix: RX/TX display

# This is the commit message #4:

fix: bit rate display for GB

# This is the commit message #5:

fix: configuration override order

# This is the commit message #6:

feat: handle invalid configuration and token gracefully

# This is the commit message #7:

test: fix test state

# This is the commit message #8:

fix: loading of custom endpoint address

# This is the commit message #9:

fix: loading default port

# This is the commit message #10:

test: clear viper settings

# This is the commit message #11:

fix: finds path to directory and gives cmd instruction

# This is the commit message #12:

feat: adds node start and node stop commands

# This is the commit message #13:

fix: add -y

# This is the commit message #14:

fix: turn script into indivudal commands

# This is the commit message #15:

feat: check if sudo, clarify shell

# This is the commit message #16:

chore: make more go idiomatic

# This is the commit message #17:

fix: fix proper path check

# This is the commit message #18:

fix: interact with systemctl, cleanup prompts

# This is the commit message #19:

fix: remove sudo

# This is the commit message #20:

fix: separate commands

# This is the commit message #21:

fix: proper algorand service name

# This is the commit message #22:

fix: calling with sudo

# This is the commit message #23:

chore: testing systemctl

# This is the commit message #24:

fix: checks algorand system service has been enabled directly

# This is the commit message #25:

feat: implements editAlgorandServiceFile

# This is the commit message #26:

fix: else statement

# This is the commit message #27:

fix: quick check branch

# This is the commit message #28:

fix: string template

# This is the commit message #29:

feat: adds upgrade

# This is the commit message #30:

chore: removeu nnecessary code

# This is the commit message #31:

fix: check that installed and candidate are the same

# This is the commit message #32:

chore: improve print

# This is the commit message #33:

chore: add more output

# This is the commit message #34:

fix: single quote

# This is the commit message #35:

fix: -y

# This is the commit message #36:

fix: systemctl

# This is the commit message #37:

fix: upgrade and sudo text

# This is the commit message #38:

chore: go mod tidy

# This is the commit message #39:

fix: upgrade
---
 cmd/node.go                                   | 609 ++++++++++++++++++
 cmd/root.go                                   |  78 ++-
 cmd/root_test.go                              |  54 +-
 cmd/status.go                                 |   1 +
 .../{ => Test_InitConfig}/algod.admin.token   |   0
 .../{ => Test_InitConfig}/config.json         |   0
 .../algod.admin.token                         |   1 +
 .../Test_InitConfigWithAddress/config.json    |   4 +
 .../algod.admin.token                         |   1 +
 .../config.json                               |   4 +
 .../algod.admin.token                         |   1 +
 .../Test_InitConfigWithoutEndpoint/algod.net  |   1 +
 .../config.json                               |   2 +
 cmd/utils.go                                  | 340 ++++++++++
 go.mod                                        |   2 +
 go.sum                                        |   9 +
 internal/metrics.go                           |   3 +
 internal/state.go                             |  11 +-
 ui/status.go                                  |  20 +-
 19 files changed, 1121 insertions(+), 20 deletions(-)
 create mode 100644 cmd/node.go
 rename cmd/testdata/{ => Test_InitConfig}/algod.admin.token (100%)
 rename cmd/testdata/{ => Test_InitConfig}/config.json (100%)
 create mode 100644 cmd/testdata/Test_InitConfigWithAddress/algod.admin.token
 create mode 100644 cmd/testdata/Test_InitConfigWithAddress/config.json
 create mode 100644 cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
 create mode 100644 cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
 create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
 create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net
 create mode 100644 cmd/testdata/Test_InitConfigWithoutEndpoint/config.json
 create mode 100644 cmd/utils.go

diff --git a/cmd/node.go b/cmd/node.go
new file mode 100644
index 00000000..bb3bd6e5
--- /dev/null
+++ b/cmd/node.go
@@ -0,0 +1,609 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/algorandfoundation/hack-tui/ui/style"
+	"github.com/spf13/cobra"
+)
+
+var nodeCmd = &cobra.Command{
+	Use:   "node",
+	Short: "Algod installation",
+	Long:  style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
+}
+
+var installCmd = &cobra.Command{
+	Use:   "install",
+	Short: "Install Algod",
+	Long:  "Install Algod on your system",
+	Run: func(cmd *cobra.Command, args []string) {
+		installNode()
+	},
+}
+
+var configureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configure Algod",
+	Long:  "Configure Algod settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		configureNode()
+	},
+}
+
+var startCmd = &cobra.Command{
+	Use:   "start",
+	Short: "Start Algod",
+	Long:  "Start Algod on your system (the one on your PATH).",
+	Run: func(cmd *cobra.Command, args []string) {
+		startNode()
+	},
+}
+
+var stopCmd = &cobra.Command{
+	Use:   "stop",
+	Short: "Stop Algod",
+	Long:  "Stop the Algod process on your system.",
+	Run: func(cmd *cobra.Command, args []string) {
+		stopNode()
+	},
+}
+
+var upgradeCmd = &cobra.Command{
+	Use:   "upgrade",
+	Short: "Upgrade Algod",
+	Long:  "Upgrade Algod (if installed with package manager).",
+	Run: func(cmd *cobra.Command, args []string) {
+		upgradeAlgod()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeCmd)
+	nodeCmd.AddCommand(installCmd)
+	nodeCmd.AddCommand(configureCmd)
+	nodeCmd.AddCommand(startCmd)
+	nodeCmd.AddCommand(stopCmd)
+	nodeCmd.AddCommand(upgradeCmd)
+}
+
+func installNode() {
+	fmt.Println("Checking if Algod is installed...")
+
+	// Check if Algod is installed
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed. Installing...")
+
+		// Install Algod based on OS
+		switch runtime.GOOS {
+		case "linux":
+			installNodeLinux()
+		case "darwin":
+			installNodeMac()
+		default:
+			panic("Unsupported OS: " + runtime.GOOS)
+		}
+	} else {
+		fmt.Println("Algod is already installed.")
+		printAlgodInfo()
+	}
+
+}
+
+func installNodeLinux() {
+	fmt.Println("Installing Algod on Linux")
+
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	var installCmds [][]string
+	var postInstallHint string
+
+	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
+
+	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
+		fmt.Println("Using apt package manager")
+		installCmds = [][]string{
+			{"apt", "update"},
+			{"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
+			{"apt", "update"},
+			{"apt", "install", "-y", "algorand-devtools"},
+		}
+	} else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get
+		fmt.Println("Using apt-get package manager")
+		installCmds = [][]string{
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "algorand-devtools"},
+		}
+	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
+		fmt.Println("Using dnf package manager")
+		installCmds = [][]string{
+			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+			{"rpmkeys", "--import", "rpm_algorand.pub"},
+			{"dnf", "install", "-y", "dnf-command(config-manager)"},
+			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"dnf", "install", "-y", "algorand-devtools"},
+			{"systemctl", "start", "algorand"},
+		}
+	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
+		fmt.Println("Using yum package manager")
+		installCmds = [][]string{
+			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+			{"rpmkeys", "--import", "rpm_algorand.pub"},
+			{"yum", "install", "yum-utils"},
+			{"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"yum", "install", "-y", "algorand-devtools"},
+			{"systemctl", "start", "algorand"},
+		}
+	} else {
+		fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.")
+		installCmds = [][]string{
+			{"mkdir", "~/node"},
+			{"sh", "-c", "cd ~/node"},
+			{"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
+			{"chmod", "744", "update.sh"},
+			{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
+		}
+
+		postInstallHint = `You may need to add the Algorand binaries to your PATH:
+					export ALGORAND_DATA="$HOME/node/data"
+					export PATH="$HOME/node:$PATH"
+			`
+	}
+
+	// Run each installation command
+	for _, cmdArgs := range installCmds {
+		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
+		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
+			cobra.CheckErr(err)
+		}
+	}
+
+	if postInstallHint != "" {
+		fmt.Println(postInstallHint)
+	}
+}
+
+func installNodeMac() {
+	fmt.Println("Installing Algod on macOS...")
+
+	// Based off of the macOS installation instructions
+	// https://developer.algorand.org/docs/run-a-node/setup/install/#installing-on-mac
+
+	installCmd := `mkdir ~/node
+		cd ~/node
+		wget https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh
+		chmod 744 update.sh
+		./update.sh -i -c stable -p ~/node -d ~/node/data -n`
+
+	postInstallHint := `You may need to add the Algorand binaries to your PATH:
+		export ALGORAND_DATA="$HOME/node/data"
+		export PATH="$HOME/node:$PATH"
+	`
+
+	// Run the installation command
+	err := exec.Command(installCmd).Run()
+	cobra.CheckErr(err)
+
+	if postInstallHint != "" {
+		fmt.Println(postInstallHint)
+	}
+}
+
+// TODO: configure not just data directory but algod path
+func configureNode() {
+	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
+		panic("Unsupported OS: " + runtime.GOOS)
+	}
+
+	var systemctlConfigure bool
+
+	// Check systemctl first
+	if checkSystemctlAlgorandServiceCreated() {
+		if !isRunningWithSudo() {
+			fmt.Println("This command must be run with super-user priviledges (sudo).")
+			os.Exit(1)
+		}
+
+		if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") {
+			if checkSystemctlAlgorandServiceActive() {
+				fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.")
+				os.Exit(1)
+			}
+			// Edit the service file with the user's new data directory
+			systemctlConfigure = true
+		} else {
+			fmt.Println("Exiting...")
+			os.Exit(0)
+		}
+	}
+
+	// At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file
+	// i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf
+	// ExecStart and Description will be changed to reflect the new data directory
+	//
+
+	if !systemctlConfigure {
+		fmt.Println("Configuring Data directory for algod started through Algorun...")
+	}
+
+	algorandData := os.Getenv("ALGORAND_DATA")
+
+	// Check if ALGORAND_DATA environment variable is set
+	if algorandData != "" {
+		fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData)
+		fmt.Println("Inspecting the set data directory...")
+
+		if validateAlgorandDataDir(algorandData) {
+			fmt.Println("Found valid Algorand Data Directory: " + algorandData)
+
+			if systemctlConfigure {
+				if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") {
+					editAlgorandServiceFile(algorandData)
+					os.Exit(0)
+				}
+			}
+
+			if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") {
+				fmt.Println("User chose not to set a completely new data directory.")
+				os.Exit(0)
+			}
+
+			if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") {
+				newPath := promptWrapperInput("Enter the new data directory path")
+
+				if !validateAlgorandDataDir(newPath) {
+					fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.")
+					os.Exit(1)
+				}
+
+				if systemctlConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(newPath)
+				} else {
+					// Affect the ALGORAND_DATA environment variable
+					affectALGORAND_DATA(newPath)
+				}
+				os.Exit(0)
+			}
+		} else {
+			fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.")
+		}
+	} else {
+		fmt.Println("ALGORAND_DATA environment variable not set.")
+	}
+
+	// Do quick "lazy" check for existing Algorand Data directories
+	paths := lazyCheckAlgorandDataDirs()
+
+	if len(paths) != 0 {
+
+		fmt.Println("Quick check found the following potential data directories:")
+		for _, path := range paths {
+			fmt.Println("✔ " + path)
+		}
+
+		if len(paths) == 1 {
+			if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") {
+				if systemctlConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(paths[0])
+				} else {
+					affectALGORAND_DATA(paths[0])
+				}
+				os.Exit(0)
+			}
+
+		} else {
+
+			if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") {
+
+				selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+				if systemctlConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(selectedPath)
+				} else {
+					affectALGORAND_DATA(selectedPath)
+				}
+				os.Exit(0)
+			}
+		}
+	}
+
+	// Deep search
+	if promptWrapperNo("Do you want Algorun to do a deep search for pre-existing Algorand Data directories? (y/n)") {
+		fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...")
+		os.Exit(0)
+	}
+
+	fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...")
+	paths = deepSearchAlgorandDataDirs()
+
+	if len(paths) == 0 {
+		fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.")
+		os.Exit(1)
+	}
+
+	fmt.Println("Found Algorand data directories:")
+	for _, path := range paths {
+		fmt.Println(path)
+	}
+
+	// Prompt user to select a directory
+	selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+	if systemctlConfigure {
+		editAlgorandServiceFile(selectedPath)
+	} else {
+		affectALGORAND_DATA(selectedPath)
+	}
+	os.Exit(0)
+}
+
+// Start Algod on your system (the one on your PATH).
+func startNode() {
+	fmt.Println("Attempting to start Algod...")
+
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed. Please run the node install command.")
+		os.Exit(1)
+	}
+
+	// Check if Algod is already running
+	if isAlgodRunning() {
+		fmt.Println("Algod is already running.")
+		os.Exit(0)
+	}
+
+	startAlgodProcess()
+}
+
+func isAlgodRunning() bool {
+	// Check if Algod is already running
+	// This works for systemctl started algorand.service as well as directly started algod
+	err := exec.Command("pgrep", "algod").Run()
+	return err == nil
+}
+
+func startAlgodProcess() {
+	// Check if algod is available as a systemctl service
+	if checkSystemctlAlgorandServiceCreated() {
+		// Algod is available as a systemd service, start it using systemctl
+
+		if !isRunningWithSudo() {
+			fmt.Println("This command must be run with super-user priviledges (sudo).")
+			os.Exit(1)
+		}
+
+		fmt.Println("Starting algod using systemctl...")
+		cmd := exec.Command("systemctl", "start", "algorand")
+		err := cmd.Run()
+		if err != nil {
+			fmt.Printf("Failed to start algod service: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		// Algod is not available as a systemd service, start it directly
+		fmt.Println("Starting algod directly...")
+
+		// Check if ALGORAND_DATA environment variable is set
+		fmt.Println("Checking if ALGORAND_DATA env var is set...")
+		algorandData := os.Getenv("ALGORAND_DATA")
+
+		if !validateAlgorandDataDir(algorandData) {
+			fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
+			os.Exit(1)
+		}
+
+		fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
+
+		cmd := exec.Command("algod")
+		cmd.SysProcAttr = &syscall.SysProcAttr{
+			Setsid: true,
+		}
+		err := cmd.Start()
+		if err != nil {
+			fmt.Printf("Failed to start algod: %v\n", err)
+			os.Exit(1)
+		}
+	}
+
+	// Wait for the process to start
+	time.Sleep(5 * time.Second)
+
+	if isAlgodRunning() {
+		fmt.Println("Algod is running.")
+	} else {
+		fmt.Println("Algod failed to start.")
+	}
+}
+
+// Stop the Algod process on your system.
+func stopNode() {
+	fmt.Println("Attempting to stop Algod...")
+
+	if !isAlgodRunning() {
+		fmt.Println("Algod was not running.")
+		os.Exit(0)
+	}
+
+	stopAlgodProcess()
+
+	time.Sleep(5 * time.Second)
+
+	if !isAlgodRunning() {
+		fmt.Println("Algod is no longer running.")
+		os.Exit(0)
+	}
+
+	fmt.Println("Failed to stop Algod.")
+	os.Exit(1)
+}
+
+func stopAlgodProcess() {
+	// Check if algod is available as a systemd service
+	if checkSystemctlAlgorandServiceCreated() {
+		if !isRunningWithSudo() {
+			fmt.Println("This command must be run with super-user priviledges (sudo).")
+			os.Exit(1)
+		}
+
+		// Algod is available as a systemd service, stop it using systemctl
+		fmt.Println("Stopping algod using systemctl...")
+		cmd := exec.Command("systemctl", "stop", "algorand")
+		err := cmd.Run()
+		if err != nil {
+			fmt.Printf("Failed to stop algod service: %v\n", err)
+			cobra.CheckErr(err)
+		}
+		fmt.Println("Algod service stopped.")
+	} else {
+		// Algod is not available as a systemd service, stop it directly
+		fmt.Println("Stopping algod directly...")
+		// Find the process ID of algod
+		pid, err := findAlgodPID()
+		if err != nil {
+			fmt.Printf("Failed to find algod process: %v\n", err)
+			cobra.CheckErr(err)
+		}
+
+		// Send SIGTERM to the process
+		process, err := os.FindProcess(pid)
+		if err != nil {
+			fmt.Printf("Failed to find process with PID %d: %v\n", pid, err)
+			cobra.CheckErr(err)
+		}
+
+		err = process.Signal(syscall.SIGTERM)
+		if err != nil {
+			fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
+			cobra.CheckErr(err)
+		}
+
+		fmt.Println("Sent SIGTERM to algod process.")
+	}
+}
+
+// Upgrade ALGOD (if installed with package manager).
+func upgradeAlgod() {
+
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed. Please run the node install command.")
+		os.Exit(1)
+	}
+
+	// Check if Algod was installed with apt/apt-get
+	if checkCmdToolExists("apt") {
+		upgradeDebianPackage("apt", "algorand-devtools")
+	} else if checkCmdToolExists("apt-get") {
+		upgradeDebianPackage("apt-get", "algorand-devtools")
+	} else if checkCmdToolExists("dnf") {
+		upgradeRpmPackage("dnf", "algorand-devtools")
+	} else if checkCmdToolExists("yum") {
+		upgradeRpmPackage("yum", "algorand-devtools")
+	} else {
+		fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.")
+		os.Exit(1)
+	}
+}
+
+// Upgrade a package using the specified Debian package manager
+func upgradeDebianPackage(packageManager, packageName string) {
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	// Check if the package is installed and if there are updates available using apt-cache policy
+	cmd := exec.Command("apt-cache", "policy", packageName)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Printf("Failed to check package policy: %v\n", err)
+		os.Exit(1)
+	}
+
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Installed: (none)") {
+		fmt.Printf("Package %s is not installed.\n", packageName)
+		os.Exit(1)
+	}
+
+	installedVersion := extractVersion(outputStr, "Installed:")
+	candidateVersion := extractVersion(outputStr, "Candidate:")
+
+	if installedVersion == candidateVersion {
+		fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion)
+		os.Exit(0)
+	}
+
+	fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion)
+
+	// Update the package list
+	fmt.Println("Updating package list...")
+	cmd = exec.Command(packageManager, "update")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to update package list: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Upgrade the package
+	fmt.Printf("Upgrading package %s...\n", packageName)
+	cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
+		os.Exit(1)
+	}
+
+	fmt.Printf("Package %s upgraded successfully.\n", packageName)
+	os.Exit(0)
+}
+
+// Upgrade a package using the specified RPM package manager
+func upgradeRpmPackage(packageManager, packageName string) {
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with sudo.")
+		os.Exit(1)
+	}
+
+	// Attempt to upgrade the package directly
+	fmt.Printf("Upgrading package %s...\n", packageName)
+	cmd := exec.Command(packageManager, "update", "-y", packageName)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
+		os.Exit(1)
+	}
+
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Nothing to do") {
+		fmt.Printf("Package %s is already up-to-date.\n", packageName)
+		os.Exit(0)
+	} else {
+		fmt.Println(outputStr)
+		fmt.Printf("Package %s upgraded successfully.\n", packageName)
+		os.Exit(0)
+	}
+}
diff --git a/cmd/root.go b/cmd/root.go
index 1f04e8e9..f50bc7a0 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"github.com/algorandfoundation/hack-tui/api"
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/algorandfoundation/hack-tui/ui"
@@ -39,11 +40,24 @@ var (
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			log.SetOutput(cmd.OutOrStdout())
+			initConfig()
+
+			if viper.GetString("server") == "" {
+				return fmt.Errorf(style.Red.Render("server is required"))
+			}
+			if viper.GetString("token") == "" {
+				return fmt.Errorf(style.Red.Render("token is required"))
+			}
+
 			client, err := getClient()
 			cobra.CheckErr(err)
 
 			partkeys, err := internal.GetPartKeys(context.Background(), client)
-			cobra.CheckErr(err)
+			if err != nil {
+				return fmt.Errorf(
+					style.Red.Render("failed to get participation keys: %s"),
+					err)
+			}
 
 			state := internal.StateModel{
 				Status: internal.StatusModel{
@@ -106,7 +120,7 @@ func check(err interface{}) {
 // Handle global flags and set usage templates
 func init() {
 	log.SetReportTimestamp(false)
-	initConfig()
+
 	// Configure Version
 	if Version == "" {
 		Version = "unknown (built from source)"
@@ -142,6 +156,15 @@ type AlgodConfig struct {
 	EndpointAddress string `json:"EndpointAddress"`
 }
 
+func replaceEndpointUrl(s string) string {
+	s = strings.Replace(s, "\n", "", 1)
+	s = strings.Replace(s, "0.0.0.0", "127.0.0.1", 1)
+	s = strings.Replace(s, "[::]", "127.0.0.1", 1)
+	return s
+}
+func hasWildcardEndpointUrl(s string) bool {
+	return strings.Contains(s, "0.0.0.0") || strings.Contains(s, "::")
+}
 func initConfig() {
 	// Find home directory.
 	home, err := os.UserHomeDir()
@@ -159,12 +182,17 @@ func initConfig() {
 
 	// Load Configurations
 	viper.AutomaticEnv()
-	err = viper.ReadInConfig()
+	_ = viper.ReadInConfig()
+
+	// Check for server
+	loadedServer := viper.GetString("server")
+	loadedToken := viper.GetString("token")
+
 	// Load ALGORAND_DATA/config.json
 	algorandData, exists := os.LookupEnv("ALGORAND_DATA")
 
 	// Load the Algorand Data Configuration
-	if exists && algorandData != "" {
+	if exists && algorandData != "" && loadedServer == "" {
 		// Placeholder for Struct
 		var algodConfig AlgodConfig
 
@@ -183,23 +211,43 @@ func initConfig() {
 		err = configFile.Close()
 		check(err)
 
-		// Replace catchall address with localhost
-		if strings.Contains(algodConfig.EndpointAddress, "0.0.0.0") {
-			algodConfig.EndpointAddress = strings.Replace(algodConfig.EndpointAddress, "0.0.0.0", "127.0.0.1", 1)
+		// Check for endpoint address
+		if hasWildcardEndpointUrl(algodConfig.EndpointAddress) {
+			algodConfig.EndpointAddress = replaceEndpointUrl(algodConfig.EndpointAddress)
+		} else if algodConfig.EndpointAddress == "" {
+			// Assume it is not set, try to discover the port from the network file
+			networkPath := algorandData + "/algod.net"
+			networkFile, err := os.Open(networkPath)
+			check(err)
+
+			byteValue, err = io.ReadAll(networkFile)
+			check(err)
+
+			if hasWildcardEndpointUrl(string(byteValue)) {
+				algodConfig.EndpointAddress = replaceEndpointUrl(string(byteValue))
+			} else {
+				algodConfig.EndpointAddress = string(byteValue)
+			}
+
 		}
+		if strings.Contains(algodConfig.EndpointAddress, ":0") {
+			algodConfig.EndpointAddress = strings.Replace(algodConfig.EndpointAddress, ":0", ":8080", 1)
+		}
+		if loadedToken == "" {
+			// Handle Token Path
+			tokenPath := algorandData + "/algod.admin.token"
 
-		// Handle Token Path
-		tokenPath := algorandData + "/algod.admin.token"
+			tokenFile, err := os.Open(tokenPath)
+			check(err)
 
-		tokenFile, err := os.Open(tokenPath)
-		check(err)
+			byteValue, err = io.ReadAll(tokenFile)
+			check(err)
 
-		byteValue, err = io.ReadAll(tokenFile)
-		check(err)
+			viper.Set("token", strings.Replace(string(byteValue), "\n", "", 1))
+		}
 
 		// Set the server configuration
-		viper.Set("server", "http://"+algodConfig.EndpointAddress)
-		viper.Set("token", string(byteValue))
+		viper.Set("server", "http://"+strings.Replace(algodConfig.EndpointAddress, "\n", "", 1))
 		viper.Set("data", dataConfigPath)
 	}
 
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 0b7b8b7e..7cae6f28 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -21,12 +21,64 @@ func Test_ExecuteRootCommand(t *testing.T) {
 
 func Test_InitConfig(t *testing.T) {
 	cwd, _ := os.Getwd()
-	t.Setenv("ALGORAND_DATA", cwd+"/testdata")
+	viper.Set("token", "")
+	viper.Set("server", "")
+	t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfig")
 
 	initConfig()
 	server := viper.Get("server")
 	if server == "" {
 		t.Fatal("Invalid Server")
 	}
+	if server != "http://127.0.0.1:8080" {
+		t.Fatal("Invalid Server")
+	}
+}
+
+func Test_InitConfigWithoutEndpoint(t *testing.T) {
+	cwd, _ := os.Getwd()
+	viper.Set("token", "")
+	viper.Set("server", "")
+	t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithoutEndpoint")
+
+	initConfig()
+	server := viper.Get("server")
+	if server == "" {
+		t.Fatal("Invalid Server")
+	}
+	if server != "http://127.0.0.1:8080" {
+		t.Fatal("Invalid Server")
+	}
+}
+
+func Test_InitConfigWithAddress(t *testing.T) {
+	cwd, _ := os.Getwd()
+	viper.Set("token", "")
+	viper.Set("server", "")
+	t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddress")
+
+	initConfig()
+	server := viper.Get("server")
+	if server == "" {
+		t.Fatal("Invalid Server")
+	}
+	if server != "http://255.255.255.255:8080" {
+		t.Fatal("Invalid Server")
+	}
+}
 
+func Test_InitConfigWithAddressAndDefaultPort(t *testing.T) {
+	cwd, _ := os.Getwd()
+	viper.Set("token", "")
+	viper.Set("server", "")
+	t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddressAndDefaultPort")
+
+	initConfig()
+	server := viper.Get("server")
+	if server == "" {
+		t.Fatal("Invalid Server")
+	}
+	if server != "http://255.255.255.255:8080" {
+		t.Fatal("Invalid Server")
+	}
 }
diff --git a/cmd/status.go b/cmd/status.go
index 3bff01e3..5af7f38b 100644
--- a/cmd/status.go
+++ b/cmd/status.go
@@ -19,6 +19,7 @@ var statusCmd = &cobra.Command{
 	Short: "Get the node status",
 	Long:  style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
 	RunE: func(cmd *cobra.Command, args []string) error {
+		initConfig()
 		if viper.GetString("server") == "" {
 			return errors.New(style.Magenta("server is required"))
 		}
diff --git a/cmd/testdata/algod.admin.token b/cmd/testdata/Test_InitConfig/algod.admin.token
similarity index 100%
rename from cmd/testdata/algod.admin.token
rename to cmd/testdata/Test_InitConfig/algod.admin.token
diff --git a/cmd/testdata/config.json b/cmd/testdata/Test_InitConfig/config.json
similarity index 100%
rename from cmd/testdata/config.json
rename to cmd/testdata/Test_InitConfig/config.json
diff --git a/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token b/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token
new file mode 100644
index 00000000..71b7a719
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithAddress/config.json b/cmd/testdata/Test_InitConfigWithAddress/config.json
new file mode 100644
index 00000000..f172cd52
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithAddress/config.json
@@ -0,0 +1,4 @@
+{
+  "EndpointAddress": "255.255.255.255:8080",
+  "OtherKey": ""
+}
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
new file mode 100644
index 00000000..71b7a719
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
new file mode 100644
index 00000000..0e455f6b
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
@@ -0,0 +1,4 @@
+{
+  "EndpointAddress": "255.255.255.255:0",
+  "OtherKey": ""
+}
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
new file mode 100644
index 00000000..71b7a719
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net
new file mode 100644
index 00000000..7985b074
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net
@@ -0,0 +1 @@
+[::]:8080
\ No newline at end of file
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json b/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json
new file mode 100644
index 00000000..7a73a41b
--- /dev/null
+++ b/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json
@@ -0,0 +1,2 @@
+{
+}
\ No newline at end of file
diff --git a/cmd/utils.go b/cmd/utils.go
new file mode 100644
index 00000000..b3c56acf
--- /dev/null
+++ b/cmd/utils.go
@@ -0,0 +1,340 @@
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"sync"
+	"text/template"
+
+	"github.com/manifoldco/promptui"
+	"github.com/spf13/cobra"
+)
+
+type Release struct {
+	Name       string `json:"name"`
+	ZipballURL string `json:"zipball_url"`
+	TarballURL string `json:"tarball_url"`
+	Commit     struct {
+		Sha string `json:"sha"`
+		URL string `json:"url"`
+	} `json:"commit"`
+	NodeID string `json:"node_id"`
+}
+
+// Queries user on the provided prompt and returns the user input
+func promptWrapperInput(promptLabel string) string {
+	prompt := promptui.Prompt{
+		Label: promptLabel,
+	}
+
+	result, err := prompt.Run()
+	cobra.CheckErr(err)
+
+	return result
+}
+
+// Queries user on the provided prompt and returns true if user inputs "y"
+func promptWrapperYes(promptLabel string) bool {
+	return promptWrapperInput(promptLabel) == "y"
+}
+
+// Queries user on the provided prompt and returns true if user does not input "y"
+// Included for improved readability of decision tree, despite being redundant.
+func promptWrapperNo(promptLabel string) bool {
+	return promptWrapperInput(promptLabel) != "y"
+}
+
+// Queries user on the provided prompt and returns the selected item
+func promptWrapperSelection(promptLabel string, items []string) string {
+	prompt := promptui.Select{
+		Label: promptLabel,
+		Items: items,
+	}
+
+	_, result, err := prompt.Run()
+	cobra.CheckErr(err)
+
+	fmt.Printf("You selected: %s\n", result)
+
+	return result
+}
+
+// Check if Algod is installed
+func isAlgodInstalled() bool {
+	if runtime.GOOS == "windows" {
+		panic("Windows is not supported.")
+	}
+
+	return checkCmdToolExists("algod")
+}
+
+// Checks that a bash cli/cmd tool exists
+func checkCmdToolExists(tool string) bool {
+	_, err := exec.LookPath(tool)
+	return err == nil
+}
+
+// Find where algod is defined and print its version
+func printAlgodInfo() {
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Error finding algod: %v\n", err)
+		return
+	}
+
+	// Get algod version
+	algodVersion, err := exec.Command("algod", "-v").Output()
+	if err != nil {
+		fmt.Printf("Error getting algod version: %v\n", err)
+		return
+	}
+
+	fmt.Printf("Algod is located at: %s\n", algodPath)
+	fmt.Printf("algod -v\n")
+	fmt.Printf("%s\n", algodVersion)
+}
+
+// TODO: consider replacing with a method that does more for the user
+func affectALGORAND_DATA(path string) {
+	fmt.Println("Please execute the following in your terminal to set the environment variable:")
+	fmt.Println("")
+	fmt.Println("export ALGORAND_DATA=" + path)
+	fmt.Println("")
+}
+
+// Update the algorand.service file
+func editAlgorandServiceFile(dataDirectoryPath string) {
+
+	// TODO: look into setting algod path as well as the data directory path
+	// Find the path to the algod binary
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Failed to find algod binary: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Path to the systemd service override file
+	// Assuming that this is the same everywhere systemd is used
+	overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
+
+	// Create the override directory if it doesn't exist
+	err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
+	if err != nil {
+		fmt.Printf("Failed to create override directory: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Content of the override file
+	const overrideTemplate = `[Unit]
+Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
+[Service]
+ExecStart=
+ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
+
+	// Data to fill the template
+	data := map[string]string{
+		"AlgodPath":         algodPath,
+		"DataDirectoryPath": dataDirectoryPath,
+	}
+
+	// Parse and execute the template
+	tmpl, err := template.New("override").Parse(overrideTemplate)
+	if err != nil {
+		fmt.Printf("Failed to parse template: %v\n", err)
+		os.Exit(1)
+	}
+
+	var overrideContent bytes.Buffer
+	err = tmpl.Execute(&overrideContent, data)
+	if err != nil {
+		fmt.Printf("Failed to execute template: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Write the override content to the file
+	err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write override file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Reload systemd manager configuration
+	cmd := exec.Command("systemctl", "daemon-reload")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to reload systemd daemon: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Algorand service file updated successfully.")
+}
+
+// Check if the program is running with admin (super-user) priviledges
+func isRunningWithSudo() bool {
+	return os.Geteuid() == 0
+}
+
+// Finds path(s) to a file in a directory and its subdirectories using parallel processing
+func findPathToFile(startDir string, targetFileName string) []string {
+	var dirPaths []string
+	var mu sync.Mutex
+	var wg sync.WaitGroup
+
+	fileChan := make(chan string)
+
+	// Worker function to process files
+	worker := func() {
+		defer wg.Done()
+		for path := range fileChan {
+			info, err := os.Stat(path)
+			if err != nil {
+				continue
+			}
+			if !info.IsDir() && info.Name() == targetFileName {
+				dirPath := filepath.Dir(path)
+				mu.Lock()
+				dirPaths = append(dirPaths, dirPath)
+				mu.Unlock()
+			}
+		}
+	}
+
+	// Start worker goroutines
+	numWorkers := 4 // Adjust the number of workers based on your system's capabilities
+	for i := 0; i < numWorkers; i++ {
+		wg.Add(1)
+		go worker()
+	}
+
+	// Walk the directory tree and send file paths to the channel
+	err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			// Ignore permission errors
+			if os.IsPermission(err) {
+				return nil
+			}
+			return err
+		}
+		fileChan <- path
+		return nil
+	})
+
+	close(fileChan)
+	wg.Wait()
+
+	if err != nil {
+		panic(err)
+	}
+
+	return dirPaths
+}
+
+func validateAlgorandDataDir(path string) bool {
+	info, err := os.Stat(path)
+
+	// Check if the path exists
+	if os.IsNotExist(err) {
+		return false
+	}
+
+	// Check if the path is a directory
+	if !info.IsDir() {
+		return false
+	}
+
+	paths := findPathToFile(path, "algod.token")
+	if len(paths) == 1 {
+		return true
+	}
+	return false
+}
+
+// Does a lazy check for Algorand data directories, based off of known common paths
+func lazyCheckAlgorandDataDirs() []string {
+	home, err := os.UserHomeDir()
+	cobra.CheckErr(err)
+
+	// Hardcoded paths known to be common Algorand data directories
+	commonAlgorandDataDirPaths := []string{
+		"/var/lib/algorand",
+		filepath.Join(home, "node", "data"),
+		filepath.Join(home, ".algorand"),
+	}
+
+	var paths []string
+
+	for _, path := range commonAlgorandDataDirPaths {
+		if validateAlgorandDataDir(path) {
+			paths = append(paths, path)
+		}
+	}
+
+	return paths
+}
+
+// Checks if Algorand data directories exist, based off of existence of the "algod.token" file
+func deepSearchAlgorandDataDirs() []string {
+	home, err := os.UserHomeDir()
+	cobra.CheckErr(err)
+
+	// TODO: consider a better way to identify an Algorand data directory
+	paths := findPathToFile(home, "algod.token")
+
+	return paths
+}
+
+func findAlgodPID() (int, error) {
+	cmd := exec.Command("pgrep", "algod")
+	output, err := cmd.Output()
+	if err != nil {
+		return 0, err
+	}
+
+	var pid int
+	_, err = fmt.Sscanf(string(output), "%d", &pid)
+	if err != nil {
+		return 0, fmt.Errorf("failed to parse PID: %v", err)
+	}
+
+	return pid, nil
+}
+
+// Check systemctl has Algorand Service been created in the first place
+func checkSystemctlAlgorandServiceCreated() bool {
+	cmd := exec.Command("systemctl", "list-unit-files", "algorand.service")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		return false
+	}
+	return strings.Contains(out.String(), "algorand.service")
+}
+
+func checkSystemctlAlgorandServiceActive() bool {
+	cmd := exec.Command("systemctl", "is-active", "algorand")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		return false
+	}
+	return strings.TrimSpace(out.String()) == "active"
+}
+
+// Extract version information from apt-cache policy output
+func extractVersion(output, prefix string) string {
+	lines := strings.Split(output, "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if strings.HasPrefix(line, prefix) {
+			return strings.TrimSpace(strings.TrimPrefix(line, prefix))
+		}
+	}
+	return ""
+}
diff --git a/go.mod b/go.mod
index ebabe667..7bac0b36 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/charmbracelet/lipgloss v0.13.1
 	github.com/charmbracelet/log v0.4.0
 	github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691
+	github.com/manifoldco/promptui v0.9.0
 	github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
 	github.com/oapi-codegen/runtime v1.1.1
 	github.com/spf13/cobra v1.8.1
@@ -19,6 +20,7 @@ require (
 )
 
 require (
+	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
diff --git a/go.sum b/go.sum
index acaa1e37..0b57cc4a 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,12 @@ github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691 h1:xiY
 github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691/go.mod h1:ektxP4TiEONm1mTGILRfo8F0a4rZMwsT1fEkXslQKtU=
 github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
 github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -68,6 +74,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
+github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -137,6 +145,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/metrics.go b/internal/metrics.go
index d2403094..b10a9f40 100644
--- a/internal/metrics.go
+++ b/internal/metrics.go
@@ -17,6 +17,9 @@ type MetricsModel struct {
 	TPS       float64
 	RX        int
 	TX        int
+	LastTS    time.Time
+	LastRX    int
+	LastTX    int
 }
 
 type MetricsResponse map[string]int
diff --git a/internal/state.go b/internal/state.go
index c20508c0..e14c07aa 100644
--- a/internal/state.go
+++ b/internal/state.go
@@ -91,8 +91,15 @@ func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client *api.Clien
 	}
 	if err == nil {
 		s.Metrics.Enabled = true
-		s.Metrics.TX = res["algod_network_sent_bytes_total"]
-		s.Metrics.RX = res["algod_network_received_bytes_total"]
+		now := time.Now()
+		diff := now.Sub(s.Metrics.LastTS)
+
+		s.Metrics.TX = max(0, int(float64(res["algod_network_sent_bytes_total"]-s.Metrics.LastTX)/diff.Seconds()))
+		s.Metrics.RX = max(0, int(float64(res["algod_network_received_bytes_total"]-s.Metrics.LastRX)/diff.Seconds()))
+
+		s.Metrics.LastTS = now
+		s.Metrics.LastTX = res["algod_network_sent_bytes_total"]
+		s.Metrics.LastRX = res["algod_network_received_bytes_total"]
 	}
 }
 func (s *StateModel) UpdateAccounts(client *api.ClientWithResponses) {
diff --git a/ui/status.go b/ui/status.go
index 281288bf..451a5758 100644
--- a/ui/status.go
+++ b/ui/status.go
@@ -6,6 +6,7 @@ import (
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
+	"math"
 	"strconv"
 	"strings"
 	"time"
@@ -44,6 +45,21 @@ func (m StatusViewModel) HandleMessage(msg tea.Msg) (StatusViewModel, tea.Cmd) {
 	return m, nil
 }
 
+func getBitRate(bytes int) string {
+	txString := fmt.Sprintf("%d B/s ", bytes)
+	if bytes >= 1024 {
+		txString = fmt.Sprintf("%d KB/s ", bytes/(1<<10))
+	}
+	if bytes >= int(math.Pow(1024, 2)) {
+		txString = fmt.Sprintf("%d MB/s ", bytes/(1<<20))
+	}
+	if bytes >= int(math.Pow(1024, 3)) {
+		txString = fmt.Sprintf("%d GB/s ", bytes/(1<<30))
+	}
+
+	return txString
+}
+
 // View handles the render cycle
 func (m StatusViewModel) View() string {
 	if !m.IsVisible {
@@ -70,13 +86,13 @@ func (m StatusViewModel) View() string {
 	row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
 
 	beginning = style.Blue.Render(" Round time: ") + fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second))
-	end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + style.Green.Render("TX ")
+	end = getBitRate(m.Data.Metrics.TX) + style.Green.Render("TX ")
 	middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
 
 	row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
 
 	beginning = style.Blue.Render(" TPS: ") + fmt.Sprintf("%.2f", m.Data.Metrics.TPS)
-	end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + style.Green.Render("RX ")
+	end = getBitRate(m.Data.Metrics.RX) + style.Green.Render("RX ")
 	middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
 
 	row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)

From 90d42dfd0270f3ae890499025a10f81827ac3c8b Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 15:00:27 +0100
Subject: [PATCH 02/23] # This is a combination of 46 commits. # This is the
 1st commit message:

feat: add prompt-ui

# This is the commit message #2:

feat: configure and set algod data directory

# This is the commit message #3:

fix: RX/TX display

# This is the commit message #4:

fix: bit rate display for GB

# This is the commit message #5:

fix: configuration override order

# This is the commit message #6:

feat: handle invalid configuration and token gracefully

# This is the commit message #7:

test: fix test state

# This is the commit message #8:

fix: loading of custom endpoint address

# This is the commit message #9:

fix: loading default port

# This is the commit message #10:

test: clear viper settings

# This is the commit message #11:

fix: finds path to directory and gives cmd instruction

# This is the commit message #12:

feat: adds node start and node stop commands

# This is the commit message #13:

fix: add -y

# This is the commit message #14:

fix: turn script into indivudal commands

# This is the commit message #15:

feat: check if sudo, clarify shell

# This is the commit message #16:

chore: make more go idiomatic

# This is the commit message #17:

fix: fix proper path check

# This is the commit message #18:

fix: interact with systemctl, cleanup prompts

# This is the commit message #19:

fix: remove sudo

# This is the commit message #20:

fix: separate commands

# This is the commit message #21:

fix: proper algorand service name

# This is the commit message #22:

fix: calling with sudo

# This is the commit message #23:

chore: testing systemctl

# This is the commit message #24:

fix: checks algorand system service has been enabled directly

# This is the commit message #25:

feat: implements editAlgorandServiceFile

# This is the commit message #26:

fix: else statement

# This is the commit message #27:

fix: quick check branch

# This is the commit message #28:

fix: string template

# This is the commit message #29:

feat: adds upgrade

# This is the commit message #30:

chore: removeu nnecessary code

# This is the commit message #31:

fix: check that installed and candidate are the same

# This is the commit message #32:

chore: improve print

# This is the commit message #33:

chore: add more output

# This is the commit message #34:

fix: single quote

# This is the commit message #35:

fix: -y

# This is the commit message #36:

fix: systemctl

# This is the commit message #37:

fix: upgrade and sudo text

# This is the commit message #38:

chore: go mod tidy

# This is the commit message #39:

fix: upgrade

# This is the commit message #40:

feat: disable ui elements while syncing

# This is the commit message #41:

feat: skip account loading on syncing
feat: remove offline account expires date

# This is the commit message #42:

feat: installs algod and sets up service on mac

# This is the commit message #43:

feat: refactor, + mac

# This is the commit message #44:

feat: adds uninstall, mac only

# This is the commit message #45:

fix: remove plist file

# This is the commit message #46:

chore: rename
---
 .../workflows/{test.yaml => code_test.yaml}   |   2 +-
 cmd/node.go                                   | 609 ------------------
 cmd/node/configure.go                         | 330 ++++++++++
 cmd/node/install.go                           | 344 ++++++++++
 cmd/node/main.go                              |  21 +
 cmd/node/start.go                             | 118 ++++
 cmd/node/stop.go                              | 111 ++++
 cmd/node/uninstall.go                         |  99 +++
 cmd/node/upgrade.go                           | 175 +++++
 cmd/{ => node}/utils.go                       | 145 ++---
 cmd/root.go                                   |  20 +-
 cmd/root_test.go                              |   3 +-
 cmd/status.go                                 |   5 +-
 internal/accounts.go                          |  25 +-
 internal/state.go                             |   5 +
 ui/pages/accounts/controller.go               |   2 +-
 ui/pages/accounts/model.go                    |  26 +-
 ui/status.go                                  |  25 +-
 ui/style/style.go                             |  12 +-
 ui/viewport.go                                |  31 +-
 20 files changed, 1367 insertions(+), 741 deletions(-)
 rename .github/workflows/{test.yaml => code_test.yaml} (98%)
 delete mode 100644 cmd/node.go
 create mode 100644 cmd/node/configure.go
 create mode 100644 cmd/node/install.go
 create mode 100644 cmd/node/main.go
 create mode 100644 cmd/node/start.go
 create mode 100644 cmd/node/stop.go
 create mode 100644 cmd/node/uninstall.go
 create mode 100644 cmd/node/upgrade.go
 rename cmd/{ => node}/utils.go (75%)

diff --git a/.github/workflows/test.yaml b/.github/workflows/code_test.yaml
similarity index 98%
rename from .github/workflows/test.yaml
rename to .github/workflows/code_test.yaml
index 2e8bb1a4..2ab07538 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -1,4 +1,4 @@
-name: Tests
+name: Code Tests
 
 on:
   pull_request:
diff --git a/cmd/node.go b/cmd/node.go
deleted file mode 100644
index bb3bd6e5..00000000
--- a/cmd/node.go
+++ /dev/null
@@ -1,609 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"runtime"
-	"strings"
-	"syscall"
-	"time"
-
-	"github.com/algorandfoundation/hack-tui/ui/style"
-	"github.com/spf13/cobra"
-)
-
-var nodeCmd = &cobra.Command{
-	Use:   "node",
-	Short: "Algod installation",
-	Long:  style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
-}
-
-var installCmd = &cobra.Command{
-	Use:   "install",
-	Short: "Install Algod",
-	Long:  "Install Algod on your system",
-	Run: func(cmd *cobra.Command, args []string) {
-		installNode()
-	},
-}
-
-var configureCmd = &cobra.Command{
-	Use:   "configure",
-	Short: "Configure Algod",
-	Long:  "Configure Algod settings",
-	Run: func(cmd *cobra.Command, args []string) {
-		configureNode()
-	},
-}
-
-var startCmd = &cobra.Command{
-	Use:   "start",
-	Short: "Start Algod",
-	Long:  "Start Algod on your system (the one on your PATH).",
-	Run: func(cmd *cobra.Command, args []string) {
-		startNode()
-	},
-}
-
-var stopCmd = &cobra.Command{
-	Use:   "stop",
-	Short: "Stop Algod",
-	Long:  "Stop the Algod process on your system.",
-	Run: func(cmd *cobra.Command, args []string) {
-		stopNode()
-	},
-}
-
-var upgradeCmd = &cobra.Command{
-	Use:   "upgrade",
-	Short: "Upgrade Algod",
-	Long:  "Upgrade Algod (if installed with package manager).",
-	Run: func(cmd *cobra.Command, args []string) {
-		upgradeAlgod()
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(nodeCmd)
-	nodeCmd.AddCommand(installCmd)
-	nodeCmd.AddCommand(configureCmd)
-	nodeCmd.AddCommand(startCmd)
-	nodeCmd.AddCommand(stopCmd)
-	nodeCmd.AddCommand(upgradeCmd)
-}
-
-func installNode() {
-	fmt.Println("Checking if Algod is installed...")
-
-	// Check if Algod is installed
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed. Installing...")
-
-		// Install Algod based on OS
-		switch runtime.GOOS {
-		case "linux":
-			installNodeLinux()
-		case "darwin":
-			installNodeMac()
-		default:
-			panic("Unsupported OS: " + runtime.GOOS)
-		}
-	} else {
-		fmt.Println("Algod is already installed.")
-		printAlgodInfo()
-	}
-
-}
-
-func installNodeLinux() {
-	fmt.Println("Installing Algod on Linux")
-
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	var installCmds [][]string
-	var postInstallHint string
-
-	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
-
-	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
-		fmt.Println("Using apt package manager")
-		installCmds = [][]string{
-			{"apt", "update"},
-			{"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"},
-			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
-			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
-			{"apt", "update"},
-			{"apt", "install", "-y", "algorand-devtools"},
-		}
-	} else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get
-		fmt.Println("Using apt-get package manager")
-		installCmds = [][]string{
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
-			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
-			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "algorand-devtools"},
-		}
-	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
-		fmt.Println("Using dnf package manager")
-		installCmds = [][]string{
-			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
-			{"rpmkeys", "--import", "rpm_algorand.pub"},
-			{"dnf", "install", "-y", "dnf-command(config-manager)"},
-			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
-			{"dnf", "install", "-y", "algorand-devtools"},
-			{"systemctl", "start", "algorand"},
-		}
-	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
-		fmt.Println("Using yum package manager")
-		installCmds = [][]string{
-			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
-			{"rpmkeys", "--import", "rpm_algorand.pub"},
-			{"yum", "install", "yum-utils"},
-			{"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"},
-			{"yum", "install", "-y", "algorand-devtools"},
-			{"systemctl", "start", "algorand"},
-		}
-	} else {
-		fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.")
-		installCmds = [][]string{
-			{"mkdir", "~/node"},
-			{"sh", "-c", "cd ~/node"},
-			{"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
-			{"chmod", "744", "update.sh"},
-			{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
-		}
-
-		postInstallHint = `You may need to add the Algorand binaries to your PATH:
-					export ALGORAND_DATA="$HOME/node/data"
-					export PATH="$HOME/node:$PATH"
-			`
-	}
-
-	// Run each installation command
-	for _, cmdArgs := range installCmds {
-		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
-		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
-		output, err := cmd.CombinedOutput()
-		if err != nil {
-			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
-			cobra.CheckErr(err)
-		}
-	}
-
-	if postInstallHint != "" {
-		fmt.Println(postInstallHint)
-	}
-}
-
-func installNodeMac() {
-	fmt.Println("Installing Algod on macOS...")
-
-	// Based off of the macOS installation instructions
-	// https://developer.algorand.org/docs/run-a-node/setup/install/#installing-on-mac
-
-	installCmd := `mkdir ~/node
-		cd ~/node
-		wget https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh
-		chmod 744 update.sh
-		./update.sh -i -c stable -p ~/node -d ~/node/data -n`
-
-	postInstallHint := `You may need to add the Algorand binaries to your PATH:
-		export ALGORAND_DATA="$HOME/node/data"
-		export PATH="$HOME/node:$PATH"
-	`
-
-	// Run the installation command
-	err := exec.Command(installCmd).Run()
-	cobra.CheckErr(err)
-
-	if postInstallHint != "" {
-		fmt.Println(postInstallHint)
-	}
-}
-
-// TODO: configure not just data directory but algod path
-func configureNode() {
-	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
-		panic("Unsupported OS: " + runtime.GOOS)
-	}
-
-	var systemctlConfigure bool
-
-	// Check systemctl first
-	if checkSystemctlAlgorandServiceCreated() {
-		if !isRunningWithSudo() {
-			fmt.Println("This command must be run with super-user priviledges (sudo).")
-			os.Exit(1)
-		}
-
-		if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") {
-			if checkSystemctlAlgorandServiceActive() {
-				fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.")
-				os.Exit(1)
-			}
-			// Edit the service file with the user's new data directory
-			systemctlConfigure = true
-		} else {
-			fmt.Println("Exiting...")
-			os.Exit(0)
-		}
-	}
-
-	// At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file
-	// i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf
-	// ExecStart and Description will be changed to reflect the new data directory
-	//
-
-	if !systemctlConfigure {
-		fmt.Println("Configuring Data directory for algod started through Algorun...")
-	}
-
-	algorandData := os.Getenv("ALGORAND_DATA")
-
-	// Check if ALGORAND_DATA environment variable is set
-	if algorandData != "" {
-		fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData)
-		fmt.Println("Inspecting the set data directory...")
-
-		if validateAlgorandDataDir(algorandData) {
-			fmt.Println("Found valid Algorand Data Directory: " + algorandData)
-
-			if systemctlConfigure {
-				if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") {
-					editAlgorandServiceFile(algorandData)
-					os.Exit(0)
-				}
-			}
-
-			if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") {
-				fmt.Println("User chose not to set a completely new data directory.")
-				os.Exit(0)
-			}
-
-			if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") {
-				newPath := promptWrapperInput("Enter the new data directory path")
-
-				if !validateAlgorandDataDir(newPath) {
-					fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.")
-					os.Exit(1)
-				}
-
-				if systemctlConfigure {
-					// Edit the service file
-					editAlgorandServiceFile(newPath)
-				} else {
-					// Affect the ALGORAND_DATA environment variable
-					affectALGORAND_DATA(newPath)
-				}
-				os.Exit(0)
-			}
-		} else {
-			fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.")
-		}
-	} else {
-		fmt.Println("ALGORAND_DATA environment variable not set.")
-	}
-
-	// Do quick "lazy" check for existing Algorand Data directories
-	paths := lazyCheckAlgorandDataDirs()
-
-	if len(paths) != 0 {
-
-		fmt.Println("Quick check found the following potential data directories:")
-		for _, path := range paths {
-			fmt.Println("✔ " + path)
-		}
-
-		if len(paths) == 1 {
-			if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") {
-				if systemctlConfigure {
-					// Edit the service file
-					editAlgorandServiceFile(paths[0])
-				} else {
-					affectALGORAND_DATA(paths[0])
-				}
-				os.Exit(0)
-			}
-
-		} else {
-
-			if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") {
-
-				selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
-
-				if systemctlConfigure {
-					// Edit the service file
-					editAlgorandServiceFile(selectedPath)
-				} else {
-					affectALGORAND_DATA(selectedPath)
-				}
-				os.Exit(0)
-			}
-		}
-	}
-
-	// Deep search
-	if promptWrapperNo("Do you want Algorun to do a deep search for pre-existing Algorand Data directories? (y/n)") {
-		fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...")
-		os.Exit(0)
-	}
-
-	fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...")
-	paths = deepSearchAlgorandDataDirs()
-
-	if len(paths) == 0 {
-		fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.")
-		os.Exit(1)
-	}
-
-	fmt.Println("Found Algorand data directories:")
-	for _, path := range paths {
-		fmt.Println(path)
-	}
-
-	// Prompt user to select a directory
-	selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
-
-	if systemctlConfigure {
-		editAlgorandServiceFile(selectedPath)
-	} else {
-		affectALGORAND_DATA(selectedPath)
-	}
-	os.Exit(0)
-}
-
-// Start Algod on your system (the one on your PATH).
-func startNode() {
-	fmt.Println("Attempting to start Algod...")
-
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed. Please run the node install command.")
-		os.Exit(1)
-	}
-
-	// Check if Algod is already running
-	if isAlgodRunning() {
-		fmt.Println("Algod is already running.")
-		os.Exit(0)
-	}
-
-	startAlgodProcess()
-}
-
-func isAlgodRunning() bool {
-	// Check if Algod is already running
-	// This works for systemctl started algorand.service as well as directly started algod
-	err := exec.Command("pgrep", "algod").Run()
-	return err == nil
-}
-
-func startAlgodProcess() {
-	// Check if algod is available as a systemctl service
-	if checkSystemctlAlgorandServiceCreated() {
-		// Algod is available as a systemd service, start it using systemctl
-
-		if !isRunningWithSudo() {
-			fmt.Println("This command must be run with super-user priviledges (sudo).")
-			os.Exit(1)
-		}
-
-		fmt.Println("Starting algod using systemctl...")
-		cmd := exec.Command("systemctl", "start", "algorand")
-		err := cmd.Run()
-		if err != nil {
-			fmt.Printf("Failed to start algod service: %v\n", err)
-			os.Exit(1)
-		}
-	} else {
-		// Algod is not available as a systemd service, start it directly
-		fmt.Println("Starting algod directly...")
-
-		// Check if ALGORAND_DATA environment variable is set
-		fmt.Println("Checking if ALGORAND_DATA env var is set...")
-		algorandData := os.Getenv("ALGORAND_DATA")
-
-		if !validateAlgorandDataDir(algorandData) {
-			fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
-			os.Exit(1)
-		}
-
-		fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
-
-		cmd := exec.Command("algod")
-		cmd.SysProcAttr = &syscall.SysProcAttr{
-			Setsid: true,
-		}
-		err := cmd.Start()
-		if err != nil {
-			fmt.Printf("Failed to start algod: %v\n", err)
-			os.Exit(1)
-		}
-	}
-
-	// Wait for the process to start
-	time.Sleep(5 * time.Second)
-
-	if isAlgodRunning() {
-		fmt.Println("Algod is running.")
-	} else {
-		fmt.Println("Algod failed to start.")
-	}
-}
-
-// Stop the Algod process on your system.
-func stopNode() {
-	fmt.Println("Attempting to stop Algod...")
-
-	if !isAlgodRunning() {
-		fmt.Println("Algod was not running.")
-		os.Exit(0)
-	}
-
-	stopAlgodProcess()
-
-	time.Sleep(5 * time.Second)
-
-	if !isAlgodRunning() {
-		fmt.Println("Algod is no longer running.")
-		os.Exit(0)
-	}
-
-	fmt.Println("Failed to stop Algod.")
-	os.Exit(1)
-}
-
-func stopAlgodProcess() {
-	// Check if algod is available as a systemd service
-	if checkSystemctlAlgorandServiceCreated() {
-		if !isRunningWithSudo() {
-			fmt.Println("This command must be run with super-user priviledges (sudo).")
-			os.Exit(1)
-		}
-
-		// Algod is available as a systemd service, stop it using systemctl
-		fmt.Println("Stopping algod using systemctl...")
-		cmd := exec.Command("systemctl", "stop", "algorand")
-		err := cmd.Run()
-		if err != nil {
-			fmt.Printf("Failed to stop algod service: %v\n", err)
-			cobra.CheckErr(err)
-		}
-		fmt.Println("Algod service stopped.")
-	} else {
-		// Algod is not available as a systemd service, stop it directly
-		fmt.Println("Stopping algod directly...")
-		// Find the process ID of algod
-		pid, err := findAlgodPID()
-		if err != nil {
-			fmt.Printf("Failed to find algod process: %v\n", err)
-			cobra.CheckErr(err)
-		}
-
-		// Send SIGTERM to the process
-		process, err := os.FindProcess(pid)
-		if err != nil {
-			fmt.Printf("Failed to find process with PID %d: %v\n", pid, err)
-			cobra.CheckErr(err)
-		}
-
-		err = process.Signal(syscall.SIGTERM)
-		if err != nil {
-			fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
-			cobra.CheckErr(err)
-		}
-
-		fmt.Println("Sent SIGTERM to algod process.")
-	}
-}
-
-// Upgrade ALGOD (if installed with package manager).
-func upgradeAlgod() {
-
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed. Please run the node install command.")
-		os.Exit(1)
-	}
-
-	// Check if Algod was installed with apt/apt-get
-	if checkCmdToolExists("apt") {
-		upgradeDebianPackage("apt", "algorand-devtools")
-	} else if checkCmdToolExists("apt-get") {
-		upgradeDebianPackage("apt-get", "algorand-devtools")
-	} else if checkCmdToolExists("dnf") {
-		upgradeRpmPackage("dnf", "algorand-devtools")
-	} else if checkCmdToolExists("yum") {
-		upgradeRpmPackage("yum", "algorand-devtools")
-	} else {
-		fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.")
-		os.Exit(1)
-	}
-}
-
-// Upgrade a package using the specified Debian package manager
-func upgradeDebianPackage(packageManager, packageName string) {
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	// Check if the package is installed and if there are updates available using apt-cache policy
-	cmd := exec.Command("apt-cache", "policy", packageName)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		fmt.Printf("Failed to check package policy: %v\n", err)
-		os.Exit(1)
-	}
-
-	outputStr := string(output)
-	if strings.Contains(outputStr, "Installed: (none)") {
-		fmt.Printf("Package %s is not installed.\n", packageName)
-		os.Exit(1)
-	}
-
-	installedVersion := extractVersion(outputStr, "Installed:")
-	candidateVersion := extractVersion(outputStr, "Candidate:")
-
-	if installedVersion == candidateVersion {
-		fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion)
-		os.Exit(0)
-	}
-
-	fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion)
-
-	// Update the package list
-	fmt.Println("Updating package list...")
-	cmd = exec.Command(packageManager, "update")
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to update package list: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Upgrade the package
-	fmt.Printf("Upgrading package %s...\n", packageName)
-	cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName)
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
-		os.Exit(1)
-	}
-
-	fmt.Printf("Package %s upgraded successfully.\n", packageName)
-	os.Exit(0)
-}
-
-// Upgrade a package using the specified RPM package manager
-func upgradeRpmPackage(packageManager, packageName string) {
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with sudo.")
-		os.Exit(1)
-	}
-
-	// Attempt to upgrade the package directly
-	fmt.Printf("Upgrading package %s...\n", packageName)
-	cmd := exec.Command(packageManager, "update", "-y", packageName)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
-		os.Exit(1)
-	}
-
-	outputStr := string(output)
-	if strings.Contains(outputStr, "Nothing to do") {
-		fmt.Printf("Package %s is already up-to-date.\n", packageName)
-		os.Exit(0)
-	} else {
-		fmt.Println(outputStr)
-		fmt.Printf("Package %s upgraded successfully.\n", packageName)
-		os.Exit(0)
-	}
-}
diff --git a/cmd/node/configure.go b/cmd/node/configure.go
new file mode 100644
index 00000000..a8746d5b
--- /dev/null
+++ b/cmd/node/configure.go
@@ -0,0 +1,330 @@
+package node
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+	"text/template"
+
+	"github.com/spf13/cobra"
+)
+
+var configureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configure Algod",
+	Long:  "Configure Algod settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		configureNode()
+	},
+}
+
+// TODO: configure not just data directory but algod path
+func configureNode() {
+	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
+		panic("Unsupported OS: " + runtime.GOOS)
+	}
+
+	var systemServiceConfigure bool
+
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	// Check systemctl first
+	if checkAlgorandServiceCreated() {
+		if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") {
+			if checkAlgorandServiceActive() {
+				fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.")
+				os.Exit(1)
+			}
+			// Edit the service file with the user's new data directory
+			systemServiceConfigure = true
+		} else {
+			fmt.Println("Exiting...")
+			os.Exit(0)
+		}
+	}
+
+	// At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file
+	// i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf
+	// ExecStart and Description will be changed to reflect the new data directory
+	//
+
+	if !systemServiceConfigure {
+		fmt.Println("Configuring Data directory for algod started through Algorun...")
+	}
+
+	algorandData := os.Getenv("ALGORAND_DATA")
+
+	// Check if ALGORAND_DATA environment variable is set
+	if algorandData != "" {
+		fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData)
+		fmt.Println("Inspecting the set data directory...")
+
+		if validateAlgorandDataDir(algorandData) {
+			fmt.Println("Found valid Algorand Data Directory: " + algorandData)
+
+			if systemServiceConfigure {
+				if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") {
+					editAlgorandServiceFile(algorandData)
+					os.Exit(0)
+				}
+			}
+
+			if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") {
+				fmt.Println("User chose not to set a completely new data directory.")
+				os.Exit(0)
+			}
+
+			if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") {
+				newPath := promptWrapperInput("Enter the new data directory path")
+
+				if !validateAlgorandDataDir(newPath) {
+					fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.")
+					os.Exit(1)
+				}
+
+				if systemServiceConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(newPath)
+				} else {
+					// Affect the ALGORAND_DATA environment variable
+					affectALGORAND_DATA(newPath)
+				}
+				os.Exit(0)
+			}
+		} else {
+			fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.")
+		}
+	} else {
+		fmt.Println("ALGORAND_DATA environment variable not set.")
+	}
+
+	// Do quick "lazy" check for existing Algorand Data directories
+	paths := lazyCheckAlgorandDataDirs()
+
+	if len(paths) != 0 {
+
+		fmt.Println("Quick check found the following potential data directories:")
+		for _, path := range paths {
+			fmt.Println("✔ " + path)
+		}
+
+		if len(paths) == 1 {
+			if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") {
+				if systemServiceConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(paths[0])
+				} else {
+					affectALGORAND_DATA(paths[0])
+				}
+				os.Exit(0)
+			}
+
+		} else {
+
+			if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") {
+
+				selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+				if systemServiceConfigure {
+					// Edit the service file
+					editAlgorandServiceFile(selectedPath)
+				} else {
+					affectALGORAND_DATA(selectedPath)
+				}
+				os.Exit(0)
+			}
+		}
+	}
+
+	// Deep search
+	if promptWrapperNo("Do you want Algorun to do a deep search for pre-existing Algorand Data directories? (y/n)") {
+		fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...")
+		os.Exit(0)
+	}
+
+	fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...")
+	paths = deepSearchAlgorandDataDirs()
+
+	if len(paths) == 0 {
+		fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.")
+		os.Exit(1)
+	}
+
+	fmt.Println("Found Algorand data directories:")
+	for _, path := range paths {
+		fmt.Println(path)
+	}
+
+	// Prompt user to select a directory
+	selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+	if systemServiceConfigure {
+		editAlgorandServiceFile(selectedPath)
+	} else {
+		affectALGORAND_DATA(selectedPath)
+	}
+	os.Exit(0)
+}
+
+func editAlgorandServiceFile(dataDirectoryPath string) {
+	switch runtime.GOOS {
+	case "linux":
+		editSystemdAlgorandServiceFile(dataDirectoryPath)
+	case "darwin":
+		editLaunchdAlgorandServiceFile(dataDirectoryPath)
+	default:
+		fmt.Println("Unsupported operating system.")
+	}
+}
+
+func editLaunchdAlgorandServiceFile(dataDirectoryPath string) {
+
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Failed to find algod binary: %v\n", err)
+		os.Exit(1)
+	}
+
+	overwriteFilePath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+
+	overwriteTemplate := `<?xml version="1.0" encoding="UTF-8"?>
+	<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+	<plist version="1.0">
+	<dict>
+					<key>Label</key>
+					<string>com.algorand.algod</string>
+					<key>ProgramArguments</key>
+					<array>
+													<string>{{.AlgodPath}}</string>
+													<string>-d</string>
+													<string>{{.DataDirectoryPath}}</string>
+					</array>
+					<key>RunAtLoad</key>
+					<true/>
+					<key>KeepAlive</key>
+					<true/>
+					<key>StandardOutPath</key>
+					<string>/tmp/algod.out</string>
+					<key>StandardErrorPath</key>
+					<string>/tmp/algod.err</string>
+	</dict>
+	</plist>`
+
+	// Data to fill the template
+	data := map[string]string{
+		"AlgodPath":         algodPath,
+		"DataDirectoryPath": dataDirectoryPath,
+	}
+
+	// Parse and execute the template
+	tmpl, err := template.New("override").Parse(overwriteTemplate)
+	if err != nil {
+		fmt.Printf("Failed to parse template: %v\n", err)
+		os.Exit(1)
+	}
+
+	var overwriteContent bytes.Buffer
+	err = tmpl.Execute(&overwriteContent, data)
+	if err != nil {
+		fmt.Printf("Failed to execute template: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Write the override content to the file
+	err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write override file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Boot out the launchd service (just in case - it should be off)
+	cmd := exec.Command("launchctl", "bootout", "system", overwriteFilePath)
+	err = cmd.Run()
+	if err != nil {
+		if !strings.Contains(err.Error(), "No such process") {
+			fmt.Printf("Failed to bootout launchd service: %v\n", err)
+			os.Exit(1)
+		}
+	}
+
+	// Load the launchd service
+	cmd = exec.Command("launchctl", "load", overwriteFilePath)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to load launchd service: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Launchd service updated and reloaded successfully.")
+}
+
+// Update the algorand.service file
+func editSystemdAlgorandServiceFile(dataDirectoryPath string) {
+
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Failed to find algod binary: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Path to the systemd service override file
+	// Assuming that this is the same everywhere systemd is used
+	overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
+
+	// Create the override directory if it doesn't exist
+	err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
+	if err != nil {
+		fmt.Printf("Failed to create override directory: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Content of the override file
+	const overrideTemplate = `[Unit]
+Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
+[Service]
+ExecStart=
+ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
+
+	// Data to fill the template
+	data := map[string]string{
+		"AlgodPath":         algodPath,
+		"DataDirectoryPath": dataDirectoryPath,
+	}
+
+	// Parse and execute the template
+	tmpl, err := template.New("override").Parse(overrideTemplate)
+	if err != nil {
+		fmt.Printf("Failed to parse template: %v\n", err)
+		os.Exit(1)
+	}
+
+	var overrideContent bytes.Buffer
+	err = tmpl.Execute(&overrideContent, data)
+	if err != nil {
+		fmt.Printf("Failed to execute template: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Write the override content to the file
+	err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write override file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Reload systemd manager configuration
+	cmd := exec.Command("systemctl", "daemon-reload")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to reload systemd daemon: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Algorand service file updated successfully.")
+}
diff --git a/cmd/node/install.go b/cmd/node/install.go
new file mode 100644
index 00000000..e714e710
--- /dev/null
+++ b/cmd/node/install.go
@@ -0,0 +1,344 @@
+package node
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+var installCmd = &cobra.Command{
+	Use:   "install",
+	Short: "Install Algorand node (Algod)",
+	Long:  "Install Algorand node (Algod) and other binaries on your system",
+	Run: func(cmd *cobra.Command, args []string) {
+		installNode()
+	},
+}
+
+func installNode() {
+	fmt.Println("Checking if Algod is installed...")
+
+	// Check if Algod is installed
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed. Installing...")
+
+		// Install Algod based on OS
+		switch runtime.GOOS {
+		case "linux":
+			installNodeLinux()
+		case "darwin":
+			installNodeMac()
+		default:
+			panic("Unsupported OS: " + runtime.GOOS)
+		}
+	} else {
+		fmt.Println("Algod is already installed.")
+		printAlgodInfo()
+	}
+
+}
+
+func installNodeLinux() {
+	fmt.Println("Installing Algod on Linux")
+
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	var installCmds [][]string
+	var postInstallHint string
+
+	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
+
+	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
+		fmt.Println("Using apt package manager")
+		installCmds = [][]string{
+			{"apt", "update"},
+			{"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
+			{"apt", "update"},
+			{"apt", "install", "-y", "algorand-devtools"},
+		}
+	} else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get
+		fmt.Println("Using apt-get package manager")
+		installCmds = [][]string{
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "algorand-devtools"},
+		}
+	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
+		fmt.Println("Using dnf package manager")
+		installCmds = [][]string{
+			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+			{"rpmkeys", "--import", "rpm_algorand.pub"},
+			{"dnf", "install", "-y", "dnf-command(config-manager)"},
+			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"dnf", "install", "-y", "algorand-devtools"},
+			{"systemctl", "start", "algorand"},
+		}
+	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
+		fmt.Println("Using yum package manager")
+		installCmds = [][]string{
+			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+			{"rpmkeys", "--import", "rpm_algorand.pub"},
+			{"yum", "install", "yum-utils"},
+			{"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"yum", "install", "-y", "algorand-devtools"},
+			{"systemctl", "start", "algorand"},
+		}
+	} else {
+		fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.")
+		installCmds = [][]string{
+			{"mkdir", "~/node"},
+			{"sh", "-c", "cd ~/node"},
+			{"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
+			{"chmod", "744", "update.sh"},
+			{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
+		}
+
+		postInstallHint = `You may need to add the Algorand binaries to your PATH:
+					export ALGORAND_DATA="$HOME/node/data"
+					export PATH="$HOME/node:$PATH"
+			`
+	}
+
+	// Run each installation command
+	for _, cmdArgs := range installCmds {
+		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
+		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
+			cobra.CheckErr(err)
+		}
+	}
+
+	if postInstallHint != "" {
+		fmt.Println(postInstallHint)
+	}
+}
+
+func installNodeMac() {
+	fmt.Println("Installing Algod on macOS...")
+
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user privileges (sudo).")
+		os.Exit(1)
+	}
+
+	// Homebrew is our package manager of choice
+	if !checkCmdToolExists("brew") {
+		fmt.Println("Could not find Homebrew installed. Please install Homebrew and try again.")
+		os.Exit(1)
+	}
+
+	originalUser := os.Getenv("SUDO_USER")
+
+	// Run Homebrew commands as the original user without sudo
+	if err := runHomebrewInstallCommandsAsUser(originalUser); err != nil {
+		fmt.Printf("Homebrew commands failed: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Handle data directory and genesis.json file
+	handleDataDirMac()
+
+	// Create and load the launchd service
+	createAndLoadLaunchdService()
+
+	// Ensure Homebrew bin directory is in the PATH
+	// So that brew installed algorand binaries can be found
+	ensureHomebrewPathInEnv()
+
+	if !isAlgodInstalled() {
+		fmt.Println("algod unexpectedly NOT in path. Installation failed.")
+		os.Exit(1)
+	}
+
+	fmt.Println(`Installed Algorand (Algod) with Homebrew.
+Algod is running in the background as a system-level service.
+	`)
+	os.Exit(0)
+}
+
+func runHomebrewInstallCommandsAsUser(user string) error {
+	homebrewCmds := [][]string{
+		{"brew", "tap", "HashMapsData2Value/homebrew-tap"},
+		{"brew", "install", "algorand"},
+		{"brew", "--prefix", "algorand", "--installed"},
+	}
+
+	for _, cmdArgs := range homebrewCmds {
+		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
+		cmd := exec.Command("sudo", append([]string{"-u", user}, cmdArgs...)...)
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		if err := cmd.Run(); err != nil {
+			return fmt.Errorf("command failed: %s\nError: %v", strings.Join(cmdArgs, " "), err)
+		}
+	}
+	return nil
+}
+
+func handleDataDirMac() {
+	// Ensure the ~/.algorand directory exists
+	algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand")
+	if err := os.MkdirAll(algorandDir, 0755); err != nil {
+		fmt.Printf("Failed to create directory %s: %v\n", algorandDir, err)
+		os.Exit(1)
+	}
+
+	// Check if genesis.json file exists in ~/.algorand
+	genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json")
+	if _, err := os.Stat(genesisFilePath); os.IsNotExist(err) {
+		fmt.Println("genesis.json file does not exist. Downloading...")
+
+		// Download the genesis.json file
+		resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
+		if err != nil {
+			fmt.Printf("Failed to download genesis.json: %v\n", err)
+			cobra.CheckErr(err)
+		}
+		defer resp.Body.Close()
+
+		// Create the file
+		out, err := os.Create(genesisFilePath)
+		if err != nil {
+			fmt.Printf("Failed to create genesis.json file: %v\n", err)
+			cobra.CheckErr(err)
+		}
+		defer out.Close()
+
+		// Write the content to the file
+		_, err = io.Copy(out, resp.Body)
+		if err != nil {
+			fmt.Printf("Failed to save genesis.json file: %v\n", err)
+			cobra.CheckErr(err)
+		}
+
+		fmt.Println("mainnet genesis.json file downloaded successfully.")
+	}
+
+}
+
+func createAndLoadLaunchdService() {
+	// Get the prefix path for Algorand
+	cmd := exec.Command("brew", "--prefix", "algorand")
+	algorandPrefix, err := cmd.Output()
+	if err != nil {
+		fmt.Printf("Failed to get Algorand prefix: %v\n", err)
+		cobra.CheckErr(err)
+	}
+	algorandPrefixPath := strings.TrimSpace(string(algorandPrefix))
+
+	// Define the launchd plist content
+	plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>Label</key>
+	<string>com.algorand.algod</string>
+	<key>ProgramArguments</key>
+	<array>
+			<string>%s/bin/algod</string>
+			<string>-d</string>
+			<string>%s/.algorand</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>KeepAlive</key>
+	<true/>
+	<key>StandardOutPath</key>
+	<string>/tmp/algod.out</string>
+	<key>StandardErrorPath</key>
+	<string>/tmp/algod.err</string>
+</dict>
+</plist>`, algorandPrefixPath, os.Getenv("HOME"))
+
+	// Write the plist content to a file
+	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+	err = os.MkdirAll(filepath.Dir(plistPath), 0755)
+	if err != nil {
+		fmt.Printf("Failed to create LaunchDaemons directory: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	err = os.WriteFile(plistPath, []byte(plistContent), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write plist file: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	// Load the launchd service
+	cmd = exec.Command("launchctl", "load", plistPath)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to load launchd service: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	// Check if the service is running
+	cmd = exec.Command("launchctl", "list", "com.algorand.algod")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("launchd service is not running: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	fmt.Println("Launchd service created and loaded successfully.")
+}
+
+// Ensure that Homebrew bin directory is in the PATH so that Algorand binaries can be found
+func ensureHomebrewPathInEnv() {
+	homebrewPrefix := os.Getenv("HOMEBREW_PREFIX")
+	homebrewCellar := os.Getenv("HOMEBREW_CELLAR")
+	homebrewRepository := os.Getenv("HOMEBREW_REPOSITORY")
+
+	if homebrewPrefix == "" || homebrewCellar == "" || homebrewRepository == "" {
+		fmt.Println("Homebrew environment variables are not set. Running brew shellenv...")
+
+		cmd := exec.Command("brew", "shellenv")
+		output, err := cmd.Output()
+		if err != nil {
+			fmt.Printf("Failed to get Homebrew environment: %v\n", err)
+			return
+		}
+
+		envVars := strings.Split(string(output), "\n")
+		for _, envVar := range envVars {
+			if envVar != "" {
+				fmt.Println("Setting environment variable:", envVar)
+				os.Setenv(strings.Split(envVar, "=")[0], strings.Trim(strings.Split(envVar, "=")[1], `"`))
+			}
+		}
+
+		// Append brew shellenv output to .zshrc
+		zshrcPath := filepath.Join(os.Getenv("HOME"), ".zshrc")
+		f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_WRONLY, 0644)
+		if err != nil {
+			fmt.Printf("Failed to open .zshrc: %v\n", err)
+			fmt.Printf("Are you running a terminal other than zsh?")
+			fmt.Printf("Please run brew shellenv and add the output to your shell's configuration file.")
+			return
+		}
+		defer f.Close()
+
+		if _, err := f.WriteString("\n# Inserted by Algorun\n# Homebrew environment variables\n" + string(output)); err != nil {
+			fmt.Printf("Failed to write to .zshrc: %v\n", err)
+		}
+	}
+}
diff --git a/cmd/node/main.go b/cmd/node/main.go
new file mode 100644
index 00000000..ddf259ff
--- /dev/null
+++ b/cmd/node/main.go
@@ -0,0 +1,21 @@
+package node
+
+import (
+	"github.com/algorandfoundation/hack-tui/ui/style"
+	"github.com/spf13/cobra"
+)
+
+var NodeCmd = &cobra.Command{
+	Use:   "node",
+	Short: "Algod installation",
+	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("View the node status"),
+}
+
+func init() {
+	NodeCmd.AddCommand(configureCmd)
+	NodeCmd.AddCommand(installCmd)
+	NodeCmd.AddCommand(startCmd)
+	NodeCmd.AddCommand(stopCmd)
+	NodeCmd.AddCommand(uninstallCmd)
+	NodeCmd.AddCommand(upgradeCmd)
+}
diff --git a/cmd/node/start.go b/cmd/node/start.go
new file mode 100644
index 00000000..e7d4092d
--- /dev/null
+++ b/cmd/node/start.go
@@ -0,0 +1,118 @@
+package node
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"syscall"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var startCmd = &cobra.Command{
+	Use:   "start",
+	Short: "Start Algod",
+	Long:  "Start Algod on your system (the one on your PATH).",
+	Run: func(cmd *cobra.Command, args []string) {
+		startNode()
+	},
+}
+
+// Start Algod on your system (the one on your PATH).
+func startNode() {
+	fmt.Println("Attempting to start Algod...")
+
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed. Please run the *node install* command.")
+		os.Exit(1)
+	}
+
+	// Check if Algod is already running
+	if isAlgodRunning() {
+		fmt.Println("Algod is already running.")
+		os.Exit(0)
+	}
+
+	startAlgodProcess()
+}
+
+func startAlgodProcess() {
+
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	// Check if algod is available as a system service
+	if checkAlgorandServiceCreated() {
+		// Algod is available as a service
+
+		switch runtime.GOOS {
+		case "linux":
+			startSystemdAlgorandService()
+		case "darwin":
+			startLaunchdAlgorandService()
+		default: // Unsupported OS
+			fmt.Println("Unsupported OS.")
+			os.Exit(1)
+		}
+
+	} else {
+		// Algod is not available as a systemd service, start it directly
+		fmt.Println("Starting algod directly...")
+
+		// Check if ALGORAND_DATA environment variable is set
+		fmt.Println("Checking if ALGORAND_DATA env var is set...")
+		algorandData := os.Getenv("ALGORAND_DATA")
+
+		if !validateAlgorandDataDir(algorandData) {
+			fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
+			os.Exit(1)
+		}
+
+		fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
+
+		cmd := exec.Command("algod")
+		cmd.SysProcAttr = &syscall.SysProcAttr{
+			Setsid: true,
+		}
+		err := cmd.Start()
+		if err != nil {
+			fmt.Printf("Failed to start algod: %v\n", err)
+			os.Exit(1)
+		}
+	}
+
+	// Wait for the process to start
+	time.Sleep(5 * time.Second)
+
+	if isAlgodRunning() {
+		fmt.Println("Algod is running.")
+	} else {
+		fmt.Println("Algod failed to start.")
+	}
+}
+
+// Linux uses systemd
+func startSystemdAlgorandService() {
+	fmt.Println("Starting algod using systemctl...")
+	cmd := exec.Command("systemctl", "start", "algorand")
+	err := cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to start algod service: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+// MacOS uses launchd instead of systemd
+func startLaunchdAlgorandService() {
+	fmt.Println("Starting algod using launchctl...")
+	cmd := exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist")
+	err := cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to start algod service: %v\n", err)
+		os.Exit(1)
+	}
+}
diff --git a/cmd/node/stop.go b/cmd/node/stop.go
new file mode 100644
index 00000000..fe5180ad
--- /dev/null
+++ b/cmd/node/stop.go
@@ -0,0 +1,111 @@
+package node
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"syscall"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var stopCmd = &cobra.Command{
+	Use:   "stop",
+	Short: "Stop Algod",
+	Long:  "Stop the Algod process on your system.",
+	Run: func(cmd *cobra.Command, args []string) {
+		stopNode()
+	},
+}
+
+// Stop the Algod process on your system.
+func stopNode() {
+	fmt.Println("Attempting to stop Algod...")
+
+	if !isAlgodRunning() {
+		fmt.Println("Algod was not running.")
+		os.Exit(0)
+	}
+
+	stopAlgodProcess()
+
+	time.Sleep(5 * time.Second)
+
+	if !isAlgodRunning() {
+		fmt.Println("Algod is no longer running.")
+		os.Exit(0)
+	}
+
+	fmt.Println("Failed to stop Algod.")
+	os.Exit(1)
+}
+
+func stopAlgodProcess() {
+
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	// Check if algod is available as a system service
+	if checkAlgorandServiceCreated() {
+		switch runtime.GOOS {
+		case "linux":
+			stopSystemdAlgorandService()
+		case "darwin":
+			stopLaunchdAlgorandService()
+		default: // Unsupported OS
+			fmt.Println("Unsupported OS.")
+			os.Exit(1)
+		}
+
+	} else {
+		// Algod is not available as a systemd service, stop it directly
+		fmt.Println("Stopping algod directly...")
+		// Find the process ID of algod
+		pid, err := findAlgodPID()
+		if err != nil {
+			fmt.Printf("Failed to find algod process: %v\n", err)
+			cobra.CheckErr(err)
+		}
+
+		// Send SIGTERM to the process
+		process, err := os.FindProcess(pid)
+		if err != nil {
+			fmt.Printf("Failed to find process with PID %d: %v\n", pid, err)
+			cobra.CheckErr(err)
+		}
+
+		err = process.Signal(syscall.SIGTERM)
+		if err != nil {
+			fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
+			cobra.CheckErr(err)
+		}
+
+		fmt.Println("Sent SIGTERM to algod process.")
+	}
+}
+
+func stopLaunchdAlgorandService() {
+	fmt.Println("Stopping algod using launchd...")
+	cmd := exec.Command("launchctl", "bootout", "system/com.algorand.algod")
+	err := cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to stop algod service: %v\n", err)
+		cobra.CheckErr(err)
+	}
+	fmt.Println("Algod service stopped.")
+}
+
+func stopSystemdAlgorandService() {
+	fmt.Println("Stopping algod using systemctl...")
+	cmd := exec.Command("systemctl", "stop", "algorand")
+	err := cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to stop algod service: %v\n", err)
+		cobra.CheckErr(err)
+	}
+	fmt.Println("Algod service stopped.")
+}
diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go
new file mode 100644
index 00000000..e20d3c74
--- /dev/null
+++ b/cmd/node/uninstall.go
@@ -0,0 +1,99 @@
+package node
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+
+	"github.com/spf13/cobra"
+)
+
+var uninstallCmd = &cobra.Command{
+	Use:   "uninstall",
+	Short: "Uninstall Algorand node (Algod)",
+	Long:  "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
+	Run: func(cmd *cobra.Command, args []string) {
+		unInstallNode()
+	},
+}
+
+// Uninstall Algorand node (Algod) and other binaries on your system
+func unInstallNode() {
+
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	fmt.Println("Checking if Algod is installed...")
+
+	// Check if Algod is installed
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed.")
+		os.Exit(0)
+	}
+
+	fmt.Println("Algod is installed. Uninstalling...")
+
+	// Check if Algod is running
+	if isAlgodRunning() {
+		fmt.Println("Algod is running. Please run *node stop*.")
+		os.Exit(1)
+	}
+
+	// Uninstall Algod based on OS
+	switch runtime.GOOS {
+	case "linux":
+		unInstallNodeLinux()
+	case "darwin":
+		unInstallNodeMac()
+	default:
+		panic("Unsupported OS: " + runtime.GOOS)
+	}
+
+	os.Exit(0)
+}
+
+func unInstallNodeMac() {
+	fmt.Println("Uninstalling Algod on macOS...")
+
+	// Homebrew is our package manager of choice
+	if !checkCmdToolExists("brew") {
+		fmt.Println("Could not find Homebrew installed. Did you install Algod some other way?.")
+		os.Exit(1)
+	}
+
+	user := os.Getenv("SUDO_USER")
+
+	// Run the brew uninstall command as the original user without sudo
+	cmd := exec.Command("sudo", "-u", user, "brew", "uninstall", "algorand", "--formula")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Printf("Failed to uninstall Algorand: %v\n", err)
+		fmt.Printf("Output: %s\n", string(output))
+		os.Exit(1)
+	}
+
+	fmt.Printf("Output: %s\n", string(output))
+
+	// Calling brew uninstall algorand without sudo user privileges
+	cmd = exec.Command("sudo", "-u", user, "brew", "--prefix", "algorand", "--installed")
+	err = cmd.Run()
+	if err == nil {
+		fmt.Println("Algorand uninstall failed.")
+		os.Exit(1)
+	}
+
+	// Delete the launchd plist file
+	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+	err = os.Remove(plistPath)
+	if err != nil {
+		fmt.Printf("Failed to delete plist file: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Algorand uninstalled successfully.")
+}
+
+func unInstallNodeLinux() {}
diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go
new file mode 100644
index 00000000..55210b09
--- /dev/null
+++ b/cmd/node/upgrade.go
@@ -0,0 +1,175 @@
+package node
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+var upgradeCmd = &cobra.Command{
+	Use:   "upgrade",
+	Short: "Upgrade Algod",
+	Long:  "Upgrade Algod (if installed with package manager).",
+	Run: func(cmd *cobra.Command, args []string) {
+		upgradeAlgod()
+	},
+}
+
+// Upgrade ALGOD (if installed with package manager).
+func upgradeAlgod() {
+	if !isAlgodInstalled() {
+		fmt.Println("Algod is not installed.")
+		os.Exit(1)
+	}
+
+	switch runtime.GOOS {
+	case "darwin":
+		if checkCmdToolExists("brew") {
+			upgradeBrewAlgorand()
+		} else {
+			fmt.Println("Homebrew is not installed. Please install Homebrew and try again.")
+			os.Exit(1)
+		}
+	case "linux":
+		// Check if Algod was installed with apt/apt-get, dnf, or yum
+		if checkCmdToolExists("apt") {
+			upgradeDebianPackage("apt", "algorand-devtools")
+		} else if checkCmdToolExists("apt-get") {
+			upgradeDebianPackage("apt-get", "algorand-devtools")
+		} else if checkCmdToolExists("dnf") {
+			upgradeRpmPackage("dnf", "algorand-devtools")
+		} else if checkCmdToolExists("yum") {
+			upgradeRpmPackage("yum", "algorand-devtools")
+		} else {
+			fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.")
+			os.Exit(1)
+		}
+	default:
+		fmt.Println("Unsupported operating system. The *node upgrade* command is only available for macOS and Linux.")
+		os.Exit(1)
+	}
+}
+
+func upgradeBrewAlgorand() {
+	fmt.Println("Upgrading Algod using Homebrew...")
+
+	var prefixCommand []string
+
+	// Brew cannot be run with sudo, so we need to run the commands as the original user.
+	// This checks if the user has ran this command with super-user privileges, and if so
+	// counteracts it by running the commands as the original user.
+	if isRunningWithSudo() {
+		originalUser := os.Getenv("SUDO_USER")
+		prefixCommand = []string{"sudo", "-u", originalUser}
+	} else {
+		prefixCommand = []string{}
+	}
+
+	// Check if algorand is installed with Homebrew
+	checkCmdArgs := append(prefixCommand, "brew", "--prefix", "algorand", "--installed")
+	fmt.Println("Running command:", strings.Join(checkCmdArgs, " "))
+	checkCmd := exec.Command(checkCmdArgs[0], checkCmdArgs[1:]...)
+	checkCmd.Stdout = os.Stdout
+	checkCmd.Stderr = os.Stderr
+	if err := checkCmd.Run(); err != nil {
+		fmt.Println("Algorand is not installed with Homebrew.")
+		os.Exit(1)
+	}
+
+	// Upgrade algorand
+	upgradeCmdArgs := append(prefixCommand, "brew", "upgrade", "algorand", "--formula")
+	fmt.Println("Running command:", strings.Join(upgradeCmdArgs, " "))
+	upgradeCmd := exec.Command(upgradeCmdArgs[0], upgradeCmdArgs[1:]...)
+	upgradeCmd.Stdout = os.Stdout
+	upgradeCmd.Stderr = os.Stderr
+	if err := upgradeCmd.Run(); err != nil {
+		fmt.Printf("Failed to upgrade Algorand: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+// Upgrade a package using the specified Debian package manager
+func upgradeDebianPackage(packageManager, packageName string) {
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
+	// Check if the package is installed and if there are updates available using apt-cache policy
+	cmd := exec.Command("apt-cache", "policy", packageName)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Printf("Failed to check package policy: %v\n", err)
+		os.Exit(1)
+	}
+
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Installed: (none)") {
+		fmt.Printf("Package %s is not installed.\n", packageName)
+		os.Exit(1)
+	}
+
+	installedVersion := extractVersion(outputStr, "Installed:")
+	candidateVersion := extractVersion(outputStr, "Candidate:")
+
+	if installedVersion == candidateVersion {
+		fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion)
+		os.Exit(0)
+	}
+
+	fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion)
+
+	// Update the package list
+	fmt.Println("Updating package list...")
+	cmd = exec.Command(packageManager, "update")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to update package list: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Upgrade the package
+	fmt.Printf("Upgrading package %s...\n", packageName)
+	cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
+		os.Exit(1)
+	}
+
+	fmt.Printf("Package %s upgraded successfully.\n", packageName)
+	os.Exit(0)
+}
+
+// Upgrade a package using the specified RPM package manager
+func upgradeRpmPackage(packageManager, packageName string) {
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with sudo.")
+		os.Exit(1)
+	}
+
+	// Attempt to upgrade the package directly
+	fmt.Printf("Upgrading package %s...\n", packageName)
+	cmd := exec.Command(packageManager, "update", "-y", packageName)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
+		os.Exit(1)
+	}
+
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Nothing to do") {
+		fmt.Printf("Package %s is already up-to-date.\n", packageName)
+		os.Exit(0)
+	} else {
+		fmt.Println(outputStr)
+		fmt.Printf("Package %s upgraded successfully.\n", packageName)
+		os.Exit(0)
+	}
+}
diff --git a/cmd/utils.go b/cmd/node/utils.go
similarity index 75%
rename from cmd/utils.go
rename to cmd/node/utils.go
index b3c56acf..e15763e1 100644
--- a/cmd/utils.go
+++ b/cmd/node/utils.go
@@ -1,4 +1,4 @@
-package cmd
+package node
 
 import (
 	"bytes"
@@ -9,7 +9,6 @@ import (
 	"runtime"
 	"strings"
 	"sync"
-	"text/template"
 
 	"github.com/manifoldco/promptui"
 	"github.com/spf13/cobra"
@@ -107,73 +106,6 @@ func affectALGORAND_DATA(path string) {
 	fmt.Println("")
 }
 
-// Update the algorand.service file
-func editAlgorandServiceFile(dataDirectoryPath string) {
-
-	// TODO: look into setting algod path as well as the data directory path
-	// Find the path to the algod binary
-	algodPath, err := exec.LookPath("algod")
-	if err != nil {
-		fmt.Printf("Failed to find algod binary: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Path to the systemd service override file
-	// Assuming that this is the same everywhere systemd is used
-	overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
-
-	// Create the override directory if it doesn't exist
-	err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
-	if err != nil {
-		fmt.Printf("Failed to create override directory: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Content of the override file
-	const overrideTemplate = `[Unit]
-Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
-[Service]
-ExecStart=
-ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
-
-	// Data to fill the template
-	data := map[string]string{
-		"AlgodPath":         algodPath,
-		"DataDirectoryPath": dataDirectoryPath,
-	}
-
-	// Parse and execute the template
-	tmpl, err := template.New("override").Parse(overrideTemplate)
-	if err != nil {
-		fmt.Printf("Failed to parse template: %v\n", err)
-		os.Exit(1)
-	}
-
-	var overrideContent bytes.Buffer
-	err = tmpl.Execute(&overrideContent, data)
-	if err != nil {
-		fmt.Printf("Failed to execute template: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Write the override content to the file
-	err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
-	if err != nil {
-		fmt.Printf("Failed to write override file: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Reload systemd manager configuration
-	cmd := exec.Command("systemctl", "daemon-reload")
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to reload systemd daemon: %v\n", err)
-		os.Exit(1)
-	}
-
-	fmt.Println("Algorand service file updated successfully.")
-}
-
 // Check if the program is running with admin (super-user) priviledges
 func isRunningWithSudo() bool {
 	return os.Geteuid() == 0
@@ -304,8 +236,21 @@ func findAlgodPID() (int, error) {
 	return pid, nil
 }
 
-// Check systemctl has Algorand Service been created in the first place
-func checkSystemctlAlgorandServiceCreated() bool {
+// Check if Algorand service has been created
+func checkAlgorandServiceCreated() bool {
+	switch runtime.GOOS {
+	case "linux":
+		return checkSystemdAlgorandServiceCreated()
+	case "darwin":
+		return checkLaunchdAlgorandServiceCreated()
+	default:
+		fmt.Println("Unsupported operating system.")
+		return false
+	}
+}
+
+// Check if Algorand service has been created with systemd (Linux)
+func checkSystemdAlgorandServiceCreated() bool {
 	cmd := exec.Command("systemctl", "list-unit-files", "algorand.service")
 	var out bytes.Buffer
 	cmd.Stdout = &out
@@ -316,7 +261,40 @@ func checkSystemctlAlgorandServiceCreated() bool {
 	return strings.Contains(out.String(), "algorand.service")
 }
 
-func checkSystemctlAlgorandServiceActive() bool {
+// Check if Algorand service has been created with launchd (macOS)
+// Note that it needs to be run in super-user privilege mode to
+// be able to view the root level services.
+func checkLaunchdAlgorandServiceCreated() bool {
+	cmd := exec.Command("launchctl", "list", "com.algorand.algod")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	output := out.String()
+	if err != nil {
+		fmt.Printf("Failed to check launchd service: %v\n", err)
+		return false
+	}
+
+	if strings.Contains(output, "Could not find service") {
+		return false
+	}
+
+	return true
+}
+
+func checkAlgorandServiceActive() bool {
+	switch runtime.GOOS {
+	case "linux":
+		return checkSystemdAlgorandServiceActive()
+	case "darwin":
+		return checkLaunchdAlgorandServiceActive()
+	default:
+		fmt.Println("Unsupported operating system.")
+		return false
+	}
+}
+
+func checkSystemdAlgorandServiceActive() bool {
 	cmd := exec.Command("systemctl", "is-active", "algorand")
 	var out bytes.Buffer
 	cmd.Stdout = &out
@@ -327,6 +305,22 @@ func checkSystemctlAlgorandServiceActive() bool {
 	return strings.TrimSpace(out.String()) == "active"
 }
 
+func checkLaunchdAlgorandServiceActive() bool {
+	cmd := exec.Command("launchctl", "print", "system/com.algorand.algod")
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	output := out.String()
+	if err != nil {
+		return false
+	}
+	if strings.Contains(output, "Bad request") || strings.Contains(output, "Could not find service") {
+		return false
+	}
+
+	return true
+}
+
 // Extract version information from apt-cache policy output
 func extractVersion(output, prefix string) string {
 	lines := strings.Split(output, "\n")
@@ -338,3 +332,10 @@ func extractVersion(output, prefix string) string {
 	}
 	return ""
 }
+
+func isAlgodRunning() bool {
+	// Check if Algod is already running
+	// This works for systemctl started algorand.service as well as directly started algod
+	err := exec.Command("pgrep", "algod").Run()
+	return err == nil
+}
diff --git a/cmd/root.go b/cmd/root.go
index f50bc7a0..ae69ec23 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -4,7 +4,12 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"os"
+	"strings"
+
 	"github.com/algorandfoundation/hack-tui/api"
+	"github.com/algorandfoundation/hack-tui/cmd/node"
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/algorandfoundation/hack-tui/ui"
 	"github.com/algorandfoundation/hack-tui/ui/style"
@@ -13,20 +18,8 @@ import (
 	"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"io"
-	"os"
-	"strings"
 )
 
-const BANNER = `
-   _____  .__                __________              
-  /  _  \ |  |    ____   ____\______   \__ __  ____  
- /  /_\  \|  |   / ___\ /  _ \|       _/  |  \/    \ 
-/    |    \  |__/ /_/  >  <_> )    |   \  |  /   |  \
-\____|__  /____/\___  / \____/|____|_  /____/|___|  /
-        \/     /_____/               \/           \/ 
-`
-
 var (
 	server  string
 	token   = strings.Repeat("a", 64)
@@ -34,7 +27,7 @@ var (
 	rootCmd = &cobra.Command{
 		Use:   "algorun",
 		Short: "Manage Algorand nodes",
-		Long:  style.Purple(BANNER) + "\n",
+		Long:  style.Purple(style.BANNER) + "\n",
 		CompletionOptions: cobra.CompletionOptions{
 			DisableDefaultCmd: true,
 		},
@@ -145,6 +138,7 @@ func init() {
 
 	// Add Commands
 	rootCmd.AddCommand(statusCmd)
+	rootCmd.AddCommand(node.NodeCmd)
 }
 
 // Execute executes the root command.
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 7cae6f28..2fcf57f8 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -1,9 +1,10 @@
 package cmd
 
 import (
-	"github.com/spf13/viper"
 	"os"
 	"testing"
+
+	"github.com/spf13/viper"
 )
 
 // Test the stub root command
diff --git a/cmd/status.go b/cmd/status.go
index 5af7f38b..286a14ee 100644
--- a/cmd/status.go
+++ b/cmd/status.go
@@ -4,20 +4,21 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"os"
+
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/algorandfoundation/hack-tui/ui"
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"os"
 )
 
 // statusCmd is the main entrypoint for the `status` cobra.Command with a tea.Program
 var statusCmd = &cobra.Command{
 	Use:   "status",
 	Short: "Get the node status",
-	Long:  style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
+	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("View the node status"),
 	RunE: func(cmd *cobra.Command, args []string) error {
 		initConfig()
 		if viper.GetString("server") == "" {
diff --git a/internal/accounts.go b/internal/accounts.go
index 7169f828..b7492b03 100644
--- a/internal/accounts.go
+++ b/internal/accounts.go
@@ -121,18 +121,23 @@ func AccountsFromState(state *StateModel, t Time, client *api.ClientWithResponse
 	for _, key := range *state.ParticipationKeys {
 		val, ok := values[key.Address]
 		if !ok {
-
-			account, err := GetAccount(client, key.Address)
-
-			// TODO: handle error
-			if err != nil {
-				// TODO: Logging
-				panic(err)
+			var account = api.Account{
+				Address: key.Address,
+				Status:  "Unknown",
+				Amount:  0,
 			}
-
-			var expires = t.Now()
+			if state.Status.State != "SYNCING" {
+				var err error
+				account, err = GetAccount(client, key.Address)
+				// TODO: handle error
+				if err != nil {
+					// TODO: Logging
+					panic(err)
+				}
+			}
+			now := t.Now()
+			var expires = now.Add(-(time.Hour * 24 * 365 * 100))
 			if key.EffectiveLastValid != nil {
-				now := t.Now()
 				roundDiff := max(0, *key.EffectiveLastValid-int(state.Status.LastRound))
 				distance := int(state.Metrics.RoundTime) * roundDiff
 				expires = now.Add(time.Duration(distance))
diff --git a/internal/state.go b/internal/state.go
index e14c07aa..7b3ccd8b 100644
--- a/internal/state.go
+++ b/internal/state.go
@@ -62,6 +62,11 @@ func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Co
 		// Fetch Keys
 		s.UpdateKeys(ctx, client)
 
+		if s.Status.State == "SYNCING" {
+			lastRound = s.Status.LastRound
+			cb(s, nil)
+			continue
+		}
 		// Run Round Averages and RX/TX every 5 rounds
 		if s.Status.LastRound%5 == 0 {
 			bm, err := GetBlockMetrics(ctx, client, s.Status.LastRound, s.Metrics.Window)
diff --git a/ui/pages/accounts/controller.go b/ui/pages/accounts/controller.go
index 139ee6dc..4e4cf942 100644
--- a/ui/pages/accounts/controller.go
+++ b/ui/pages/accounts/controller.go
@@ -21,7 +21,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) {
 
 	switch msg := msg.(type) {
 	case internal.StateModel:
-		m.Data = msg.Accounts
+		m.Data = &msg
 		m.table.SetRows(*m.makeRows())
 	case tea.KeyMsg:
 		switch msg.String() {
diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go
index 425adf40..91b550cb 100644
--- a/ui/pages/accounts/model.go
+++ b/ui/pages/accounts/model.go
@@ -4,6 +4,7 @@ import (
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	"sort"
 	"strconv"
+	"time"
 
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/charmbracelet/bubbles/table"
@@ -13,7 +14,7 @@ import (
 type ViewModel struct {
 	Width  int
 	Height int
-	Data   map[string]internal.Account
+	Data   *internal.StateModel
 
 	table      table.Model
 	navigation string
@@ -24,7 +25,7 @@ func New(state *internal.StateModel) ViewModel {
 	m := ViewModel{
 		Width:      0,
 		Height:     0,
-		Data:       state.Accounts,
+		Data:       state,
 		controls:   "( (g)enerate )",
 		navigation: "| " + style.Green.Render("(a)ccounts") + " | (k)eys | (t)xn |",
 	}
@@ -52,7 +53,7 @@ func (m ViewModel) SelectedAccount() internal.Account {
 	var account internal.Account
 	var selectedRow = m.table.SelectedRow()
 	if selectedRow != nil {
-		account = m.Data[selectedRow[0]]
+		account = m.Data.Accounts[selectedRow[0]]
 	}
 	return account
 }
@@ -70,13 +71,20 @@ func (m ViewModel) makeColumns(width int) []table.Column {
 func (m ViewModel) makeRows() *[]table.Row {
 	rows := make([]table.Row, 0)
 
-	for key := range m.Data {
+	for key := range m.Data.Accounts {
+		expires := m.Data.Accounts[key].Expires.String()
+		if m.Data.Status.State == "SYNCING" {
+			expires = "SYNCING"
+		}
+		if !m.Data.Accounts[key].Expires.After(time.Now().Add(-(time.Hour * 24 * 365 * 50))) {
+			expires = "NA"
+		}
 		rows = append(rows, table.Row{
-			m.Data[key].Address,
-			strconv.Itoa(m.Data[key].Keys),
-			m.Data[key].Status,
-			m.Data[key].Expires.String(),
-			strconv.Itoa(m.Data[key].Balance),
+			m.Data.Accounts[key].Address,
+			strconv.Itoa(m.Data.Accounts[key].Keys),
+			m.Data.Accounts[key].Status,
+			expires,
+			strconv.Itoa(m.Data.Accounts[key].Balance),
 		})
 	}
 	sort.SliceStable(rows, func(i, j int) bool {
diff --git a/ui/status.go b/ui/status.go
index 451a5758..ade98da4 100644
--- a/ui/status.go
+++ b/ui/status.go
@@ -2,14 +2,15 @@ package ui
 
 import (
 	"fmt"
-	"github.com/algorandfoundation/hack-tui/internal"
-	"github.com/algorandfoundation/hack-tui/ui/style"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/lipgloss"
 	"math"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/algorandfoundation/hack-tui/internal"
+	"github.com/algorandfoundation/hack-tui/ui/style"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
 )
 
 // StatusViewModel is extended from the internal.StatusModel
@@ -85,14 +86,22 @@ func (m StatusViewModel) View() string {
 	// Last Round
 	row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
 
-	beginning = style.Blue.Render(" Round time: ") + fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second))
-	end = getBitRate(m.Data.Metrics.TX) + style.Green.Render("TX ")
+	roundTime := fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second))
+	if m.Data.Status.State == "SYNCING" {
+		roundTime = "--"
+	}
+	beginning = style.Blue.Render(" Round time: ") + roundTime
+	end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.TX/1024) + style.Green.Render("TX ")
 	middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
 
 	row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
 
-	beginning = style.Blue.Render(" TPS: ") + fmt.Sprintf("%.2f", m.Data.Metrics.TPS)
-	end = getBitRate(m.Data.Metrics.RX) + style.Green.Render("RX ")
+	tps := fmt.Sprintf("%.2f", m.Data.Metrics.TPS)
+	if m.Data.Status.State == "SYNCING" {
+		tps = "--"
+	}
+	beginning = style.Blue.Render(" TPS: ") + tps
+	end = fmt.Sprintf("%d KB/s ", m.Data.Metrics.RX/1024) + style.Green.Render("RX ")
 	middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
 
 	row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
diff --git a/ui/style/style.go b/ui/style/style.go
index 1647f3f7..dcb5fdb2 100644
--- a/ui/style/style.go
+++ b/ui/style/style.go
@@ -1,8 +1,9 @@
 package style
 
 import (
-	"github.com/charmbracelet/lipgloss"
 	"strings"
+
+	"github.com/charmbracelet/lipgloss"
 )
 
 var (
@@ -73,3 +74,12 @@ func WithNavigation(controls string, view string) string {
 	}
 	return view
 }
+
+const BANNER = `
+   _____  .__                __________              
+  /  _  \ |  |    ____   ____\______   \__ __  ____  
+ /  /_\  \|  |   / ___\ /  _ \|       _/  |  \/    \ 
+/    |    \  |__/ /_/  >  <_> )    |   \  |  /   |  \
+\____|__  /____/\___  / \____/|____|_  /____/|___|  /
+        \/     /_____/               \/           \/ 
+`
diff --git a/ui/viewport.go b/ui/viewport.go
index 660c73cf..9f87aa8b 100644
--- a/ui/viewport.go
+++ b/ui/viewport.go
@@ -123,7 +123,7 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			if m.page == KeysPage {
 				selKey := m.keysPage.SelectedKey()
-				if selKey != nil {
+				if selKey != nil && m.Data.Status.State != "SYNCING" {
 					m.page = TransactionPage
 					return m, keys.EmitKeySelected(selKey)
 				}
@@ -143,21 +143,24 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			return m, nil
 		case "t":
-			if m.page == AccountsPage {
-				acct := m.accountsPage.SelectedAccount()
-				data := *m.Data.ParticipationKeys
-				for i, key := range data {
-					if key.Address == acct.Address {
-						m.page = TransactionPage
-						return m, keys.EmitKeySelected(&data[i])
+			if m.Data.Status.State != "SYNCING" {
+
+				if m.page == AccountsPage {
+					acct := m.accountsPage.SelectedAccount()
+					data := *m.Data.ParticipationKeys
+					for i, key := range data {
+						if key.Address == acct.Address {
+							m.page = TransactionPage
+							return m, keys.EmitKeySelected(&data[i])
+						}
 					}
 				}
-			}
-			if m.page == KeysPage {
-				selKey := m.keysPage.SelectedKey()
-				if selKey != nil {
-					m.page = TransactionPage
-					return m, keys.EmitKeySelected(selKey)
+				if m.page == KeysPage {
+					selKey := m.keysPage.SelectedKey()
+					if selKey != nil {
+						m.page = TransactionPage
+						return m, keys.EmitKeySelected(selKey)
+					}
 				}
 			}
 			return m, nil

From 70456da3042c817791b37a5c69b43fe5b6a1dc7e Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 15:01:02 +0100
Subject: [PATCH 03/23] feat: add prompt-ui

feat: configure and set algod data directory

fix: RX/TX display

fix: bit rate display for GB

fix: configuration override order

feat: handle invalid configuration and token gracefully

test: fix test state

fix: loading of custom endpoint address

fix: loading default port

test: clear viper settings

fix: finds path to directory and gives cmd instruction

feat: adds node start and node stop commands

fix: add -y

fix: turn script into indivudal commands

feat: check if sudo, clarify shell

chore: make more go idiomatic

fix: fix proper path check

fix: interact with systemctl, cleanup prompts

fix: remove sudo

fix: separate commands

fix: proper algorand service name

fix: calling with sudo

chore: testing systemctl

fix: checks algorand system service has been enabled directly

feat: implements editAlgorandServiceFile

fix: else statement

fix: quick check branch

fix: string template

feat: adds upgrade

chore: removeu nnecessary code

fix: check that installed and candidate are the same

chore: improve print

chore: add more output

fix: single quote

fix: -y

fix: systemctl

fix: upgrade and sudo text

chore: go mod tidy

fix: upgrade

feat: disable ui elements while syncing

feat: skip account loading on syncing
feat: remove offline account expires date

feat: installs algod and sets up service on mac

feat: refactor, + mac

feat: adds uninstall, mac only

fix: remove plist file

chore: rename

test: protocol snapshots and 100%
---
 ui/protocol_test.go                           | 120 +++++++++++++++++-
 .../Test_ProtocolSnapshot/Hidden.golden       |   0
 .../Test_ProtocolSnapshot/HiddenHeight.golden |   0
 .../NoVoteOrUpgrade.golden                    |   7 +
 .../NoVoteOrUpgradeSmall.golden               |   7 +
 .../Test_ProtocolSnapshot/Visible.golden      |   7 +
 .../Test_ProtocolSnapshot/VisibleSmall.golden |   7 +
 7 files changed, 143 insertions(+), 5 deletions(-)
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/Hidden.golden
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/Visible.golden
 create mode 100644 ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden

diff --git a/ui/protocol_test.go b/ui/protocol_test.go
index 74e1d672..99c3e63b 100644
--- a/ui/protocol_test.go
+++ b/ui/protocol_test.go
@@ -2,14 +2,108 @@ package ui
 
 import (
 	"bytes"
+	"testing"
+	"time"
+
 	"github.com/algorandfoundation/hack-tui/internal"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/golden"
 	"github.com/charmbracelet/x/exp/teatest"
-	"testing"
-	"time"
 )
 
-func Test_ProtocolViewRender(t *testing.T) {
+var protocolViewSnapshots = map[string]ProtocolViewModel{
+	"Hidden": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      true,
+			NeedsUpdate: true,
+			LastRound:   0,
+		},
+		TerminalWidth:  60,
+		TerminalHeight: 40,
+		IsVisible:      false,
+	},
+	"HiddenHeight": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      true,
+			NeedsUpdate: true,
+			LastRound:   0,
+		},
+		TerminalWidth:  70,
+		TerminalHeight: 20,
+		IsVisible:      true,
+	},
+	"Visible": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      true,
+			NeedsUpdate: true,
+			LastRound:   0,
+		},
+		TerminalWidth:  160,
+		TerminalHeight: 80,
+		IsVisible:      true,
+	},
+	"VisibleSmall": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      true,
+			NeedsUpdate: true,
+			LastRound:   0,
+		},
+		TerminalWidth:  80,
+		TerminalHeight: 40,
+		IsVisible:      true,
+	},
+	"NoVoteOrUpgrade": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      false,
+			NeedsUpdate: false,
+			LastRound:   0,
+		},
+		TerminalWidth:  160,
+		TerminalHeight: 80,
+		IsVisible:      true,
+	},
+	"NoVoteOrUpgradeSmall": {
+		Data: internal.StatusModel{
+			State:       "SYNCING",
+			Version:     "v0.0.0-test",
+			Network:     "test-v1",
+			Voting:      false,
+			NeedsUpdate: false,
+			LastRound:   0,
+		},
+		TerminalWidth:  80,
+		TerminalHeight: 40,
+		IsVisible:      true,
+	},
+}
+
+func Test_ProtocolSnapshot(t *testing.T) {
+	for name, model := range protocolViewSnapshots {
+		t.Run(name, func(t *testing.T) {
+			got := ansi.Strip(model.View())
+			golden.RequireEqual(t, []byte(got))
+		})
+	}
+}
+
+// Test_ProtocolMessages handles any additional tests like sending messages
+func Test_ProtocolMessages(t *testing.T) {
 	state := internal.StateModel{
 		Status: internal.StatusModel{
 			LastRound:   1337,
@@ -41,9 +135,25 @@ func Test_ProtocolViewRender(t *testing.T) {
 		teatest.WithCheckInterval(time.Millisecond*100),
 		teatest.WithDuration(time.Second*3),
 	)
+	tm.Send(internal.StatusModel{
+		State:       "",
+		Version:     "",
+		Network:     "",
+		Voting:      false,
+		NeedsUpdate: false,
+		LastRound:   0,
+	})
+	// Send hide key
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("h"),
+	})
 
-	// Send quit msg
-	tm.Send(tea.QuitMsg{})
+	// Send quit key
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("ctrl+c"),
+	})
 
 	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
 }
diff --git a/ui/testdata/Test_ProtocolSnapshot/Hidden.golden b/ui/testdata/Test_ProtocolSnapshot/Hidden.golden
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden b/ui/testdata/Test_ProtocolSnapshot/HiddenHeight.golden
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
new file mode 100644
index 00000000..6eca90f5
--- /dev/null
+++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
@@ -0,0 +1,7 @@
+╭──────────────────────────────────────────────────────────────────────────────╮
+│ Node: v0.0.0-test                                                            │
+│                                                                              │
+│ Network: test-v1                                                             │
+│                                                                              │
+│ Protocol Voting: false                                                       │
+╰──────────────────────────────────────────────────────────────────────────────╯
\ No newline at end of file
diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
new file mode 100644
index 00000000..a0cc43b9
--- /dev/null
+++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
@@ -0,0 +1,7 @@
+╭──────────────────────────────────────────────────────────────────────────────╮
+│ Node: v0.0.0-test                                                            │
+│ Network: test-v1                                                             │
+│ Protocol Voting: false                                                       │
+│                                                                              │
+│                                                                              │
+╰──────────────────────────────────────────────────────────────────────────────╯
\ No newline at end of file
diff --git a/ui/testdata/Test_ProtocolSnapshot/Visible.golden b/ui/testdata/Test_ProtocolSnapshot/Visible.golden
new file mode 100644
index 00000000..a8adee3f
--- /dev/null
+++ b/ui/testdata/Test_ProtocolSnapshot/Visible.golden
@@ -0,0 +1,7 @@
+╭──────────────────────────────────────────────────────────────────────────────╮
+│ Node: v0.0.0-test                                         [UPDATE AVAILABLE] │
+│                                                                              │
+│ Network: test-v1                                                             │
+│                                                                              │
+│ Protocol Voting: true                                                        │
+╰──────────────────────────────────────────────────────────────────────────────╯
\ No newline at end of file
diff --git a/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden b/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden
new file mode 100644
index 00000000..10a5f701
--- /dev/null
+++ b/ui/testdata/Test_ProtocolSnapshot/VisibleSmall.golden
@@ -0,0 +1,7 @@
+╭──────────────────────────────────────────────────────────────────────────────╮
+│ Node: v0.0.0-test                                                            │
+│ Network: test-v1                                                             │
+│ Protocol Voting: true                                                        │
+│ Upgrade Available: true                                                      │
+│                                                                              │
+╰──────────────────────────────────────────────────────────────────────────────╯
\ No newline at end of file

From ecf00bd8ac945646fc3d5d0d8e615ddd9ba0c237 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 15:01:59 +0100
Subject: [PATCH 04/23] # This is a combination of 51 commits. # This is the
 1st commit message:

feat: add prompt-ui

# This is the commit message #2:

feat: configure and set algod data directory

# This is the commit message #3:

fix: RX/TX display

# This is the commit message #4:

fix: bit rate display for GB

# This is the commit message #5:

fix: configuration override order

# This is the commit message #6:

feat: handle invalid configuration and token gracefully

# This is the commit message #7:

test: fix test state

# This is the commit message #8:

fix: loading of custom endpoint address

# This is the commit message #9:

fix: loading default port

# This is the commit message #10:

test: clear viper settings

# This is the commit message #11:

fix: finds path to directory and gives cmd instruction

# This is the commit message #12:

feat: adds node start and node stop commands

# This is the commit message #13:

fix: add -y

# This is the commit message #14:

fix: turn script into indivudal commands

# This is the commit message #15:

feat: check if sudo, clarify shell

# This is the commit message #16:

chore: make more go idiomatic

# This is the commit message #17:

fix: fix proper path check

# This is the commit message #18:

fix: interact with systemctl, cleanup prompts

# This is the commit message #19:

fix: remove sudo

# This is the commit message #20:

fix: separate commands

# This is the commit message #21:

fix: proper algorand service name

# This is the commit message #22:

fix: calling with sudo

# This is the commit message #23:

chore: testing systemctl

# This is the commit message #24:

fix: checks algorand system service has been enabled directly

# This is the commit message #25:

feat: implements editAlgorandServiceFile

# This is the commit message #26:

fix: else statement

# This is the commit message #27:

fix: quick check branch

# This is the commit message #28:

fix: string template

# This is the commit message #29:

feat: adds upgrade

# This is the commit message #30:

chore: removeu nnecessary code

# This is the commit message #31:

fix: check that installed and candidate are the same

# This is the commit message #32:

chore: improve print

# This is the commit message #33:

chore: add more output

# This is the commit message #34:

fix: single quote

# This is the commit message #35:

fix: -y

# This is the commit message #36:

fix: systemctl

# This is the commit message #37:

fix: upgrade and sudo text

# This is the commit message #38:

chore: go mod tidy

# This is the commit message #39:

fix: upgrade

# This is the commit message #40:

feat: disable ui elements while syncing

# This is the commit message #41:

feat: skip account loading on syncing
feat: remove offline account expires date

# This is the commit message #42:

feat: installs algod and sets up service on mac

# This is the commit message #43:

feat: refactor, + mac

# This is the commit message #44:

feat: adds uninstall, mac only

# This is the commit message #45:

fix: remove plist file

# This is the commit message #46:

chore: rename

# This is the commit message #47:

test: protocol snapshots and 100%

# This is the commit message #48:

test: status snapshots and 100%

# This is the commit message #49:

test: error page snapshots and 100%

# This is the commit message #50:

test: controls snapshots

# This is the commit message #51:

test: accounts snapshots
---
 ui/controls/controls_test.go                  |  12 +-
 .../testdata/Test_Snapshot/Visible.golden     |   3 +
 ui/error_test.go                              |  52 +++++++++
 ui/pages/accounts/accounts_test.go            | 106 ++++++++++++++++++
 .../testdata/Test_Snapshot/Visible.golden     |  26 +++++
 ui/status_test.go                             |  82 +++++++++++++-
 ui/testdata/Test_ErrorSnapshot/Visible.golden |  24 ++++
 ui/testdata/Test_StatusSnapshot/Hidden.golden |   0
 .../Test_StatusSnapshot/Loading.golden        |   6 +
 .../Test_StatusSnapshot/Syncing.golden        |   7 ++
 10 files changed, 316 insertions(+), 2 deletions(-)
 create mode 100644 ui/controls/testdata/Test_Snapshot/Visible.golden
 create mode 100644 ui/error_test.go
 create mode 100644 ui/pages/accounts/accounts_test.go
 create mode 100644 ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
 create mode 100644 ui/testdata/Test_ErrorSnapshot/Visible.golden
 create mode 100644 ui/testdata/Test_StatusSnapshot/Hidden.golden
 create mode 100644 ui/testdata/Test_StatusSnapshot/Loading.golden
 create mode 100644 ui/testdata/Test_StatusSnapshot/Syncing.golden

diff --git a/ui/controls/controls_test.go b/ui/controls/controls_test.go
index ed1811a1..2693afa3 100644
--- a/ui/controls/controls_test.go
+++ b/ui/controls/controls_test.go
@@ -3,12 +3,22 @@ package controls
 import (
 	"bytes"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/golden"
 	"github.com/charmbracelet/x/exp/teatest"
 	"testing"
 	"time"
 )
 
-func Test_Controls(t *testing.T) {
+func Test_Snapshot(t *testing.T) {
+	t.Run("Visible", func(t *testing.T) {
+		model := New(" test ")
+		got := ansi.Strip(model.View())
+		golden.RequireEqual(t, []byte(got))
+	})
+}
+
+func Test_Messages(t *testing.T) {
 	expected := "(q)uit | (d)elete | (g)enerate | (t)xn | (h)ide"
 	// Create the Model
 	m := New(expected)
diff --git a/ui/controls/testdata/Test_Snapshot/Visible.golden b/ui/controls/testdata/Test_Snapshot/Visible.golden
new file mode 100644
index 00000000..8f20fb4b
--- /dev/null
+++ b/ui/controls/testdata/Test_Snapshot/Visible.golden
@@ -0,0 +1,3 @@
+                                    ╭──────╮                                    
+────────────────────────────────────┤ test ├────────────────────────────────────
+                                    ╰──────╯                                    
\ No newline at end of file
diff --git a/ui/error_test.go b/ui/error_test.go
new file mode 100644
index 00000000..10020eac
--- /dev/null
+++ b/ui/error_test.go
@@ -0,0 +1,52 @@
+package ui
+
+import (
+	"bytes"
+	"github.com/algorandfoundation/hack-tui/ui/controls"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/golden"
+	"github.com/charmbracelet/x/exp/teatest"
+	"testing"
+	"time"
+)
+
+func Test_ErrorSnapshot(t *testing.T) {
+	t.Run("Visible", func(t *testing.T) {
+		model := ErrorViewModel{
+			Height:   20,
+			Width:    40,
+			controls: controls.New(" Error "),
+			Message:  "a test error",
+		}
+		got := ansi.Strip(model.View())
+		golden.RequireEqual(t, []byte(got))
+	})
+}
+
+func Test_ErrorMessages(t *testing.T) {
+	tm := teatest.NewTestModel(
+		t, ErrorViewModel{Message: "a test error"},
+		teatest.WithInitialTermSize(120, 80),
+	)
+
+	// Wait for prompt to exit
+	teatest.WaitFor(
+		t, tm.Output(),
+		func(bts []byte) bool {
+			return bytes.Contains(bts, []byte("a test error"))
+		},
+		teatest.WithCheckInterval(time.Millisecond*100),
+		teatest.WithDuration(time.Second*3),
+	)
+	// Resize Message
+	tm.Send(tea.WindowSizeMsg{
+		Width:  50,
+		Height: 20,
+	})
+
+	// Send quit key
+	tm.Send(tea.QuitMsg{})
+
+	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
+}
diff --git a/ui/pages/accounts/accounts_test.go b/ui/pages/accounts/accounts_test.go
new file mode 100644
index 00000000..d6afa218
--- /dev/null
+++ b/ui/pages/accounts/accounts_test.go
@@ -0,0 +1,106 @@
+package accounts
+
+import (
+	"bytes"
+	"github.com/algorandfoundation/hack-tui/api"
+	"github.com/algorandfoundation/hack-tui/internal"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/golden"
+	"github.com/charmbracelet/x/exp/teatest"
+	"testing"
+	"time"
+)
+
+func Test_Snapshot(t *testing.T) {
+	t.Run("Visible", func(t *testing.T) {
+		model := New(&internal.StateModel{
+			Status:            internal.StatusModel{},
+			Metrics:           internal.MetricsModel{},
+			Accounts:          nil,
+			ParticipationKeys: nil,
+			Admin:             false,
+			Watching:          false,
+		})
+		got := ansi.Strip(model.View())
+		golden.RequireEqual(t, []byte(got))
+	})
+}
+
+func Test_Messages(t *testing.T) {
+	var testKeys = []api.ParticipationKey{
+		{
+			Address:             "ABC",
+			EffectiveFirstValid: nil,
+			EffectiveLastValid:  nil,
+			Id:                  "",
+			Key: api.AccountParticipation{
+				SelectionParticipationKey: nil,
+				StateProofKey:             nil,
+				VoteFirstValid:            0,
+				VoteKeyDilution:           0,
+				VoteLastValid:             0,
+				VoteParticipationKey:      nil,
+			},
+			LastBlockProposal: nil,
+			LastStateProof:    nil,
+			LastVote:          nil,
+		},
+	}
+	sm := &internal.StateModel{
+		Status:            internal.StatusModel{},
+		Metrics:           internal.MetricsModel{},
+		Accounts:          nil,
+		ParticipationKeys: &testKeys,
+		Admin:             false,
+		Watching:          false,
+	}
+	values := make(map[string]internal.Account)
+	for _, key := range *sm.ParticipationKeys {
+		val, ok := values[key.Address]
+		if !ok {
+			values[key.Address] = internal.Account{
+				Address: key.Address,
+				Status:  "Offline",
+				Balance: 0,
+				Expires: time.Unix(0, 0),
+				Keys:    1,
+			}
+		} else {
+			val.Keys++
+			values[key.Address] = val
+		}
+	}
+	sm.Accounts = values
+	// Create the Model
+	m := New(sm)
+
+	tm := teatest.NewTestModel(
+		t, m,
+		teatest.WithInitialTermSize(80, 40),
+	)
+
+	// Wait for prompt to exit
+	teatest.WaitFor(
+		t, tm.Output(),
+		func(bts []byte) bool {
+			return bytes.Contains(bts, []byte("(k)eys"))
+		},
+		teatest.WithCheckInterval(time.Millisecond*100),
+		teatest.WithDuration(time.Second*3),
+	)
+
+	tm.Send(*sm)
+
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("enter"),
+	})
+
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("ctrl+c"),
+	})
+
+	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
+}
diff --git a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
new file mode 100644
index 00000000..0c6418cd
--- /dev/null
+++ b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
@@ -0,0 +1,26 @@
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                  ╭──────────────────────────────────────────╮                  
+──────────────────┤ (g)enerate | (a)ccounts | (k)eys | (t)xn ├──────────────────
+                  ╰──────────────────────────────────────────╯                  
\ No newline at end of file
diff --git a/ui/status_test.go b/ui/status_test.go
index 83e77ab8..823a0202 100644
--- a/ui/status_test.go
+++ b/ui/status_test.go
@@ -3,6 +3,10 @@ package ui
 import (
 	"bytes"
 	"github.com/algorandfoundation/hack-tui/internal"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/golden"
+	"github.com/charmbracelet/x/exp/teatest"
 	"testing"
 	"time"
 
@@ -10,8 +14,67 @@ import (
 	"github.com/charmbracelet/x/exp/teatest"
 )
 
-func Test_StatusViewRender(t *testing.T) {
+var statusViewSnapshots = map[string]StatusViewModel{
+	"Syncing": {
+		Data: &internal.StateModel{
+			Status: internal.StatusModel{
+				LastRound:   1337,
+				NeedsUpdate: true,
+				State:       "SYNCING",
+			},
+			Metrics: internal.MetricsModel{
+				RoundTime: 0,
+				TX:        0,
+			},
+		},
+		TerminalWidth:  180,
+		TerminalHeight: 80,
+		IsVisible:      true,
+	},
+	"Hidden": {
+		Data: &internal.StateModel{
+			Status: internal.StatusModel{
+				LastRound:   1337,
+				NeedsUpdate: true,
+				State:       "SYNCING",
+			},
+			Metrics: internal.MetricsModel{
+				RoundTime: 0,
+				TX:        0,
+			},
+		},
+		TerminalWidth:  180,
+		TerminalHeight: 80,
+		IsVisible:      false,
+	},
+	"Loading": {
+		Data: &internal.StateModel{
+			Status: internal.StatusModel{
+				LastRound:   1337,
+				NeedsUpdate: true,
+				State:       "SYNCING",
+			},
+			Metrics: internal.MetricsModel{
+				RoundTime: 0,
+				TX:        0,
+			},
+		},
+		TerminalWidth:  0,
+		TerminalHeight: 0,
+		IsVisible:      true,
+	},
+}
 
+func Test_StatusSnapshot(t *testing.T) {
+	for name, model := range statusViewSnapshots {
+		t.Run(name, func(t *testing.T) {
+			got := ansi.Strip(model.View())
+			golden.RequireEqual(t, []byte(got))
+		})
+	}
+}
+
+func Test_StatusMessages(t *testing.T) {
 	state := internal.StateModel{
 		Status: internal.StatusModel{
 			LastRound:   1337,
@@ -47,8 +110,25 @@ func Test_StatusViewRender(t *testing.T) {
 		teatest.WithDuration(time.Second*3),
 	)
 
+<<<<<<< HEAD
 	// Send quit msg
 	tm.Send(tea.QuitMsg{})
+=======
+	// Send the state
+	tm.Send(state)
+
+	// Send hide key
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("h"),
+	})
+
+	// Send quit key
+	tm.Send(tea.KeyMsg{
+		Type:  tea.KeyRunes,
+		Runes: []rune("ctrl+c"),
+	})
+>>>>>>> 967fa6b (test: status snapshots and 100%)
 
 	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
 }
diff --git a/ui/testdata/Test_ErrorSnapshot/Visible.golden b/ui/testdata/Test_ErrorSnapshot/Visible.golden
new file mode 100644
index 00000000..80bcfb88
--- /dev/null
+++ b/ui/testdata/Test_ErrorSnapshot/Visible.golden
@@ -0,0 +1,24 @@
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                  a test error                                 
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                                                               
+                                   ╭───────╮                                   
+───────────────────────────────────┤ Error ├───────────────────────────────────
+                                   ╰───────╯                                   
\ No newline at end of file
diff --git a/ui/testdata/Test_StatusSnapshot/Hidden.golden b/ui/testdata/Test_StatusSnapshot/Hidden.golden
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/testdata/Test_StatusSnapshot/Loading.golden b/ui/testdata/Test_StatusSnapshot/Loading.golden
new file mode 100644
index 00000000..fe45233a
--- /dev/null
+++ b/ui/testdata/Test_StatusSnapshot/Loading.golden
@@ -0,0 +1,6 @@
+Loading...
+
+
+
+
+
diff --git a/ui/testdata/Test_StatusSnapshot/Syncing.golden b/ui/testdata/Test_StatusSnapshot/Syncing.golden
new file mode 100644
index 00000000..7edf0f7e
--- /dev/null
+++ b/ui/testdata/Test_StatusSnapshot/Syncing.golden
@@ -0,0 +1,7 @@
+╭────────────────────────────────────────────────────────────────────────────────────────╮
+│ Latest Round: 1337                                                             SYNCING │
+│                                                                                        │
+│ -- 0 round average --                                                                  │
+│ Round time: 0.00s                                                            0 KB/s TX │
+│ TPS: 0.00                                                                    0 KB/s RX │
+╰────────────────────────────────────────────────────────────────────────────────────────╯
\ No newline at end of file

From 415ea4628ff867f9dd7a8998a2b3a49f83779bd9 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 15:02:20 +0100
Subject: [PATCH 05/23] feat: add prompt-ui

feat: configure and set algod data directory

fix: RX/TX display

fix: bit rate display for GB

fix: configuration override order

feat: handle invalid configuration and token gracefully

test: fix test state

fix: loading of custom endpoint address

fix: loading default port

test: clear viper settings

fix: finds path to directory and gives cmd instruction

feat: adds node start and node stop commands

fix: add -y

fix: turn script into indivudal commands

feat: check if sudo, clarify shell

chore: make more go idiomatic

fix: fix proper path check

fix: interact with systemctl, cleanup prompts

fix: remove sudo

fix: separate commands

fix: proper algorand service name

fix: calling with sudo

chore: testing systemctl

fix: checks algorand system service has been enabled directly

feat: implements editAlgorandServiceFile

fix: else statement

fix: quick check branch

fix: string template

feat: adds upgrade

chore: removeu nnecessary code

fix: check that installed and candidate are the same

chore: improve print

chore: add more output

fix: single quote

fix: -y

fix: systemctl

fix: upgrade and sudo text

chore: go mod tidy

fix: upgrade

feat: disable ui elements while syncing

feat: skip account loading on syncing
feat: remove offline account expires date

feat: installs algod and sets up service on mac

feat: refactor, + mac

feat: adds uninstall, mac only

fix: remove plist file

chore: rename

test: protocol snapshots and 100%

test: status snapshots and 100%

test: error page snapshots and 100%

test: controls snapshots

test: accounts snapshots

build: go mod tidy

README: Add alpha/dev warnings & notes

README: Moved note

README: added note

README: added note about admin token

README: reordered

README: numbered build steps

README: final note

test: add codecov reporter

ci: temp node test flow

chore: only when push

systemctl ENABLE

fix: rm -f file

fix: install start and uninstall for redhat

fix: add systemctl check

chore: printout

fix: ubuntu-based uninstall

fix: uninstallation remove service

fix: adds extar systemctl daemon-reload

chroe: temp

fix: provide output

chore: dont fail silently

fix: wildcard

fix: remove extra daemon-reload

fix: remore unnecessary output

chore: rephrase

ci: run in pushes
---
 .github/workflows/code_test.yaml |  8 ++--
 .github/workflows/node_test.yaml | 58 +++++++++++++++++++++++++++
 .testcoverage.yaml               | 26 -------------
 README.md                        | 33 ++++++++++++----
 cmd/node/install.go              | 26 ++++++-------
 cmd/node/uninstall.go            | 67 ++++++++++++++++++++++++++++++--
 go.mod                           |  4 +-
 7 files changed, 165 insertions(+), 57 deletions(-)
 create mode 100644 .github/workflows/node_test.yaml
 delete mode 100644 .testcoverage.yaml

diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml
index 2ab07538..cd6a095e 100644
--- a/.github/workflows/code_test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -56,9 +56,9 @@ jobs:
         run: go build -o bin/algorun *.go
 
       - name: Test with the Go CLI
-        run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./...
+        run: go test ./... -coverprofile=./coverage.txt -covermode=atomic -coverpkg=./...
 
-      - name: Check test coverage
-        uses: vladopajic/go-test-coverage@v2
+      - name: Upload results to Codecov
+        uses: codecov/codecov-action@v4
         with:
-          config: ./.testcoverage.yaml
+          token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
new file mode 100644
index 00000000..4e62f691
--- /dev/null
+++ b/.github/workflows/node_test.yaml
@@ -0,0 +1,58 @@
+## This is a temporary flow, until we have our custom docker images that work with systemd for linux.
+## Once we have that, we can remove this and use docker containers in parallel, covering the various OS:es.
+
+name: Node Command OS-Matrix Test
+
+on:
+  workflow_dispatch:
+  push:
+    paths:
+      - "cmd/**"
+  pull_request:
+    paths:
+      - "cmd/**"
+
+jobs:
+  ubuntu:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Setup Go
+        uses: actions/setup-go@v4
+        with:
+          go-version: 1.22
+      - name: Run Ubuntu commands
+        run: |
+          git clone https://github.com/algorandfoundation/hack-tui.git
+          cd hack-tui
+          go build .
+          sudo ./hack-tui node install
+          sudo ./hack-tui node start
+          systemctl status algorand.service
+          export TOKEN=$(cat /var/lib/algorand/algod.admin.token)
+          curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
+          sudo ./hack-tui node stop
+          sudo ./hack-tui node upgrade
+          # sudo ./hack-tui node configure
+          sudo ./hack-tui node uninstall
+
+  macos:
+    runs-on: macos-latest
+    steps:
+      - name: Run MacOs commands
+        run: |
+          brew install go
+          cd ~/
+          git clone https://github.com/algorandfoundation/hack-tui.git
+          cd hack-tui
+          go build .
+          sudo ./hack-tui node install
+          sudo ./hack-tui node start
+          sudo launchctl print system/com.algorand.algod
+          sleep 5
+          export TOKEN=$(cat ~/.algorand/algod.admin.token)
+          curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
+          sudo ./hack-tui node stop
+          sudo ./hack-tui node upgrade
+          # sudo ./hack-tui node configure
+          sudo ./hack-tui node uninstall
diff --git a/.testcoverage.yaml b/.testcoverage.yaml
deleted file mode 100644
index 18363129..00000000
--- a/.testcoverage.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-profile: cover.out
-
-# (optional; but recommended to set)
-# When specified reported file paths will not contain local prefix in the output
-local-prefix: "github.com/algorandfoundation/hack-tui"
-
-# Holds coverage thresholds percentages, values should be in range [0-100]
-threshold:
-  # (optional; default 0)
-  # The minimum coverage that each file should have
-  file: 0
-
-  # (optional; default 0)
-  # The minimum coverage that each package should have
-  package: 0
-
-  # (optional; default 0)
-  # The minimum total coverage project should have
-  total: 0
-
-# Holds regexp rules which will exclude matched files or packages
-# from coverage statistics
-exclude:
-  # Exclude files or packages matching their paths
-  paths:
-    - api/if.go
diff --git a/README.md b/README.md
index 4e0418b1..d419c150 100644
--- a/README.md
+++ b/README.md
@@ -20,44 +20,60 @@
 ---
 
 Terminal UI for managing Algorand nodes.
-Built with [bubbles](https://github.com/charmbracelet/bubbles)/[bubbletea](https://github.com/charmbracelet/bubbletea)
+Built with [bubbles](https://github.com/charmbracelet/bubbles) & [bubbletea](https://github.com/charmbracelet/bubbletea)
+
+> [!CAUTION]
+> This project is in alpha state and under heavy development. We do not recommend performing actions (e.g. key management) on participation nodes connected to public networks.
 
 # 🚀 Get Started
 
 Run the build or ~~download the latest cli(WIP)~~.
 
+> [!NOTE]
+> We do not have pre-built binaries yet. If you are comfortable doing so, you are welcome to build it yourself and provide feedback.
+
 ## Building
 
-Clone the repository
+1. Clone the repository
 
 ```bash
-git clone git@github.com:algorandfoundation/hack-tui.git
+git clone https://github.com/algorandfoundation/hack-tui.git
 ```
 
-Change to the project directory
+2. Change to the project directory
 
 ```bash
 cd hack-tui
 ```
 
-Run the build command
+3. Run the build command
 
 ```bash
 make build
 ```
 
-Start a participation node
+4. Start a participation node
 
 ```bash
 docker compose up
 ```
 
-Connect to the node
+> [!NOTE]
+> The docker image is used for development and testing purposes. TUI will also work with native algod.
+> If you have a node installed already, you can skip this step.
+
+5. Connect to the node
 
 ```bash
 ./bin/algorun --server http://localhost:8080 --token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 ```
 
+> [!CAUTION]
+> This project is in alpha state and under heavy development. We do not recommend performing actions (e.g. key management) on participation nodes connected to public networks.
+
+> [!NOTE]
+> If you skipped the docker container, try running `./bin/algorun` standalone, which will detect your algorand data directory from the `ALGORAND_DATA` environment variable that works for `goal`. Otherwise, provide the `--server` and `--token` arguments so that it can find your node. Note that algorun requires the admin algod token.
+
 # ℹ️ Usage
 
 ## ⚙️ Configuration
@@ -99,6 +115,9 @@ The application supports the `server` and `token` flags for configuration.
 algorun --server http://localhost:8080 --token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 ```
 
+> [!IMPORTANT]
+> TUI requires the *admin* token in order to access participation key information. This can be found in the `algod.admin.token` file, e.g. `/var/lib/algorand/algod.admin.token`
+
 ## 🧑‍💻 Commands
 
 The default command will launch the full TUI application
diff --git a/cmd/node/install.go b/cmd/node/install.go
index e714e710..c0f08fee 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -25,6 +25,12 @@ var installCmd = &cobra.Command{
 func installNode() {
 	fmt.Println("Checking if Algod is installed...")
 
+	// Check that we are calling with sudo
+	if !isRunningWithSudo() {
+		fmt.Println("This command must be run with super-user priviledges (sudo).")
+		os.Exit(1)
+	}
+
 	// Check if Algod is installed
 	if !isAlgodInstalled() {
 		fmt.Println("Algod is not installed. Installing...")
@@ -48,12 +54,6 @@ func installNode() {
 func installNodeLinux() {
 	fmt.Println("Installing Algod on Linux")
 
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
 	var installCmds [][]string
 	var postInstallHint string
 
@@ -87,7 +87,9 @@ func installNodeLinux() {
 			{"dnf", "install", "-y", "dnf-command(config-manager)"},
 			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
 			{"dnf", "install", "-y", "algorand-devtools"},
-			{"systemctl", "start", "algorand"},
+			{"systemctl", "enable", "algorand.service"},
+			{"systemctl", "start", "algorand.service"},
+			{"rm", "-f", "rpm_algorand.pub"},
 		}
 	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
 		fmt.Println("Using yum package manager")
@@ -97,7 +99,9 @@ func installNodeLinux() {
 			{"yum", "install", "yum-utils"},
 			{"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"},
 			{"yum", "install", "-y", "algorand-devtools"},
-			{"systemctl", "start", "algorand"},
+			{"systemctl", "enable", "algorand.service"},
+			{"systemctl", "start", "algorand.service"},
+			{"rm", "-f", "rpm_algorand.pub"},
 		}
 	} else {
 		fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.")
@@ -134,12 +138,6 @@ func installNodeLinux() {
 func installNodeMac() {
 	fmt.Println("Installing Algod on macOS...")
 
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user privileges (sudo).")
-		os.Exit(1)
-	}
-
 	// Homebrew is our package manager of choice
 	if !checkCmdToolExists("brew") {
 		fmt.Println("Could not find Homebrew installed. Please install Homebrew and try again.")
diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go
index e20d3c74..99834409 100644
--- a/cmd/node/uninstall.go
+++ b/cmd/node/uninstall.go
@@ -5,6 +5,7 @@ import (
 	"os"
 	"os/exec"
 	"runtime"
+	"strings"
 
 	"github.com/spf13/cobra"
 )
@@ -34,14 +35,14 @@ func unInstallNode() {
 		os.Exit(0)
 	}
 
-	fmt.Println("Algod is installed. Uninstalling...")
-
 	// Check if Algod is running
 	if isAlgodRunning() {
-		fmt.Println("Algod is running. Please run *node stop*.")
+		fmt.Println("Algod is running. Please run *node stop* first to stop it.")
 		os.Exit(1)
 	}
 
+	fmt.Println("Algod is installed. Proceeding...")
+
 	// Uninstall Algod based on OS
 	switch runtime.GOOS {
 	case "linux":
@@ -96,4 +97,62 @@ func unInstallNodeMac() {
 	fmt.Println("Algorand uninstalled successfully.")
 }
 
-func unInstallNodeLinux() {}
+func unInstallNodeLinux() {
+
+	var unInstallCmds [][]string
+
+	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
+		fmt.Println("Using apt package manager")
+		unInstallCmds = [][]string{
+			{"apt", "remove", "algorand-devtools", "-y"},
+			{"apt", "autoremove", "-y"},
+		}
+	} else if checkCmdToolExists("apt-get") {
+		fmt.Println("Using apt-get package manager")
+		unInstallCmds = [][]string{
+			{"apt-get", "remove", "algorand-devtools", "-y"},
+			{"apt-get", "autoremove", "-y"},
+		}
+	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
+		fmt.Println("Using dnf package manager")
+		unInstallCmds = [][]string{
+			{"dnf", "remove", "algorand-devtools", "-y"},
+		}
+	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
+		fmt.Println("Using yum package manager")
+		unInstallCmds = [][]string{
+			{"yum", "remove", "algorand-devtools", "-y"},
+		}
+	} else {
+		fmt.Println("Could not find a package manager to uninstall Algorand.")
+		os.Exit(1)
+	}
+
+	// Commands to clear systemd algorand.service and any other files, like the configuration override
+	unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "sudo rm -rf /etc/systemd/system/algorand*"})
+	unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"})
+
+	// Run each installation command
+	for _, cmdArgs := range unInstallCmds {
+		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
+		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
+			cobra.CheckErr(err)
+		}
+	}
+
+	// Check the status of the algorand service
+	cmd := exec.Command("systemctl", "status", "algorand")
+	output, err := cmd.CombinedOutput()
+	if err != nil && strings.Contains(string(output), "Unit algorand.service could not be found.") {
+		fmt.Println("Algorand service has been successfully removed.")
+	} else {
+		fmt.Printf("Failed to verify Algorand service uninstallation: %v\n", err)
+		fmt.Printf("Output: %s\n", string(output))
+		os.Exit(1)
+	}
+
+	fmt.Println("Algorand successfully uninstalled.")
+}
diff --git a/go.mod b/go.mod
index 7bac0b36..217ad5af 100644
--- a/go.mod
+++ b/go.mod
@@ -34,8 +34,8 @@ require (
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/aymanbagabas/go-udiff v0.2.0 // indirect
-	github.com/charmbracelet/x/ansi v0.3.2 // indirect
-	github.com/charmbracelet/x/exp/golden v0.0.0-20241022174419-46d9bb99a691 // indirect
+	github.com/charmbracelet/x/ansi v0.3.2
+	github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b
 	github.com/charmbracelet/x/term v0.2.0 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect

From 0ffecc52c6b81105c27c2df2467b5baadc581c29 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 15:13:40 +0100
Subject: [PATCH 06/23] chore: fix symbols

chore: return msg.quit

chore: fix imports

chore: adjust imports

chore: space

chore: fix space

chore: fix status

chore: fix merge error

chore: return docs
---
 cmd/status_test.go                            |  3 ++-
 internal/accounts_test.go                     |  5 ++--
 internal/block.go                             |  3 ++-
 internal/block_test.go                        |  3 ++-
 internal/metrics.go                           |  3 ++-
 internal/state_test.go                        |  5 ++--
 main_test.go                                  |  3 ++-
 ui/README.md                                  | 10 ++++---
 ui/controls/controls_test.go                  |  5 ++--
 ui/controls/view.go                           |  3 ++-
 ui/error.go                                   |  3 ++-
 ui/error_test.go                              |  5 ++--
 ui/pages/accounts/model.go                    |  3 ++-
 ui/pages/generate/controller.go               |  3 ++-
 ui/pages/generate/style.go                    |  1 +
 ui/pages/generate/view.go                     |  3 ++-
 ui/pages/transaction/view.go                  |  3 ++-
 ui/protocol.go                                |  5 ++--
 ui/protocol_test.go                           | 13 ++--------
 ui/status_test.go                             | 26 +------------------
 .../NoVoteOrUpgrade.golden                    |  4 ---
 .../NoVoteOrUpgradeSmall.golden               |  4 ---
 .../Test_ProtocolSnapshot/Visible.golden      |  4 ---
 23 files changed, 47 insertions(+), 73 deletions(-)

diff --git a/cmd/status_test.go b/cmd/status_test.go
index 9d2a6087..5e694234 100644
--- a/cmd/status_test.go
+++ b/cmd/status_test.go
@@ -2,8 +2,9 @@ package cmd
 
 import (
 	"context"
-	"github.com/spf13/viper"
 	"testing"
+
+	"github.com/spf13/viper"
 )
 
 func Test_ExecuteInvalidStatusCommand(t *testing.T) {
diff --git a/internal/accounts_test.go b/internal/accounts_test.go
index 234bf7f8..04648169 100644
--- a/internal/accounts_test.go
+++ b/internal/accounts_test.go
@@ -1,11 +1,12 @@
 package internal
 
 import (
+	"testing"
+	"time"
+
 	"github.com/algorandfoundation/hack-tui/api"
 	"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
 	"github.com/stretchr/testify/assert"
-	"testing"
-	"time"
 )
 
 type TestClock struct{}
diff --git a/internal/block.go b/internal/block.go
index 5af2fbae..825932bc 100644
--- a/internal/block.go
+++ b/internal/block.go
@@ -3,8 +3,9 @@ package internal
 import (
 	"context"
 	"errors"
-	"github.com/algorandfoundation/hack-tui/api"
 	"time"
+
+	"github.com/algorandfoundation/hack-tui/api"
 )
 
 type BlockMetrics struct {
diff --git a/internal/block_test.go b/internal/block_test.go
index 16156eed..d886db26 100644
--- a/internal/block_test.go
+++ b/internal/block_test.go
@@ -2,9 +2,10 @@ package internal
 
 import (
 	"context"
-	"github.com/algorandfoundation/hack-tui/api"
 	"testing"
 	"time"
+
+	"github.com/algorandfoundation/hack-tui/api"
 )
 
 func Test_GetBlockMetrics(t *testing.T) {
diff --git a/internal/metrics.go b/internal/metrics.go
index b10a9f40..6f4e09bc 100644
--- a/internal/metrics.go
+++ b/internal/metrics.go
@@ -3,11 +3,12 @@ package internal
 import (
 	"context"
 	"errors"
-	"github.com/algorandfoundation/hack-tui/api"
 	"regexp"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/algorandfoundation/hack-tui/api"
 )
 
 type MetricsModel struct {
diff --git a/internal/state_test.go b/internal/state_test.go
index 769c6639..2afeaf1c 100644
--- a/internal/state_test.go
+++ b/internal/state_test.go
@@ -2,10 +2,11 @@ package internal
 
 import (
 	"context"
-	"github.com/algorandfoundation/hack-tui/api"
-	"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
 	"testing"
 	"time"
+
+	"github.com/algorandfoundation/hack-tui/api"
+	"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
 )
 
 func Test_StateModel(t *testing.T) {
diff --git a/main_test.go b/main_test.go
index 40521d59..a7e99a2e 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,8 +1,9 @@
 package main
 
 import (
-	"github.com/spf13/viper"
 	"testing"
+
+	"github.com/spf13/viper"
 )
 
 func Test_Main(t *testing.T) {
diff --git a/ui/README.md b/ui/README.md
index a80ddf51..ed10387c 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -1,13 +1,13 @@
 # Overview
 
-The ui package contains bubbletea interfaces. 
+The ui package contains bubbletea interfaces.
 
 ## Common practices
 
 A `style.go` file holds lipgloss predefined styles for the package.
 
 All components are instances of a `tea.Model` which is composed of models
-from the `internal` package. 
+from the `internal` package.
 Components can either be single file or independent packages.
 
 Example for `status.go` single file component:
@@ -26,12 +26,13 @@ func (m StatusViewModel) Int(){}
 //other tea.Model interfaces ...
 ```
 
-Once the component is sufficiently complex or needs to be reused, it can be moved 
+Once the component is sufficiently complex or needs to be reused, it can be moved
 to its own package
 
 Example refactor for `status.go` to a package:
 
 #### ui/status/model.go
+
 ```go
 package status
 import "github.com/algorandfoundation/hack-tui/internal"
@@ -43,6 +44,7 @@ type ViewModel struct {
 ```
 
 #### ui/status/controller.go
+
 ```go
 package status
 
@@ -84,4 +86,4 @@ package status
 import "github.com/charmbracelet/lipgloss"
 
 var someStyle = lipgloss.NewStyle()
-```
\ No newline at end of file
+```
diff --git a/ui/controls/controls_test.go b/ui/controls/controls_test.go
index 2693afa3..ccef26c0 100644
--- a/ui/controls/controls_test.go
+++ b/ui/controls/controls_test.go
@@ -2,12 +2,13 @@ package controls
 
 import (
 	"bytes"
+	"testing"
+	"time"
+
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/golden"
 	"github.com/charmbracelet/x/exp/teatest"
-	"testing"
-	"time"
 )
 
 func Test_Snapshot(t *testing.T) {
diff --git a/ui/controls/view.go b/ui/controls/view.go
index 3754f42c..c10265cf 100644
--- a/ui/controls/view.go
+++ b/ui/controls/view.go
@@ -1,8 +1,9 @@
 package controls
 
 import (
-	"github.com/charmbracelet/lipgloss"
 	"strings"
+
+	"github.com/charmbracelet/lipgloss"
 )
 
 // View renders the model's content if it is visible, aligning it horizontally and ensuring it fits within the specified width.
diff --git a/ui/error.go b/ui/error.go
index c0678d7d..2346b896 100644
--- a/ui/error.go
+++ b/ui/error.go
@@ -1,11 +1,12 @@
 package ui
 
 import (
+	"strings"
+
 	"github.com/algorandfoundation/hack-tui/ui/controls"
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"strings"
 )
 
 type ErrorViewModel struct {
diff --git a/ui/error_test.go b/ui/error_test.go
index 10020eac..0613c433 100644
--- a/ui/error_test.go
+++ b/ui/error_test.go
@@ -2,13 +2,14 @@ package ui
 
 import (
 	"bytes"
+	"testing"
+	"time"
+
 	"github.com/algorandfoundation/hack-tui/ui/controls"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/golden"
 	"github.com/charmbracelet/x/exp/teatest"
-	"testing"
-	"time"
 )
 
 func Test_ErrorSnapshot(t *testing.T) {
diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go
index 91b550cb..dabe1b46 100644
--- a/ui/pages/accounts/model.go
+++ b/ui/pages/accounts/model.go
@@ -1,11 +1,12 @@
 package accounts
 
 import (
-	"github.com/algorandfoundation/hack-tui/ui/style"
 	"sort"
 	"strconv"
 	"time"
 
+	"github.com/algorandfoundation/hack-tui/ui/style"
+
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/charmbracelet/bubbles/table"
 	"github.com/charmbracelet/lipgloss"
diff --git a/ui/pages/generate/controller.go b/ui/pages/generate/controller.go
index c8adbaa6..0ebe2346 100644
--- a/ui/pages/generate/controller.go
+++ b/ui/pages/generate/controller.go
@@ -2,6 +2,8 @@ package generate
 
 import (
 	"context"
+	"strconv"
+
 	"github.com/algorandfoundation/hack-tui/api"
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/algorandfoundation/hack-tui/ui/pages/accounts"
@@ -10,7 +12,6 @@ import (
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/log"
-	"strconv"
 )
 
 func (m ViewModel) Init() tea.Cmd {
diff --git a/ui/pages/generate/style.go b/ui/pages/generate/style.go
index 8783d051..7d53d321 100644
--- a/ui/pages/generate/style.go
+++ b/ui/pages/generate/style.go
@@ -2,6 +2,7 @@ package generate
 
 import (
 	"fmt"
+
 	"github.com/charmbracelet/lipgloss"
 )
 
diff --git a/ui/pages/generate/view.go b/ui/pages/generate/view.go
index 667410ea..b61f2c0f 100644
--- a/ui/pages/generate/view.go
+++ b/ui/pages/generate/view.go
@@ -2,8 +2,9 @@ package generate
 
 import (
 	"fmt"
-	"github.com/algorandfoundation/hack-tui/ui/style"
 	"strings"
+
+	"github.com/algorandfoundation/hack-tui/ui/style"
 )
 
 func (m ViewModel) View() string {
diff --git a/ui/pages/transaction/view.go b/ui/pages/transaction/view.go
index 795841c1..029fa843 100644
--- a/ui/pages/transaction/view.go
+++ b/ui/pages/transaction/view.go
@@ -1,9 +1,10 @@
 package transaction
 
 import (
+	"strings"
+
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	"github.com/charmbracelet/lipgloss"
-	"strings"
 )
 
 func (m ViewModel) View() string {
diff --git a/ui/protocol.go b/ui/protocol.go
index 7126e6b5..177abc18 100644
--- a/ui/protocol.go
+++ b/ui/protocol.go
@@ -1,12 +1,13 @@
 package ui
 
 import (
+	"strconv"
+	"strings"
+
 	"github.com/algorandfoundation/hack-tui/internal"
 	"github.com/algorandfoundation/hack-tui/ui/style"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	"strconv"
-	"strings"
 )
 
 // ProtocolViewModel includes the internal.StatusModel and internal.MetricsModel
diff --git a/ui/protocol_test.go b/ui/protocol_test.go
index 2b04df54..da60005c 100644
--- a/ui/protocol_test.go
+++ b/ui/protocol_test.go
@@ -10,12 +10,6 @@ import (
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/golden"
 	"github.com/charmbracelet/x/exp/teatest"
-
-	"github.com/algorandfoundation/hack-tui/internal"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/x/ansi"
-	"github.com/charmbracelet/x/exp/golden"
-	"github.com/charmbracelet/x/exp/teatest"
 )
 
 var protocolViewSnapshots = map[string]ProtocolViewModel{
@@ -161,11 +155,8 @@ func Test_ProtocolMessages(t *testing.T) {
 		Runes: []rune("ctrl+c"),
 	})
 
-	// Send quit key
-	tm.Send(tea.KeyMsg{
-		Type:  tea.KeyRunes,
-		Runes: []rune("ctrl+c"),
-	})
+	// Send quit msg
+	tm.Send(tea.QuitMsg{})
 
 	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
 }
diff --git a/ui/status_test.go b/ui/status_test.go
index f0d8d072..79f4c326 100644
--- a/ui/status_test.go
+++ b/ui/status_test.go
@@ -2,14 +2,10 @@ package ui
 
 import (
 	"bytes"
-	"github.com/algorandfoundation/hack-tui/internal"
-	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/x/ansi"
-	"github.com/charmbracelet/x/exp/golden"
-	"github.com/charmbracelet/x/exp/teatest"
 	"testing"
 	"time"
 
+	"github.com/algorandfoundation/hack-tui/internal"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/golden"
@@ -112,9 +108,6 @@ func Test_StatusMessages(t *testing.T) {
 		teatest.WithDuration(time.Second*3),
 	)
 
-<<<<<<< HEAD
-<<<<<<< HEAD
-=======
 	// Send the state
 	tm.Send(state)
 
@@ -129,25 +122,8 @@ func Test_StatusMessages(t *testing.T) {
 		Type:  tea.KeyRunes,
 		Runes: []rune("ctrl+c"),
 	})
->>>>>>> main
 	// Send quit msg
 	tm.Send(tea.QuitMsg{})
-=======
-	// Send the state
-	tm.Send(state)
-
-	// Send hide key
-	tm.Send(tea.KeyMsg{
-		Type:  tea.KeyRunes,
-		Runes: []rune("h"),
-	})
-
-	// Send quit key
-	tm.Send(tea.KeyMsg{
-		Type:  tea.KeyRunes,
-		Runes: []rune("ctrl+c"),
-	})
->>>>>>> 967fa6b (test: status snapshots and 100%)
 
 	tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
 }
diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
index f942f810..14627bb1 100644
--- a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
+++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgrade.golden
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-╭──────────────────────────────────────────────────────────────────────────────╮
-=======
 ╭──Protocol────────────────────────────────────────────────────────────────────╮
->>>>>>> main
 │ Node: v0.0.0-test                                                            │
 │                                                                              │
 │ Network: test-v1                                                             │
diff --git a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
index 87910af8..5c48ef5e 100644
--- a/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
+++ b/ui/testdata/Test_ProtocolSnapshot/NoVoteOrUpgradeSmall.golden
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-╭──────────────────────────────────────────────────────────────────────────────╮
-=======
 ╭──Protocol────────────────────────────────────────────────────────────────────╮
->>>>>>> main
 │ Node: v0.0.0-test                                                            │
 │ Network: test-v1                                                             │
 │ Protocol Voting: false                                                       │
diff --git a/ui/testdata/Test_ProtocolSnapshot/Visible.golden b/ui/testdata/Test_ProtocolSnapshot/Visible.golden
index c06766db..e26f5535 100644
--- a/ui/testdata/Test_ProtocolSnapshot/Visible.golden
+++ b/ui/testdata/Test_ProtocolSnapshot/Visible.golden
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-╭──────────────────────────────────────────────────────────────────────────────╮
-=======
 ╭──Protocol────────────────────────────────────────────────────────────────────╮
->>>>>>> main
 │ Node: v0.0.0-test                                         [UPDATE AVAILABLE] │
 │                                                                              │
 │ Network: test-v1                                                             │

From 2b48b3402ac5c61b27bcb40c87ebdd83cd1b8c85 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 16:57:47 +0100
Subject: [PATCH 07/23] ci: only PR

---
 .github/workflows/node_test.yaml | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
index 4e62f691..e7812c9e 100644
--- a/.github/workflows/node_test.yaml
+++ b/.github/workflows/node_test.yaml
@@ -5,9 +5,6 @@ name: Node Command OS-Matrix Test
 
 on:
   workflow_dispatch:
-  push:
-    paths:
-      - "cmd/**"
   pull_request:
     paths:
       - "cmd/**"

From 0e888c648b9f0174aa046b014f70bf28d4bbae0e Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 17:01:05 +0100
Subject: [PATCH 08/23] ci: use checkout action

---
 .github/workflows/node_test.yaml | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
index e7812c9e..31df5376 100644
--- a/.github/workflows/node_test.yaml
+++ b/.github/workflows/node_test.yaml
@@ -14,14 +14,15 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
+      - name: Checkout Repo
+        uses: actions/checkout@v4
+
       - name: Setup Go
         uses: actions/setup-go@v4
         with:
           go-version: 1.22
       - name: Run Ubuntu commands
         run: |
-          git clone https://github.com/algorandfoundation/hack-tui.git
-          cd hack-tui
           go build .
           sudo ./hack-tui node install
           sudo ./hack-tui node start
@@ -36,12 +37,14 @@ jobs:
   macos:
     runs-on: macos-latest
     steps:
+      - name: Checkout Repo
+        uses: actions/checkout@v4
+
+      - name: Setup Go
+        run: brew install go
+
       - name: Run MacOs commands
         run: |
-          brew install go
-          cd ~/
-          git clone https://github.com/algorandfoundation/hack-tui.git
-          cd hack-tui
           go build .
           sudo ./hack-tui node install
           sudo ./hack-tui node start

From 55d72252c62043474107500549cdacead6fb0590 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 19:14:36 +0100
Subject: [PATCH 09/23] chore: ignore coverage.txt to avoid accidentally
 checking it in

---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index fce50c3a..ab058d2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@ bin
 # Test binary, built with `go test -c`
 *.test
 
+# coverage.txt, in case you run tests locally
+coverage.txt
+
 # Output of the go coverage tool, specifically when used with LiteIDE
 *.out
 

From e699915d51494e6abcc933cfcfe8cc2ff0ce1346 Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Mon, 25 Nov 2024 19:14:49 +0100
Subject: [PATCH 10/23] fix: newlines

---
 ui/pages/accounts/testdata/Test_Snapshot/Visible.golden | 2 +-
 ui/testdata/Test_ErrorSnapshot/Visible.golden           | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
index d3bc0f03..03e6474b 100644
--- a/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
+++ b/ui/pages/accounts/testdata/Test_Snapshot/Visible.golden
@@ -37,4 +37,4 @@
 │                                                                              │
 │                                                                              │
 │                                                                              │
-╰────( (g)enerate )─────────────────────────| (a)ccounts | (k)eys | (t)xn |────╯
+╰────( (g)enerate )─────────────────────────| (a)ccounts | (k)eys | (t)xn |────╯
\ No newline at end of file
diff --git a/ui/testdata/Test_ErrorSnapshot/Visible.golden b/ui/testdata/Test_ErrorSnapshot/Visible.golden
index 8b89731c..93d1e257 100644
--- a/ui/testdata/Test_ErrorSnapshot/Visible.golden
+++ b/ui/testdata/Test_ErrorSnapshot/Visible.golden
@@ -19,4 +19,4 @@
 │                                        │
 │                                        │
 │                                        │
-╰─────────( Waiting for recovery... )────╯
+╰─────────( Waiting for recovery... )────╯
\ No newline at end of file

From 3c9f104a64c5bda00835fc209f597985e4e282f4 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Mon, 25 Nov 2024 17:34:10 -0500
Subject: [PATCH 11/23] test(node): basic playbook and coverage strategy

---
 .docker/Fedora.dockerfile       | 43 +++++++++++++++++++++++++++++
 .docker/Ubuntu.dockerfile       | 34 +++++++++++++++++++++++
 .gitignore                      |  1 +
 Makefile                        |  4 +++
 docker-compose.integration.yaml | 49 +++++++++++++++++++++++++++++++++
 playbook.yaml                   | 21 ++++++++++++++
 run_integraiton.sh              |  9 ++++++
 7 files changed, 161 insertions(+)
 create mode 100644 .docker/Fedora.dockerfile
 create mode 100644 .docker/Ubuntu.dockerfile
 create mode 100644 docker-compose.integration.yaml
 create mode 100644 playbook.yaml
 create mode 100755 run_integraiton.sh

diff --git a/.docker/Fedora.dockerfile b/.docker/Fedora.dockerfile
new file mode 100644
index 00000000..305cdfc8
--- /dev/null
+++ b/.docker/Fedora.dockerfile
@@ -0,0 +1,43 @@
+FROM golang:1.23-bookworm as BUILDER
+
+WORKDIR /app
+
+ADD . .
+
+RUN go build -cover -o ./bin/algorun *.go
+
+
+FROM fedora:39 as legacy
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+RUN dnf install systemd ansible-core -y && \
+    mkdir -p /app/coverage/int/fedora/39 && \
+    echo GOCOVERDIR=/app/coverage/int/fedora/39 >> /etc/environment
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/usr/lib/systemd/systemd"]
+
+FROM fedora:40 as previous
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+
+RUN dnf install systemd ansible-core -y && \
+    mkdir -p /app/coverage/int/fedora/40 && \
+    echo GOCOVERDIR=/app/coverage/int/fedora/40 >> /etc/environment
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/usr/lib/systemd/systemd"]
+
+FROM fedora:41 as latest
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+
+RUN dnf install systemd ansible-core -y && \
+    mkdir -p /app/coverage/int/fedora/41 && \
+    echo GOCOVERDIR=/app/coverage/int/fedora/41 >> /etc/environment
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/usr/lib/systemd/systemd"]
diff --git a/.docker/Ubuntu.dockerfile b/.docker/Ubuntu.dockerfile
new file mode 100644
index 00000000..2f0312de
--- /dev/null
+++ b/.docker/Ubuntu.dockerfile
@@ -0,0 +1,34 @@
+FROM golang:1.23-bookworm as BUILDER
+
+WORKDIR /app
+
+ADD . .
+
+RUN go build -cover -o ./bin/algorun *.go
+
+
+FROM ubuntu:22.04 as jammy
+
+RUN apt-get update && apt-get install systemd software-properties-common -y && add-apt-repository --yes --update ppa:ansible/ansible
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+RUN mkdir -p /app/coverage/int/ubuntu/22.04 && \
+    echo GOCOVERDIR=/app/coverage/int/ubuntu/22.04 >> /etc/environment && \
+    apt-get install ansible -y
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/usr/lib/systemd/systemd"]
+
+FROM ubuntu:24.04 as noble
+
+RUN apt-get update && apt-get install systemd software-properties-common -y  && add-apt-repository --yes --update ppa:ansible/ansible
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+RUN mkdir -p /app/coverage/int/ubuntu/24.04 && \
+    echo GOCOVERDIR=/app/coverage/int/ubuntu/24.04 >> /etc/environment && \
+    apt-get install ansible -y
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/usr/lib/systemd/systemd"]
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ab058d2c..baec94da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+coverage
 hack-tui
 bin
 .data
diff --git a/Makefile b/Makefile
index 40bbaa98..e87f96ff 100644
--- a/Makefile
+++ b/Makefile
@@ -4,3 +4,7 @@ test:
 	go test -coverpkg=./... -covermode=atomic ./...
 generate:
 	oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml
+unit:
+	mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit
+combine:
+	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt
\ No newline at end of file
diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml
new file mode 100644
index 00000000..0b495d47
--- /dev/null
+++ b/docker-compose.integration.yaml
@@ -0,0 +1,49 @@
+services:
+  ubuntu.22.04:
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/ubuntu/22.04
+    build:
+      context: .
+      target: jammy
+      dockerfile: .docker/Ubuntu.dockerfile
+    ports:
+      - "2222:22"
+    volumes:
+      - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04"
+  ubuntu.24.04:
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/ubuntu/24.04
+    build:
+      context: .
+      target: noble
+      dockerfile: .docker/Ubuntu.dockerfile
+    ports:
+      - "2223:22"
+    volumes:
+      - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04"
+  fedora.39:
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/fedora/39
+    build:
+      context: .
+      target: legacy
+      dockerfile: .docker/Fedora.dockerfile
+    ports:
+      - "2224:22"
+    volumes:
+      - "./coverage/int/fedora/39:/app/coverage/int/fedora/39"
+  fedora.40:
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/fedora/40
+    build:
+      context: .
+      target: previous
+      dockerfile: .docker/Fedora.dockerfile
+    ports:
+      - "2225:22"  
+    volumes:
+      - "./coverage/int/fedora/40:/app/coverage/int/fedora/40"
\ No newline at end of file
diff --git a/playbook.yaml b/playbook.yaml
new file mode 100644
index 00000000..3c2e0ae7
--- /dev/null
+++ b/playbook.yaml
@@ -0,0 +1,21 @@
+- name: Test Instance
+  hosts: localhost
+  tasks:
+    - name: Ensure algorun exists
+      stat:
+        path: /usr/bin/algorun
+      register: binpath
+    - name: Fail missing binary
+      fail:
+        msg: "Must have algorun installed!"
+      when: not binpath.stat.exists
+    - name: Run installer
+      command: algorun node install
+    - name: Run installer twice
+      command: algorun node install
+    - name: Run upgrade
+      command: algorun node upgrade
+    - name: Run stop
+      command: algorun node stop
+    - name: Run Start
+      command: algorun node start
\ No newline at end of file
diff --git a/run_integraiton.sh b/run_integraiton.sh
new file mode 100755
index 00000000..48695ff4
--- /dev/null
+++ b/run_integraiton.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+docker compose -f docker-compose.integration.yaml build --no-cache
+docker compose -f docker-compose.integration.yaml up -d
+
+for testInstance in $(docker compose ps --services)
+do
+    docker compose exec -it "$testInstance" ansible-playbook --connection=local -i localhost /root/playbook.yaml
+done
\ No newline at end of file

From df73d85d441d8482cb07efd42e2d12598e21a586 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Mon, 25 Nov 2024 19:43:57 -0500
Subject: [PATCH 12/23] build(node): run integration code tests

---
 .github/workflows/code_test.yaml | 13 +++++++--
 Makefile                         |  4 +--
 docker-compose.integration.yaml  | 48 ++++++++++++++++----------------
 playbook.yaml                    |  3 +-
 run_integraiton.sh               |  1 -
 5 files changed, 39 insertions(+), 30 deletions(-)

diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml
index cd6a095e..6e22cf12 100644
--- a/.github/workflows/code_test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -55,8 +55,17 @@ jobs:
       - name: Build
         run: go build -o bin/algorun *.go
 
-      - name: Test with the Go CLI
-        run: go test ./... -coverprofile=./coverage.txt -covermode=atomic -coverpkg=./...
+      - name: Unit Tests
+        run: make unit
+
+      - name: Kill docker
+        run: docker compose down
+
+      - name: Integration tests
+        run: ./run_integration.sh
+
+      - name: Combine coverage
+        run: make combine-coverage
 
       - name: Upload results to Codecov
         uses: codecov/codecov-action@v4
diff --git a/Makefile b/Makefile
index e87f96ff..53dc959e 100644
--- a/Makefile
+++ b/Makefile
@@ -6,5 +6,5 @@ generate:
 	oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml
 unit:
 	mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit
-combine:
-	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt
\ No newline at end of file
+combine-coverage:
+	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt
\ No newline at end of file
diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml
index 0b495d47..1456d818 100644
--- a/docker-compose.integration.yaml
+++ b/docker-compose.integration.yaml
@@ -1,16 +1,16 @@
 services:
-  ubuntu.22.04:
-    privileged: true
-    environment:
-      - GOCOVERDIR=/app/coverage/int/ubuntu/22.04
-    build:
-      context: .
-      target: jammy
-      dockerfile: .docker/Ubuntu.dockerfile
-    ports:
-      - "2222:22"
-    volumes:
-      - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04"
+#  ubuntu.22.04:
+#    privileged: true
+#    environment:
+#      - GOCOVERDIR=/app/coverage/int/ubuntu/22.04
+#    build:
+#      context: .
+#      target: jammy
+#      dockerfile: .docker/Ubuntu.dockerfile
+#    ports:
+#      - "2222:22"
+#    volumes:
+#      - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04"
   ubuntu.24.04:
     privileged: true
     environment:
@@ -23,18 +23,18 @@ services:
       - "2223:22"
     volumes:
       - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04"
-  fedora.39:
-    privileged: true
-    environment:
-      - GOCOVERDIR=/app/coverage/int/fedora/39
-    build:
-      context: .
-      target: legacy
-      dockerfile: .docker/Fedora.dockerfile
-    ports:
-      - "2224:22"
-    volumes:
-      - "./coverage/int/fedora/39:/app/coverage/int/fedora/39"
+#  fedora.39:
+#    privileged: true
+#    environment:
+#      - GOCOVERDIR=/app/coverage/int/fedora/39
+#    build:
+#      context: .
+#      target: legacy
+#      dockerfile: .docker/Fedora.dockerfile
+#    ports:
+#      - "2224:22"
+#    volumes:
+#      - "./coverage/int/fedora/39:/app/coverage/int/fedora/39"
   fedora.40:
     privileged: true
     environment:
diff --git a/playbook.yaml b/playbook.yaml
index 3c2e0ae7..db717bec 100644
--- a/playbook.yaml
+++ b/playbook.yaml
@@ -18,4 +18,5 @@
     - name: Run stop
       command: algorun node stop
     - name: Run Start
-      command: algorun node start
\ No newline at end of file
+      command: algorun node start
+      # TODO: start a private network, fund TUI account and run TUI integration
\ No newline at end of file
diff --git a/run_integraiton.sh b/run_integraiton.sh
index 48695ff4..01eac615 100755
--- a/run_integraiton.sh
+++ b/run_integraiton.sh
@@ -1,6 +1,5 @@
 #!/usr/bin/env bash
 
-docker compose -f docker-compose.integration.yaml build --no-cache
 docker compose -f docker-compose.integration.yaml up -d
 
 for testInstance in $(docker compose ps --services)

From 4476c5a1be8e053a2def98f96db680a459888a0e Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Mon, 25 Nov 2024 19:54:24 -0500
Subject: [PATCH 13/23] build(node): convert integration to makefile

---
 .github/workflows/code_test.yaml | 2 +-
 Makefile                         | 5 +++++
 run_integraiton.sh               | 8 --------
 3 files changed, 6 insertions(+), 9 deletions(-)
 delete mode 100755 run_integraiton.sh

diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml
index 6e22cf12..4133f6e8 100644
--- a/.github/workflows/code_test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -62,7 +62,7 @@ jobs:
         run: docker compose down
 
       - name: Integration tests
-        run: ./run_integration.sh
+        run: make integration
 
       - name: Combine coverage
         run: make combine-coverage
diff --git a/Makefile b/Makefile
index 53dc959e..2fdd62fe 100644
--- a/Makefile
+++ b/Makefile
@@ -6,5 +6,10 @@ generate:
 	oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml
 unit:
 	mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit
+integration:
+	docker compose -f docker-compose.integration.yaml up -d ; \
+	for service in $(shell docker compose -f docker-compose.integration.yaml ps --services) ; do \
+        docker compose exec -it "$$service" ansible-playbook --connection=local /root/playbook.yaml ; \
+    done
 combine-coverage:
 	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt
\ No newline at end of file
diff --git a/run_integraiton.sh b/run_integraiton.sh
deleted file mode 100755
index 01eac615..00000000
--- a/run_integraiton.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-docker compose -f docker-compose.integration.yaml up -d
-
-for testInstance in $(docker compose ps --services)
-do
-    docker compose exec -it "$testInstance" ansible-playbook --connection=local -i localhost /root/playbook.yaml
-done
\ No newline at end of file

From 26bf8d3741728f0a46a569cfafa194719929ab57 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Mon, 25 Nov 2024 20:28:40 -0500
Subject: [PATCH 14/23] build(node): move compose to action, wait for binds

---
 .github/workflows/code_test.yaml | 6 ++++++
 Makefile                         | 3 +--
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/code_test.yaml b/.github/workflows/code_test.yaml
index 4133f6e8..df125f2e 100644
--- a/.github/workflows/code_test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -61,6 +61,12 @@ jobs:
       - name: Kill docker
         run: docker compose down
 
+      - name: Start Integration
+        run: docker compose -f docker-compose.integration.yaml up -d
+        
+      - name: Wait for mount
+        run: npx wait-on ./coverage/int/fedora/40 ./coverage/int/ubuntu/24.04/
+        
       - name: Integration tests
         run: make integration
 
diff --git a/Makefile b/Makefile
index 2fdd62fe..6a49a618 100644
--- a/Makefile
+++ b/Makefile
@@ -7,9 +7,8 @@ generate:
 unit:
 	mkdir -p $(CURDIR)/coverage/unit && go test -cover ./... -args -test.gocoverdir=$(CURDIR)/coverage/unit
 integration:
-	docker compose -f docker-compose.integration.yaml up -d ; \
 	for service in $(shell docker compose -f docker-compose.integration.yaml ps --services) ; do \
         docker compose exec -it "$$service" ansible-playbook --connection=local /root/playbook.yaml ; \
     done
 combine-coverage:
-	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/22.04,./coverage/int/ubuntu/24.04,./coverage/int/fedora/39,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt
\ No newline at end of file
+	go tool covdata textfmt -i=./coverage/unit,./coverage/int/ubuntu/24.04,./coverage/int/fedora/40 -o coverage.txt && sed -i 2,3d coverage.txt
\ No newline at end of file

From 244313073163bad7faf4a97b7bd2782c39e99bfb Mon Sep 17 00:00:00 2001
From: HashMapsData2Value
 <83883690+HashMapsData2Value@users.noreply.github.com>
Date: Tue, 26 Nov 2024 16:37:17 +0100
Subject: [PATCH 15/23] feat: migrate to algorand foundation homebrew node tap

---
 cmd/node/install.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/node/install.go b/cmd/node/install.go
index c0f08fee..4cb13b96 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -175,7 +175,7 @@ Algod is running in the background as a system-level service.
 
 func runHomebrewInstallCommandsAsUser(user string) error {
 	homebrewCmds := [][]string{
-		{"brew", "tap", "HashMapsData2Value/homebrew-tap"},
+		{"brew", "tap", "algorandfoundation/homebrew-node"},
 		{"brew", "install", "algorand"},
 		{"brew", "--prefix", "algorand", "--installed"},
 	}

From 5c26721c8474e3484d39472714cbcd74d21c94ca Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Tue, 26 Nov 2024 16:40:24 -0500
Subject: [PATCH 16/23] test(node): disable CGO, test Ubuntu 18.04 with apt
 disabled

---
 .docker/Fedora.dockerfile       |  2 +-
 .docker/Ubuntu.dockerfile       | 15 +++++++-
 Makefile                        |  2 +-
 docker-compose.integration.yaml | 65 +++++++++++++++++++--------------
 4 files changed, 53 insertions(+), 31 deletions(-)

diff --git a/.docker/Fedora.dockerfile b/.docker/Fedora.dockerfile
index 305cdfc8..1891af84 100644
--- a/.docker/Fedora.dockerfile
+++ b/.docker/Fedora.dockerfile
@@ -4,7 +4,7 @@ WORKDIR /app
 
 ADD . .
 
-RUN go build -cover -o ./bin/algorun *.go
+RUN CGO_ENABLED=0 go build -cover -o ./bin/algorun *.go
 
 
 FROM fedora:39 as legacy
diff --git a/.docker/Ubuntu.dockerfile b/.docker/Ubuntu.dockerfile
index 2f0312de..f5cca2f7 100644
--- a/.docker/Ubuntu.dockerfile
+++ b/.docker/Ubuntu.dockerfile
@@ -4,8 +4,21 @@ WORKDIR /app
 
 ADD . .
 
-RUN go build -cover -o ./bin/algorun *.go
+RUN CGO_ENABLED=0 go build -cover -o ./bin/algorun *.go
 
+FROM ubuntu:18.04 as bionic
+
+RUN apt-get update && apt-get install systemd software-properties-common -y && add-apt-repository --yes --update ppa:ansible/ansible
+
+ADD playbook.yaml /root/playbook.yaml
+COPY --from=BUILDER /app/bin/algorun /usr/bin/algorun
+RUN mkdir -p /app/coverage/int/ubuntu/18.04 && \
+    echo GOCOVERDIR=/app/coverage/int/ubuntu/18.04 >> /etc/environment && \
+    apt-get install ansible -y && \
+    chmod 0 /usr/bin/apt # Liam Neeson
+
+STOPSIGNAL SIGRTMIN+3
+CMD ["/bin/systemd"]
 
 FROM ubuntu:22.04 as jammy
 
diff --git a/Makefile b/Makefile
index 6a49a618..195d9e3a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 build:
-	go build -o bin/algorun *.go
+	CGO_ENABLED=0 go build -o bin/algorun *.go
 test:
 	go test -coverpkg=./... -covermode=atomic ./...
 generate:
diff --git a/docker-compose.integration.yaml b/docker-compose.integration.yaml
index 1456d818..9a89539d 100644
--- a/docker-compose.integration.yaml
+++ b/docker-compose.integration.yaml
@@ -1,16 +1,29 @@
 services:
-#  ubuntu.22.04:
-#    privileged: true
-#    environment:
-#      - GOCOVERDIR=/app/coverage/int/ubuntu/22.04
-#    build:
-#      context: .
-#      target: jammy
-#      dockerfile: .docker/Ubuntu.dockerfile
-#    ports:
-#      - "2222:22"
-#    volumes:
-#      - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04"
+  # Legacy with apt disabled
+  ubuntu.18.04:
+    deploy:
+      replicas: 0
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/ubuntu/18.04
+    build:
+      context: .
+      target: bionic
+      dockerfile: .docker/Ubuntu.dockerfile
+    volumes:
+      - "./coverage/int/ubuntu/18.04:/app/coverage/int/ubuntu/18.04"
+  ubuntu.22.04:
+    deploy:
+      replicas: 0
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/ubuntu/22.04
+    build:
+      context: .
+      target: jammy
+      dockerfile: .docker/Ubuntu.dockerfile
+    volumes:
+      - "./coverage/int/ubuntu/22.04:/app/coverage/int/ubuntu/22.04"
   ubuntu.24.04:
     privileged: true
     environment:
@@ -19,22 +32,20 @@ services:
       context: .
       target: noble
       dockerfile: .docker/Ubuntu.dockerfile
-    ports:
-      - "2223:22"
     volumes:
       - "./coverage/int/ubuntu/24.04:/app/coverage/int/ubuntu/24.04"
-#  fedora.39:
-#    privileged: true
-#    environment:
-#      - GOCOVERDIR=/app/coverage/int/fedora/39
-#    build:
-#      context: .
-#      target: legacy
-#      dockerfile: .docker/Fedora.dockerfile
-#    ports:
-#      - "2224:22"
-#    volumes:
-#      - "./coverage/int/fedora/39:/app/coverage/int/fedora/39"
+  fedora.39:
+    deploy:
+      replicas: 0
+    privileged: true
+    environment:
+      - GOCOVERDIR=/app/coverage/int/fedora/39
+    build:
+      context: .
+      target: legacy
+      dockerfile: .docker/Fedora.dockerfile
+    volumes:
+      - "./coverage/int/fedora/39:/app/coverage/int/fedora/39"
   fedora.40:
     privileged: true
     environment:
@@ -43,7 +54,5 @@ services:
       context: .
       target: previous
       dockerfile: .docker/Fedora.dockerfile
-    ports:
-      - "2225:22"  
     volumes:
       - "./coverage/int/fedora/40:/app/coverage/int/fedora/40"
\ No newline at end of file

From bae2d6f42466e5bdbdf07369486c1f7dd405706f Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Mon, 9 Dec 2024 22:59:05 -0500
Subject: [PATCH 17/23] refactor: algod commands

---
 cmd/{node => configure}/configure.go |  48 ++--
 cmd/configure/service.go             |  26 ++
 cmd/configure/utils.go               |  97 ++++++++
 cmd/node/debug.go                    |  25 ++
 cmd/node/install.go                  | 345 +-------------------------
 cmd/node/main.go                     |  21 --
 cmd/node/node.go                     |  58 +++++
 cmd/node/start.go                    | 117 +--------
 cmd/node/stop.go                     | 115 ++-------
 cmd/node/uninstall.go                | 157 +-----------
 cmd/node/upgrade.go                  | 175 +-------------
 cmd/node/utils.go                    | 341 --------------------------
 cmd/root.go                          |  15 +-
 internal/algod/algod.go              | 154 ++++++++++++
 internal/algod/fallback/algod.go     |  90 +++++++
 internal/algod/linux/linux.go        | 198 +++++++++++++++
 internal/algod/mac/mac.go            | 348 +++++++++++++++++++++++++++
 internal/algod/utils/utils.go        |  69 ++++++
 internal/system/cmds.go              | 106 ++++++++
 internal/system/service.go           |  16 ++
 20 files changed, 1280 insertions(+), 1241 deletions(-)
 rename cmd/{node => configure}/configure.go (91%)
 create mode 100644 cmd/configure/service.go
 create mode 100644 cmd/configure/utils.go
 create mode 100644 cmd/node/debug.go
 delete mode 100644 cmd/node/main.go
 create mode 100644 cmd/node/node.go
 delete mode 100644 cmd/node/utils.go
 create mode 100644 internal/algod/algod.go
 create mode 100644 internal/algod/fallback/algod.go
 create mode 100644 internal/algod/linux/linux.go
 create mode 100644 internal/algod/mac/mac.go
 create mode 100644 internal/algod/utils/utils.go
 create mode 100644 internal/system/cmds.go
 create mode 100644 internal/system/service.go

diff --git a/cmd/node/configure.go b/cmd/configure/configure.go
similarity index 91%
rename from cmd/node/configure.go
rename to cmd/configure/configure.go
index a8746d5b..5b5d6336 100644
--- a/cmd/node/configure.go
+++ b/cmd/configure/configure.go
@@ -1,8 +1,11 @@
-package node
+package configure
 
 import (
 	"bytes"
 	"fmt"
+	"github.com/algorandfoundation/algorun-tui/cmd/node"
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
 	"os"
 	"os/exec"
 	"runtime"
@@ -12,35 +15,34 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var configureCmd = &cobra.Command{
-	Use:   "configure",
-	Short: "Configure Algod",
-	Long:  "Configure Algod settings",
-	Run: func(cmd *cobra.Command, args []string) {
-		configureNode()
-	},
+var Cmd = &cobra.Command{
+	Use:               "configure",
+	Short:             "Configure Algod",
+	Long:              "Configure Algod settings",
+	SilenceUsage:      true,
+	PersistentPreRunE: node.NeedsToBeStopped,
+	//RunE: func(cmd *cobra.Command, args []string) error {
+	//	return configureNode()
+	//},
 }
 
-// TODO: configure not just data directory but algod path
-func configureNode() {
-	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
-		panic("Unsupported OS: " + runtime.GOOS)
-	}
+func init() {
+	Cmd.AddCommand(serviceCmd)
+}
+
+const ConfigureRunningErrorMsg = "algorand is currently running. Please stop the node with *node stop* before configuring"
 
+// TODO: configure not just data directory but algod path
+func configureNode() error {
 	var systemServiceConfigure bool
 
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
+	if algod.IsRunning() {
+		return fmt.Errorf(ConfigureRunningErrorMsg)
 	}
 
 	// Check systemctl first
-	if checkAlgorandServiceCreated() {
+	if algod.IsService() {
 		if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") {
-			if checkAlgorandServiceActive() {
-				fmt.Println("Algorand service is currently running. Please stop the service with *node stop* before editing the service file.")
-				os.Exit(1)
-			}
 			// Edit the service file with the user's new data directory
 			systemServiceConfigure = true
 		} else {
@@ -105,7 +107,7 @@ func configureNode() {
 	}
 
 	// Do quick "lazy" check for existing Algorand Data directories
-	paths := lazyCheckAlgorandDataDirs()
+	paths := utils.GetKnownDataPaths()
 
 	if len(paths) != 0 {
 
@@ -169,7 +171,7 @@ func configureNode() {
 	} else {
 		affectALGORAND_DATA(selectedPath)
 	}
-	os.Exit(0)
+	return nil
 }
 
 func editAlgorandServiceFile(dataDirectoryPath string) {
diff --git a/cmd/configure/service.go b/cmd/configure/service.go
new file mode 100644
index 00000000..8a84ae75
--- /dev/null
+++ b/cmd/configure/service.go
@@ -0,0 +1,26 @@
+package configure
+
+import (
+	"errors"
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/spf13/cobra"
+)
+
+var serviceCmd = &cobra.Command{
+	Use:   "service",
+	Short: "Configure the node service",
+	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("Configure the service that runs the node."),
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		if !system.IsSudo() {
+			return errors.New(
+				"you need to be root to run this command. Please run this command with sudo")
+		}
+		return nil
+	},
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// TODO: Combine this with algod.UpdateService and algod.SetNetwork
+		return algod.EnsureService()
+	},
+}
diff --git a/cmd/configure/utils.go b/cmd/configure/utils.go
new file mode 100644
index 00000000..16926921
--- /dev/null
+++ b/cmd/configure/utils.go
@@ -0,0 +1,97 @@
+package configure
+
+import (
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/manifoldco/promptui"
+	"github.com/spf13/cobra"
+	"os"
+)
+
+type Release struct {
+	Name       string `json:"name"`
+	ZipballURL string `json:"zipball_url"`
+	TarballURL string `json:"tarball_url"`
+	Commit     struct {
+		Sha string `json:"sha"`
+		URL string `json:"url"`
+	} `json:"commit"`
+	NodeID string `json:"node_id"`
+}
+
+// Queries user on the provided prompt and returns the user input
+func promptWrapperInput(promptLabel string) string {
+	prompt := promptui.Prompt{
+		Label: promptLabel,
+	}
+
+	result, err := prompt.Run()
+	cobra.CheckErr(err)
+
+	return result
+}
+
+// Queries user on the provided prompt and returns true if user inputs "y"
+func promptWrapperYes(promptLabel string) bool {
+	return promptWrapperInput(promptLabel) == "y"
+}
+
+// Queries user on the provided prompt and returns true if user does not input "y"
+// Included for improved readability of decision tree, despite being redundant.
+func promptWrapperNo(promptLabel string) bool {
+	return promptWrapperInput(promptLabel) != "y"
+}
+
+// Queries user on the provided prompt and returns the selected item
+func promptWrapperSelection(promptLabel string, items []string) string {
+	prompt := promptui.Select{
+		Label: promptLabel,
+		Items: items,
+	}
+
+	_, result, err := prompt.Run()
+	cobra.CheckErr(err)
+
+	fmt.Printf("You selected: %s\n", result)
+
+	return result
+}
+
+// TODO: consider replacing with a method that does more for the user
+func affectALGORAND_DATA(path string) {
+	fmt.Println("Please execute the following in your terminal to set the environment variable:")
+	fmt.Println("")
+	fmt.Println("export ALGORAND_DATA=" + path)
+	fmt.Println("")
+}
+
+func validateAlgorandDataDir(path string) bool {
+	info, err := os.Stat(path)
+
+	// Check if the path exists
+	if os.IsNotExist(err) {
+		return false
+	}
+
+	// Check if the path is a directory
+	if !info.IsDir() {
+		return false
+	}
+
+	paths := system.FindPathToFile(path, "algod.token")
+	if len(paths) == 1 {
+		return true
+	}
+	return false
+}
+
+// Checks if Algorand data directories exist, based off of existence of the "algod.token" file
+func deepSearchAlgorandDataDirs() []string {
+	home, err := os.UserHomeDir()
+	cobra.CheckErr(err)
+
+	// TODO: consider a better way to identify an Algorand data directory
+	paths := system.FindPathToFile(home, "algod.token")
+
+	return paths
+}
diff --git a/cmd/node/debug.go b/cmd/node/debug.go
new file mode 100644
index 00000000..80f36409
--- /dev/null
+++ b/cmd/node/debug.go
@@ -0,0 +1,25 @@
+package node
+
+import (
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/spf13/cobra"
+)
+
+var debugCmd = &cobra.Command{
+	Use:          "debug",
+	Short:        "Display debug information for developers",
+	Long:         "Prints debug data to be copy and pasted to a bug report.",
+	SilenceUsage: true,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		paths := utils.GetKnownDataPaths()
+		fmt.Printf("Algod in PATH: %v\n", system.CmdExists("algod"))
+		fmt.Printf("Algod is installed: %v\n", algod.IsInstalled())
+		fmt.Printf("Algod is running: %v\n", algod.IsRunning())
+		fmt.Printf("Algod is service: %v\n", algod.IsService())
+		fmt.Printf("Algod paths: %+v\n", paths)
+		return nil
+	},
+}
diff --git a/cmd/node/install.go b/cmd/node/install.go
index 4cb13b96..051631c1 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -2,341 +2,22 @@ package node
 
 import (
 	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"strings"
-
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/spf13/cobra"
 )
 
+const InstallExistsMsg = "algod is already installed"
+
 var installCmd = &cobra.Command{
-	Use:   "install",
-	Short: "Install Algorand node (Algod)",
-	Long:  "Install Algorand node (Algod) and other binaries on your system",
-	Run: func(cmd *cobra.Command, args []string) {
-		installNode()
+	Use:          "install",
+	Short:        "Install Algorand node (Algod)",
+	Long:         "Install Algorand node (Algod) and other binaries on your system",
+	SilenceUsage: true,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		fmt.Println("Checking if Algod is installed...")
+		if algod.IsInstalled() {
+			return fmt.Errorf(InstallExistsMsg)
+		}
+		return algod.Install()
 	},
 }
-
-func installNode() {
-	fmt.Println("Checking if Algod is installed...")
-
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	// Check if Algod is installed
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed. Installing...")
-
-		// Install Algod based on OS
-		switch runtime.GOOS {
-		case "linux":
-			installNodeLinux()
-		case "darwin":
-			installNodeMac()
-		default:
-			panic("Unsupported OS: " + runtime.GOOS)
-		}
-	} else {
-		fmt.Println("Algod is already installed.")
-		printAlgodInfo()
-	}
-
-}
-
-func installNodeLinux() {
-	fmt.Println("Installing Algod on Linux")
-
-	var installCmds [][]string
-	var postInstallHint string
-
-	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
-
-	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
-		fmt.Println("Using apt package manager")
-		installCmds = [][]string{
-			{"apt", "update"},
-			{"apt", "install", "-y", "gnupg2", "curl", "software-properties-common"},
-			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
-			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
-			{"apt", "update"},
-			{"apt", "install", "-y", "algorand-devtools"},
-		}
-	} else if checkCmdToolExists("apt-get") { // On some Debian systems we use apt-get
-		fmt.Println("Using apt-get package manager")
-		installCmds = [][]string{
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
-			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
-			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "algorand-devtools"},
-		}
-	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
-		fmt.Println("Using dnf package manager")
-		installCmds = [][]string{
-			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
-			{"rpmkeys", "--import", "rpm_algorand.pub"},
-			{"dnf", "install", "-y", "dnf-command(config-manager)"},
-			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
-			{"dnf", "install", "-y", "algorand-devtools"},
-			{"systemctl", "enable", "algorand.service"},
-			{"systemctl", "start", "algorand.service"},
-			{"rm", "-f", "rpm_algorand.pub"},
-		}
-	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
-		fmt.Println("Using yum package manager")
-		installCmds = [][]string{
-			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
-			{"rpmkeys", "--import", "rpm_algorand.pub"},
-			{"yum", "install", "yum-utils"},
-			{"yum-config-manager", "--add-repo", "https://releases.algorand.com/rpm/stable/algorand.repo"},
-			{"yum", "install", "-y", "algorand-devtools"},
-			{"systemctl", "enable", "algorand.service"},
-			{"systemctl", "start", "algorand.service"},
-			{"rm", "-f", "rpm_algorand.pub"},
-		}
-	} else {
-		fmt.Println("Unsupported package manager, possibly due to non-Debian or non-Red Hat based Linux distribution. Will attempt to install using updater script.")
-		installCmds = [][]string{
-			{"mkdir", "~/node"},
-			{"sh", "-c", "cd ~/node"},
-			{"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
-			{"chmod", "744", "update.sh"},
-			{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
-		}
-
-		postInstallHint = `You may need to add the Algorand binaries to your PATH:
-					export ALGORAND_DATA="$HOME/node/data"
-					export PATH="$HOME/node:$PATH"
-			`
-	}
-
-	// Run each installation command
-	for _, cmdArgs := range installCmds {
-		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
-		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
-		output, err := cmd.CombinedOutput()
-		if err != nil {
-			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
-			cobra.CheckErr(err)
-		}
-	}
-
-	if postInstallHint != "" {
-		fmt.Println(postInstallHint)
-	}
-}
-
-func installNodeMac() {
-	fmt.Println("Installing Algod on macOS...")
-
-	// Homebrew is our package manager of choice
-	if !checkCmdToolExists("brew") {
-		fmt.Println("Could not find Homebrew installed. Please install Homebrew and try again.")
-		os.Exit(1)
-	}
-
-	originalUser := os.Getenv("SUDO_USER")
-
-	// Run Homebrew commands as the original user without sudo
-	if err := runHomebrewInstallCommandsAsUser(originalUser); err != nil {
-		fmt.Printf("Homebrew commands failed: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Handle data directory and genesis.json file
-	handleDataDirMac()
-
-	// Create and load the launchd service
-	createAndLoadLaunchdService()
-
-	// Ensure Homebrew bin directory is in the PATH
-	// So that brew installed algorand binaries can be found
-	ensureHomebrewPathInEnv()
-
-	if !isAlgodInstalled() {
-		fmt.Println("algod unexpectedly NOT in path. Installation failed.")
-		os.Exit(1)
-	}
-
-	fmt.Println(`Installed Algorand (Algod) with Homebrew.
-Algod is running in the background as a system-level service.
-	`)
-	os.Exit(0)
-}
-
-func runHomebrewInstallCommandsAsUser(user string) error {
-	homebrewCmds := [][]string{
-		{"brew", "tap", "algorandfoundation/homebrew-node"},
-		{"brew", "install", "algorand"},
-		{"brew", "--prefix", "algorand", "--installed"},
-	}
-
-	for _, cmdArgs := range homebrewCmds {
-		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
-		cmd := exec.Command("sudo", append([]string{"-u", user}, cmdArgs...)...)
-		cmd.Stdout = os.Stdout
-		cmd.Stderr = os.Stderr
-		if err := cmd.Run(); err != nil {
-			return fmt.Errorf("command failed: %s\nError: %v", strings.Join(cmdArgs, " "), err)
-		}
-	}
-	return nil
-}
-
-func handleDataDirMac() {
-	// Ensure the ~/.algorand directory exists
-	algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand")
-	if err := os.MkdirAll(algorandDir, 0755); err != nil {
-		fmt.Printf("Failed to create directory %s: %v\n", algorandDir, err)
-		os.Exit(1)
-	}
-
-	// Check if genesis.json file exists in ~/.algorand
-	genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json")
-	if _, err := os.Stat(genesisFilePath); os.IsNotExist(err) {
-		fmt.Println("genesis.json file does not exist. Downloading...")
-
-		// Download the genesis.json file
-		resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
-		if err != nil {
-			fmt.Printf("Failed to download genesis.json: %v\n", err)
-			cobra.CheckErr(err)
-		}
-		defer resp.Body.Close()
-
-		// Create the file
-		out, err := os.Create(genesisFilePath)
-		if err != nil {
-			fmt.Printf("Failed to create genesis.json file: %v\n", err)
-			cobra.CheckErr(err)
-		}
-		defer out.Close()
-
-		// Write the content to the file
-		_, err = io.Copy(out, resp.Body)
-		if err != nil {
-			fmt.Printf("Failed to save genesis.json file: %v\n", err)
-			cobra.CheckErr(err)
-		}
-
-		fmt.Println("mainnet genesis.json file downloaded successfully.")
-	}
-
-}
-
-func createAndLoadLaunchdService() {
-	// Get the prefix path for Algorand
-	cmd := exec.Command("brew", "--prefix", "algorand")
-	algorandPrefix, err := cmd.Output()
-	if err != nil {
-		fmt.Printf("Failed to get Algorand prefix: %v\n", err)
-		cobra.CheckErr(err)
-	}
-	algorandPrefixPath := strings.TrimSpace(string(algorandPrefix))
-
-	// Define the launchd plist content
-	plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>Label</key>
-	<string>com.algorand.algod</string>
-	<key>ProgramArguments</key>
-	<array>
-			<string>%s/bin/algod</string>
-			<string>-d</string>
-			<string>%s/.algorand</string>
-	</array>
-	<key>RunAtLoad</key>
-	<true/>
-	<key>KeepAlive</key>
-	<true/>
-	<key>StandardOutPath</key>
-	<string>/tmp/algod.out</string>
-	<key>StandardErrorPath</key>
-	<string>/tmp/algod.err</string>
-</dict>
-</plist>`, algorandPrefixPath, os.Getenv("HOME"))
-
-	// Write the plist content to a file
-	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
-	err = os.MkdirAll(filepath.Dir(plistPath), 0755)
-	if err != nil {
-		fmt.Printf("Failed to create LaunchDaemons directory: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	err = os.WriteFile(plistPath, []byte(plistContent), 0644)
-	if err != nil {
-		fmt.Printf("Failed to write plist file: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	// Load the launchd service
-	cmd = exec.Command("launchctl", "load", plistPath)
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to load launchd service: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	// Check if the service is running
-	cmd = exec.Command("launchctl", "list", "com.algorand.algod")
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("launchd service is not running: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	fmt.Println("Launchd service created and loaded successfully.")
-}
-
-// Ensure that Homebrew bin directory is in the PATH so that Algorand binaries can be found
-func ensureHomebrewPathInEnv() {
-	homebrewPrefix := os.Getenv("HOMEBREW_PREFIX")
-	homebrewCellar := os.Getenv("HOMEBREW_CELLAR")
-	homebrewRepository := os.Getenv("HOMEBREW_REPOSITORY")
-
-	if homebrewPrefix == "" || homebrewCellar == "" || homebrewRepository == "" {
-		fmt.Println("Homebrew environment variables are not set. Running brew shellenv...")
-
-		cmd := exec.Command("brew", "shellenv")
-		output, err := cmd.Output()
-		if err != nil {
-			fmt.Printf("Failed to get Homebrew environment: %v\n", err)
-			return
-		}
-
-		envVars := strings.Split(string(output), "\n")
-		for _, envVar := range envVars {
-			if envVar != "" {
-				fmt.Println("Setting environment variable:", envVar)
-				os.Setenv(strings.Split(envVar, "=")[0], strings.Trim(strings.Split(envVar, "=")[1], `"`))
-			}
-		}
-
-		// Append brew shellenv output to .zshrc
-		zshrcPath := filepath.Join(os.Getenv("HOME"), ".zshrc")
-		f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_WRONLY, 0644)
-		if err != nil {
-			fmt.Printf("Failed to open .zshrc: %v\n", err)
-			fmt.Printf("Are you running a terminal other than zsh?")
-			fmt.Printf("Please run brew shellenv and add the output to your shell's configuration file.")
-			return
-		}
-		defer f.Close()
-
-		if _, err := f.WriteString("\n# Inserted by Algorun\n# Homebrew environment variables\n" + string(output)); err != nil {
-			fmt.Printf("Failed to write to .zshrc: %v\n", err)
-		}
-	}
-}
diff --git a/cmd/node/main.go b/cmd/node/main.go
deleted file mode 100644
index 18af211b..00000000
--- a/cmd/node/main.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package node
-
-import (
-	"github.com/algorandfoundation/algorun-tui/ui/style"
-	"github.com/spf13/cobra"
-)
-
-var NodeCmd = &cobra.Command{
-	Use:   "node",
-	Short: "Algod installation",
-	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("View the node status"),
-}
-
-func init() {
-	NodeCmd.AddCommand(configureCmd)
-	NodeCmd.AddCommand(installCmd)
-	NodeCmd.AddCommand(startCmd)
-	NodeCmd.AddCommand(stopCmd)
-	NodeCmd.AddCommand(uninstallCmd)
-	NodeCmd.AddCommand(upgradeCmd)
-}
diff --git a/cmd/node/node.go b/cmd/node/node.go
new file mode 100644
index 00000000..4741c2ea
--- /dev/null
+++ b/cmd/node/node.go
@@ -0,0 +1,58 @@
+package node
+
+import (
+	"errors"
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/spf13/cobra"
+	"os"
+	"runtime"
+)
+
+const PermissionErrorMsg = "this command must be run with super-user privileges (sudo)"
+const NotInstalledErrorMsg = "algod is not installed. please run the *node install* command"
+const RunningErrorMsg = "algod is running, please run the *node stop* command"
+const NotRunningErrorMsg = "algod is not running"
+
+var Cmd = &cobra.Command{
+	Use:   "node",
+	Short: "Node Management",
+	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"),
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		// Check that we are calling with sudo on linux
+		if os.Geteuid() != 0 && runtime.GOOS == "linux" {
+			return errors.New(PermissionErrorMsg)
+		}
+		return nil
+	},
+}
+
+func NeedsToBeRunning(cmd *cobra.Command, args []string) error {
+	if !algod.IsInstalled() {
+		return fmt.Errorf(NotInstalledErrorMsg)
+	}
+	if !algod.IsRunning() {
+		return fmt.Errorf(NotRunningErrorMsg)
+	}
+	return nil
+}
+
+func NeedsToBeStopped(cmd *cobra.Command, args []string) error {
+	if !algod.IsInstalled() {
+		return fmt.Errorf(NotInstalledErrorMsg)
+	}
+	if algod.IsRunning() {
+		return fmt.Errorf(RunningErrorMsg)
+	}
+	return nil
+}
+
+func init() {
+	Cmd.AddCommand(installCmd)
+	Cmd.AddCommand(startCmd)
+	Cmd.AddCommand(stopCmd)
+	Cmd.AddCommand(uninstallCmd)
+	Cmd.AddCommand(upgradeCmd)
+	Cmd.AddCommand(debugCmd)
+}
diff --git a/cmd/node/start.go b/cmd/node/start.go
index e7d4092d..0691e615 100644
--- a/cmd/node/start.go
+++ b/cmd/node/start.go
@@ -1,118 +1,17 @@
 package node
 
 import (
-	"fmt"
-	"os"
-	"os/exec"
-	"runtime"
-	"syscall"
-	"time"
-
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/spf13/cobra"
 )
 
 var startCmd = &cobra.Command{
-	Use:   "start",
-	Short: "Start Algod",
-	Long:  "Start Algod on your system (the one on your PATH).",
-	Run: func(cmd *cobra.Command, args []string) {
-		startNode()
+	Use:               "start",
+	Short:             "Start Algod",
+	Long:              "Start Algod on your system (the one on your PATH).",
+	SilenceUsage:      true,
+	PersistentPreRunE: NeedsToBeStopped,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return algod.Start()
 	},
 }
-
-// Start Algod on your system (the one on your PATH).
-func startNode() {
-	fmt.Println("Attempting to start Algod...")
-
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed. Please run the *node install* command.")
-		os.Exit(1)
-	}
-
-	// Check if Algod is already running
-	if isAlgodRunning() {
-		fmt.Println("Algod is already running.")
-		os.Exit(0)
-	}
-
-	startAlgodProcess()
-}
-
-func startAlgodProcess() {
-
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	// Check if algod is available as a system service
-	if checkAlgorandServiceCreated() {
-		// Algod is available as a service
-
-		switch runtime.GOOS {
-		case "linux":
-			startSystemdAlgorandService()
-		case "darwin":
-			startLaunchdAlgorandService()
-		default: // Unsupported OS
-			fmt.Println("Unsupported OS.")
-			os.Exit(1)
-		}
-
-	} else {
-		// Algod is not available as a systemd service, start it directly
-		fmt.Println("Starting algod directly...")
-
-		// Check if ALGORAND_DATA environment variable is set
-		fmt.Println("Checking if ALGORAND_DATA env var is set...")
-		algorandData := os.Getenv("ALGORAND_DATA")
-
-		if !validateAlgorandDataDir(algorandData) {
-			fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
-			os.Exit(1)
-		}
-
-		fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
-
-		cmd := exec.Command("algod")
-		cmd.SysProcAttr = &syscall.SysProcAttr{
-			Setsid: true,
-		}
-		err := cmd.Start()
-		if err != nil {
-			fmt.Printf("Failed to start algod: %v\n", err)
-			os.Exit(1)
-		}
-	}
-
-	// Wait for the process to start
-	time.Sleep(5 * time.Second)
-
-	if isAlgodRunning() {
-		fmt.Println("Algod is running.")
-	} else {
-		fmt.Println("Algod failed to start.")
-	}
-}
-
-// Linux uses systemd
-func startSystemdAlgorandService() {
-	fmt.Println("Starting algod using systemctl...")
-	cmd := exec.Command("systemctl", "start", "algorand")
-	err := cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to start algod service: %v\n", err)
-		os.Exit(1)
-	}
-}
-
-// MacOS uses launchd instead of systemd
-func startLaunchdAlgorandService() {
-	fmt.Println("Starting algod using launchctl...")
-	cmd := exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist")
-	err := cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to start algod service: %v\n", err)
-		os.Exit(1)
-	}
-}
diff --git a/cmd/node/stop.go b/cmd/node/stop.go
index fe5180ad..02a6a5c0 100644
--- a/cmd/node/stop.go
+++ b/cmd/node/stop.go
@@ -2,110 +2,35 @@ package node
 
 import (
 	"fmt"
-	"os"
-	"os/exec"
-	"runtime"
-	"syscall"
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"time"
 
 	"github.com/spf13/cobra"
 )
 
-var stopCmd = &cobra.Command{
-	Use:   "stop",
-	Short: "Stop Algod",
-	Long:  "Stop the Algod process on your system.",
-	Run: func(cmd *cobra.Command, args []string) {
-		stopNode()
-	},
-}
-
-// Stop the Algod process on your system.
-func stopNode() {
-	fmt.Println("Attempting to stop Algod...")
-
-	if !isAlgodRunning() {
-		fmt.Println("Algod was not running.")
-		os.Exit(0)
-	}
-
-	stopAlgodProcess()
-
-	time.Sleep(5 * time.Second)
-
-	if !isAlgodRunning() {
-		fmt.Println("Algod is no longer running.")
-		os.Exit(0)
-	}
-
-	fmt.Println("Failed to stop Algod.")
-	os.Exit(1)
-}
-
-func stopAlgodProcess() {
-
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
+const StopTimeout = 5 * time.Second
+const StopSuccessMsg = "Algod stopped successfully"
+const StopFailureMsg = "failed to stop Algod"
 
-	// Check if algod is available as a system service
-	if checkAlgorandServiceCreated() {
-		switch runtime.GOOS {
-		case "linux":
-			stopSystemdAlgorandService()
-		case "darwin":
-			stopLaunchdAlgorandService()
-		default: // Unsupported OS
-			fmt.Println("Unsupported OS.")
-			os.Exit(1)
-		}
-
-	} else {
-		// Algod is not available as a systemd service, stop it directly
-		fmt.Println("Stopping algod directly...")
-		// Find the process ID of algod
-		pid, err := findAlgodPID()
-		if err != nil {
-			fmt.Printf("Failed to find algod process: %v\n", err)
-			cobra.CheckErr(err)
-		}
-
-		// Send SIGTERM to the process
-		process, err := os.FindProcess(pid)
+var stopCmd = &cobra.Command{
+	Use:               "stop",
+	Short:             "Stop Algod",
+	Long:              "Stop the Algod process on your system.",
+	SilenceUsage:      true,
+	PersistentPreRunE: NeedsToBeRunning,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		fmt.Println("Stopping Algod...")
+		err := algod.Stop()
 		if err != nil {
-			fmt.Printf("Failed to find process with PID %d: %v\n", pid, err)
-			cobra.CheckErr(err)
+			return fmt.Errorf(StopFailureMsg)
 		}
+		time.Sleep(StopTimeout)
 
-		err = process.Signal(syscall.SIGTERM)
-		if err != nil {
-			fmt.Printf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
-			cobra.CheckErr(err)
+		if algod.IsRunning() {
+			return fmt.Errorf(StopFailureMsg)
 		}
 
-		fmt.Println("Sent SIGTERM to algod process.")
-	}
-}
-
-func stopLaunchdAlgorandService() {
-	fmt.Println("Stopping algod using launchd...")
-	cmd := exec.Command("launchctl", "bootout", "system/com.algorand.algod")
-	err := cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to stop algod service: %v\n", err)
-		cobra.CheckErr(err)
-	}
-	fmt.Println("Algod service stopped.")
-}
-
-func stopSystemdAlgorandService() {
-	fmt.Println("Stopping algod using systemctl...")
-	cmd := exec.Command("systemctl", "stop", "algorand")
-	err := cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to stop algod service: %v\n", err)
-		cobra.CheckErr(err)
-	}
-	fmt.Println("Algod service stopped.")
+		fmt.Println(StopSuccessMsg)
+		return nil
+	},
 }
diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go
index 99834409..9a60b86f 100644
--- a/cmd/node/uninstall.go
+++ b/cmd/node/uninstall.go
@@ -1,158 +1,17 @@
 package node
 
 import (
-	"fmt"
-	"os"
-	"os/exec"
-	"runtime"
-	"strings"
-
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/spf13/cobra"
 )
 
 var uninstallCmd = &cobra.Command{
-	Use:   "uninstall",
-	Short: "Uninstall Algorand node (Algod)",
-	Long:  "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
-	Run: func(cmd *cobra.Command, args []string) {
-		unInstallNode()
+	Use:               "uninstall",
+	Short:             "Uninstall Algorand node (Algod)",
+	Long:              "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
+	SilenceUsage:      true,
+	PersistentPreRunE: NeedsToBeStopped,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return algod.Uninstall()
 	},
 }
-
-// Uninstall Algorand node (Algod) and other binaries on your system
-func unInstallNode() {
-
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	fmt.Println("Checking if Algod is installed...")
-
-	// Check if Algod is installed
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed.")
-		os.Exit(0)
-	}
-
-	// Check if Algod is running
-	if isAlgodRunning() {
-		fmt.Println("Algod is running. Please run *node stop* first to stop it.")
-		os.Exit(1)
-	}
-
-	fmt.Println("Algod is installed. Proceeding...")
-
-	// Uninstall Algod based on OS
-	switch runtime.GOOS {
-	case "linux":
-		unInstallNodeLinux()
-	case "darwin":
-		unInstallNodeMac()
-	default:
-		panic("Unsupported OS: " + runtime.GOOS)
-	}
-
-	os.Exit(0)
-}
-
-func unInstallNodeMac() {
-	fmt.Println("Uninstalling Algod on macOS...")
-
-	// Homebrew is our package manager of choice
-	if !checkCmdToolExists("brew") {
-		fmt.Println("Could not find Homebrew installed. Did you install Algod some other way?.")
-		os.Exit(1)
-	}
-
-	user := os.Getenv("SUDO_USER")
-
-	// Run the brew uninstall command as the original user without sudo
-	cmd := exec.Command("sudo", "-u", user, "brew", "uninstall", "algorand", "--formula")
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		fmt.Printf("Failed to uninstall Algorand: %v\n", err)
-		fmt.Printf("Output: %s\n", string(output))
-		os.Exit(1)
-	}
-
-	fmt.Printf("Output: %s\n", string(output))
-
-	// Calling brew uninstall algorand without sudo user privileges
-	cmd = exec.Command("sudo", "-u", user, "brew", "--prefix", "algorand", "--installed")
-	err = cmd.Run()
-	if err == nil {
-		fmt.Println("Algorand uninstall failed.")
-		os.Exit(1)
-	}
-
-	// Delete the launchd plist file
-	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
-	err = os.Remove(plistPath)
-	if err != nil {
-		fmt.Printf("Failed to delete plist file: %v\n", err)
-		os.Exit(1)
-	}
-
-	fmt.Println("Algorand uninstalled successfully.")
-}
-
-func unInstallNodeLinux() {
-
-	var unInstallCmds [][]string
-
-	if checkCmdToolExists("apt") { // On Ubuntu and Debian we use the apt package manager
-		fmt.Println("Using apt package manager")
-		unInstallCmds = [][]string{
-			{"apt", "remove", "algorand-devtools", "-y"},
-			{"apt", "autoremove", "-y"},
-		}
-	} else if checkCmdToolExists("apt-get") {
-		fmt.Println("Using apt-get package manager")
-		unInstallCmds = [][]string{
-			{"apt-get", "remove", "algorand-devtools", "-y"},
-			{"apt-get", "autoremove", "-y"},
-		}
-	} else if checkCmdToolExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
-		fmt.Println("Using dnf package manager")
-		unInstallCmds = [][]string{
-			{"dnf", "remove", "algorand-devtools", "-y"},
-		}
-	} else if checkCmdToolExists("yum") { // On CentOs7 we use the yum package manager
-		fmt.Println("Using yum package manager")
-		unInstallCmds = [][]string{
-			{"yum", "remove", "algorand-devtools", "-y"},
-		}
-	} else {
-		fmt.Println("Could not find a package manager to uninstall Algorand.")
-		os.Exit(1)
-	}
-
-	// Commands to clear systemd algorand.service and any other files, like the configuration override
-	unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "sudo rm -rf /etc/systemd/system/algorand*"})
-	unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"})
-
-	// Run each installation command
-	for _, cmdArgs := range unInstallCmds {
-		fmt.Println("Running command:", strings.Join(cmdArgs, " "))
-		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
-		output, err := cmd.CombinedOutput()
-		if err != nil {
-			fmt.Printf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(cmdArgs, " "), output, err)
-			cobra.CheckErr(err)
-		}
-	}
-
-	// Check the status of the algorand service
-	cmd := exec.Command("systemctl", "status", "algorand")
-	output, err := cmd.CombinedOutput()
-	if err != nil && strings.Contains(string(output), "Unit algorand.service could not be found.") {
-		fmt.Println("Algorand service has been successfully removed.")
-	} else {
-		fmt.Printf("Failed to verify Algorand service uninstallation: %v\n", err)
-		fmt.Printf("Output: %s\n", string(output))
-		os.Exit(1)
-	}
-
-	fmt.Println("Algorand successfully uninstalled.")
-}
diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go
index 55210b09..ee675686 100644
--- a/cmd/node/upgrade.go
+++ b/cmd/node/upgrade.go
@@ -1,175 +1,18 @@
 package node
 
 import (
-	"fmt"
-	"os"
-	"os/exec"
-	"runtime"
-	"strings"
-
+	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/spf13/cobra"
 )
 
 var upgradeCmd = &cobra.Command{
-	Use:   "upgrade",
-	Short: "Upgrade Algod",
-	Long:  "Upgrade Algod (if installed with package manager).",
-	Run: func(cmd *cobra.Command, args []string) {
-		upgradeAlgod()
+	Use:               "upgrade",
+	Short:             "Upgrade Algod",
+	Long:              "Upgrade Algod (if installed with package manager).",
+	SilenceUsage:      true,
+	PersistentPreRunE: NeedsToBeStopped,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// TODO: Check Version from S3 against the local binary
+		return algod.Update()
 	},
 }
-
-// Upgrade ALGOD (if installed with package manager).
-func upgradeAlgod() {
-	if !isAlgodInstalled() {
-		fmt.Println("Algod is not installed.")
-		os.Exit(1)
-	}
-
-	switch runtime.GOOS {
-	case "darwin":
-		if checkCmdToolExists("brew") {
-			upgradeBrewAlgorand()
-		} else {
-			fmt.Println("Homebrew is not installed. Please install Homebrew and try again.")
-			os.Exit(1)
-		}
-	case "linux":
-		// Check if Algod was installed with apt/apt-get, dnf, or yum
-		if checkCmdToolExists("apt") {
-			upgradeDebianPackage("apt", "algorand-devtools")
-		} else if checkCmdToolExists("apt-get") {
-			upgradeDebianPackage("apt-get", "algorand-devtools")
-		} else if checkCmdToolExists("dnf") {
-			upgradeRpmPackage("dnf", "algorand-devtools")
-		} else if checkCmdToolExists("yum") {
-			upgradeRpmPackage("yum", "algorand-devtools")
-		} else {
-			fmt.Println("The *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade.")
-			os.Exit(1)
-		}
-	default:
-		fmt.Println("Unsupported operating system. The *node upgrade* command is only available for macOS and Linux.")
-		os.Exit(1)
-	}
-}
-
-func upgradeBrewAlgorand() {
-	fmt.Println("Upgrading Algod using Homebrew...")
-
-	var prefixCommand []string
-
-	// Brew cannot be run with sudo, so we need to run the commands as the original user.
-	// This checks if the user has ran this command with super-user privileges, and if so
-	// counteracts it by running the commands as the original user.
-	if isRunningWithSudo() {
-		originalUser := os.Getenv("SUDO_USER")
-		prefixCommand = []string{"sudo", "-u", originalUser}
-	} else {
-		prefixCommand = []string{}
-	}
-
-	// Check if algorand is installed with Homebrew
-	checkCmdArgs := append(prefixCommand, "brew", "--prefix", "algorand", "--installed")
-	fmt.Println("Running command:", strings.Join(checkCmdArgs, " "))
-	checkCmd := exec.Command(checkCmdArgs[0], checkCmdArgs[1:]...)
-	checkCmd.Stdout = os.Stdout
-	checkCmd.Stderr = os.Stderr
-	if err := checkCmd.Run(); err != nil {
-		fmt.Println("Algorand is not installed with Homebrew.")
-		os.Exit(1)
-	}
-
-	// Upgrade algorand
-	upgradeCmdArgs := append(prefixCommand, "brew", "upgrade", "algorand", "--formula")
-	fmt.Println("Running command:", strings.Join(upgradeCmdArgs, " "))
-	upgradeCmd := exec.Command(upgradeCmdArgs[0], upgradeCmdArgs[1:]...)
-	upgradeCmd.Stdout = os.Stdout
-	upgradeCmd.Stderr = os.Stderr
-	if err := upgradeCmd.Run(); err != nil {
-		fmt.Printf("Failed to upgrade Algorand: %v\n", err)
-		os.Exit(1)
-	}
-}
-
-// Upgrade a package using the specified Debian package manager
-func upgradeDebianPackage(packageManager, packageName string) {
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with super-user priviledges (sudo).")
-		os.Exit(1)
-	}
-
-	// Check if the package is installed and if there are updates available using apt-cache policy
-	cmd := exec.Command("apt-cache", "policy", packageName)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		fmt.Printf("Failed to check package policy: %v\n", err)
-		os.Exit(1)
-	}
-
-	outputStr := string(output)
-	if strings.Contains(outputStr, "Installed: (none)") {
-		fmt.Printf("Package %s is not installed.\n", packageName)
-		os.Exit(1)
-	}
-
-	installedVersion := extractVersion(outputStr, "Installed:")
-	candidateVersion := extractVersion(outputStr, "Candidate:")
-
-	if installedVersion == candidateVersion {
-		fmt.Printf("Package %s is installed (v%s) and up-to-date with latest (v%s).\n", packageName, installedVersion, candidateVersion)
-		os.Exit(0)
-	}
-
-	fmt.Printf("Package %s is installed (v%s) and has updates available (v%s).\n", packageName, installedVersion, candidateVersion)
-
-	// Update the package list
-	fmt.Println("Updating package list...")
-	cmd = exec.Command(packageManager, "update")
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to update package list: %v\n", err)
-		os.Exit(1)
-	}
-
-	// Upgrade the package
-	fmt.Printf("Upgrading package %s...\n", packageName)
-	cmd = exec.Command(packageManager, "install", "--only-upgrade", "-y", packageName)
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
-		os.Exit(1)
-	}
-
-	fmt.Printf("Package %s upgraded successfully.\n", packageName)
-	os.Exit(0)
-}
-
-// Upgrade a package using the specified RPM package manager
-func upgradeRpmPackage(packageManager, packageName string) {
-	// Check that we are calling with sudo
-	if !isRunningWithSudo() {
-		fmt.Println("This command must be run with sudo.")
-		os.Exit(1)
-	}
-
-	// Attempt to upgrade the package directly
-	fmt.Printf("Upgrading package %s...\n", packageName)
-	cmd := exec.Command(packageManager, "update", "-y", packageName)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		fmt.Printf("Failed to upgrade package %s: %v\n", packageName, err)
-		os.Exit(1)
-	}
-
-	outputStr := string(output)
-	if strings.Contains(outputStr, "Nothing to do") {
-		fmt.Printf("Package %s is already up-to-date.\n", packageName)
-		os.Exit(0)
-	} else {
-		fmt.Println(outputStr)
-		fmt.Printf("Package %s upgraded successfully.\n", packageName)
-		os.Exit(0)
-	}
-}
diff --git a/cmd/node/utils.go b/cmd/node/utils.go
deleted file mode 100644
index e15763e1..00000000
--- a/cmd/node/utils.go
+++ /dev/null
@@ -1,341 +0,0 @@
-package node
-
-import (
-	"bytes"
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"sync"
-
-	"github.com/manifoldco/promptui"
-	"github.com/spf13/cobra"
-)
-
-type Release struct {
-	Name       string `json:"name"`
-	ZipballURL string `json:"zipball_url"`
-	TarballURL string `json:"tarball_url"`
-	Commit     struct {
-		Sha string `json:"sha"`
-		URL string `json:"url"`
-	} `json:"commit"`
-	NodeID string `json:"node_id"`
-}
-
-// Queries user on the provided prompt and returns the user input
-func promptWrapperInput(promptLabel string) string {
-	prompt := promptui.Prompt{
-		Label: promptLabel,
-	}
-
-	result, err := prompt.Run()
-	cobra.CheckErr(err)
-
-	return result
-}
-
-// Queries user on the provided prompt and returns true if user inputs "y"
-func promptWrapperYes(promptLabel string) bool {
-	return promptWrapperInput(promptLabel) == "y"
-}
-
-// Queries user on the provided prompt and returns true if user does not input "y"
-// Included for improved readability of decision tree, despite being redundant.
-func promptWrapperNo(promptLabel string) bool {
-	return promptWrapperInput(promptLabel) != "y"
-}
-
-// Queries user on the provided prompt and returns the selected item
-func promptWrapperSelection(promptLabel string, items []string) string {
-	prompt := promptui.Select{
-		Label: promptLabel,
-		Items: items,
-	}
-
-	_, result, err := prompt.Run()
-	cobra.CheckErr(err)
-
-	fmt.Printf("You selected: %s\n", result)
-
-	return result
-}
-
-// Check if Algod is installed
-func isAlgodInstalled() bool {
-	if runtime.GOOS == "windows" {
-		panic("Windows is not supported.")
-	}
-
-	return checkCmdToolExists("algod")
-}
-
-// Checks that a bash cli/cmd tool exists
-func checkCmdToolExists(tool string) bool {
-	_, err := exec.LookPath(tool)
-	return err == nil
-}
-
-// Find where algod is defined and print its version
-func printAlgodInfo() {
-	algodPath, err := exec.LookPath("algod")
-	if err != nil {
-		fmt.Printf("Error finding algod: %v\n", err)
-		return
-	}
-
-	// Get algod version
-	algodVersion, err := exec.Command("algod", "-v").Output()
-	if err != nil {
-		fmt.Printf("Error getting algod version: %v\n", err)
-		return
-	}
-
-	fmt.Printf("Algod is located at: %s\n", algodPath)
-	fmt.Printf("algod -v\n")
-	fmt.Printf("%s\n", algodVersion)
-}
-
-// TODO: consider replacing with a method that does more for the user
-func affectALGORAND_DATA(path string) {
-	fmt.Println("Please execute the following in your terminal to set the environment variable:")
-	fmt.Println("")
-	fmt.Println("export ALGORAND_DATA=" + path)
-	fmt.Println("")
-}
-
-// Check if the program is running with admin (super-user) priviledges
-func isRunningWithSudo() bool {
-	return os.Geteuid() == 0
-}
-
-// Finds path(s) to a file in a directory and its subdirectories using parallel processing
-func findPathToFile(startDir string, targetFileName string) []string {
-	var dirPaths []string
-	var mu sync.Mutex
-	var wg sync.WaitGroup
-
-	fileChan := make(chan string)
-
-	// Worker function to process files
-	worker := func() {
-		defer wg.Done()
-		for path := range fileChan {
-			info, err := os.Stat(path)
-			if err != nil {
-				continue
-			}
-			if !info.IsDir() && info.Name() == targetFileName {
-				dirPath := filepath.Dir(path)
-				mu.Lock()
-				dirPaths = append(dirPaths, dirPath)
-				mu.Unlock()
-			}
-		}
-	}
-
-	// Start worker goroutines
-	numWorkers := 4 // Adjust the number of workers based on your system's capabilities
-	for i := 0; i < numWorkers; i++ {
-		wg.Add(1)
-		go worker()
-	}
-
-	// Walk the directory tree and send file paths to the channel
-	err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			// Ignore permission errors
-			if os.IsPermission(err) {
-				return nil
-			}
-			return err
-		}
-		fileChan <- path
-		return nil
-	})
-
-	close(fileChan)
-	wg.Wait()
-
-	if err != nil {
-		panic(err)
-	}
-
-	return dirPaths
-}
-
-func validateAlgorandDataDir(path string) bool {
-	info, err := os.Stat(path)
-
-	// Check if the path exists
-	if os.IsNotExist(err) {
-		return false
-	}
-
-	// Check if the path is a directory
-	if !info.IsDir() {
-		return false
-	}
-
-	paths := findPathToFile(path, "algod.token")
-	if len(paths) == 1 {
-		return true
-	}
-	return false
-}
-
-// Does a lazy check for Algorand data directories, based off of known common paths
-func lazyCheckAlgorandDataDirs() []string {
-	home, err := os.UserHomeDir()
-	cobra.CheckErr(err)
-
-	// Hardcoded paths known to be common Algorand data directories
-	commonAlgorandDataDirPaths := []string{
-		"/var/lib/algorand",
-		filepath.Join(home, "node", "data"),
-		filepath.Join(home, ".algorand"),
-	}
-
-	var paths []string
-
-	for _, path := range commonAlgorandDataDirPaths {
-		if validateAlgorandDataDir(path) {
-			paths = append(paths, path)
-		}
-	}
-
-	return paths
-}
-
-// Checks if Algorand data directories exist, based off of existence of the "algod.token" file
-func deepSearchAlgorandDataDirs() []string {
-	home, err := os.UserHomeDir()
-	cobra.CheckErr(err)
-
-	// TODO: consider a better way to identify an Algorand data directory
-	paths := findPathToFile(home, "algod.token")
-
-	return paths
-}
-
-func findAlgodPID() (int, error) {
-	cmd := exec.Command("pgrep", "algod")
-	output, err := cmd.Output()
-	if err != nil {
-		return 0, err
-	}
-
-	var pid int
-	_, err = fmt.Sscanf(string(output), "%d", &pid)
-	if err != nil {
-		return 0, fmt.Errorf("failed to parse PID: %v", err)
-	}
-
-	return pid, nil
-}
-
-// Check if Algorand service has been created
-func checkAlgorandServiceCreated() bool {
-	switch runtime.GOOS {
-	case "linux":
-		return checkSystemdAlgorandServiceCreated()
-	case "darwin":
-		return checkLaunchdAlgorandServiceCreated()
-	default:
-		fmt.Println("Unsupported operating system.")
-		return false
-	}
-}
-
-// Check if Algorand service has been created with systemd (Linux)
-func checkSystemdAlgorandServiceCreated() bool {
-	cmd := exec.Command("systemctl", "list-unit-files", "algorand.service")
-	var out bytes.Buffer
-	cmd.Stdout = &out
-	err := cmd.Run()
-	if err != nil {
-		return false
-	}
-	return strings.Contains(out.String(), "algorand.service")
-}
-
-// Check if Algorand service has been created with launchd (macOS)
-// Note that it needs to be run in super-user privilege mode to
-// be able to view the root level services.
-func checkLaunchdAlgorandServiceCreated() bool {
-	cmd := exec.Command("launchctl", "list", "com.algorand.algod")
-	var out bytes.Buffer
-	cmd.Stdout = &out
-	err := cmd.Run()
-	output := out.String()
-	if err != nil {
-		fmt.Printf("Failed to check launchd service: %v\n", err)
-		return false
-	}
-
-	if strings.Contains(output, "Could not find service") {
-		return false
-	}
-
-	return true
-}
-
-func checkAlgorandServiceActive() bool {
-	switch runtime.GOOS {
-	case "linux":
-		return checkSystemdAlgorandServiceActive()
-	case "darwin":
-		return checkLaunchdAlgorandServiceActive()
-	default:
-		fmt.Println("Unsupported operating system.")
-		return false
-	}
-}
-
-func checkSystemdAlgorandServiceActive() bool {
-	cmd := exec.Command("systemctl", "is-active", "algorand")
-	var out bytes.Buffer
-	cmd.Stdout = &out
-	err := cmd.Run()
-	if err != nil {
-		return false
-	}
-	return strings.TrimSpace(out.String()) == "active"
-}
-
-func checkLaunchdAlgorandServiceActive() bool {
-	cmd := exec.Command("launchctl", "print", "system/com.algorand.algod")
-	var out bytes.Buffer
-	cmd.Stdout = &out
-	err := cmd.Run()
-	output := out.String()
-	if err != nil {
-		return false
-	}
-	if strings.Contains(output, "Bad request") || strings.Contains(output, "Could not find service") {
-		return false
-	}
-
-	return true
-}
-
-// Extract version information from apt-cache policy output
-func extractVersion(output, prefix string) string {
-	lines := strings.Split(output, "\n")
-	for _, line := range lines {
-		line = strings.TrimSpace(line)
-		if strings.HasPrefix(line, prefix) {
-			return strings.TrimSpace(strings.TrimPrefix(line, prefix))
-		}
-	}
-	return ""
-}
-
-func isAlgodRunning() bool {
-	// Check if Algod is already running
-	// This works for systemctl started algorand.service as well as directly started algod
-	err := exec.Command("pgrep", "algod").Run()
-	return err == nil
-}
diff --git a/cmd/root.go b/cmd/root.go
index df55723e..2e03a8d9 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/algorandfoundation/algorun-tui/api"
+	"github.com/algorandfoundation/algorun-tui/cmd/configure"
 	"github.com/algorandfoundation/algorun-tui/cmd/node"
 	"github.com/algorandfoundation/algorun-tui/internal"
 	"github.com/algorandfoundation/algorun-tui/ui"
@@ -18,6 +19,7 @@ import (
 	"github.com/spf13/viper"
 	"io"
 	"os"
+	"runtime"
 	"strings"
 )
 
@@ -137,15 +139,15 @@ func init() {
 	rootCmd.Version = Version
 
 	// Bindings
-	rootCmd.PersistentFlags().StringVarP(&algod, "algod-endpoint", "a", "", style.LightBlue("algod endpoint address URI, including http[s]"))
-	rootCmd.PersistentFlags().StringVarP(&token, "algod-token", "t", "", lipgloss.JoinHorizontal(
+	rootCmd.Flags().StringVarP(&algod, "algod-endpoint", "a", "", style.LightBlue("algod endpoint address URI, including http[s]"))
+	rootCmd.Flags().StringVarP(&token, "algod-token", "t", "", lipgloss.JoinHorizontal(
 		lipgloss.Left,
 		style.LightBlue("algod "),
 		style.BoldUnderline("admin"),
 		style.LightBlue(" token"),
 	))
-	_ = viper.BindPFlag("algod-endpoint", rootCmd.PersistentFlags().Lookup("algod-endpoint"))
-	_ = viper.BindPFlag("algod-token", rootCmd.PersistentFlags().Lookup("algod-token"))
+	_ = viper.BindPFlag("algod-endpoint", rootCmd.Flags().Lookup("algod-endpoint"))
+	_ = viper.BindPFlag("algod-token", rootCmd.Flags().Lookup("algod-token"))
 
 	// Update Long Text
 	rootCmd.Long +=
@@ -159,7 +161,10 @@ func init() {
 
 	// Add Commands
 	rootCmd.AddCommand(statusCmd)
-	rootCmd.AddCommand(node.NodeCmd)
+	if runtime.GOOS != "windows" {
+		rootCmd.AddCommand(node.Cmd)
+		rootCmd.AddCommand(configure.Cmd)
+	}
 }
 
 // Execute executes the root command.
diff --git a/internal/algod/algod.go b/internal/algod/algod.go
new file mode 100644
index 00000000..e19737f6
--- /dev/null
+++ b/internal/algod/algod.go
@@ -0,0 +1,154 @@
+package algod
+
+import (
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/linux"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/mac"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"os/exec"
+	"runtime"
+)
+
+const UnsupportedOSError = "unsupported operating system"
+
+// IsInstalled checks if the Algod software is installed on the system
+// by verifying its presence and service setup.
+func IsInstalled() bool {
+	//If algod is not in the path
+	if !system.CmdExists("algod") {
+		return false
+	}
+	// If the service is listed
+	switch runtime.GOOS {
+	case "linux":
+		return linux.IsService()
+	case "darwin":
+		return mac.IsService()
+	default:
+		fmt.Println("Unsupported operating system.")
+		return false
+	}
+}
+
+// IsRunning checks if the algod is currently running on the host operating system.
+// It returns true if the application is running, or false if it is not or if an error occurs.
+// This function supports Linux and macOS platforms. It returns an error for unsupported operating systems.
+func IsRunning() bool {
+	switch runtime.GOOS {
+	case "linux", "darwin":
+		fmt.Println("Checking if algod is running...")
+		err := exec.Command("pgrep", "algod").Run()
+		return err == nil
+	default:
+		return false
+	}
+}
+
+// IsService determines if the Algorand service is configured as
+// a system service on the current operating system.
+func IsService() bool {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.IsService()
+	case "darwin":
+		return mac.IsService()
+	default:
+		return false
+	}
+}
+
+// SetNetwork configures the network to the specified setting
+// or returns an error on unsupported operating systems.
+func SetNetwork(network string) error {
+	return fmt.Errorf(UnsupportedOSError)
+}
+
+// Install installs Algorand software based on the host OS
+// and returns an error if the installation fails or is unsupported.
+func Install() error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.Install()
+	case "darwin":
+		return mac.Install()
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// Update checks the operating system and performs an
+// upgrade using OS-specific package managers, if supported.
+func Update() error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.Upgrade()
+	case "darwin":
+		return mac.Upgrade()
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// Uninstall removes the Algorand software from the system based
+// on the host operating system using appropriate methods.
+func Uninstall() error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.Uninstall()
+	case "darwin":
+		return mac.Uninstall()
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// UpdateService updates the service configuration for the
+// Algorand daemon based on the OS and reloads the service.
+func UpdateService(dataDirectoryPath string) error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.UpdateService(dataDirectoryPath)
+	case "darwin":
+		return mac.UpdateService(dataDirectoryPath)
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// EnsureService ensures the `algod` service is configured and running
+// as a service based on the OS;
+// Returns an error for unsupported systems.
+func EnsureService() error {
+	switch runtime.GOOS {
+	case "darwin":
+		return mac.EnsureService()
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// Start attempts to initiate the Algod service based on the
+// host operating system. Returns an error for unsupported OS.
+func Start() error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.Start()
+	case "darwin":
+		return mac.Start()
+	default: // Unsupported OS
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
+
+// Stop shuts down the Algorand algod system process based on the current operating system.
+// Returns an error if the operation fails or the operating system is unsupported.
+func Stop() error {
+	switch runtime.GOOS {
+	case "linux":
+		return linux.Stop()
+	case "darwin":
+		return mac.Stop()
+	default:
+		return fmt.Errorf(UnsupportedOSError)
+	}
+}
diff --git a/internal/algod/fallback/algod.go b/internal/algod/fallback/algod.go
new file mode 100644
index 00000000..e0ff3dc7
--- /dev/null
+++ b/internal/algod/fallback/algod.go
@@ -0,0 +1,90 @@
+package fallback
+
+import (
+	"errors"
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"os"
+	"os/exec"
+	"syscall"
+)
+
+func isRunning() (bool, error) {
+	return false, errors.New("not implemented")
+}
+func Install() error {
+	return system.RunAll(system.CmdsList{
+		{"mkdir", "~/node"},
+		{"sh", "-c", "cd ~/node"},
+		{"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
+		{"chmod", "744", "update.sh"},
+		{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
+	})
+}
+
+func Start() error {
+	// Algod is not available as a systemd service, start it directly
+	fmt.Println("Starting algod directly...")
+
+	// Check if ALGORAND_DATA environment variable is set
+	fmt.Println("Checking if ALGORAND_DATA env var is set...")
+	algorandData := os.Getenv("ALGORAND_DATA")
+
+	if !utils.IsDataDir(algorandData) {
+		fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
+		os.Exit(1)
+	}
+
+	fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
+
+	cmd := exec.Command("algod")
+	cmd.SysProcAttr = &syscall.SysProcAttr{
+		Setsid: true,
+	}
+	err := cmd.Start()
+	if err != nil {
+		return fmt.Errorf("Failed to start algod: %v\n", err)
+	}
+	return nil
+}
+
+func Stop() error {
+	// Algod is not available as a systemd service, stop it directly
+	fmt.Println("Stopping algod directly...")
+	// Find the process ID of algod
+	pid, err := findAlgodPID()
+	if err != nil {
+		return fmt.Errorf("Failed to find algod process: %v\n", err)
+	}
+
+	// Send SIGTERM to the process
+	process, err := os.FindProcess(pid)
+	if err != nil {
+		return fmt.Errorf("Failed to find process with PID %d: %v\n", pid, err)
+	}
+
+	err = process.Signal(syscall.SIGTERM)
+	if err != nil {
+		return fmt.Errorf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
+	}
+
+	fmt.Println("Sent SIGTERM to algod process.")
+	return nil
+}
+
+func findAlgodPID() (int, error) {
+	cmd := exec.Command("pgrep", "algod")
+	output, err := cmd.Output()
+	if err != nil {
+		return 0, err
+	}
+
+	var pid int
+	_, err = fmt.Sscanf(string(output), "%d", &pid)
+	if err != nil {
+		return 0, fmt.Errorf("failed to parse PID: %v", err)
+	}
+
+	return pid, nil
+}
diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go
new file mode 100644
index 00000000..acd61804
--- /dev/null
+++ b/internal/algod/linux/linux.go
@@ -0,0 +1,198 @@
+package linux
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/fallback"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+	"text/template"
+)
+
+const PackageManagerNotFoundMsg = "could not find a package manager to uninstall Algorand"
+
+type Algod struct {
+	system.Interface
+	Path              string
+	DataDirectoryPath string
+}
+
+// Install installs Algorand development tools or node software depending on the package manager.
+func Install() error {
+	log.Println("Installing Algod on Linux")
+	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
+	if system.CmdExists("apt-get") { // On some Debian systems we use apt-get
+		log.Printf("Installing with apt-get")
+		return system.RunAll(system.CmdsList{
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "algorand-devtools"},
+		})
+	}
+
+	if system.CmdExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
+		log.Printf("Installing with dnf")
+		return system.RunAll(system.CmdsList{
+			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+			{"rpmkeys", "--import", "rpm_algorand.pub"},
+			{"dnf", "install", "-y", "dnf-command(config-manager)"},
+			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"dnf", "install", "-y", "algorand-devtools"},
+			{"systemctl", "enable", "algorand.service"},
+			{"systemctl", "start", "algorand.service"},
+			{"rm", "-f", "rpm_algorand.pub"},
+		})
+
+	}
+
+	// TODO: watch this method to see if it is ever used
+	return fallback.Install()
+}
+
+// Uninstall removes the Algorand software using a supported package manager or clears related system files if necessary.
+// Returns an error if a supported package manager is not found or if any command fails during execution.
+func Uninstall() error {
+	fmt.Println("Uninstalling Algorand")
+	var unInstallCmds system.CmdsList
+	// On Ubuntu and Debian there's the apt package manager
+	if system.CmdExists("apt-get") {
+		fmt.Println("Using apt-get package manager")
+		unInstallCmds = [][]string{
+			{"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"},
+		}
+	}
+	// On Fedora and CentOs8 there's the dnf package manager
+	if system.CmdExists("dnf") {
+		fmt.Println("Using dnf package manager")
+		unInstallCmds = [][]string{
+			{"dnf", "remove", "algorand-devtools", "algorand", "-y"},
+		}
+	}
+	// Error on unsupported package managers
+	if len(unInstallCmds) == 0 {
+		return fmt.Errorf(PackageManagerNotFoundMsg)
+	}
+
+	// Commands to clear systemd algorand.service and any other files, like the configuration override
+	unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "rm -rf /etc/systemd/system/algorand*"})
+	unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"})
+
+	return system.RunAll(unInstallCmds)
+}
+
+// Upgrade updates Algorand and its dev tools using an approved package
+// manager if available, otherwise returns an error.
+func Upgrade() error {
+	if system.CmdExists("apt-get") {
+		return system.RunAll(system.CmdsList{
+			{"apt-get", "update"},
+			{"apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"},
+		})
+	}
+	if system.CmdExists("dnf") {
+		return system.RunAll(system.CmdsList{
+			{"dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"},
+		})
+	}
+	return fmt.Errorf("the *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade")
+}
+
+// Start attempts to start the Algorand service using the system's service manager.
+// It executes the appropriate command for systemd on Linux-based systems.
+// Returns an error if the command fails.
+// TODO: Replace with D-Bus integration
+func Start() error {
+	return exec.Command("systemctl", "start", "algorand").Run()
+}
+
+// Stop shuts down the Algorand algod system process on Linux using the systemctl stop command.
+// Returns an error if the operation fails.
+// TODO: Replace with D-Bus integration
+func Stop() error {
+	return exec.Command("systemctl", "stop", "algorand").Run()
+}
+
+// IsService checks if the "algorand.service" is listed as a systemd unit file on Linux.
+// Returns true if it exists.
+// TODO: Replace with D-Bus integration
+func IsService() bool {
+	out, err := system.Run([]string{"systemctl", "list-unit-files", "algorand.service"})
+	if err != nil {
+		return false
+	}
+	return strings.Contains(out, "algorand.service")
+}
+
+// UpdateService updates the systemd service file for the Algorand daemon
+// with a new data directory path and reloads the daemon.
+func UpdateService(dataDirectoryPath string) error {
+
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Failed to find algod binary: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Path to the systemd service override file
+	// Assuming that this is the same everywhere systemd is used
+	overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
+
+	// Create the override directory if it doesn't exist
+	err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
+	if err != nil {
+		fmt.Printf("Failed to create override directory: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Content of the override file
+	const overrideTemplate = `[Unit]
+Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
+[Service]
+ExecStart=
+ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
+
+	// Data to fill the template
+	data := map[string]string{
+		"AlgodPath":         algodPath,
+		"DataDirectoryPath": dataDirectoryPath,
+	}
+
+	// Parse and execute the template
+	tmpl, err := template.New("override").Parse(overrideTemplate)
+	if err != nil {
+		fmt.Printf("Failed to parse template: %v\n", err)
+		os.Exit(1)
+	}
+
+	var overrideContent bytes.Buffer
+	err = tmpl.Execute(&overrideContent, data)
+	if err != nil {
+		fmt.Printf("Failed to execute template: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Write the override content to the file
+	err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write override file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Reload systemd manager configuration
+	cmd := exec.Command("systemctl", "daemon-reload")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to reload systemd daemon: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Algorand service file updated successfully.")
+
+	return nil
+}
diff --git a/internal/algod/mac/mac.go b/internal/algod/mac/mac.go
new file mode 100644
index 00000000..f26eb7cc
--- /dev/null
+++ b/internal/algod/mac/mac.go
@@ -0,0 +1,348 @@
+package mac
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/spf13/cobra"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"text/template"
+)
+
+// IsService check if Algorand service has been created with launchd (macOS)
+// Note that it needs to be run in super-user privilege mode to
+// be able to view the root level services.
+func IsService() bool {
+	_, err := system.Run([]string{"launchctl", "list", "com.algorand.algod"})
+	return err == nil
+}
+
+// Install sets up Algod on macOS using Homebrew,
+// configures necessary directories, and ensures it
+// runs as a background service.
+func Install() error {
+	fmt.Println("Installing Algod on macOS...")
+
+	// Homebrew is our package manager of choice
+	if !system.CmdExists("brew") {
+		return fmt.Errorf("could not find Homebrew installed. Please install Homebrew and try again")
+	}
+
+	err := system.RunAll(system.CmdsList{
+		{"brew", "tap", "algorandfoundation/homebrew-node"},
+		{"brew", "install", "algorand"},
+		{"brew", "--prefix", "algorand", "--installed"},
+	})
+	if err != nil {
+		return err
+	}
+
+	// Handle data directory and genesis.json file
+	handleDataDirMac()
+
+	path, err := os.Executable()
+	if err != nil {
+		return err
+	}
+	// Create and load the launchd service
+	_, err = system.Run([]string{"sudo", path, "configure", "service"})
+	if err != nil {
+		return fmt.Errorf("failed to create and load launchd service: %v\n", err)
+	}
+
+	// Ensure Homebrew bin directory is in the PATH
+	// So that brew installed algorand binaries can be found
+	ensureHomebrewPathInEnv()
+
+	if !IsService() {
+		return fmt.Errorf("algod unexpectedly NOT in path. Installation failed")
+	}
+
+	fmt.Println(`Installed Algorand (Algod) with Homebrew.
+Algod is running in the background as a system-level service.
+	`)
+
+	return nil
+}
+
+// Uninstall removes the Algorand application from the system using Homebrew if it is installed.
+func Uninstall() error {
+	if !system.CmdExists("brew") {
+		return errors.New("homebrew is not installed")
+	}
+	return exec.Command("brew", "uninstall", "algorand").Run()
+}
+
+// Upgrade updates the installed Algorand package using Homebrew if it's available and properly configured.
+func Upgrade() error {
+	if !system.CmdExists("brew") {
+		return errors.New("homebrew is not installed")
+	}
+	// Check if algorand is installed with Homebrew
+	checkCmdArgs := system.CmdsList{{"brew", "--prefix", "algorand", "--installed"}}
+	if system.IsSudo() {
+		checkCmdArgs = checkCmdArgs.Su(os.Getenv("SUDO_USER"))
+	}
+	err := system.RunAll(checkCmdArgs)
+	if err != nil {
+		return err
+	}
+	// Upgrade algorand
+	upgradeCmdArgs := system.CmdsList{{"brew", "upgrade", "algorand", "--formula"}}
+	if system.IsSudo() {
+		upgradeCmdArgs = upgradeCmdArgs.Su(os.Getenv("SUDO_USER"))
+	}
+	return system.RunAll(upgradeCmdArgs)
+}
+
+// Start algorand with launchd
+func Start() error {
+	return exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist").Run()
+}
+
+// Stop shuts down the Algorand algod system process using the launchctl bootout command.
+// Returns an error if the operation fails.
+func Stop() error {
+	return exec.Command("launchctl", "bootout", "system/com.algorand.algod").Run()
+}
+
+// UpdateService updates the Algorand launchd service with
+// a new data directory path and reloads the service configuration.
+func UpdateService(dataDirectoryPath string) error {
+
+	algodPath, err := exec.LookPath("algod")
+	if err != nil {
+		fmt.Printf("Failed to find algod binary: %v\n", err)
+		os.Exit(1)
+	}
+
+	overwriteFilePath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+
+	overwriteTemplate := `<?xml version="1.0" encoding="UTF-8"?>
+	<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+	<plist version="1.0">
+	<dict>
+					<key>Label</key>
+					<string>com.algorand.algod</string>
+					<key>ProgramArguments</key>
+					<array>
+													<string>{{.AlgodPath}}</string>
+													<string>-d</string>
+													<string>{{.DataDirectoryPath}}</string>
+					</array>
+					<key>RunAtLoad</key>
+					<true/>
+					<key>KeepAlive</key>
+					<true/>
+					<key>StandardOutPath</key>
+					<string>/tmp/algod.out</string>
+					<key>StandardErrorPath</key>
+					<string>/tmp/algod.err</string>
+	</dict>
+	</plist>`
+
+	// Data to fill the template
+	data := map[string]string{
+		"AlgodPath":         algodPath,
+		"DataDirectoryPath": dataDirectoryPath,
+	}
+
+	// Parse and execute the template
+	tmpl, err := template.New("override").Parse(overwriteTemplate)
+	if err != nil {
+		fmt.Printf("Failed to parse template: %v\n", err)
+		os.Exit(1)
+	}
+
+	var overwriteContent bytes.Buffer
+	err = tmpl.Execute(&overwriteContent, data)
+	if err != nil {
+		fmt.Printf("Failed to execute template: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Write the override content to the file
+	err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write override file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Boot out the launchd service (just in case - it should be off)
+	cmd := exec.Command("launchctl", "bootout", "system", overwriteFilePath)
+	err = cmd.Run()
+	if err != nil {
+		if !strings.Contains(err.Error(), "No such process") {
+			fmt.Printf("Failed to bootout launchd service: %v\n", err)
+			os.Exit(1)
+		}
+	}
+
+	// Load the launchd service
+	cmd = exec.Command("launchctl", "load", overwriteFilePath)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to load launchd service: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Launchd service updated and reloaded successfully.")
+	return nil
+}
+
+func handleDataDirMac() {
+	// Ensure the ~/.algorand directory exists
+	algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand")
+	if err := os.MkdirAll(algorandDir, 0755); err != nil {
+		fmt.Printf("Failed to create directory %s: %v\n", algorandDir, err)
+		os.Exit(1)
+	}
+
+	// Check if genesis.json file exists in ~/.algorand
+	genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json")
+	if _, err := os.Stat(genesisFilePath); os.IsNotExist(err) {
+		fmt.Println("genesis.json file does not exist. Downloading...")
+
+		// Download the genesis.json file
+		resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
+		if err != nil {
+			fmt.Printf("Failed to download genesis.json: %v\n", err)
+			cobra.CheckErr(err)
+		}
+		defer resp.Body.Close()
+
+		// Create the file
+		out, err := os.Create(genesisFilePath)
+		if err != nil {
+			fmt.Printf("Failed to create genesis.json file: %v\n", err)
+			cobra.CheckErr(err)
+		}
+		defer out.Close()
+
+		// Write the content to the file
+		_, err = io.Copy(out, resp.Body)
+		if err != nil {
+			fmt.Printf("Failed to save genesis.json file: %v\n", err)
+			cobra.CheckErr(err)
+		}
+
+		fmt.Println("mainnet genesis.json file downloaded successfully.")
+	}
+
+}
+
+func EnsureService() error {
+	// Get the prefix path for Algorand
+	cmd := exec.Command("brew", "--prefix", "algorand")
+	algorandPrefix, err := cmd.Output()
+	if err != nil {
+		fmt.Printf("Failed to get Algorand prefix: %v\n", err)
+		cobra.CheckErr(err)
+	}
+	algorandPrefixPath := strings.TrimSpace(string(algorandPrefix))
+
+	// Define the launchd plist content
+	plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>Label</key>
+	<string>com.algorand.algod</string>
+	<key>ProgramArguments</key>
+	<array>
+			<string>%s/bin/algod</string>
+			<string>-d</string>
+			<string>%s/.algorand</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>KeepAlive</key>
+	<true/>
+	<key>StandardOutPath</key>
+	<string>/tmp/algod.out</string>
+	<key>StandardErrorPath</key>
+	<string>/tmp/algod.err</string>
+</dict>
+</plist>`, algorandPrefixPath, os.Getenv("HOME"))
+
+	// Write the plist content to a file
+	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+	err = os.MkdirAll(filepath.Dir(plistPath), 0755)
+	if err != nil {
+		fmt.Printf("Failed to create LaunchDaemons directory: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	err = os.WriteFile(plistPath, []byte(plistContent), 0644)
+	if err != nil {
+		fmt.Printf("Failed to write plist file: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	// Load the launchd service
+	cmd = exec.Command("launchctl", "load", plistPath)
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("Failed to load launchd service: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	// Check if the service is running
+	cmd = exec.Command("launchctl", "list", "com.algorand.algod")
+	err = cmd.Run()
+	if err != nil {
+		fmt.Printf("launchd service is not running: %v\n", err)
+		cobra.CheckErr(err)
+	}
+
+	fmt.Println("Launchd service created and loaded successfully.")
+
+	return nil
+}
+
+// Ensure that Homebrew bin directory is in the PATH so that Algorand binaries can be found
+func ensureHomebrewPathInEnv() {
+	homebrewPrefix := os.Getenv("HOMEBREW_PREFIX")
+	homebrewCellar := os.Getenv("HOMEBREW_CELLAR")
+	homebrewRepository := os.Getenv("HOMEBREW_REPOSITORY")
+
+	if homebrewPrefix == "" || homebrewCellar == "" || homebrewRepository == "" {
+		fmt.Println("Homebrew environment variables are not set. Running brew shellenv...")
+
+		cmd := exec.Command("brew", "shellenv")
+		output, err := cmd.Output()
+		if err != nil {
+			fmt.Printf("Failed to get Homebrew environment: %v\n", err)
+			return
+		}
+
+		envVars := strings.Split(string(output), "\n")
+		for _, envVar := range envVars {
+			if envVar != "" {
+				fmt.Println("Setting environment variable:", envVar)
+				os.Setenv(strings.Split(envVar, "=")[0], strings.Trim(strings.Split(envVar, "=")[1], `"`))
+			}
+		}
+
+		// Append brew shellenv output to .zshrc
+		zshrcPath := filepath.Join(os.Getenv("HOME"), ".zshrc")
+		f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_WRONLY, 0644)
+		if err != nil {
+			fmt.Printf("Failed to open .zshrc: %v\n", err)
+			fmt.Printf("Are you running a terminal other than zsh?")
+			fmt.Printf("Please run brew shellenv and add the output to your shell's configuration file.")
+			return
+		}
+		defer f.Close()
+
+		if _, err := f.WriteString("\n# Inserted by Algorun\n# Homebrew environment variables\n" + string(output)); err != nil {
+			fmt.Printf("Failed to write to .zshrc: %v\n", err)
+		}
+	}
+}
diff --git a/internal/algod/utils/utils.go b/internal/algod/utils/utils.go
new file mode 100644
index 00000000..a32de05d
--- /dev/null
+++ b/internal/algod/utils/utils.go
@@ -0,0 +1,69 @@
+package utils
+
+import (
+	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/spf13/cobra"
+	"os"
+	"path/filepath"
+)
+
+func IsDataDir(path string) bool {
+	info, err := os.Stat(path)
+
+	// Check if the path exists
+	if os.IsNotExist(err) {
+		return false
+	}
+
+	// Check if the path is a directory
+	if !info.IsDir() {
+		return false
+	}
+
+	paths := system.FindPathToFile(path, "algod.token")
+	if len(paths) == 1 {
+		return true
+	}
+	return false
+}
+
+func GetKnownPaths() []string {
+	// Hardcoded paths known to be common Algorand data directories
+	binPaths := []string{
+		"/opt/homebrew/bin/algod",
+		"/opt/homebrew/bin/algod",
+	}
+
+	var paths []string
+
+	for _, path := range binPaths {
+		if IsDataDir(path) {
+			paths = append(paths, path)
+		}
+	}
+
+	return paths
+}
+
+// GetKnownDataPaths Does a lazy check for Algorand data directories, based off of known common paths
+func GetKnownDataPaths() []string {
+	home, err := os.UserHomeDir()
+	cobra.CheckErr(err)
+
+	// Hardcoded paths known to be common Algorand data directories
+	commonAlgorandDataDirPaths := []string{
+		"/var/lib/algorand",
+		filepath.Join(home, "node", "data"),
+		filepath.Join(home, ".algorand"),
+	}
+
+	var paths []string
+
+	for _, path := range commonAlgorandDataDirPaths {
+		if IsDataDir(path) {
+			paths = append(paths, path)
+		}
+	}
+
+	return paths
+}
diff --git a/internal/system/cmds.go b/internal/system/cmds.go
new file mode 100644
index 00000000..02f5c633
--- /dev/null
+++ b/internal/system/cmds.go
@@ -0,0 +1,106 @@
+package system
+
+import (
+	"fmt"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"sync"
+)
+
+func IsSudo() bool {
+	return os.Geteuid() == 0
+}
+
+// CmdExists checks that a bash cli/cmd tool exists
+func CmdExists(tool string) bool {
+	_, err := exec.LookPath(tool)
+	return err == nil
+}
+
+type CmdsList [][]string
+
+func (l CmdsList) Su(user string) CmdsList {
+	for i, args := range l {
+		if !strings.HasPrefix(args[0], "sudo") {
+			l[i] = append([]string{"sudo", "-u", user}, args...)
+		}
+	}
+	return l
+}
+
+func Run(args []string) (string, error) {
+	cmd := exec.Command(args[0], args[1:]...)
+	output, err := cmd.CombinedOutput()
+	return string(output), err
+}
+
+func RunAll(list CmdsList) error {
+	// Run each installation command
+	for _, args := range list {
+		cmd := exec.Command(args[0], args[1:]...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			return fmt.Errorf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(args, " "), output, err)
+		}
+		fmt.Printf("%s: %s\n", style.Green.Render("Running"), strings.Join(args, " "))
+	}
+	return nil
+}
+
+// FindPathToFile finds path(s) to a file in a directory and its subdirectories using parallel processing
+func FindPathToFile(startDir string, targetFileName string) []string {
+	var dirPaths []string
+	var mu sync.Mutex
+	var wg sync.WaitGroup
+
+	fileChan := make(chan string)
+
+	// Worker function to process files
+	worker := func() {
+		defer wg.Done()
+		for path := range fileChan {
+			info, err := os.Stat(path)
+			if err != nil {
+				continue
+			}
+			if !info.IsDir() && info.Name() == targetFileName {
+				dirPath := filepath.Dir(path)
+				mu.Lock()
+				dirPaths = append(dirPaths, dirPath)
+				mu.Unlock()
+			}
+		}
+	}
+
+	// Start worker goroutines
+	numWorkers := 4 // Adjust the number of workers based on your system's capabilities
+	for i := 0; i < numWorkers; i++ {
+		wg.Add(1)
+		go worker()
+	}
+
+	// Walk the directory tree and send file paths to the channel
+	err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			// Ignore permission errors
+			if os.IsPermission(err) {
+				return nil
+			}
+			return err
+		}
+		fileChan <- path
+		return nil
+	})
+
+	close(fileChan)
+	wg.Wait()
+
+	if err != nil {
+		panic(err)
+	}
+
+	return dirPaths
+}
diff --git a/internal/system/service.go b/internal/system/service.go
new file mode 100644
index 00000000..7a573f02
--- /dev/null
+++ b/internal/system/service.go
@@ -0,0 +1,16 @@
+package system
+
+type Interface interface {
+	IsInstalled() bool
+	IsRunning() bool
+	IsService() bool
+	SetNetwork(network string) error
+	Install() error
+	Update() error
+	Uninstall() error
+	Start() error
+	Stop() error
+	Restart() error
+	UpdateService(dataDirectoryPath string) error
+	EnsureService() error
+}

From 8414f718e8a59a11b52fab22f3debc8b0042b821 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Wed, 11 Dec 2024 19:44:55 -0500
Subject: [PATCH 18/23] feat(node): charmbracelet logger

---
 cmd/configure/configure.go       |  10 +-
 cmd/node/debug.go                |  39 ++++-
 cmd/node/install.go              |  38 ++++-
 cmd/node/node.go                 |  27 ++--
 cmd/node/start.go                |  27 +++-
 cmd/node/stop.go                 |  23 ++-
 cmd/node/uninstall.go            |  31 +++-
 cmd/node/upgrade.go              |  10 +-
 internal/algod/algod.go          |  31 +---
 internal/algod/fallback/algod.go |  34 ++--
 internal/algod/linux/linux.go    |  15 +-
 internal/algod/mac/mac.go        | 260 ++++++++++++++-----------------
 internal/algod/msgs/errors.go    |   5 +
 internal/system/cmds.go          |  15 +-
 main.go                          |  13 ++
 ui/app/app_test.go               |   4 +-
 16 files changed, 331 insertions(+), 251 deletions(-)
 create mode 100644 internal/algod/msgs/errors.go

diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go
index 5b5d6336..3669e7b6 100644
--- a/cmd/configure/configure.go
+++ b/cmd/configure/configure.go
@@ -16,11 +16,11 @@ import (
 )
 
 var Cmd = &cobra.Command{
-	Use:               "configure",
-	Short:             "Configure Algod",
-	Long:              "Configure Algod settings",
-	SilenceUsage:      true,
-	PersistentPreRunE: node.NeedsToBeStopped,
+	Use:              "configure",
+	Short:            "Configure Algod",
+	Long:             "Configure Algod settings",
+	SilenceUsage:     true,
+	PersistentPreRun: node.NeedsToBeStopped,
 	//RunE: func(cmd *cobra.Command, args []string) error {
 	//	return configureNode()
 	//},
diff --git a/cmd/node/debug.go b/cmd/node/debug.go
index 80f36409..eeb77754 100644
--- a/cmd/node/debug.go
+++ b/cmd/node/debug.go
@@ -1,25 +1,54 @@
 package node
 
 import (
+	"encoding/json"
 	"fmt"
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
+	"os/exec"
 )
 
+type DebugInfo struct {
+	InPath      bool     `json:"inPath"`
+	IsRunning   bool     `json:"isRunning"`
+	IsService   bool     `json:"isService"`
+	IsInstalled bool     `json:"isInstalled"`
+	Algod       string   `json:"algod"`
+	Data        []string `json:"data"`
+}
+
 var debugCmd = &cobra.Command{
 	Use:          "debug",
 	Short:        "Display debug information for developers",
 	Long:         "Prints debug data to be copy and pasted to a bug report.",
 	SilenceUsage: true,
 	RunE: func(cmd *cobra.Command, args []string) error {
+		log.Info("Collecting debug information...")
+
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(SudoWarningMsg))
+
 		paths := utils.GetKnownDataPaths()
-		fmt.Printf("Algod in PATH: %v\n", system.CmdExists("algod"))
-		fmt.Printf("Algod is installed: %v\n", algod.IsInstalled())
-		fmt.Printf("Algod is running: %v\n", algod.IsRunning())
-		fmt.Printf("Algod is service: %v\n", algod.IsService())
-		fmt.Printf("Algod paths: %+v\n", paths)
+		path, _ := exec.LookPath("algod")
+		info := DebugInfo{
+			InPath:      system.CmdExists("algod"),
+			IsRunning:   algod.IsRunning(),
+			IsService:   algod.IsService(),
+			IsInstalled: algod.IsInstalled(),
+			Algod:       path,
+			Data:        paths,
+		}
+		data, err := json.MarshalIndent(info, "", " ")
+		if err != nil {
+			return err
+		}
+
+		log.Info(style.Blue.Render("Copy and paste the following to a bug report:"))
+		fmt.Println(style.Bold(string(data)))
 		return nil
 	},
 }
diff --git a/cmd/node/install.go b/cmd/node/install.go
index 051631c1..e66c0217 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -1,23 +1,45 @@
 package node
 
 import (
-	"fmt"
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
+	"os"
 )
 
+const InstallMsg = "Installing Algorand"
 const InstallExistsMsg = "algod is already installed"
 
 var installCmd = &cobra.Command{
 	Use:          "install",
-	Short:        "Install Algorand node (Algod)",
-	Long:         "Install Algorand node (Algod) and other binaries on your system",
+	Short:        "Install the algorand daemon",
+	Long:         style.Purple(style.BANNER) + "\n" + style.LightBlue("Install the algorand daemon on your local machine"),
 	SilenceUsage: true,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		fmt.Println("Checking if Algod is installed...")
-		if algod.IsInstalled() {
-			return fmt.Errorf(InstallExistsMsg)
+	Run: func(cmd *cobra.Command, args []string) {
+		// TODO: yes flag
+
+		// TODO: get expected version
+		log.Info(style.Green.Render(InstallMsg + " vX.X.X"))
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(SudoWarningMsg))
+
+		// TODO: compare expected version to existing version
+		if algod.IsInstalled() && !force {
+			log.Error(InstallExistsMsg)
+			os.Exit(1)
+		}
+
+		// Run the installation
+		err := algod.Install()
+		if err != nil {
+			log.Error(err)
+			os.Exit(1)
 		}
-		return algod.Install()
+		log.Info(style.Green.Render("Algorand installed successfully 🎉"))
 	},
 }
+
+func init() {
+	installCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully install the node"))
+}
diff --git a/cmd/node/node.go b/cmd/node/node.go
index 4741c2ea..96eb36e3 100644
--- a/cmd/node/node.go
+++ b/cmd/node/node.go
@@ -2,24 +2,29 @@ package node
 
 import (
 	"errors"
-	"fmt"
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"os"
 	"runtime"
 )
 
+const SudoWarningMsg = "(You may be prompted for your password)"
 const PermissionErrorMsg = "this command must be run with super-user privileges (sudo)"
 const NotInstalledErrorMsg = "algod is not installed. please run the *node install* command"
 const RunningErrorMsg = "algod is running, please run the *node stop* command"
 const NotRunningErrorMsg = "algod is not running"
 
+var (
+	force bool = false
+)
 var Cmd = &cobra.Command{
 	Use:   "node",
 	Short: "Node Management",
 	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"),
 	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+
 		// Check that we are calling with sudo on linux
 		if os.Geteuid() != 0 && runtime.GOOS == "linux" {
 			return errors.New(PermissionErrorMsg)
@@ -28,24 +33,28 @@ var Cmd = &cobra.Command{
 	},
 }
 
-func NeedsToBeRunning(cmd *cobra.Command, args []string) error {
+func NeedsToBeRunning(cmd *cobra.Command, args []string) {
+	if force {
+		return
+	}
 	if !algod.IsInstalled() {
-		return fmt.Errorf(NotInstalledErrorMsg)
+		log.Fatal(NotInstalledErrorMsg)
 	}
 	if !algod.IsRunning() {
-		return fmt.Errorf(NotRunningErrorMsg)
+		log.Fatal(NotRunningErrorMsg)
 	}
-	return nil
 }
 
-func NeedsToBeStopped(cmd *cobra.Command, args []string) error {
+func NeedsToBeStopped(cmd *cobra.Command, args []string) {
+	if force {
+		return
+	}
 	if !algod.IsInstalled() {
-		return fmt.Errorf(NotInstalledErrorMsg)
+		log.Fatal(NotInstalledErrorMsg)
 	}
 	if algod.IsRunning() {
-		return fmt.Errorf(RunningErrorMsg)
+		log.Fatal(RunningErrorMsg)
 	}
-	return nil
 }
 
 func init() {
diff --git a/cmd/node/start.go b/cmd/node/start.go
index 0691e615..43cfb444 100644
--- a/cmd/node/start.go
+++ b/cmd/node/start.go
@@ -2,16 +2,29 @@ package node
 
 import (
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 )
 
 var startCmd = &cobra.Command{
-	Use:               "start",
-	Short:             "Start Algod",
-	Long:              "Start Algod on your system (the one on your PATH).",
-	SilenceUsage:      true,
-	PersistentPreRunE: NeedsToBeStopped,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		return algod.Start()
+	Use:              "start",
+	Short:            "Start Algod",
+	Long:             "Start Algod on your system (the one on your PATH).",
+	SilenceUsage:     true,
+	PersistentPreRun: NeedsToBeStopped,
+	Run: func(cmd *cobra.Command, args []string) {
+		log.Info(style.Green.Render("Starting Algod 🚀"))
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(SudoWarningMsg))
+		err := algod.Start()
+		if err != nil {
+			log.Fatal(err)
+		}
+		log.Info(style.Green.Render("Algorand started successfully 🎉"))
 	},
 }
+
+func init() {
+	startCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully start the node"))
+}
diff --git a/cmd/node/stop.go b/cmd/node/stop.go
index 02a6a5c0..878c3c37 100644
--- a/cmd/node/stop.go
+++ b/cmd/node/stop.go
@@ -3,6 +3,8 @@ package node
 import (
 	"fmt"
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"time"
 
 	"github.com/spf13/cobra"
@@ -13,13 +15,16 @@ const StopSuccessMsg = "Algod stopped successfully"
 const StopFailureMsg = "failed to stop Algod"
 
 var stopCmd = &cobra.Command{
-	Use:               "stop",
-	Short:             "Stop Algod",
-	Long:              "Stop the Algod process on your system.",
-	SilenceUsage:      true,
-	PersistentPreRunE: NeedsToBeRunning,
+	Use:              "stop",
+	Short:            "Stop Algod",
+	Long:             "Stop the Algod process on your system.",
+	SilenceUsage:     true,
+	PersistentPreRun: NeedsToBeRunning,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		fmt.Println("Stopping Algod...")
+		log.Info(style.Green.Render("Stopping Algod 😢"))
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(SudoWarningMsg))
+
 		err := algod.Stop()
 		if err != nil {
 			return fmt.Errorf(StopFailureMsg)
@@ -30,7 +35,11 @@ var stopCmd = &cobra.Command{
 			return fmt.Errorf(StopFailureMsg)
 		}
 
-		fmt.Println(StopSuccessMsg)
+		log.Info(style.Green.Render("Algorand stopped successfully 🎉"))
 		return nil
 	},
 }
+
+func init() {
+	stopCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully stop the node"))
+}
diff --git a/cmd/node/uninstall.go b/cmd/node/uninstall.go
index 9a60b86f..350c5ee3 100644
--- a/cmd/node/uninstall.go
+++ b/cmd/node/uninstall.go
@@ -2,16 +2,33 @@ package node
 
 import (
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 )
 
+const UninstallWarningMsg = "(You may be prompted for your password to uninstall)"
+
 var uninstallCmd = &cobra.Command{
-	Use:               "uninstall",
-	Short:             "Uninstall Algorand node (Algod)",
-	Long:              "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
-	SilenceUsage:      true,
-	PersistentPreRunE: NeedsToBeStopped,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		return algod.Uninstall()
+	Use:              "uninstall",
+	Short:            "Uninstall Algorand node (Algod)",
+	Long:             "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
+	SilenceUsage:     true,
+	PersistentPreRun: NeedsToBeStopped,
+	Run: func(cmd *cobra.Command, args []string) {
+		if force {
+			log.Warn(style.Red.Render("Uninstalling Algorand (forcefully)"))
+		}
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(UninstallWarningMsg))
+
+		err := algod.Uninstall(force)
+		if err != nil {
+			log.Fatal(err)
+		}
 	},
 }
+
+func init() {
+	uninstallCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully uninstall the node"))
+}
diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go
index ee675686..5b8cce5e 100644
--- a/cmd/node/upgrade.go
+++ b/cmd/node/upgrade.go
@@ -6,11 +6,11 @@ import (
 )
 
 var upgradeCmd = &cobra.Command{
-	Use:               "upgrade",
-	Short:             "Upgrade Algod",
-	Long:              "Upgrade Algod (if installed with package manager).",
-	SilenceUsage:      true,
-	PersistentPreRunE: NeedsToBeStopped,
+	Use:              "upgrade",
+	Short:            "Upgrade Algod",
+	Long:             "Upgrade Algod (if installed with package manager).",
+	SilenceUsage:     true,
+	PersistentPreRun: NeedsToBeStopped,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		// TODO: Check Version from S3 against the local binary
 		return algod.Update()
diff --git a/internal/algod/algod.go b/internal/algod/algod.go
index e19737f6..34ad1537 100644
--- a/internal/algod/algod.go
+++ b/internal/algod/algod.go
@@ -5,7 +5,6 @@ import (
 	"github.com/algorandfoundation/algorun-tui/internal/algod/linux"
 	"github.com/algorandfoundation/algorun-tui/internal/algod/mac"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
-	"os/exec"
 	"runtime"
 )
 
@@ -14,20 +13,7 @@ const UnsupportedOSError = "unsupported operating system"
 // IsInstalled checks if the Algod software is installed on the system
 // by verifying its presence and service setup.
 func IsInstalled() bool {
-	//If algod is not in the path
-	if !system.CmdExists("algod") {
-		return false
-	}
-	// If the service is listed
-	switch runtime.GOOS {
-	case "linux":
-		return linux.IsService()
-	case "darwin":
-		return mac.IsService()
-	default:
-		fmt.Println("Unsupported operating system.")
-		return false
-	}
+	return system.CmdExists("algod")
 }
 
 // IsRunning checks if the algod is currently running on the host operating system.
@@ -36,9 +22,8 @@ func IsInstalled() bool {
 func IsRunning() bool {
 	switch runtime.GOOS {
 	case "linux", "darwin":
-		fmt.Println("Checking if algod is running...")
-		err := exec.Command("pgrep", "algod").Run()
-		return err == nil
+		return system.IsCmdRunning("algod")
+
 	default:
 		return false
 	}
@@ -83,7 +68,7 @@ func Update() error {
 	case "linux":
 		return linux.Upgrade()
 	case "darwin":
-		return mac.Upgrade()
+		return mac.Upgrade(false)
 	default:
 		return fmt.Errorf(UnsupportedOSError)
 	}
@@ -91,12 +76,12 @@ func Update() error {
 
 // Uninstall removes the Algorand software from the system based
 // on the host operating system using appropriate methods.
-func Uninstall() error {
+func Uninstall(force bool) error {
 	switch runtime.GOOS {
 	case "linux":
 		return linux.Uninstall()
 	case "darwin":
-		return mac.Uninstall()
+		return mac.Uninstall(force)
 	default:
 		return fmt.Errorf(UnsupportedOSError)
 	}
@@ -134,7 +119,7 @@ func Start() error {
 	case "linux":
 		return linux.Start()
 	case "darwin":
-		return mac.Start()
+		return mac.Start(false)
 	default: // Unsupported OS
 		return fmt.Errorf(UnsupportedOSError)
 	}
@@ -147,7 +132,7 @@ func Stop() error {
 	case "linux":
 		return linux.Stop()
 	case "darwin":
-		return mac.Stop()
+		return mac.Stop(false)
 	default:
 		return fmt.Errorf(UnsupportedOSError)
 	}
diff --git a/internal/algod/fallback/algod.go b/internal/algod/fallback/algod.go
index e0ff3dc7..c83c4454 100644
--- a/internal/algod/fallback/algod.go
+++ b/internal/algod/fallback/algod.go
@@ -3,16 +3,17 @@ package fallback
 import (
 	"errors"
 	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/msgs"
 	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/charmbracelet/log"
 	"os"
 	"os/exec"
 	"syscall"
 )
 
-func isRunning() (bool, error) {
-	return false, errors.New("not implemented")
-}
+// Install executes a series of commands to set up the Algorand node and development tools on a Unix environment.
+// TODO: Allow for changing of the paths
 func Install() error {
 	return system.RunAll(system.CmdsList{
 		{"mkdir", "~/node"},
@@ -21,28 +22,28 @@ func Install() error {
 		{"chmod", "744", "update.sh"},
 		{"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
 	})
+
 }
 
 func Start() error {
-	// Algod is not available as a systemd service, start it directly
-	fmt.Println("Starting algod directly...")
+	path, err := exec.LookPath("algod")
+	log.Debug("Starting algod", "path", path)
 
 	// Check if ALGORAND_DATA environment variable is set
-	fmt.Println("Checking if ALGORAND_DATA env var is set...")
+	log.Info("Checking if ALGORAND_DATA env var is set...")
 	algorandData := os.Getenv("ALGORAND_DATA")
 
 	if !utils.IsDataDir(algorandData) {
-		fmt.Println("ALGORAND_DATA environment variable is not set or is invalid. Please run node configure and follow the instructions.")
-		os.Exit(1)
+		return errors.New(msgs.InvalidDataDirectory)
 	}
 
-	fmt.Println("ALGORAND_DATA env var set to valid directory: " + algorandData)
+	log.Info("ALGORAND_DATA env var set to valid directory: " + algorandData)
 
 	cmd := exec.Command("algod")
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Setsid: true,
 	}
-	err := cmd.Start()
+	err = cmd.Start()
 	if err != nil {
 		return fmt.Errorf("Failed to start algod: %v\n", err)
 	}
@@ -50,30 +51,29 @@ func Start() error {
 }
 
 func Stop() error {
-	// Algod is not available as a systemd service, stop it directly
-	fmt.Println("Stopping algod directly...")
+	log.Debug("Manually shutting down algod")
 	// Find the process ID of algod
 	pid, err := findAlgodPID()
 	if err != nil {
-		return fmt.Errorf("Failed to find algod process: %v\n", err)
+		return err
 	}
 
 	// Send SIGTERM to the process
 	process, err := os.FindProcess(pid)
 	if err != nil {
-		return fmt.Errorf("Failed to find process with PID %d: %v\n", pid, err)
+		return err
 	}
 
 	err = process.Signal(syscall.SIGTERM)
 	if err != nil {
-		return fmt.Errorf("Failed to send SIGTERM to process with PID %d: %v\n", pid, err)
+		return err
 	}
 
-	fmt.Println("Sent SIGTERM to algod process.")
 	return nil
 }
 
 func findAlgodPID() (int, error) {
+	log.Debug("Scanning for algod process")
 	cmd := exec.Command("pgrep", "algod")
 	output, err := cmd.Output()
 	if err != nil {
@@ -83,7 +83,7 @@ func findAlgodPID() (int, error) {
 	var pid int
 	_, err = fmt.Sscanf(string(output), "%d", &pid)
 	if err != nil {
-		return 0, fmt.Errorf("failed to parse PID: %v", err)
+		return 0, err
 	}
 
 	return pid, nil
diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go
index acd61804..83ceed9c 100644
--- a/internal/algod/linux/linux.go
+++ b/internal/algod/linux/linux.go
@@ -5,7 +5,8 @@ import (
 	"fmt"
 	"github.com/algorandfoundation/algorun-tui/internal/algod/fallback"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
-	"log"
+	"github.com/charmbracelet/log"
+
 	"os"
 	"os/exec"
 	"strings"
@@ -22,10 +23,10 @@ type Algod struct {
 
 // Install installs Algorand development tools or node software depending on the package manager.
 func Install() error {
-	log.Println("Installing Algod on Linux")
+	log.Info("Installing Algod on Linux")
 	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
 	if system.CmdExists("apt-get") { // On some Debian systems we use apt-get
-		log.Printf("Installing with apt-get")
+		log.Info("Installing with apt-get")
 		return system.RunAll(system.CmdsList{
 			{"apt-get", "update"},
 			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
@@ -58,18 +59,18 @@ func Install() error {
 // Uninstall removes the Algorand software using a supported package manager or clears related system files if necessary.
 // Returns an error if a supported package manager is not found or if any command fails during execution.
 func Uninstall() error {
-	fmt.Println("Uninstalling Algorand")
+	log.Info("Uninstalling Algorand")
 	var unInstallCmds system.CmdsList
 	// On Ubuntu and Debian there's the apt package manager
 	if system.CmdExists("apt-get") {
-		fmt.Println("Using apt-get package manager")
+		log.Info("Using apt-get package manager")
 		unInstallCmds = [][]string{
 			{"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"},
 		}
 	}
 	// On Fedora and CentOs8 there's the dnf package manager
 	if system.CmdExists("dnf") {
-		fmt.Println("Using dnf package manager")
+		log.Info("Using dnf package manager")
 		unInstallCmds = [][]string{
 			{"dnf", "remove", "algorand-devtools", "algorand", "-y"},
 		}
@@ -192,7 +193,7 @@ ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
 		os.Exit(1)
 	}
 
-	fmt.Println("Algorand service file updated successfully.")
+	log.Info("Algorand service file updated successfully.")
 
 	return nil
 }
diff --git a/internal/algod/mac/mac.go b/internal/algod/mac/mac.go
index f26eb7cc..8afeeeb7 100644
--- a/internal/algod/mac/mac.go
+++ b/internal/algod/mac/mac.go
@@ -4,7 +4,9 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
+	"github.com/algorandfoundation/algorun-tui/internal/algod/utils"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"io"
 	"net/http"
@@ -15,11 +17,14 @@ import (
 	"text/template"
 )
 
+const MustBeServiceMsg = "service must be installed to be able to manage it"
+const HomeBrewNotFoundMsg = "homebrew is not installed. please install Homebrew and try again"
+
 // IsService check if Algorand service has been created with launchd (macOS)
 // Note that it needs to be run in super-user privilege mode to
 // be able to view the root level services.
 func IsService() bool {
-	_, err := system.Run([]string{"launchctl", "list", "com.algorand.algod"})
+	_, err := system.Run([]string{"sudo", "launchctl", "list", "com.algorand.algod"})
 	return err == nil
 }
 
@@ -27,11 +32,11 @@ func IsService() bool {
 // configures necessary directories, and ensures it
 // runs as a background service.
 func Install() error {
-	fmt.Println("Installing Algod on macOS...")
+	log.Info("Installing Algod on macOS...")
 
 	// Homebrew is our package manager of choice
 	if !system.CmdExists("brew") {
-		return fmt.Errorf("could not find Homebrew installed. Please install Homebrew and try again")
+		return errors.New(HomeBrewNotFoundMsg)
 	}
 
 	err := system.RunAll(system.CmdsList{
@@ -44,81 +49,105 @@ func Install() error {
 	}
 
 	// Handle data directory and genesis.json file
-	handleDataDirMac()
+	err = handleDataDirMac()
+	if err != nil {
+		return err
+	}
 
 	path, err := os.Executable()
 	if err != nil {
 		return err
 	}
+
 	// Create and load the launchd service
-	_, err = system.Run([]string{"sudo", path, "configure", "service"})
+	// TODO: find a clever way to avoid this or make sudo persist for the second call
+	err = system.RunAll(system.CmdsList{{"sudo", path, "configure", "service"}})
 	if err != nil {
-		return fmt.Errorf("failed to create and load launchd service: %v\n", err)
+		return err
 	}
 
-	// Ensure Homebrew bin directory is in the PATH
-	// So that brew installed algorand binaries can be found
-	ensureHomebrewPathInEnv()
-
 	if !IsService() {
-		return fmt.Errorf("algod unexpectedly NOT in path. Installation failed")
+		return fmt.Errorf("algod is not a service")
 	}
 
-	fmt.Println(`Installed Algorand (Algod) with Homebrew.
-Algod is running in the background as a system-level service.
-	`)
+	log.Info("Installed Algorand (Algod) with Homebrew ")
 
 	return nil
 }
 
 // Uninstall removes the Algorand application from the system using Homebrew if it is installed.
-func Uninstall() error {
-	if !system.CmdExists("brew") {
+func Uninstall(force bool) error {
+	if force {
+		if system.IsCmdRunning("algod") {
+			err := Stop(force)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	cmds := system.CmdsList{}
+	if IsService() {
+		cmds = append(cmds, []string{"sudo", "launchctl", "unload", "/Library/LaunchDaemons/com.algorand.algod.plist"})
+	}
+
+	if !system.CmdExists("brew") && !force {
 		return errors.New("homebrew is not installed")
+	} else {
+		cmds = append(cmds, []string{"brew", "uninstall", "algorand"})
+	}
+
+	if force {
+		cmds = append(cmds, []string{"sudo", "rm", "-rf", strings.Join(utils.GetKnownDataPaths(), " ")})
+		cmds = append(cmds, []string{"sudo", "rm", "-rf", "/Library/LaunchDaemons/com.algorand.algod.plist"})
 	}
-	return exec.Command("brew", "uninstall", "algorand").Run()
+
+	return system.RunAll(cmds)
 }
 
 // Upgrade updates the installed Algorand package using Homebrew if it's available and properly configured.
-func Upgrade() error {
+func Upgrade(force bool) error {
 	if !system.CmdExists("brew") {
 		return errors.New("homebrew is not installed")
 	}
-	// Check if algorand is installed with Homebrew
-	checkCmdArgs := system.CmdsList{{"brew", "--prefix", "algorand", "--installed"}}
-	if system.IsSudo() {
-		checkCmdArgs = checkCmdArgs.Su(os.Getenv("SUDO_USER"))
-	}
-	err := system.RunAll(checkCmdArgs)
-	if err != nil {
-		return err
-	}
-	// Upgrade algorand
-	upgradeCmdArgs := system.CmdsList{{"brew", "upgrade", "algorand", "--formula"}}
-	if system.IsSudo() {
-		upgradeCmdArgs = upgradeCmdArgs.Su(os.Getenv("SUDO_USER"))
-	}
-	return system.RunAll(upgradeCmdArgs)
+
+	return system.RunAll(system.CmdsList{
+		{"brew", "--prefix", "algorand", "--installed"},
+		{"brew", "upgrade", "algorand", "--formula"},
+	})
 }
 
 // Start algorand with launchd
-func Start() error {
-	return exec.Command("launchctl", "load", "/Library/LaunchDaemons/com.algorand.algod.plist").Run()
+func Start(force bool) error {
+	log.Debug("Attempting to start algorand with launchd")
+	//if !IsService() && !force {
+	//	return fmt.Errorf(MustBeServiceMsg)
+	//}
+	return system.RunAll(system.CmdsList{
+		{"sudo", "launchctl", "start", "com.algorand.algod"},
+	})
 }
 
 // Stop shuts down the Algorand algod system process using the launchctl bootout command.
 // Returns an error if the operation fails.
-func Stop() error {
-	return exec.Command("launchctl", "bootout", "system/com.algorand.algod").Run()
+func Stop(force bool) error {
+	if !IsService() && !force {
+		return fmt.Errorf(MustBeServiceMsg)
+	}
+
+	return system.RunAll(system.CmdsList{
+		{"sudo", "launchctl", "stop", "com.algorand.algod"},
+	})
 }
 
 // UpdateService updates the Algorand launchd service with
 // a new data directory path and reloads the service configuration.
+// TODO: Deduplicate this method, redundant version of EnsureService.
 func UpdateService(dataDirectoryPath string) error {
 
 	algodPath, err := exec.LookPath("algod")
 	if err != nil {
-		fmt.Printf("Failed to find algod binary: %v\n", err)
+		log.Info("Failed to find algod binary: %v\n", err)
 		os.Exit(1)
 	}
 
@@ -138,8 +167,6 @@ func UpdateService(dataDirectoryPath string) error {
 					</array>
 					<key>RunAtLoad</key>
 					<true/>
-					<key>KeepAlive</key>
-					<true/>
 					<key>StandardOutPath</key>
 					<string>/tmp/algod.out</string>
 					<key>StandardErrorPath</key>
@@ -156,21 +183,21 @@ func UpdateService(dataDirectoryPath string) error {
 	// Parse and execute the template
 	tmpl, err := template.New("override").Parse(overwriteTemplate)
 	if err != nil {
-		fmt.Printf("Failed to parse template: %v\n", err)
+		log.Info("Failed to parse template: %v\n", err)
 		os.Exit(1)
 	}
 
 	var overwriteContent bytes.Buffer
 	err = tmpl.Execute(&overwriteContent, data)
 	if err != nil {
-		fmt.Printf("Failed to execute template: %v\n", err)
+		log.Info("Failed to execute template: %v\n", err)
 		os.Exit(1)
 	}
 
 	// Write the override content to the file
 	err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644)
 	if err != nil {
-		fmt.Printf("Failed to write override file: %v\n", err)
+		log.Info("Failed to write override file: %v\n", err)
 		os.Exit(1)
 	}
 
@@ -179,7 +206,7 @@ func UpdateService(dataDirectoryPath string) error {
 	err = cmd.Run()
 	if err != nil {
 		if !strings.Contains(err.Error(), "No such process") {
-			fmt.Printf("Failed to bootout launchd service: %v\n", err)
+			log.Info("Failed to bootout launchd service: %v\n", err)
 			os.Exit(1)
 		}
 	}
@@ -188,7 +215,7 @@ func UpdateService(dataDirectoryPath string) error {
 	cmd = exec.Command("launchctl", "load", overwriteFilePath)
 	err = cmd.Run()
 	if err != nil {
-		fmt.Printf("Failed to load launchd service: %v\n", err)
+		log.Info("Failed to load launchd service: %v\n", err)
 		os.Exit(1)
 	}
 
@@ -196,57 +223,55 @@ func UpdateService(dataDirectoryPath string) error {
 	return nil
 }
 
-func handleDataDirMac() {
+// TODO move to configure as a generic
+func handleDataDirMac() error {
 	// Ensure the ~/.algorand directory exists
 	algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand")
 	if err := os.MkdirAll(algorandDir, 0755); err != nil {
-		fmt.Printf("Failed to create directory %s: %v\n", algorandDir, err)
-		os.Exit(1)
+		return err
 	}
 
 	// Check if genesis.json file exists in ~/.algorand
+	// TODO: replace with algocfg or goal templates
 	genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json")
-	if _, err := os.Stat(genesisFilePath); os.IsNotExist(err) {
-		fmt.Println("genesis.json file does not exist. Downloading...")
-
-		// Download the genesis.json file
-		resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
-		if err != nil {
-			fmt.Printf("Failed to download genesis.json: %v\n", err)
-			cobra.CheckErr(err)
-		}
-		defer resp.Body.Close()
+	_, err := os.Stat(genesisFilePath)
+	if !os.IsNotExist(err) {
+		return nil
+	}
 
-		// Create the file
-		out, err := os.Create(genesisFilePath)
-		if err != nil {
-			fmt.Printf("Failed to create genesis.json file: %v\n", err)
-			cobra.CheckErr(err)
-		}
-		defer out.Close()
+	log.Info("Downloading mainnet genesis.json file to ~/.algorand/genesis.json")
 
-		// Write the content to the file
-		_, err = io.Copy(out, resp.Body)
-		if err != nil {
-			fmt.Printf("Failed to save genesis.json file: %v\n", err)
-			cobra.CheckErr(err)
-		}
+	// Download the genesis.json file
+	resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
 
-		fmt.Println("mainnet genesis.json file downloaded successfully.")
+	// Create the file
+	out, err := os.Create(genesisFilePath)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	// Write the content to the file
+	_, err = io.Copy(out, resp.Body)
+	if err != nil {
+		return err
 	}
 
+	log.Info("mainnet genesis.json file downloaded successfully.")
+	return nil
 }
 
 func EnsureService() error {
-	// Get the prefix path for Algorand
-	cmd := exec.Command("brew", "--prefix", "algorand")
-	algorandPrefix, err := cmd.Output()
+	log.Debug("Ensuring Algorand service is running")
+	path, err := exec.LookPath("algod")
 	if err != nil {
-		fmt.Printf("Failed to get Algorand prefix: %v\n", err)
-		cobra.CheckErr(err)
+		log.Error("algod does not exist in path")
+		return err
 	}
-	algorandPrefixPath := strings.TrimSpace(string(algorandPrefix))
-
 	// Define the launchd plist content
 	plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -256,93 +281,36 @@ func EnsureService() error {
 	<string>com.algorand.algod</string>
 	<key>ProgramArguments</key>
 	<array>
-			<string>%s/bin/algod</string>
+			<string>%s</string>
 			<string>-d</string>
 			<string>%s/.algorand</string>
 	</array>
 	<key>RunAtLoad</key>
 	<true/>
-	<key>KeepAlive</key>
-	<true/>
+    <key>Debug</key>
+    <true/>
 	<key>StandardOutPath</key>
 	<string>/tmp/algod.out</string>
 	<key>StandardErrorPath</key>
 	<string>/tmp/algod.err</string>
 </dict>
-</plist>`, algorandPrefixPath, os.Getenv("HOME"))
+</plist>`, path, os.Getenv("HOME"))
 
 	// Write the plist content to a file
 	plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
 	err = os.MkdirAll(filepath.Dir(plistPath), 0755)
 	if err != nil {
-		fmt.Printf("Failed to create LaunchDaemons directory: %v\n", err)
+		log.Info("Failed to create LaunchDaemons directory: %v\n", err)
 		cobra.CheckErr(err)
 	}
 
 	err = os.WriteFile(plistPath, []byte(plistContent), 0644)
 	if err != nil {
-		fmt.Printf("Failed to write plist file: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	// Load the launchd service
-	cmd = exec.Command("launchctl", "load", plistPath)
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("Failed to load launchd service: %v\n", err)
+		log.Info("Failed to write plist file: %v\n", err)
 		cobra.CheckErr(err)
 	}
-
-	// Check if the service is running
-	cmd = exec.Command("launchctl", "list", "com.algorand.algod")
-	err = cmd.Run()
-	if err != nil {
-		fmt.Printf("launchd service is not running: %v\n", err)
-		cobra.CheckErr(err)
-	}
-
-	fmt.Println("Launchd service created and loaded successfully.")
-
-	return nil
-}
-
-// Ensure that Homebrew bin directory is in the PATH so that Algorand binaries can be found
-func ensureHomebrewPathInEnv() {
-	homebrewPrefix := os.Getenv("HOMEBREW_PREFIX")
-	homebrewCellar := os.Getenv("HOMEBREW_CELLAR")
-	homebrewRepository := os.Getenv("HOMEBREW_REPOSITORY")
-
-	if homebrewPrefix == "" || homebrewCellar == "" || homebrewRepository == "" {
-		fmt.Println("Homebrew environment variables are not set. Running brew shellenv...")
-
-		cmd := exec.Command("brew", "shellenv")
-		output, err := cmd.Output()
-		if err != nil {
-			fmt.Printf("Failed to get Homebrew environment: %v\n", err)
-			return
-		}
-
-		envVars := strings.Split(string(output), "\n")
-		for _, envVar := range envVars {
-			if envVar != "" {
-				fmt.Println("Setting environment variable:", envVar)
-				os.Setenv(strings.Split(envVar, "=")[0], strings.Trim(strings.Split(envVar, "=")[1], `"`))
-			}
-		}
-
-		// Append brew shellenv output to .zshrc
-		zshrcPath := filepath.Join(os.Getenv("HOME"), ".zshrc")
-		f, err := os.OpenFile(zshrcPath, os.O_APPEND|os.O_WRONLY, 0644)
-		if err != nil {
-			fmt.Printf("Failed to open .zshrc: %v\n", err)
-			fmt.Printf("Are you running a terminal other than zsh?")
-			fmt.Printf("Please run brew shellenv and add the output to your shell's configuration file.")
-			return
-		}
-		defer f.Close()
-
-		if _, err := f.WriteString("\n# Inserted by Algorun\n# Homebrew environment variables\n" + string(output)); err != nil {
-			fmt.Printf("Failed to write to .zshrc: %v\n", err)
-		}
-	}
+	return system.RunAll(system.CmdsList{
+		{"launchctl", "load", plistPath},
+		{"launchctl", "list", "com.algorand.algod"},
+	})
 }
diff --git a/internal/algod/msgs/errors.go b/internal/algod/msgs/errors.go
new file mode 100644
index 00000000..d68b0c52
--- /dev/null
+++ b/internal/algod/msgs/errors.go
@@ -0,0 +1,5 @@
+package msgs
+
+const (
+	InvalidDataDirectory = "algorand data directory is invalid"
+)
diff --git a/internal/system/cmds.go b/internal/system/cmds.go
index 02f5c633..45b9ab48 100644
--- a/internal/system/cmds.go
+++ b/internal/system/cmds.go
@@ -3,6 +3,7 @@ package system
 import (
 	"fmt"
 	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -10,10 +11,17 @@ import (
 	"sync"
 )
 
+const CmdFailedErrorMsg = "command failed: %s output: %s error: %v"
+
 func IsSudo() bool {
 	return os.Geteuid() == 0
 }
 
+func IsCmdRunning(name string) bool {
+	err := exec.Command("pgrep", name).Run()
+	return err == nil
+}
+
 // CmdExists checks that a bash cli/cmd tool exists
 func CmdExists(tool string) bool {
 	_, err := exec.LookPath(tool)
@@ -43,9 +51,10 @@ func RunAll(list CmdsList) error {
 		cmd := exec.Command(args[0], args[1:]...)
 		output, err := cmd.CombinedOutput()
 		if err != nil {
-			return fmt.Errorf("Command failed: %s\nOutput: %s\nError: %v\n", strings.Join(args, " "), output, err)
+			log.Error(fmt.Sprintf("%s: %s", style.Red.Render("Failed"), strings.Join(args, " ")))
+			return fmt.Errorf(CmdFailedErrorMsg, strings.Join(args, " "), output, err)
 		}
-		fmt.Printf("%s: %s\n", style.Green.Render("Running"), strings.Join(args, " "))
+		log.Debug(fmt.Sprintf("%s: %s", style.Green.Render("Running"), strings.Join(args, " ")))
 	}
 	return nil
 }
@@ -85,7 +94,7 @@ func FindPathToFile(startDir string, targetFileName string) []string {
 	// Walk the directory tree and send file paths to the channel
 	err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
-			// Ignore permission errors
+			// Ignore permission msgs
 			if os.IsPermission(err) {
 				return nil
 			}
diff --git a/main.go b/main.go
index f282015a..5dc55870 100644
--- a/main.go
+++ b/main.go
@@ -2,8 +2,21 @@ package main
 
 import (
 	"github.com/algorandfoundation/algorun-tui/cmd"
+	"github.com/charmbracelet/log"
+	"os"
 )
 
+func init() {
+	// Log as JSON instead of the default ASCII formatter.
+	//log.SetFormatter(log.JSONFormatter)
+
+	// Output to stdout instead of the default stderr
+	// Can be any io.Writer, see below for File example
+	log.SetOutput(os.Stdout)
+
+	// Only log the warning severity or above.
+	log.SetLevel(log.DebugLevel)
+}
 func main() {
 	err := cmd.Execute()
 	if err != nil {
diff --git a/ui/app/app_test.go b/ui/app/app_test.go
index 311ffdb4..90ce2deb 100644
--- a/ui/app/app_test.go
+++ b/ui/app/app_test.go
@@ -46,7 +46,7 @@ func Test_EmitDeleteKey(t *testing.T) {
 		t.Error("Expected ABC")
 	}
 	if evt.Err != nil {
-		t.Error("Expected no errors")
+		t.Error("Expected no msgs")
 	}
 
 	client = test.GetClient(true)
@@ -60,7 +60,7 @@ func Test_EmitDeleteKey(t *testing.T) {
 		t.Error("Expected no response")
 	}
 	if evt.Err == nil {
-		t.Error("Expected errors")
+		t.Error("Expected msgs")
 	}
 
 }

From 965404b73e652d4ddfc45aeb76c51cba34f9ffa7 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Wed, 11 Dec 2024 21:23:15 -0500
Subject: [PATCH 19/23] test(node): update playbook and workflow

---
 .github/workflows/node_test.yaml | 20 ++++++++------------
 playbook.yaml                    |  2 --
 2 files changed, 8 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
index 52421251..2a3ebebf 100644
--- a/.github/workflows/node_test.yaml
+++ b/.github/workflows/node_test.yaml
@@ -24,15 +24,13 @@ jobs:
       - name: Run Ubuntu commands
         run: |
           go build .
-          sudo ./algorun-tui node install
-          sudo ./algorun-tui node start
+          ./algorun-tui node install
           systemctl status algorand.service
           export TOKEN=$(cat /var/lib/algorand/algod.admin.token)
           curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
-          sudo ./algorun-tui node stop
-          sudo ./algorun-tui node upgrade
-          # sudo ./algorun-tui node configure
-          sudo ./algorun-tui node uninstall
+          ./algorun-tui node stop
+          ./algorun-tui node upgrade
+          ./algorun-tui node uninstall
 
   macos:
     runs-on: macos-latest
@@ -46,13 +44,11 @@ jobs:
       - name: Run MacOs commands
         run: |
           go build .
-          sudo ./algorun-tui node install
-          sudo ./algorun-tui node start
+          ./algorun-tui node install
           sudo launchctl print system/com.algorand.algod
           sleep 5
           export TOKEN=$(cat ~/.algorand/algod.admin.token)
           curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
-          sudo ./algorun-tui node stop
-          sudo ./algorun-tui node upgrade
-          # sudo ./algorun-tui node configure
-          sudo ./algorun-tui node uninstall
+          ./algorun-tui node stop
+          ./algorun-tui node upgrade
+          ./algorun-tui node uninstall
diff --git a/playbook.yaml b/playbook.yaml
index db717bec..7510210f 100644
--- a/playbook.yaml
+++ b/playbook.yaml
@@ -11,8 +11,6 @@
       when: not binpath.stat.exists
     - name: Run installer
       command: algorun node install
-    - name: Run installer twice
-      command: algorun node install
     - name: Run upgrade
       command: algorun node upgrade
     - name: Run stop

From 5561b8cc26747739948d8ba1d1db3a35f03c5310 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Wed, 11 Dec 2024 21:43:13 -0500
Subject: [PATCH 20/23] chore(node): call sudo on demand

---
 cmd/node/install.go           | 10 +++++
 cmd/node/node.go              | 11 -----
 cmd/node/upgrade.go           | 24 ++++++++++-
 internal/algod/linux/linux.go | 75 ++++++++++++++++++++++-------------
 playbook.yaml                 |  2 +
 5 files changed, 82 insertions(+), 40 deletions(-)

diff --git a/cmd/node/install.go b/cmd/node/install.go
index e66c0217..61a68bf1 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -36,6 +36,16 @@ var installCmd = &cobra.Command{
 			log.Error(err)
 			os.Exit(1)
 		}
+
+		// If it's not running, start the daemon (can happen)
+		if !algod.IsRunning() {
+			err = algod.Start()
+			if err != nil {
+				log.Error(err)
+				os.Exit(1)
+			}
+		}
+
 		log.Info(style.Green.Render("Algorand installed successfully 🎉"))
 	},
 }
diff --git a/cmd/node/node.go b/cmd/node/node.go
index 96eb36e3..1fca2ecd 100644
--- a/cmd/node/node.go
+++ b/cmd/node/node.go
@@ -1,13 +1,10 @@
 package node
 
 import (
-	"errors"
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
 	"github.com/algorandfoundation/algorun-tui/ui/style"
 	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
-	"os"
-	"runtime"
 )
 
 const SudoWarningMsg = "(You may be prompted for your password)"
@@ -23,14 +20,6 @@ var Cmd = &cobra.Command{
 	Use:   "node",
 	Short: "Node Management",
 	Long:  style.Purple(style.BANNER) + "\n" + style.LightBlue("Manage your Algorand node"),
-	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-
-		// Check that we are calling with sudo on linux
-		if os.Geteuid() != 0 && runtime.GOOS == "linux" {
-			return errors.New(PermissionErrorMsg)
-		}
-		return nil
-	},
 }
 
 func NeedsToBeRunning(cmd *cobra.Command, args []string) {
diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go
index 5b8cce5e..139d232a 100644
--- a/cmd/node/upgrade.go
+++ b/cmd/node/upgrade.go
@@ -2,17 +2,37 @@ package node
 
 import (
 	"github.com/algorandfoundation/algorun-tui/internal/algod"
+	"github.com/algorandfoundation/algorun-tui/ui/style"
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
+	"os"
 )
 
+const UpgradeMsg = "Upgrading Algod"
+
 var upgradeCmd = &cobra.Command{
 	Use:              "upgrade",
 	Short:            "Upgrade Algod",
 	Long:             "Upgrade Algod (if installed with package manager).",
 	SilenceUsage:     true,
 	PersistentPreRun: NeedsToBeStopped,
-	RunE: func(cmd *cobra.Command, args []string) error {
+	Run: func(cmd *cobra.Command, args []string) {
+		// TODO: get expected version and check if update is required
+		log.Info(style.Green.Render(UpgradeMsg + " vX.X.X"))
+		// Warn user for prompt
+		log.Warn(style.Yellow.Render(SudoWarningMsg))
 		// TODO: Check Version from S3 against the local binary
-		return algod.Update()
+		err := algod.Update()
+		if err != nil {
+			log.Error(err)
+		}
+		// If it's not running, start the daemon (can happen)
+		if !algod.IsRunning() {
+			err = algod.Start()
+			if err != nil {
+				log.Error(err)
+				os.Exit(1)
+			}
+		}
 	},
 }
diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go
index 83ceed9c..4820a42b 100644
--- a/internal/algod/linux/linux.go
+++ b/internal/algod/linux/linux.go
@@ -6,9 +6,9 @@ import (
 	"github.com/algorandfoundation/algorun-tui/internal/algod/fallback"
 	"github.com/algorandfoundation/algorun-tui/internal/system"
 	"github.com/charmbracelet/log"
-
 	"os"
 	"os/exec"
+	"runtime"
 	"strings"
 	"text/template"
 )
@@ -21,34 +21,55 @@ type Algod struct {
 	DataDirectoryPath string
 }
 
+// InstallRequirements generates installation commands for "sudo" based on the detected package manager and system state.
+func InstallRequirements() system.CmdsList {
+	var cmds system.CmdsList
+	if (system.CmdExists("sudo") && system.CmdExists("prep")) || os.Geteuid() != 0 {
+		return cmds
+	}
+	if system.CmdExists("apt-get") {
+		return system.CmdsList{
+			{"apt-get", "update"},
+			{"apt-get", "install", "-y", "sudo", "procps"},
+		}
+	}
+
+	if system.CmdExists("dnf") {
+		return system.CmdsList{
+			{"dnf", "install", "-y", "sudo", "procps-ng"},
+		}
+	}
+	return cmds
+}
+
 // Install installs Algorand development tools or node software depending on the package manager.
 func Install() error {
 	log.Info("Installing Algod on Linux")
 	// Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
 	if system.CmdExists("apt-get") { // On some Debian systems we use apt-get
 		log.Info("Installing with apt-get")
-		return system.RunAll(system.CmdsList{
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
-			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | tee /etc/apt/trusted.gpg.d/algorand.asc"},
-			{"sh", "-c", `add-apt-repository -y "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"`},
-			{"apt-get", "update"},
-			{"apt-get", "install", "-y", "algorand-devtools"},
-		})
+		return system.RunAll(append(InstallRequirements(), system.CmdsList{
+			{"sudo", "apt-get", "update"},
+			{"sudo", "apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+			{"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | sudo tee /etc/apt/trusted.gpg.d/algorand.asc"},
+			{"sudo", "add-apt-repository", "-y", fmt.Sprintf("deb [arch=%s] https://releases.algorand.com/deb/ stable main", runtime.GOARCH)},
+			{"sudo", "apt-get", "update"},
+			{"sudo", "apt-get", "install", "-y", "algorand-devtools"},
+		}...))
 	}
 
 	if system.CmdExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
 		log.Printf("Installing with dnf")
-		return system.RunAll(system.CmdsList{
+		return system.RunAll(append(InstallRequirements(), system.CmdsList{
 			{"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
-			{"rpmkeys", "--import", "rpm_algorand.pub"},
-			{"dnf", "install", "-y", "dnf-command(config-manager)"},
-			{"dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
-			{"dnf", "install", "-y", "algorand-devtools"},
-			{"systemctl", "enable", "algorand.service"},
-			{"systemctl", "start", "algorand.service"},
+			{"sudo", "rpmkeys", "--import", "rpm_algorand.pub"},
+			{"sudo", "dnf", "install", "-y", "dnf-command(config-manager)"},
+			{"sudo", "dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
+			{"sudo", "dnf", "install", "-y", "algorand-devtools"},
+			{"sudo", "systemctl", "enable", "algorand.service"},
+			{"sudo", "systemctl", "start", "algorand.service"},
 			{"rm", "-f", "rpm_algorand.pub"},
-		})
+		}...))
 
 	}
 
@@ -65,14 +86,14 @@ func Uninstall() error {
 	if system.CmdExists("apt-get") {
 		log.Info("Using apt-get package manager")
 		unInstallCmds = [][]string{
-			{"apt-get", "autoremove", "algorand-devtools", "algorand", "-y"},
+			{"sudo", "apt-get", "autoremove", "algorand-devtools", "algorand", "-y"},
 		}
 	}
 	// On Fedora and CentOs8 there's the dnf package manager
 	if system.CmdExists("dnf") {
 		log.Info("Using dnf package manager")
 		unInstallCmds = [][]string{
-			{"dnf", "remove", "algorand-devtools", "algorand", "-y"},
+			{"sudo", "dnf", "remove", "algorand-devtools", "algorand", "-y"},
 		}
 	}
 	// Error on unsupported package managers
@@ -81,8 +102,8 @@ func Uninstall() error {
 	}
 
 	// Commands to clear systemd algorand.service and any other files, like the configuration override
-	unInstallCmds = append(unInstallCmds, []string{"bash", "-c", "rm -rf /etc/systemd/system/algorand*"})
-	unInstallCmds = append(unInstallCmds, []string{"systemctl", "daemon-reload"})
+	unInstallCmds = append(unInstallCmds, []string{"sudo", "bash", "-c", "rm -rf /etc/systemd/system/algorand*"})
+	unInstallCmds = append(unInstallCmds, []string{"sudo", "systemctl", "daemon-reload"})
 
 	return system.RunAll(unInstallCmds)
 }
@@ -92,13 +113,13 @@ func Uninstall() error {
 func Upgrade() error {
 	if system.CmdExists("apt-get") {
 		return system.RunAll(system.CmdsList{
-			{"apt-get", "update"},
-			{"apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"},
+			{"sudo", "apt-get", "update"},
+			{"sudo", "apt-get", "install", "--only-upgrade", "-y", "algorand-devtools", "algorand"},
 		})
 	}
 	if system.CmdExists("dnf") {
 		return system.RunAll(system.CmdsList{
-			{"dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"},
+			{"sudo", "dnf", "update", "-y", "--refresh", "algorand-devtools", "algorand"},
 		})
 	}
 	return fmt.Errorf("the *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade")
@@ -109,21 +130,21 @@ func Upgrade() error {
 // Returns an error if the command fails.
 // TODO: Replace with D-Bus integration
 func Start() error {
-	return exec.Command("systemctl", "start", "algorand").Run()
+	return exec.Command("sudo", "systemctl", "start", "algorand").Run()
 }
 
 // Stop shuts down the Algorand algod system process on Linux using the systemctl stop command.
 // Returns an error if the operation fails.
 // TODO: Replace with D-Bus integration
 func Stop() error {
-	return exec.Command("systemctl", "stop", "algorand").Run()
+	return exec.Command("sudo", "systemctl", "stop", "algorand").Run()
 }
 
 // IsService checks if the "algorand.service" is listed as a systemd unit file on Linux.
 // Returns true if it exists.
 // TODO: Replace with D-Bus integration
 func IsService() bool {
-	out, err := system.Run([]string{"systemctl", "list-unit-files", "algorand.service"})
+	out, err := system.Run([]string{"sudo", "systemctl", "list-unit-files", "algorand.service"})
 	if err != nil {
 		return false
 	}
diff --git a/playbook.yaml b/playbook.yaml
index 7510210f..097634f7 100644
--- a/playbook.yaml
+++ b/playbook.yaml
@@ -11,6 +11,8 @@
       when: not binpath.stat.exists
     - name: Run installer
       command: algorun node install
+    - name: Run stop
+      command: algorun node stop
     - name: Run upgrade
       command: algorun node upgrade
     - name: Run stop

From 652368bdb62eba5e635d823bb4229cd03b4c7021 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Thu, 12 Dec 2024 14:58:23 -0500
Subject: [PATCH 21/23] chore(node): add wait before starting

---
 cmd/node/install.go | 3 +++
 cmd/node/upgrade.go | 4 ++++
 2 files changed, 7 insertions(+)

diff --git a/cmd/node/install.go b/cmd/node/install.go
index 61a68bf1..e2a6d516 100644
--- a/cmd/node/install.go
+++ b/cmd/node/install.go
@@ -6,6 +6,7 @@ import (
 	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"os"
+	"time"
 )
 
 const InstallMsg = "Installing Algorand"
@@ -37,6 +38,8 @@ var installCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
+		time.Sleep(5 * time.Second)
+
 		// If it's not running, start the daemon (can happen)
 		if !algod.IsRunning() {
 			err = algod.Start()
diff --git a/cmd/node/upgrade.go b/cmd/node/upgrade.go
index 139d232a..b464f899 100644
--- a/cmd/node/upgrade.go
+++ b/cmd/node/upgrade.go
@@ -6,6 +6,7 @@ import (
 	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"os"
+	"time"
 )
 
 const UpgradeMsg = "Upgrading Algod"
@@ -26,6 +27,9 @@ var upgradeCmd = &cobra.Command{
 		if err != nil {
 			log.Error(err)
 		}
+
+		time.Sleep(5 * time.Second)
+
 		// If it's not running, start the daemon (can happen)
 		if !algod.IsRunning() {
 			err = algod.Start()

From 3d76e025e0554f4b180c323be5b8416608d18dbc Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Thu, 12 Dec 2024 15:02:00 -0500
Subject: [PATCH 22/23] build: stop service before uninstalling

---
 .github/workflows/node_test.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
index 2a3ebebf..cacf280c 100644
--- a/.github/workflows/node_test.yaml
+++ b/.github/workflows/node_test.yaml
@@ -30,6 +30,7 @@ jobs:
           curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
           ./algorun-tui node stop
           ./algorun-tui node upgrade
+          ./algorun-tui node stop
           ./algorun-tui node uninstall
 
   macos:
@@ -51,4 +52,5 @@ jobs:
           curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
           ./algorun-tui node stop
           ./algorun-tui node upgrade
+          ./algorun-tui node stop
           ./algorun-tui node uninstall

From 694564ce845f3bf66a64fabf597c462890f046a2 Mon Sep 17 00:00:00 2001
From: Michael Feher <michael.feher@algorand.foundation>
Date: Thu, 12 Dec 2024 15:32:43 -0500
Subject: [PATCH 23/23] chore: set patch threshold

---
 codecov.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/codecov.yaml b/codecov.yaml
index 2c7de746..eaf149d2 100644
--- a/codecov.yaml
+++ b/codecov.yaml
@@ -6,4 +6,4 @@ coverage:
         threshold: 10%
     patch:
       default:
-        target: 60%
\ No newline at end of file
+        target: 10%
\ No newline at end of file