diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6df9d25 Binary files /dev/null and b/.DS_Store differ diff --git a/bin/mist b/bin/mist new file mode 100644 index 0000000..f7aff89 Binary files /dev/null and b/bin/mist differ diff --git a/bin/mist.exe b/bin/mist.exe new file mode 100644 index 0000000..f7aff89 Binary files /dev/null and b/bin/mist.exe differ diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go new file mode 100644 index 0000000..6df3e45 --- /dev/null +++ b/cli/cmd/auth.go @@ -0,0 +1,13 @@ +package cmd + +type AuthCmd struct { + Login LoginCmd `cmd:"" help:"Log in to your account"` + // Logout LogoutCmd `cmd:"" help:"Log out of your account"` + // Status AuthStatusCmd `cmd:"" help:"Check your authentication status" default:1` +} + +func (a *AuthCmd) Run() error { + // Possible fallback if no subcommand is provided + // fmt.Println("(auth root) – try 'mist auth login|logout|status' or mist help") + return nil +} diff --git a/cli/cmd/auth_login.go b/cli/cmd/auth_login.go new file mode 100644 index 0000000..1f24521 --- /dev/null +++ b/cli/cmd/auth_login.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" +) + +// TODO: What credentials are we taking? +type LoginCmd struct { +} + +func verifyUser(username, password string) error { + // Placeholder for actual authentication logic + if username == "admin" && password == "password" { + return nil + } + return errors.New("invalid credentials") +} + +// TODO: Figure out how to handle password input without exposing it in the terminal historyn (go get golang.org/x/term) +// TODO: Where are we storing auth token? Are we getting JWT? + +func (l *LoginCmd) Run() error { + // mist auth login + + fmt.Print("Username: ") + + reader := bufio.NewReader(os.Stdin) + username, _ := reader.ReadString('\n') + username = strings.TrimSpace(strings.ToLower(username)) + + fmt.Print("Password: ") + password, _ := reader.ReadString('\n') + password = strings.TrimSpace(strings.ToLower(password)) + err := verifyUser(username, password) + if err != nil { + fmt.Println("Error during authentication:", err) + } + + fmt.Println("Logging in with username:", username) + + return nil +} diff --git a/cli/cmd/config.go b/cli/cmd/config.go new file mode 100644 index 0000000..acb9ba8 --- /dev/null +++ b/cli/cmd/config.go @@ -0,0 +1,42 @@ +package cmd + +import "fmt" + + +// Config flags +type ConfigCmd struct{ + DefaultCluster string `help:"Set the default compute cluster." optional: ""` + Show bool `help:"Show current configuration."` + +} + +func (h *ConfigCmd) Run() error { + // Some dummy config; Call API or something + defaultConfig := map[string]string{ + "defaultCluster": "AMD-cluster-1", + "region": "us-east", + } + + if h.Show && h.DefaultCluster!= "" { + fmt.Printf("Cannot use --show and --default-cluster together") + return nil + } + + if h.DefaultCluster != "" { + // This is not actually set. + fmt.Printf("Setting default cluster to: %s\n", h.DefaultCluster) + return nil + } + + if h.Show { + fmt.Println("Current configuration: ") + for key, value := range defaultConfig { + fmt.Printf(" %s: %s \n", key, value) + } + return nil + } + + fmt.Println("No config action specified. Use --help for options.") + + return nil +} diff --git a/cli/cmd/config_test.go b/cli/cmd/config_test.go new file mode 100644 index 0000000..60758db --- /dev/null +++ b/cli/cmd/config_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "testing" + // "fmt" + // "bytes" +) + +// No Flag config +func TestConfigNoFlags(t *testing.T){ + cmd := &ConfigCmd{} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + if want := "No config action specified. Use --help for options."; !contains(output, want){ + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + +// Set Default Cluster to tt-gpu-cluster-1 +func TestConfigDefaultCluster(t *testing.T){ + cmd := &ConfigCmd{DefaultCluster: "tt-gpu-cluster-1"} // Create config object + + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + + // fmt.Printf("Captured the output: %s\n", output) + + if want := "Setting default cluster to: tt-gpu-cluster-1"; !contains(output, want) { + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + +// Show Config +func TestConfigCmd_Show(t *testing.T){ + cmd := &ConfigCmd{Show: true} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + + if want := "Current configuration:"; !contains(output, want){ + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + +// Show Error message if both flags are sent +func TestConfigBothFlagError(t *testing.T){ + cmd := &ConfigCmd{DefaultCluster: "tt-gpu-cluster-1", Show: true} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + + if want := "Cannot use --show and --default-cluster together"; !contains(output, want){ + t.Errorf("Expected the error message of \"%s\", got %q", want, output) + } + +} diff --git a/cli/cmd/help.go b/cli/cmd/help.go new file mode 100644 index 0000000..cf571e2 --- /dev/null +++ b/cli/cmd/help.go @@ -0,0 +1,16 @@ +package cmd + +import "fmt" + +type HelpCmd struct{} + +func (h *HelpCmd) Run() error { + fmt.Println("MIST CLI Help") + fmt.Println("Usage: mist [command] [options]") + fmt.Println("Commands:") + fmt.Println(" auth Authentication commands") + fmt.Println(" job Job management commands") + fmt.Println(" config Configuration commands") + fmt.Println(" help Show help information") + return nil +} diff --git a/cli/cmd/job.go b/cli/cmd/job.go new file mode 100644 index 0000000..6fcf5b2 --- /dev/null +++ b/cli/cmd/job.go @@ -0,0 +1,16 @@ +package cmd + +type JobCmd struct { + Submit JobSubmitCmd `cmd:"" help:"Submit a new job"` + Cancel JobCancelCmd `cmd:"" help:"Cancel an existing job"` + // Delete JobDeleteCmd `cmd: "" help: "Delete an existing job"` + Status JobStatusCmd `cmd:"" help:"Check the status of a job"` + // Cancel CancelCmd `cmd:"" help:"Cancel a running job"` + List ListCmd `cmd:"" help:"List all jobs" default:1` +} + +func (j *JobCmd) Run() error { + // Possible fallback if no subcommand is providFAre yed + // fmt.Println("(job root) – try 'mist job submit|status|list|cancel' or mist help") + return nil +} diff --git a/cli/cmd/job_cancel.go b/cli/cmd/job_cancel.go new file mode 100644 index 0000000..446787a --- /dev/null +++ b/cli/cmd/job_cancel.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + "time" +) + + +type JobCancelCmd struct { + ID string `arg:"" help:"ID of job you want to cancel"` +} + +func (c *JobCancelCmd) Run() error { + // Same Mock data from job list. + jobs := []Job{ + { + ID: "ID:1", + Name: "docker_container_name_1", + Status: "Running", + GPUType: "AMD", + CreatedAt: time.Now(), + }, + { + ID: "ID:2", + Name: "docker_container_name_2", + Status: "Enqueued", + GPUType: "TT", + CreatedAt: time.Now().Add(-time.Hour * 24), + }, + { + ID: "ID:3", + Name: "docker_container_name_3", + Status: "Running", + GPUType: "TT", + CreatedAt: time.Now().Add(-time.Hour * 24), + }, + } + + // Check if job exists + if !jobExists(jobs, c.ID) { + fmt.Printf("%s does not exist in your jobs.\n", c.ID) + fmt.Printf("Use the command \"job list\" for your list of jobs.") + return nil + } + + + fmt.Printf("Are you sure you want to cancel %s? (y/n): \n", c.ID) + + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(strings.ToLower(input)) + + if input == "y" || input == "yes"{ + fmt.Println("Confirmed, proceeding job cancellation....") + + // Confirmed job cancellation logic + + fmt.Println("Cancelling job with ID:", c.ID) + fmt.Printf("Job cancelled successfully with ID: %s\n", c.ID) + return nil + } else if input == "n" || input == "no"{ + fmt.Println("Cancelled.") + return nil + } else{ + fmt.Println("Invalid response.") + return nil + } + + return nil + +} \ No newline at end of file diff --git a/cli/cmd/job_cancel_test.go b/cli/cmd/job_cancel_test.go new file mode 100644 index 0000000..eff91c6 --- /dev/null +++ b/cli/cmd/job_cancel_test.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "testing" + "fmt" +) + + +// Added job, with no compute type added +func TestJobCancelJobDoesNotExist(t *testing.T){ + // This job should not exist in the dummy + cmd := &JobCancelCmd{ID: "job_12345"} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + if want := "job_12345 does not exist in your jobs.\nUse the command \"job list\" for your list of jobs."; !contains(output, want){ + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + +// Added job, with compute type +func TestJobCancelValid(t *testing.T){ + // This job should not exist in the dummy + cmd := &JobCancelCmd{ID: "ID:1"} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + if want := "Are you sure you want to cancel ID:1? (y/n):"; !contains(output, want){ + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + + +func TestJobCancelProceed(t *testing.T){ + cmd := &JobCancelCmd{ID: "ID:1"} + // Lowkey, we should refactor this into a + output := CaptureOutput(func(){ + MockInput("y\n", func() { + _ = cmd.Run() + }) + }) + if !contains(output, "Confirmed, proceeding job cancellation...."){ + t.Errorf("expected 'Confirmed, proceeding job cancellation....' but got:\n%s", output) + } + // fmt.Printf("Got the output %s\n", output) +} \ No newline at end of file diff --git a/cli/cmd/job_list.go b/cli/cmd/job_list.go new file mode 100644 index 0000000..118df0e --- /dev/null +++ b/cli/cmd/job_list.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + "time" +) + +type ListCmd struct { + All bool `help:"List all jobs, including completed and failed ones." short:"a"` +} + +type Job struct { + ID string + Name string + Status string + GPUType string + CreatedAt time.Time +} + +func (l *ListCmd) Run() error { + // Mock data - pull from API in real implementation + jobs := []Job{ + { + ID: "ID:1", + Name: "docker_container_name_1", + Status: "Running", + GPUType: "AMD", + CreatedAt: time.Now(), + }, + { + ID: "ID:2", + Name: "docker_container_name_2", + Status: "Enqueued", + GPUType: "TT", + CreatedAt: time.Now().Add(-time.Hour * 24), + }, + { + ID: "ID:3", + Name: "docker_container_name_3", + Status: "Running", + GPUType: "TT", + CreatedAt: time.Now().Add(-time.Hour * 24), + }, + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "Job ID\tName\tStatus\tGPU Type\tCreated At") + fmt.Fprintln(w, "--------------------------------------------------------------") + + for _, job := range jobs { + // Maybe filter based on running? + fmt.Fprintf( + w, + "%s\t%s\t%s\t%s\t%s\n", + job.ID, + job.Name, + job.Status, + job.GPUType, + job.CreatedAt.Format(time.RFC1123), + ) + } + + w.Flush() + + return nil +} diff --git a/cli/cmd/job_list_test.go b/cli/cmd/job_list_test.go new file mode 100644 index 0000000..ad9b088 --- /dev/null +++ b/cli/cmd/job_list_test.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "testing" +) + +// Will be more specific in the future! +func TestJobList(t *testing.T){ + cmd := &ListCmd{All: true} + output := CaptureOutput(func(){ + _ = cmd.Run() + }) + // Note the time is dynamic. + want := "Job ID Name Status GPU Type Created At\n--------------------------------------------------------------\nID:1 docker_container_name_1 Running AMD" + if !contains(output, want){ + t.Errorf("expected output to contain %q, got %q", want, output) + } + } \ No newline at end of file diff --git a/cli/cmd/job_status.go b/cli/cmd/job_status.go new file mode 100644 index 0000000..cff9187 --- /dev/null +++ b/cli/cmd/job_status.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + "time" +) + +type JobStatusCmd struct { + ID string `arg:"" help:"The ID of the job to check the status for"` +} + +func (j *JobStatusCmd) Run() error { + // Mock data - pull from API in real implementation + jobs := []Job{{ + ID: "ID:1", + Name: "docker_container_name_1", + Status: "Running", + GPUType: "AMD", + CreatedAt: time.Now(), + }} + + job, err := findJobByID(jobs, j.ID) + if err != nil { + fmt.Printf("%s does not exist in your jobs.\n", j.ID) + fmt.Printf("Use the command \"job list\" for your list of jobs.") + return nil + } + + println("Checking status for job ID:", j.ID) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "Job ID\tName\tStatus\tGPU Type\tCreated At") + fmt.Fprintln(w, "--------------------------------------------------------------") + + fmt.Fprintf( + w, + "%s\t%s\t%s\t%s\t%s\n", + job.ID, + job.Name, + job.Status, + job.GPUType, + job.CreatedAt.Format(time.RFC1123), + ) + w.Flush() + return nil +} diff --git a/cli/cmd/job_status_test.go b/cli/cmd/job_status_test.go new file mode 100644 index 0000000..d7d82a8 --- /dev/null +++ b/cli/cmd/job_status_test.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "testing" +) + +// Added job, with no compute type added +func TestJobStatusJobDoesNotExist(t *testing.T) { + // This job should not exist in the dummy + cmd := &JobStatusCmd{ID: "job_12345"} + output := CaptureOutput(func() { + _ = cmd.Run() + }) + if want := "job_12345 does not exist in your jobs.\nUse the command \"job list\" for your list of jobs."; !contains(output, want) { + t.Errorf("expected output to contain %q, got %q", want, output) + } +} + +// Added job, with compute type +func TestJobStatusValid(t *testing.T) { + // This job should not exist in the dummy + cmd := &JobStatusCmd{ID: "ID:1"} + output := CaptureOutput(func() { + _ = cmd.Run() + }) + if want := "docker_container_name_1"; !contains(output, want) { + t.Errorf("expected output to contain %q, got %q", want, output) + } +} diff --git a/cli/cmd/job_submit.go b/cli/cmd/job_submit.go new file mode 100644 index 0000000..d090934 --- /dev/null +++ b/cli/cmd/job_submit.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type JobSubmitCmd struct { + Script string `arg:"" help:"Path to the job script file to submit"` + Compute string `help:"Type of compute required for the job: AMD|TT|CPU" default:"AMD"` +} + + +func (j *JobSubmitCmd) Run() error { + // mist job submit