diff --git a/cmd/generate/codeowners/codeowners.go b/cmd/generate/codeowners/codeowners.go
index 20491dd..2f4bbdc 100644
--- a/cmd/generate/codeowners/codeowners.go
+++ b/cmd/generate/codeowners/codeowners.go
@@ -11,10 +11,6 @@ import (
 	"github.com/jpmcb/gopherlogs/pkg/colors"
 	"github.com/spf13/cobra"
 
-	"github.com/open-sauced/pizza-cli/api"
-	"github.com/open-sauced/pizza-cli/api/auth"
-	"github.com/open-sauced/pizza-cli/api/services/workspaces"
-	"github.com/open-sauced/pizza-cli/api/services/workspaces/userlists"
 	"github.com/open-sauced/pizza-cli/pkg/config"
 	"github.com/open-sauced/pizza-cli/pkg/constants"
 	"github.com/open-sauced/pizza-cli/pkg/logging"
@@ -33,9 +29,6 @@ type Options struct {
 	// the number of days to look back
 	previousDays int
 
-	// the session token adding codeowners to a workspace contributor list
-	token string
-
 	logger   gopherlogs.Logger
 	tty      bool
 	loglevel int
@@ -194,188 +187,9 @@ func run(opts *Options, cmd *cobra.Command) error {
 	opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Finished generating file: %s\n", outputPath)
 	_ = opts.telemetry.CaptureCodeownersGenerate()
 
-	// ignore the interactive prompts for CI/CD environments
-	if opts.tty {
-		return nil
-	}
-
-	// 1. Ask if they want to add users to a list
-	var input string
-	fmt.Print("Do you want to add these codeowners to an OpenSauced Contributor Insight? (y/n): ")
-	_, err = fmt.Scanln(&input)
-	if err != nil {
-		return fmt.Errorf("could not scan input from terminal: %w", err)
-	}
-
-	switch input {
-	case "y", "Y", "yes":
-		opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Adding codeowners to contributor insight\n")
-	case "n", "N", "no":
-		return nil
-	default:
-		return errors.New("invalid answer. Please enter y or n")
-	}
-
-	// 2. Check if user is logged in. Log them in if not.
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Initiating log in flow\n")
-	authenticator := auth.NewAuthenticator()
-	err = authenticator.CheckSession()
-	if err != nil {
-		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Log in session invalid: %s\n", err)
-		fmt.Print("Do you want to log into OpenSauced? (y/n): ")
-		_, err := fmt.Scanln(&input)
-		if err != nil {
-			return fmt.Errorf("could not scan input from terminal: %w", err)
-		}
-
-		switch input {
-		case "y", "Y", "yes":
-			user, err := authenticator.Login()
-			if err != nil {
-				_ = opts.telemetry.CaptureFailedCodeownersGenerateAuth()
-				opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error logging in\n")
-				return fmt.Errorf("could not log in: %w", err)
-			}
-			_ = opts.telemetry.CaptureCodeownersGenerateAuth(user)
-			opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Logged in as: %s\n", user)
-
-		case "n", "N", "no":
-			return nil
-
-		default:
-			return errors.New("invalid answer. Please enter y or n")
-		}
-	}
-
-	opts.token, err = authenticator.GetSessionToken()
-	if err != nil {
-		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
-		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error getting session token\n")
-		return fmt.Errorf("could not get session token: %w", err)
-	}
-
-	listName := filepath.Base(opts.path)
-
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up OpenSauced workspace: Pizza CLI\n")
-	workspace, err := findCreatePizzaCliWorkspace(opts)
-	if err != nil {
-		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
-		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace: Pizza CLI\n")
-		return fmt.Errorf("could not find Pizza CLI workspace: %w", err)
-	}
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Found workspace: Pizza CLI\n")
-
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up Contributor Insight for local repository: %s\n", listName)
-	userList, err := updateCreateLocalWorkspaceUserList(opts, listName, workspace, codeowners)
-	if err != nil {
-		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
-		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace Contributor Insight: %s\n", listName)
-		return fmt.Errorf("could not find Workspace Contributor Insight: %s - %w", listName, err)
-	}
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Updated Contributor Insight for local repository: %s\n", listName)
-	opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("Access list on OpenSauced:\n%s\n", fmt.Sprintf("https://app.opensauced.pizza/workspaces/%s/contributor-insights/%s", workspace.ID, userList.ID))
+	opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("\nCreate an OpenSauced Contributor Insight to get metrics and insights on these codeowners:\n")
+	opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("$ pizza generate insight " + opts.path + "\n")
 	_ = opts.telemetry.CaptureCodeownersGenerateContributorInsight()
 
 	return nil
 }
-
-// findCreatePizzaCliWorkspace finds or creates a "Pizza CLI" workspace
-// for the authenticated user
-func findCreatePizzaCliWorkspace(opts *Options) (*workspaces.DbWorkspace, error) {
-	nextPage := true
-	page := 1
-	apiClient := api.NewClient("https://api.opensauced.pizza")
-
-	for nextPage {
-		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user workspaces page: %d\n", page)
-		workspaceResp, _, err := apiClient.WorkspacesService.GetWorkspaces(opts.token, page, 100)
-		if err != nil {
-			return nil, err
-		}
-
-		for _, workspace := range workspaceResp.Data {
-			if workspace.Name == "Pizza CLI" {
-				opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing workspace named: Pizza CLI\n")
-				return &workspace, nil
-			}
-		}
-
-		nextPage = workspaceResp.Meta.HasNextPage
-		page++
-	}
-
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user workspace: Pizza CLI\n")
-	newWorkspace, _, err := apiClient.WorkspacesService.CreateWorkspaceForUser(opts.token, "Pizza CLI", "A workspace for the Pizza CLI", []string{})
-	if err != nil {
-		return nil, err
-	}
-
-	return newWorkspace, nil
-}
-
-// updateCreateLocalWorkspaceUserList updates or creates a workspace contributor list
-// for the authenticated user with the given codeowners
-func updateCreateLocalWorkspaceUserList(opts *Options, listName string, workspace *workspaces.DbWorkspace, codeowners FileStats) (*userlists.DbUserList, error) {
-	nextPage := true
-	page := 1
-	apiClient := api.NewClient("https://api.opensauced.pizza")
-
-	var targetUserListID string
-
-	for nextPage {
-		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user Workspace Contributor Insight page: %d\n", page)
-		userListsResp, _, err := apiClient.WorkspacesService.UserListService.GetUserLists(opts.token, workspace.ID, page, 100)
-		if err != nil {
-			return nil, err
-		}
-
-		nextPage = userListsResp.Meta.HasNextPage
-		page++
-
-		for _, userList := range userListsResp.Data {
-			if userList.Name == listName {
-				opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing Workspace Contributor Insight named: %s\n", listName)
-				targetUserListID = userList.ID
-				nextPage = false
-			}
-		}
-	}
-
-	if targetUserListID == "" {
-		var err error
-
-		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user Workspace Contributor List: %s\n", listName)
-		createdUserList, _, err := apiClient.WorkspacesService.UserListService.CreateUserListForUser(opts.token, workspace.ID, listName, []string{})
-		if err != nil {
-			return nil, err
-		}
-
-		targetUserListID = createdUserList.UserListID
-	}
-
-	targetUserList, _, err := apiClient.WorkspacesService.UserListService.GetUserList(opts.token, workspace.ID, targetUserListID)
-	if err != nil {
-		return nil, err
-	}
-
-	// create a mapping of author logins to empty structs (i.e., a unique set).
-	// this de-structures the { filename: author-stats } mapping that originally
-	// built the codeowners
-	uniqueLogins := make(map[string]struct{})
-	for _, codeowner := range codeowners {
-		for _, k := range codeowner {
-			if k.GitHubAlias != "" {
-				uniqueLogins[k.GitHubAlias] = struct{}{}
-			}
-		}
-	}
-
-	logins := []string{}
-	for login := range uniqueLogins {
-		logins = append(logins, login)
-	}
-
-	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Updating Contributor Insight with codeowners with GitHub aliases: %v\n", logins)
-	userlist, _, err := apiClient.WorkspacesService.UserListService.PatchUserListForUser(opts.token, workspace.ID, targetUserList.ID, targetUserList.Name, logins)
-	return userlist, err
-}
diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go
index 13d7396..47edbd0 100644
--- a/cmd/generate/generate.go
+++ b/cmd/generate/generate.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/open-sauced/pizza-cli/cmd/generate/codeowners"
 	"github.com/open-sauced/pizza-cli/cmd/generate/config"
+	"github.com/open-sauced/pizza-cli/cmd/generate/insight"
 )
 
 const generateLongDesc string = `The 'generate' command provides tools to automate the creation of important project documentation and derive insights from your codebase.`
@@ -28,6 +29,7 @@ func NewGenerateCommand() *cobra.Command {
 
 	cmd.AddCommand(codeowners.NewCodeownersCommand())
 	cmd.AddCommand(config.NewConfigCommand())
+	cmd.AddCommand(insight.NewGenerateInsightCommand())
 
 	return cmd
 }
diff --git a/cmd/generate/insight/insight.go b/cmd/generate/insight/insight.go
new file mode 100644
index 0000000..5769316
--- /dev/null
+++ b/cmd/generate/insight/insight.go
@@ -0,0 +1,332 @@
+package insight
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
+
+	"github.com/jpmcb/gopherlogs"
+	"github.com/jpmcb/gopherlogs/pkg/colors"
+	"github.com/spf13/cobra"
+
+	"github.com/open-sauced/pizza-cli/api"
+	"github.com/open-sauced/pizza-cli/api/auth"
+	"github.com/open-sauced/pizza-cli/api/services/workspaces"
+	"github.com/open-sauced/pizza-cli/api/services/workspaces/userlists"
+	"github.com/open-sauced/pizza-cli/pkg/constants"
+	"github.com/open-sauced/pizza-cli/pkg/logging"
+	"github.com/open-sauced/pizza-cli/pkg/utils"
+)
+
+// Options for the codeowners generation command
+type Options struct {
+	// the path to the git repository on disk to generate a codeowners file for
+	path string
+
+	logger   gopherlogs.Logger
+	tty      bool
+	loglevel int
+
+	token string
+
+	// telemetry for capturing CLI events via PostHog
+	telemetry *utils.PosthogCliClient
+}
+
+const insightLongDesc string = `Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file
+to get metrics and insights on those users.
+
+The provided path must be a local git repo with a valid CODEOWNERS file and GitHub "@login"
+for each codeowner.
+
+After logging in, the generated Contributor Insight on OpenSauced will have insights on
+active contributors, contributon velocity, and more.`
+
+const insightExamples string = `  # Use CODEOWNERS file in explicit directory
+  $ pizza generate insight /path/to/repo
+
+  # Use CODEOWNERS file in local directory
+  $ pizza generate insight .`
+
+func NewGenerateInsightCommand() *cobra.Command {
+	opts := &Options{}
+
+	cmd := &cobra.Command{
+		Use:     "insight path/to/repo/with/CODEOWNERS/file [flags]",
+		Short:   "Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file",
+		Long:    insightLongDesc,
+		Example: insightExamples,
+		Args: func(_ *cobra.Command, args []string) error {
+			if len(args) != 1 {
+				return errors.New("you must provide exactly one argument: the path to a repository with a codeowners file")
+			}
+
+			path := args[0]
+
+			// Validate that the path is a real path on disk and accessible by the user
+			absPath, err := filepath.Abs(path)
+			if err != nil {
+				return err
+			}
+
+			if _, err := os.Stat(absPath); os.IsNotExist(err) {
+				return fmt.Errorf("the provided path does not exist: %w", err)
+			}
+
+			opts.path = absPath
+			return nil
+		},
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			var err error
+
+			disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry)
+
+			opts.telemetry = utils.NewPosthogCliClient(!disableTelem)
+			opts.tty, _ = cmd.Flags().GetBool("tty-disable")
+
+			loglevelS, _ := cmd.Flags().GetString("log-level")
+
+			switch loglevelS {
+			case "error":
+				opts.loglevel = logging.LogError
+			case "warn":
+				opts.loglevel = logging.LogWarn
+			case "info":
+				opts.loglevel = logging.LogInfo
+			case "debug":
+				opts.loglevel = logging.LogDebug
+			}
+
+			err = run(opts, cmd)
+
+			_ = opts.telemetry.Done()
+
+			return err
+		},
+	}
+
+	return cmd
+}
+
+func run(opts *Options, _ *cobra.Command) error {
+	var err error
+	opts.logger, err = gopherlogs.NewLogger(
+		gopherlogs.WithLogVerbosity(opts.loglevel),
+		gopherlogs.WithTty(!opts.tty),
+	)
+	if err != nil {
+		return fmt.Errorf("could not build logger: %w", err)
+	}
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Built logger with log level: %d\n", opts.loglevel)
+
+	codeowners, err := getUniqueCodeowners(filepath.Join(opts.path, "CODEOWNERS"))
+	if err != nil {
+		return fmt.Errorf("could not get codeowners from file: %s - %w", opts.path, err)
+	}
+
+	// 1. Ask if they want to add users to a list
+	var input string
+	fmt.Print("Do you want to add these codeowners to an OpenSauced Contributor Insight? (y/n): ")
+	_, err = fmt.Scanln(&input)
+	if err != nil {
+		return fmt.Errorf("could not scan input from terminal: %w", err)
+	}
+
+	switch input {
+	case "y", "Y", "yes":
+		opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Adding codeowners to Contributor Insight\n")
+	case "n", "N", "no":
+		return nil
+	default:
+		return errors.New("invalid answer. Please enter y or n")
+	}
+
+	// 2. Check if user is logged in. Log them in if not.
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Initiating log in flow\n")
+	authenticator := auth.NewAuthenticator()
+	err = authenticator.CheckSession()
+	if err != nil {
+		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Log in session invalid: %s\n", err)
+		fmt.Print("Do you want to log into OpenSauced? (y/n): ")
+		_, err := fmt.Scanln(&input)
+		if err != nil {
+			return fmt.Errorf("could not scan input from terminal: %w", err)
+		}
+
+		switch input {
+		case "y", "Y", "yes":
+			user, err := authenticator.Login()
+			if err != nil {
+				_ = opts.telemetry.CaptureFailedCodeownersGenerateAuth()
+				opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error logging in\n")
+				return fmt.Errorf("could not log in: %w", err)
+			}
+			_ = opts.telemetry.CaptureCodeownersGenerateAuth(user)
+			opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Logged in as: %s\n", user)
+
+		case "n", "N", "no":
+			return nil
+
+		default:
+			return errors.New("invalid answer. Please enter y or n")
+		}
+	}
+
+	opts.token, err = authenticator.GetSessionToken()
+	if err != nil {
+		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
+		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error getting session token\n")
+		return fmt.Errorf("could not get session token: %w", err)
+	}
+
+	listName := filepath.Base(opts.path)
+
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up OpenSauced workspace: Pizza CLI\n")
+	workspace, err := findCreatePizzaCliWorkspace(opts)
+	if err != nil {
+		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
+		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace: Pizza CLI\n")
+		return fmt.Errorf("could not find Pizza CLI workspace: %w", err)
+	}
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Found workspace: Pizza CLI\n")
+
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up Contributor Insight for local repository: %s\n", listName)
+	userList, err := updateCreateLocalWorkspaceUserList(opts, listName, workspace, codeowners)
+	if err != nil {
+		_ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight()
+		opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace Contributor Insight: %s\n", listName)
+		return fmt.Errorf("could not find Workspace Contributor Insight: %s - %w", listName, err)
+	}
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Updated Contributor Insight for local repository: %s\n", listName)
+	opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("\nAccess Contributor Insight on OpenSauced:\n%s\n", fmt.Sprintf("https://app.opensauced.pizza/workspaces/%s/contributor-insights/%s", workspace.ID, userList.ID))
+	_ = opts.telemetry.CaptureCodeownersGenerateContributorInsight()
+
+	return nil
+}
+
+func getUniqueCodeowners(path string) ([]string, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, fmt.Errorf("error opening file: %w", err)
+	}
+	defer file.Close()
+
+	// Create a map to store unique uniqueLogins
+	uniqueLogins := make(map[string]struct{})
+
+	// Create a regular expression to match GitHub usernames
+	re := regexp.MustCompile(`@(\w+)`)
+
+	// Read the file line by line
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+
+		// Find all matches in the line
+		matches := re.FindAllStringSubmatch(line, -1)
+
+		// Add each match to the map
+		for _, match := range matches {
+			uniqueLogins[match[1]] = struct{}{}
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, fmt.Errorf("error reading file: %w", err)
+	}
+
+	logins := []string{}
+	for login := range uniqueLogins {
+		logins = append(logins, login)
+	}
+
+	fmt.Printf("%v\n", logins)
+	return logins, nil
+}
+
+// findCreatePizzaCliWorkspace finds or creates a "Pizza CLI" workspace
+// for the authenticated user
+func findCreatePizzaCliWorkspace(opts *Options) (*workspaces.DbWorkspace, error) {
+	nextPage := true
+	page := 1
+	apiClient := api.NewClient("https://api.opensauced.pizza")
+
+	for nextPage {
+		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user workspaces page: %d\n", page)
+		workspaceResp, _, err := apiClient.WorkspacesService.GetWorkspaces(opts.token, page, 100)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, workspace := range workspaceResp.Data {
+			if workspace.Name == "Pizza CLI" {
+				opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing workspace named: Pizza CLI\n")
+				return &workspace, nil
+			}
+		}
+
+		nextPage = workspaceResp.Meta.HasNextPage
+		page++
+	}
+
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user workspace: Pizza CLI\n")
+	newWorkspace, _, err := apiClient.WorkspacesService.CreateWorkspaceForUser(opts.token, "Pizza CLI", "A workspace for the Pizza CLI", []string{})
+	if err != nil {
+		return nil, err
+	}
+
+	return newWorkspace, nil
+}
+
+// updateCreateLocalWorkspaceUserList updates or creates a workspace contributor list
+// for the authenticated user with the given codeowners
+func updateCreateLocalWorkspaceUserList(opts *Options, listName string, workspace *workspaces.DbWorkspace, logins []string) (*userlists.DbUserList, error) {
+	nextPage := true
+	page := 1
+	apiClient := api.NewClient("https://api.opensauced.pizza")
+
+	var targetUserListID string
+
+	for nextPage {
+		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user Workspace Contributor Insight page: %d\n", page)
+		userListsResp, _, err := apiClient.WorkspacesService.UserListService.GetUserLists(opts.token, workspace.ID, page, 100)
+		if err != nil {
+			return nil, err
+		}
+
+		nextPage = userListsResp.Meta.HasNextPage
+		page++
+
+		for _, userList := range userListsResp.Data {
+			if userList.Name == listName {
+				opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing Workspace Contributor Insight named: %s\n", listName)
+				targetUserListID = userList.ID
+				nextPage = false
+			}
+		}
+	}
+
+	if targetUserListID == "" {
+		var err error
+
+		opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user Workspace Contributor List: %s\n", listName)
+		createdUserList, _, err := apiClient.WorkspacesService.UserListService.CreateUserListForUser(opts.token, workspace.ID, listName, []string{})
+		if err != nil {
+			return nil, err
+		}
+
+		targetUserListID = createdUserList.UserListID
+	}
+
+	targetUserList, _, err := apiClient.WorkspacesService.UserListService.GetUserList(opts.token, workspace.ID, targetUserListID)
+	if err != nil {
+		return nil, err
+	}
+
+	opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Updating Contributor Insight with codeowners with GitHub aliases: %v\n", logins)
+	userlist, _, err := apiClient.WorkspacesService.UserListService.PatchUserListForUser(opts.token, workspace.ID, targetUserList.ID, targetUserList.Name, logins)
+	return userlist, err
+}