Edit: I have the program running and everything works on my Macs. If you want to try it out and see if it works for you, let me know.
import SwiftUI
import Foundation
import Dispatch
struct ContentView: View {
@State private var logText: String = "Welcome to Timeleap Setup!\nDownload the CLI, then run the setup.\n"
@State private var statusText: String = "Status: Idle\n"
@State private var monitoringText: String = "Monitoring Output...\n"
@State private var isWorkerRunning: Bool = false
@State private var isBrokerRunning: Bool = false
@State private var releaseVersion: String = "v0.14.0-alpha.2"
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Timeleap Devnet Setup").font(.title).padding(.top, 20)
// Add a TextField for the release version
HStack {
Text("Release Version:")
TextField("Enter release version", text: $releaseVersion)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
}
HStack(spacing: 30) {
Button(action: { downloadTimeleapCLI() }) {
Text("Download Timeleap CLI")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { setupDevnet() }) {
Text("Setup Timeleap Devnet")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { toggleBrokerNode() }) {
Text(isBrokerRunning ? "Stop Broker Node" : "Start Broker Node")
.font(.headline)
.padding()
.background(isBrokerRunning ? Color.green : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { toggleWorkerNode() }) {
Text(isWorkerRunning ? "Stop Worker Node" : "Start Worker Node")
.font(.headline)
.padding()
.background(isWorkerRunning ? Color.green : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { clearLogs() }) {
Text("Clear Logs")
.font(.headline)
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.buttonStyle(PlainButtonStyle())
VStack(alignment: .leading, spacing: 10) {
Text("Logs").font(.headline)
TextEditor(text: $logText)
.frame(height: 150)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
VStack(alignment: .leading, spacing: 10) {
Text("Node Status").font(.headline)
TextEditor(text: $statusText)
.frame(height: 100)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
VStack(alignment: .leading, spacing: 10) {
Text("Monitoring").font(.headline)
TextEditor(text: $monitoringText)
.frame(height: 120)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
}
.padding()
.onAppear {
monitorLogs()
}
}
func downloadTimeleapCLI() {
let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
let timeleapDir = "\(homeDirectory)/timeleap"
let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
let osName: String
#if os(macOS)
osName = "darwin"
#elseif os(Linux)
osName = "linux"
#elseif os(Windows)
osName = "windows"
#else
osName = "unknown"
#endif
let architecture = ProcessInfo.processInfo.machineHardwareName
let archName: String
if architecture.contains("arm64") {
archName = "arm64"
} else if architecture.contains("x86_64") || architecture.contains("amd64") {
archName = "amd64"
} else {
archName = "unknown"
}
// Use the releaseVersion state variable in the URL
let timeleapURL = "https://github.com/TimeleapLabs/timeleap/releases/download/\(releaseVersion)/timeleap.\(osName).\(archName)"
logText += "Download URL: \(timeleapURL)\n"
runShellCommand("mkdir -p \(timeleapDir)")
logText += "Ensuring directory exists: \(timeleapDir)...\n"
let curlCommand = "curl -L -o \(timeleapPath) \(timeleapURL)"
logText += "Running command: \(curlCommand)\n"
let curlOutput = runShellCommandAndReturnOutput(curlCommand)
logText += "curl output: \(curlOutput)\n"
if FileManager.default.fileExists(atPath: timeleapPath) {
runShellCommand("chmod +x \(timeleapPath)")
logText += "✅ Timeleap CLI downloaded and made executable!\n"
} else {
logText += "❌ ERROR: Failed to download Timeleap CLI.\n"
}
}
// Rest of the functions remain unchanged...
// Toggle Broker Node
func toggleBrokerNode()
{let debugOutput = runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'")
self.logText += "Broker PIDs at toggle: \(debugOutput)\n"
if isBrokerRunning {
isBrokerRunning = false // Update state immediately
stopBrokerNode()
} else {
isBrokerRunning = true // Update state immediately
startBrokerNode()
}
}
// Toggle Worker Node
func toggleWorkerNode() {
if isWorkerRunning {
isWorkerRunning = false // Update state immediately
stopWorkerNode()
} else {
isWorkerRunning = true // Update state immediately
startWorkerNode()
}
}
func startBrokerNode() {
let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
let timeleapDir = "\(homeDirectory)/timeleap"
let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
let logFilePath = "\(timeleapDir)/timeleap.log"
DispatchQueue.global().async {
DispatchQueue.main.async {
self.statusText += "Starting Broker Node...\n"
}
// Run broker in background
self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) broker >> \(logFilePath) 2>&1 &")
// Wait for the broker process to start
var retryCount = 5
while retryCount > 0 {
let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'").trimmingCharacters(in: .whitespacesAndNewlines)
if !output.isEmpty {
// Broker process is running
DispatchQueue.main.async {
self.isBrokerRunning = true
self.statusText += "Broker Node started.\n"
}
return
}
Thread.sleep(forTimeInterval: 1) // Wait before checking again
retryCount -= 1
}
// If the broker process didn't start
DispatchQueue.main.async {
self.statusText += "❌ ERROR: Broker Node failed to start.\n"
}
}
}
func stopBrokerNode() {
Foundation.DispatchQueue.global().async {
Foundation.DispatchQueue.main.async {
self.statusText += "Stopping Broker Node...\n"
}
// Kill the process
self.runShellCommand("pkill -f 'timeleap broker'")
// Ensure the process is actually stopped
var retryCount = 5
while retryCount > 0 {
let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap broker'").trimmingCharacters(in: .whitespacesAndNewlines)
if output.isEmpty {
break // Process is fully stopped
}
Thread.sleep(forTimeInterval: 1) // Wait before checking again
retryCount -= 1
}
// Confirm it's stopped before updating the UI
Foundation.DispatchQueue.main.async {
self.isBrokerRunning = false
self.statusText += "Broker Node stopped.\n"
}
}
}
// Start Worker Node
func startWorkerNode() {
let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
let timeleapDir = "\(homeDirectory)/timeleap"
let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
let logFilePath = "\(timeleapDir)/timeleap.log"
DispatchQueue.global().async {
DispatchQueue.main.async {
self.statusText += "Starting Worker Node...\n"
}
// Run worker in background
self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) worker >> \(logFilePath) 2>&1 &")
// Wait for the worker process to start
var retryCount = 5
while retryCount > 0 {
let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap worker'").trimmingCharacters(in: .whitespacesAndNewlines)
if !output.isEmpty {
// Worker process is running
DispatchQueue.main.async {
self.isWorkerRunning = true
self.statusText += "Worker Node started.\n"
}
return
}
Thread.sleep(forTimeInterval: 1) // Wait before checking again
retryCount -= 1
}
// If the worker process didn't start
DispatchQueue.main.async {
self.statusText += "❌ ERROR: Worker Node failed to start.\n"
}
}
}
func stopWorkerNode() {
DispatchQueue.global().async {
DispatchQueue.main.async {
self.statusText += "Stopping Worker Node...\n"
}
// Kill the process
self.runShellCommand("pkill -f 'timeleap worker'")
// Ensure the process is actually stopped
var retryCount = 5
while retryCount > 0 {
let output = self.runShellCommandAndReturnOutput("pgrep -f 'timeleap worker'").trimmingCharacters(in: .whitespacesAndNewlines)
if output.isEmpty {
break // Process is fully stopped
}
Thread.sleep(forTimeInterval: 1) // Wait before checking again
retryCount -= 1
}
// Confirm it's stopped before updating the UI
DispatchQueue.main.async {
self.isWorkerRunning = false
self.statusText += "Worker Node stopped.\n"
}
}
}
// Clear Logs
func clearLogs() {
logText = ""
statusText = "Status: Idle\n"
monitoringText = "Monitoring Output...\n"
let logFilePath = FileManager.default.homeDirectoryForCurrentUser.path + "/timeleap/timeleap.log"
do {
if FileManager.default.fileExists(atPath: logFilePath) {
try FileManager.default.removeItem(atPath: logFilePath)
}
FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil)
logText += "✅ Log file cleared.\n"
} catch {
logText += "❌ ERROR: Failed to clear log file - \(error.localizedDescription)\n"
}
}
// Monitor Logs
func monitorLogs() {
let logFilePath = FileManager.default.homeDirectoryForCurrentUser.path + "/timeleap/timeleap.log"
DispatchQueue.global(qos: .background).async {
while true {
if FileManager.default.fileExists(atPath: logFilePath) {
if let logs = try? String(contentsOfFile: logFilePath, encoding: .utf8) {
DispatchQueue.main.async {
self.monitoringText = logs
}
}
} else {
DispatchQueue.main.async {
self.monitoringText = "⚠️ No logs found. Is Timeleap running?"
}
}
Thread.sleep(forTimeInterval: 2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
func setupDevnet() {
logText += "Starting setup...\n"
statusText = "Status: Setting up...\n"
DispatchQueue.global().async {
self.checkAndInstallDependencies()
let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
let timeleapDir = "\(homeDirectory)/timeleap"
let confYamlPath = "\(timeleapDir)/conf.yaml"
let secretsYamlPath = "\(timeleapDir)/secrets.yaml"
let logFilePath = "\(timeleapDir)/timeleap.log"
let timeleapPath = "\(homeDirectory)/.local/bin/timeleap"
if !FileManager.default.fileExists(atPath: timeleapPath) {
DispatchQueue.main.async {
self.logText += "⚠️ Timeleap CLI not found. Please download it first.\n"
}
return
}
self.runShellCommand("mkdir -p \(timeleapDir)")
self.runShellCommand("touch \(logFilePath)")
self.runShellCommand("rm -rf \(timeleapDir) && mkdir -p \(timeleapDir)")
self.runShellCommand("chmod -R 755 \(timeleapDir)") // Set permissions
if !FileManager.default.fileExists(atPath: timeleapDir) {
DispatchQueue.main.async {
self.logText += "Error: Failed to download Timeleap CLI.\n"
}
return
}
self.runShellCommand("file \(timeleapDir)")
self.runShellCommand("ls -l \(timeleapDir)")
self.runShellCommand("file \(timeleapDir)")
self.runShellCommand("chmod +x \(timeleapPath)")
let initialConfYaml = """
system:
log: info
name: Timeleap
network:
bind: 0.0.0.0:9123
broker:
uri: ws://localhost:9123
publicKey: "publicKey"
rpc:
cpus: 1
gpus: 0
ram: 1024
pos:
rpc:
- https://arbitrum.llamarpc.com
"""
do {
try initialConfYaml.write(toFile: confYamlPath, atomically: true, encoding: .utf8)
DispatchQueue.main.async {
self.logText += "✅ Initial conf.yaml written successfully!\n"
}
} catch {
DispatchQueue.main.async {
self.logText += "❌ ERROR: Could not write initial conf.yaml\n"
}
return
}
DispatchQueue.main.async {
self.logText += "🔑 Generating secrets.yaml...\n"
}
// Check for port conflicts
let portCheckCommand = "lsof -i :9123"
let portCheckOutput = self.runShellCommandAndReturnOutput(portCheckCommand)
DispatchQueue.main.async {
self.logText += "Port check output: \(portCheckOutput)\n"
}
if !portCheckOutput.isEmpty {
self.runShellCommand("kill -9 $(lsof -t -i :9123)")
DispatchQueue.main.async {
self.logText += "Terminated processes using port 9123.\n"
}
}
// Terminate any existing broker process
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { // Wait 5 seconds
self.runShellCommand("pkill -f 'timeleap broker'")
}
// Run the broker process to generate secrets
self.runShellCommand("cd \(timeleapDir) && \(timeleapPath) broker --allow-generate-secrets")
// Terminate the broker process after generating secrets
self.runShellCommand("pkill -f 'timeleap broker'")
// Check if secrets.yaml was generated
if !FileManager.default.fileExists(atPath: secretsYamlPath) {
DispatchQueue.main.async {
self.logText += "❌ ERROR: secrets.yaml was not generated. Check if the Timeleap binary is working.\n"
}
}
// Read and update conf.yaml with the public key from secrets.yaml
if let secretsYamlContents = try? String(contentsOfFile: secretsYamlPath, encoding: .utf8) {
let lines = secretsYamlContents.components(separatedBy: .newlines)
for line in lines {
if line.contains("publicKey:") {
let publicKey = line.components(separatedBy: " ")[1].trimmingCharacters(in: .whitespacesAndNewlines)
do {
var confYamlContents = try String(contentsOfFile: confYamlPath, encoding: .utf8)
confYamlContents = confYamlContents.replacingOccurrences(of: "publicKey: \"publicKey\"", with: "publicKey: \"\(publicKey)\"")
try confYamlContents.write(toFile: confYamlPath, atomically: true, encoding: .utf8)
DispatchQueue.main.async {
self.logText += "✅ Updated conf.yaml with public key: \(publicKey)\n"
}
} catch {
DispatchQueue.main.async {
self.logText += "❌ ERROR: Could not update conf.yaml with public key\n"
}
}
break
}
}
} else {
DispatchQueue.main.async {
self.logText += "❌ ERROR: Could not read secrets.yaml\n"
}
}
DispatchQueue.main.async {
self.logText += "Setup complete!\n"
self.statusText = "Status: Ready!\n"
}
}
}
func runShellCommand(_ command: String) {
let task = Process()
let pipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = pipe
task.standardError = errorPipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/bash")
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
let errorOutput = String(data: errorData, encoding: .utf8) ?? ""
DispatchQueue.main.async {
self.logText += "Executing: \(command)\nOutput: \(output)\nError: \(errorOutput)\n"
}
} catch {
DispatchQueue.main.async {
self.logText += "Error: \(error.localizedDescription)\n"
}
}
}
func runShellCommandAndReturnOutput(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/bash")
var output = ""
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
output = String(data: data, encoding: .utf8) ?? ""
} catch {
DispatchQueue.main.async {
self.logText += "Error executing: \(command)\n"
}
}
return output.trimmingCharacters(in: .whitespacesAndNewlines)
}
func checkPermissions(at path: String) -> Bool {
return FileManager.default.isWritableFile(atPath: path)
}
func checkAndInstallDependencies() {
if !commandExists("/opt/homebrew/bin/brew") {
logText += "Homebrew not found. Installing Homebrew...\n"
runShellCommand("""
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
""")
} else {
logText += "Homebrew is already installed.\n"
}
if !commandExists("/opt/homebrew/bin/node") {
logText += "Node.js not found. Installing Node.js...\n"
runShellCommand("/opt/homebrew/bin/brew install node")
runShellCommand("export PATH=/opt/homebrew/bin:$PATH")
} else {
logText += "Node.js is already installed.\n"
}
runShellCommand("/opt/homebrew/bin/node -v")
if !commandExists("/opt/homebrew/bin/yarn") {
logText += "Yarn not found. Installing Yarn...\n"
runShellCommand("/opt/homebrew/bin/brew install yarn")
} else {
logText += "Yarn is already installed.\n"
}
if !commandExists("curl") {
logText += "curl not found. Please install curl manually.\n"
} else {
logText += "curl is already installed.\n"
}
}
func commandExists(_ command: String) -> Bool {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", "which \(command)"]
task.executableURL = URL(fileURLWithPath: "/bin/bash")
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
return !output.isEmpty
} catch {
return false
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension Data {
init?(hexString: String) {
let length = hexString.count / 2
var data = Data(capacity: length)
var index = hexString.startIndex
for _ in 0..<length {
let nextIndex = hexString.index(index, offsetBy: 2)
if let byte = UInt8(hexString[index..<nextIndex], radix: 16) {
data.append(byte)
} else {
return nil
}
index = nextIndex
}
self = data
}
}
extension ProcessInfo {
var machineHardwareName: String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}
}