Build a CLI using Cobra in Golang

Task tracker is a project used to track and manage your tasks. In this task, you will build a simple command line interface (CLI) to track what you need to do, what you have done, and what you are currently working on. This project will help you practice your programming skills, including working with the filesystem, handling user inputs, and building a simple CLI application.
https://roadmap.sh/projects/task-tracker
Project URL : https://github.com/pranav767/GO_PROJECTS/tree/main/task-tracker
Requirements
The application should run from the command line, accept user actions and inputs as arguments, and store the tasks in a JSON file. The user should be able to:
Add, Update, and Delete tasks
Mark a task as in progress or done
List all tasks
List all tasks that are done
List all tasks that are not done
List all tasks that are in progress
Cobra CLI docs
https://cobra.dev/docs/
https://cobra.dev/docs/tutorials/getting-started/
Install Cobra
$ go get -u github.com/spf13/cobra@latest
$ go install github.com/spf13/cobra-cli@latest
Create your application
$ cobra-cli init task-tracker
Your Cobra application is ready at task-tracker
Add commands to your application by using the `add` command
This creates a new directory called task-tracker with a complete Go module and basic Cobra application structure. Let’s explore what was created:
$ cd task-tracker
$ ls -la
total 16
drwxr-xr-x 6 jinx jinx 192 Aug 10 12:00 .
drwxr-xr-x 3 jinx jinx 96 Aug 10 12:00 ..
drwxr-xr-x 3 jinx jinx 96 Aug 10 12:00 cmd
-rw-r--r-- 1 jinx jinx 87 Aug 10 12:00 go.mod
-rw-r--r-- 1 jinx jinx 155 Aug 10 12:00 go.sum
-rw-r--r-- 1 jinx jinx 615 Aug 10 12:00 main.go
Test your base application
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
$ go run main.go --help
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
task-tracker [flags]
Flags:
-h, --help help for task-tracker
Add your first command
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ cobra-cli --help
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
cobra-cli [command]
Available Commands:
add Add a command to a Cobra Application
completion Generate the autocompletion script for the specified shell
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra-cli
-l, --license string name of license for the project
--viper use Viper for configuration
Use "cobra-cli [command] --help" for more information about a command.
$ cobra-cli add add
add created at cmd/add.go
$ go run main.go add
Add from your new Cobra CLI application!
This project involves implementing basic CRUD operations, Understanding the structure of our project:

add.go : adds a todo item
delete.go: deletes a todo item
list.go: list all todo items
markDone.go: marks an item as done
markInProgress.go: marks an item as in progress
update.go: updates an item's description
Add all the above files using command cobra-cli add <task-name>, this will automatically add files to the cmd folder, these files will just have the function calls to do a certain task.
Add a new file under folder tasks , storage.go this file will have logic to do actual tasks , i.e. function defination
Remember root.go is the main entrypoint for cli, here all child commands like add, delete,list,update etc are executed
Let us build this project bit by bit,
Since this is just a small project to learn CRUD operations in golang we’ll just have a local json file which will hold all the data, let us start with inserting a task that is add.go, if we try to execute this file it will just say add.go called, we’ll have to make few tweaks to the file
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"task-tracker/tasks"
"os"
)
// addCmd represents the add command
var addCmd = &cobra.Command{
Use: "add",
Short: "Add a new task",
Args: cobra.MinimumNArgs(1), // Ensure at least one argument is provided
Long: `Adds a new task to existing list. For example:
./task-tracker add "Buy groceries"`,
Run: func(cmd *cobra.Command, args []string) {
task, err := tasks.AddTask(args[0])
if err != nil {
fmt.Println("Error adding task:", err)
os.Exit(1)
}
fmt.Println("Task added successfully:", task.ID)
},
}
func init() {
rootCmd.AddCommand(addCmd)
}
Update the Short & Long args, these are just short & long description of the command, for adding a task , their must always be a description of the task that is one argument should always be there with task-tracker add <description>, Args: cobra.MinimumNArgs(1), this does the job
Run: func(cmd *cobra.Command,args []string){, this will be the entry point for the cli command add.
task, err := tasks.AddTask(args[0]): Calls theAddTaskfunction with the first argument (the task description) and returns the created task and any error.if err != nilthis callsos.Exit,which exits the program if there are errors.AddTaskwill be a function defined instorage.go, which will have logic to add todo item.
Similar to this we’ll have
delete.go: callsDeleteTask(id)function in tasks package. (deletes a task using ID)list.go: callsLoadTasks()function in tasks package. (lists all tasks)markDone.go: callsmarkDone()function in tasks packge.(mark a task as Done using ID)markInProgress.go: callsmarkTaskInProgress()function in tasks package. (marks a task as In Progress using ID)update.go: callsUpdateTask()function in tasks package. (update the description of a task using ID)
Remember root.go, is the parent file for all other go file command calls:
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "task-tracker",
Short: "Task Tracker CLI",
Long: `Task Tracker CLI is a command-line tool to manage your tasks efficiently.
It allows you to add, delete, and manage tasks with ease. You can mark tasks as done, in-progress, or to-do.
For example:
./task-tracker add "Buy groceries"
./task-tracker delete 1
./task-tracker mark-done 2
./task-tracker list
./task-tracker list done
./task-tracker list todo
./task-tracker list in-progress`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
Thinking about logic for adding a task item, we’ll first have to create a structure which contains key value pairs for tasks, then we’ll have a json file which stores the actual data, we’ll have to read/load the json file everytime, update with new data i.e. add new tasks/update existing tasks/delete any specific tasks , this is how the shared logic would look like in storage.json
// Shared task logic
package tasks
import (
"fmt"
"encoding/json"
"os"
"time"
)
// structure of the task storage file
type Task struct {
ID int `json:"id"`
Description string `json:"description"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// there will be a file to store tasks
const storageFile = "tasks.json"
// LoadTasks loads tasks from the storage file
func LoadTasks() ([]Task, error) {
data, err := os.ReadFile(storageFile)
if err != nil {
// If the file does not exist, return an empty slice
if os.IsNotExist(err) {
return []Task{}, nil
}
fmt.Println("Error reading tasks file:", err)
return nil, err
}
// If the file is empty, return an empty slice
if len(data) == 0 {
return []Task{}, nil // Return empty slice if file is empty
}
var tasks []Task
err = json.Unmarshal(data, &tasks)
if err != nil {
fmt.Println("Error unmarshalling tasks:", err)
return nil, err
}
return tasks,nil
}
// AddTask adds a new task to the storage file
func AddTask(Description string) (Task, error) {
// get existing tasks
tasks, err := LoadTasks()
if err != nil {
return Task{}, err
}
// Create a new task
newID := 1
for _, t := range tasks {
if t.ID >=newID {
newID = t.ID +1
}
}
currentTime := time.Now().Format(time.RFC3339)
task := Task{
ID: newID,
Description: Description,
Status: "TO-DO",
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
tasks = append(tasks, task)
// We have entire tasks list, rewrite this to the file
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return Task{}, err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return Task{}, err
}
return task, nil
}
// DeleteTask deletes a task by ID
func DeleteTask(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
// Update existing task by ID
func UpdateTask(id int, Description string) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task
t.Description = Description
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
//Mark a task as done by ID
func MarkTaskDone(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task as Done
t.Status = "DONE"
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
//Mark a task as done by ID
func MarkTaskInProgress(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task as Done
t.Status = "IN-PROGRESS"
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
Let’s break down each line and try to understand working of this file,
package tasks
import (
"fmt"
"encoding/json"
"os"
"time"
)
Since this file is in inside a tasks folder , we’ll need to say package tasks, then importing certain packages which will be useful later, fmt(print stuff), encoding/json (read/write/load json files), os (use os commands like exit), time (get current time), can be found here: https://pkg.go.dev/
Structure of a Task item will have ID(int), Description, Status, CreatedAt & UpdatedAt. We’ll also have a constant variable for the filename.
// structure of the task storage file
type Task struct {
ID int `json:"id"`
Description string `json:"description"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// there will be a file to store tasks
const storageFile = "tasks.json"
For playing around with json, it is important to understand marshalling & unmarshalling in golang.
Marshalling (Struct → JSON)
When you call
data, err := json.MarshalIndent(tasks, "", " ")
You’re telling Go:
“Take my
tasksslice (a Go object) and convert it into a JSON-formatted byte array.”
Example:
tasks := []Task{
{ID: 1, Description: "Buy milk", Done: false},
}
data, _ := json.MarshalIndent(tasks, "", " ")
fmt.Println(string(data))
Output:
[
{
"ID": 1,
"Description": "Buy milk",
"Done": false
}
]
You then write this data to a file:
os.WriteFile("tasks.json", data, 0644)
Unmarshalling (JSON → Struct)
When your CLI restarts, you need to load that saved data back into Go structs.
data, err := os.ReadFile("tasks.json")
json.Unmarshal(data, &tasks)
Here, Go reads the JSON bytes and fills your tasks slice with values.
Example:
var tasks []Task
data := []byte(`[{"ID":1,"Description":"Buy milk","Done":false}]`)
json.Unmarshal(data, &tasks)
fmt.Println(tasks[0].Description) // Output: Buy milk
When you’re building your Task Tracker, you need to store tasks (Go structs) in a file (tasks.json) and later load them back into Go structs when your CLI runs again.
That’s exactly where marshalling and unmarshalling come in.
Let us first try to read/load the jsonfile.
LoadTasks
// LoadTasks loads tasks from the storage file
func LoadTasks() ([]Task, error) {
data, err := os.ReadFile(storageFile)
if err != nil {
// If the file does not exist, return an empty slice
if os.IsNotExist(err) {
return []Task{}, nil
}
fmt.Println("Error reading tasks file:", err)
return nil, err
}
// If the file is empty, return an empty slice
if len(data) == 0 {
return []Task{}, nil // Return empty slice if file is empty
}
var tasks []Task
err = json.Unmarshal(data, &tasks)
if err != nil {
fmt.Println("Error unmarshalling tasks:", err)
return nil, err
}
return tasks,nil
}
function loadtasks returns a structure task i.e. items from the file and a err (if there are any errors during execution of the file)
data, err := os.ReadFile(storageFile): reads the file using os package, data stores the actual data from the file ,nextif err != nil {if there are some errors we’ll have to return the errors or if the file does not exists we have to intialize the data with a empty task structure which is done in lineif os.IsNotExist(err)which returns[]Task{}, nilempty task structure and nil as error, if the err is not nil due to empty file then there is an error while reading the file, so return the error here.Next would be a case if the file is empty check is done via
if len(data) == 0 {in case also we’ll return a empty task structure with nil error.(return []Task{}, nil)After reading/loading the data from the json file we’ll decode/unmarshal the data using json package
var tasks []Task: Declares a variable tasks as a slice ofTaskstructs. In Go, a slice is a reference type (pointer to an underlying array).err = json.Unmarshal(data, &tasks): Uses the address-of operator (&tasks) to pass a pointer to the tasks slice tojson.Unmarshal. This allowsjson.Unmarshalto fill the slice with decoded data fromdata.Next we handle error gracefully
return nil, err: Returnsniland the error if unmarshalling fails.return tasks,nil: Returns the populated tasks slice andnilerror if successful.
Using this function can list all tasks, but we also have to give filtered list like :
./task-tracker list done
./task-tracker list todo
./task-tracker list in-progress
To add sub commands ,do
cobra-cli add done --parent listcobra-cli add todo --parent listcobra-cli add in-progress --parent list
This will add the subflags in list.go file:
/*
Copyright © 2025 Pranav <pranavppatil767@gmail.com>
*/
package cmd
import (
"fmt"
"task-tracker/tasks"
"github.com/spf13/cobra"
)
// listCmd represents the list command
var listCmd = &cobra.Command{
Use: "list",
Short: "List all tasks",
Long: `List all tasks. For example:
./task-tracker list`,
Run: func(cmd *cobra.Command, args []string) {
task, err := tasks.LoadTasks()
if err != nil {
fmt.Println("Error loading tasks:", err)
}
for _, t := range task {
fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n", t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
}
},
}
var listDoneCmd = &cobra.Command{
Use: "done",
Short: "List all tasks that are marked as done",
Long: `A Subcommand for list which lists out all tasks marked as done For example:
./task-tracker list done`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("list done called")
task,err := tasks.LoadTasks()
if err != nil {
fmt.Println("Error loading tasks:", err)
}
for _, t := range task {
if t.Status == "DONE" {
fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
}
}
},
}
var listToDoCmd = &cobra.Command{
Use: "todo",
Short: "List all tasks that are marked as To-Do",
Long: `A Subcommand for list which lists out all tasks marked as To-Do For example:
./task-tracker list todo`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("list todo called")
task,err := tasks.LoadTasks()
if err != nil {
fmt.Println("Error loading tasks:", err)
}
for _, t := range task {
if t.Status == "To-DO" {
fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
}
}
},
}
var listInProgressCmd = &cobra.Command{
Use: "in-progress",
Short: "List all tasks that are marked as In-Progress",
Long: `A Subcommand for list which lists out all tasks marked as IN-PROGRESS. For example:
./task-tracker list in-progress`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("list in-progress called")
task,err := tasks.LoadTasks()
if err != nil {
fmt.Println("Error loading tasks:", err)
}
for _, t := range task {
if t.Status == "IN-PROGRESS" {
fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
}
}
},
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.AddCommand(listDoneCmd)
listCmd.AddCommand(listToDoCmd)
listCmd.AddCommand(listInProgressCmd)
}
The only difference between just a list command and list done command will be we’ll iterate through each task in the loaded list and use a for loop to get only tasks which are in status DONE/TODO/IN-PROGRESS, and print details for those tasks.
AddTask
After loading existing data we will try to add a new task using function AddTask, for adding a task we’ll have to create a unique ID and add the description from user input.
// AddTask adds a new task to the storage file
func AddTask(Description string) (Task, error) {
// get existing tasks
tasks, err := LoadTasks()
if err != nil {
return Task{}, err
}
// Create a new task
newID := 1
for _, t := range tasks {
if t.ID >=newID {
newID = t.ID +1
}
}
currentTime := time.Now().Format(time.RFC3339)
task := Task{
ID: newID,
Description: Description,
Status: "TO-DO",
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
tasks = append(tasks, task)
// We have entire tasks list, rewrite this to the file
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return Task{}, err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return Task{}, err
}
return task, nil
}
AddTask takes in description as a string and returns the task struct & err if any present.
Loads existing tasks using
LoadTasks()function, if loading fails return empty task and the errorNext the for loop determines a unique ID for the new task by finding the highest existing ID and adding 1.
Get the current time in RFC3339 format for timestamps.
Create a new
Taskstructure with the provided description, status asTO-DOand timestamps.Appends the new task to the list of tasks.
Serializes the updated task list to JSON using
json.MarshalIndentIf serialization fails, returns an empty
Taskand the error.Write the JSON data back to the storage file , if writing fails returns and empty
taskand the error.Returns the newly created task & nil error if succesfull.
DeleteTask
Delete task deletes a task using ID
func DeleteTask(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
Deletetask takes input as a ID and returns any errors if found while deleting a task
Loads existing tasks using
LoadTasks()function, if loading fails return empty task and the errorCreates a new slice
newTasksto hold tasks that are not being deleted.([]Task : is a slice of task structure)Iterates through each task in the loaded list, appends tasks to
newTasksonly if their ID does not match the givenidSerialized the filtered list
newTasksto JSON usingjson.MarshalIndent, if serialization fails returns the errorWrites the updates JSON data back to storageFile, if writing fails returns the error.
Returns nil if the deletion & file update are successfull.
UpdateTask
To update a task, user will provide a ID of the task and the new description, we’ll have to update the description & UpdatedAt timestamp.
func UpdateTask(id int, Description string) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task
t.Description = Description
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
Similar to addtask:
Loads existing tasks using
LoadTasks()function, if loading fails return empty task and the errorCreates a new slice
newTasksof structtaskto hold all tasks.Iterates through each task in the loaded list, appends tasks to
newTasksonly if their ID does not match the givenid, this will keep original task list as it was, for the matching ID we’ll update theDescriptionand theUpdatedAttimestamp.Serialized the filtered list
newTasksto JSON usingjson.MarshalIndent, if serialization fails returns the errorWrites the updates JSON data back to storageFile, if writing fails returns the error.
Returns nil if the updating task & file update are successfull.
MarkTaskDone
MarkTaskDone this function takes a task id and updates the status from In-progress to Done, we’ll have to update 2 fields in the tasks structure : status & UpdatedAt.
func MarkTaskDone(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task as Done
t.Status = "DONE"
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
Loads existing tasks using
LoadTasks()function, if loading fails return empty task and the errorCreates a new slice
newTasksof structtaskto hold all tasks.Iterates through each task in the loaded list, appends tasks to
newTasksonly if their ID does not match the givenid, this will keep original task list as it was, for the matching ID we’ll update thestatusasDoneand theUpdatedAttimestamp withcurrenttimestamp.Serialized the filtered list
newTasksto JSON usingjson.MarshalIndent, if serialization fails returns the errorWrites the updates JSON data back to storageFile, if writing fails returns the error.
Returns nil if the updating task & file update are successfull.
MarkTaskInProgress
MarkTaskInProgress this function takes a task id and updates the status from In-progress to Done, we’ll have to update 2 fields in the tasks structure : status & UpdatedAt.
func MarkTaskInProgress(id int) error {
tasks, err := LoadTasks()
newTasks := make([]Task, 0, len(tasks))
for _,t := range tasks{
if t.ID !=id {
newTasks = append(newTasks, t)
} else {
// Update the task as Done
t.Status = "IN-PROGRESS"
t.UpdatedAt = time.Now().Format(time.RFC3339)
newTasks = append(newTasks, t)
}
}
data, err := json.MarshalIndent(newTasks, "", " ")
if err != nil {
return err
}
err = os.WriteFile(storageFile, data, 0644)
if err != nil {
return err
}
return nil
}
Loads existing tasks using
LoadTasks()function, if loading fails return empty task and the errorCreates a new slice
newTasksof structtaskto hold all tasks.Iterates through each task in the loaded list, appends tasks to
newTasksonly if their ID does not match the givenid, this will keep original task list as it was, for the matching ID we’ll update thestatusasIn-progressand theUpdatedAttimestamp withcurrenttimestamp.Serialized the filtered list
newTasksto JSON usingjson.MarshalIndent, if serialization fails returns the errorWrites the updates JSON data back to storageFile, if writing fails returns the error.
Returns nil if the updating task & file update are successfull.
We now have all things in place, each command like add,list,delete have a file inside cmd folder, which will have logic for cli command calls and the actual logic for CRUD operations on the tasks will be in tasks/storage.go.
Moment of truth
let’s test this out now.
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ go build -o task-tracker
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker add "Buy groceries"
Task added successfully: 1
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:35:46+05:30
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker add "Buy PS5"
Task added successfully: 2
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:35:46+05:30
ID: 2, Description: Buy PS5, Status: TO-DO, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:38:39+05:30
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker update 1 "Buy groceries and cook dinner"
Task Updated successfully: 1
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30
ID: 2, Description: Buy PS5, Status: TO-DO, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:38:39+05:30
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker mark-in-progress 2
Task Updated successfully: 2
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30
ID: 2, Description: Buy PS5, Status: IN-PROGRESS, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:39:35+05:30
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list todo
list todo called
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker delete 1
Task deleted successfully: 1
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 2, Description: Buy PS5, Status: IN-PROGRESS, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:39:35+05:30
Works as expected 🥳🥳🥳
Ciao 👋



