<|start_filename|>core/dao/print_tree.go<|end_filename|>
package dao

import (
	"fmt"
	"strings"

	"github.com/jedib0t/go-pretty/v6/list"

	"github.com/alajmo/mani/core"
)

func PrintTree(config *Config, treeFlags *core.TreeFlags, tree []core.TreeNode) {
	theme, err := config.GetTheme(treeFlags.Theme)
	core.CheckIfError(err)

	switch theme.Tree {
	case "bullet-square":
		core.TreeStyle = list.StyleBulletSquare
	case "bullet-circle":
		core.TreeStyle = list.StyleBulletCircle
	case "bullet-star":
		core.TreeStyle = list.StyleBulletStar
	case "connected-bold":
		core.TreeStyle = list.StyleConnectedBold
	default: // connected-light
		core.TreeStyle = list.StyleConnectedLight
	}

	l := list.NewWriter()
	l.SetStyle(core.TreeStyle)
	printTreeNodes(l, tree, 0)

	switch treeFlags.Output {
	case "markdown":
		printTree(l.RenderMarkdown())
	case "html":
		printTree(l.RenderHTML())
	default:
		printTree(l.Render())
	}
}

func printTreeNodes(l list.Writer, tree []core.TreeNode, depth int) {
	for _, n := range tree {
		for i := 0; i < depth; i++ {
			l.Indent()
		}

		l.AppendItem(n.Name)
		printTreeNodes(l, n.Children, depth+1)

		for i := 0; i < depth; i++ {
			l.UnIndent()
		}
	}
}

func printTree(content string) {
	for _, line := range strings.Split(content, "\n") {
		fmt.Printf("%s\n", line)
	}
	fmt.Println()
}


<|start_filename|>cmd/init.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func initCmd() *cobra.Command {
	var initFlags core.InitFlags

	cmd := cobra.Command{
		Use:   "init",
		Short: "Initialize a mani repository",
		Long: `Initialize a mani repository.

Creates a mani repository - a directory with configuration file mani.yaml and a .gitignore file.`,
		Example: `  # Basic example
  mani init

  # Skip auto-discovery of projects
  mani init --auto-discovery=false`,

		Args: cobra.MaximumNArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			dao.InitMani(args, initFlags)
		},
	}

	cmd.Flags().BoolVar(&initFlags.AutoDiscovery, "auto-discovery", true, "walk current directory and find git repositories to add to mani.yaml")

	return &cmd
}


<|start_filename|>Makefile<|end_filename|>
NAME    := mani
PACKAGE := github.com/alajmo/$(NAME)
DATE    := $(shell date +%FT%T%Z)
GIT     := $(shell [ -d .git ] && git rev-parse --short HEAD)
VERSION := v0.10.0

default: build

tidy:
	go get -u && go mod tidy

lint:
	golangci-lint run ./cmd/... ./core/...

test:
	golangci-lint run
	./test/scripts/test --build --count 5 --clean

build:
	CGO_ENABLED=0 go build \
	-ldflags "-w -X ${PACKAGE}/cmd.version=${VERSION} -X ${PACKAGE}/cmd.commit=${GIT} -X ${PACKAGE}/cmd.date=${DATE}" \
	-a -tags netgo -o dist/${NAME} main.go

build-all:
	goreleaser --rm-dist --snapshot

build-test:
	CGO_ENABLED=0 go build \
	-ldflags "-X '${PACKAGE}/core/dao.build_mode=TEST'" \
	-a -tags netgo -o dist/${NAME} main.go

build-exec:
	./test/scripts/exec

build-and-link:
	go build \
		-ldflags "-w -X ${PACKAGE}/cmd.version=${VERSION} -X ${PACKAGE}/cmd.commit=${GIT} -X ${PACKAGE}/cmd.date=${DATE}" \
		-a -tags netgo -o dist/${NAME} main.go
	cp ./dist/mani ~/.local/bin/mani
	./dist/mani completion bash > ~/workstation/code/scripts/completions/mani-completion.sh

release:
	git tag ${VERSION} && git push origin ${VERSION}

clean:
	$(RM) -r dist target

.PHONY: lint test interactive build build-all build-test release clean


<|start_filename|>test/integration/exec_test.go<|end_filename|>
package integration

import (
	"testing"
)

var execTests = []TemplateTest{
	{
		TestName:   "Should fail to exec when no configuration file found",
		InputFiles: []string{},
		TestCmd: `
			mani exec --all-projects -o table ls
		`,
		Golden:  "exec/no-config",
		WantErr: true,
	},

	{
		TestName:   "Should exec in zero projects",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			mani exec -o table ls
		`,
		Golden:  "exec/zero",
		WantErr: false,
	},

	{
		TestName:   "Should exec in all projects",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			mani exec --all-projects -o table ls
		`,
		Golden:  "exec/all",
		WantErr: false,
	},

	{
		TestName:   "Should exec when filtered on project name",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			mani exec -o table --projects pinto ls
		`,
		Golden:  "exec/filter-on-1-project",
		WantErr: false,
	},

	{
		TestName:   "Should exec when filtered on tags",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			mani exec -o table --tags frontend ls
		`,
		Golden:  "exec/filter-on-1-tag",
		WantErr: false,
	},

	{
		TestName:   "Should exec when filtered on cwd",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			cd template-generator
			mani exec -o table --cwd pwd
		`,
		Golden:  "exec/filter-on-cwd",
		WantErr: false,
	},

	{
		TestName:   "Should dry run exec",
		InputFiles: []string{"mani-advanced/mani.yaml", "mani-advanced/.gitignore"},
		TestCmd: `
			mani sync
			mani exec -o table --dry-run --projects template-generator pwd
		`,
		Golden:  "exec/dry-run",
		WantErr: false,
	},
}

func TestExecCmd(t *testing.T) {
	for _, tt := range execTests {
		t.Run(tt.TestName, func(t *testing.T) {
			Run(t, tt)
		})
	}
}


<|start_filename|>core/dao/dir.go<|end_filename|>
package dao

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/alajmo/mani/core"
)

type Dir struct {
	Name string   `yaml:"name"`
	Path string   `yaml:"path"`
	Desc string   `yaml:"desc"`
	Tags []string `yaml:"tags"`

	Context string
	RelPath string
}

func (d Dir) GetValue(key string) string {
	switch key {
	case "Name", "name":
		return d.Name
	case "Path", "path":
		return d.Path
	case "RelPath", "relpath":
		return d.RelPath
	case "Desc", "desc", "Description", "description":
		return d.Desc
	case "Tags", "tags":
		return strings.Join(d.Tags, ", ")
	}

	return ""
}

func (c *Config) GetDirList() []Dir {
	var dirs []Dir
	count := len(c.Dirs.Content)

	var err error
	for i := 0; i < count; i += 2 {
		dir := &Dir{}
		err = c.Dirs.Content[i+1].Decode(dir)
		core.CheckIfError(err)

		dir.Name = c.Dirs.Content[i].Value

		// Add absolute and relative path for each dir
		var err error
		dir.Path, err = core.GetAbsolutePath(c.Dir, dir.Path, dir.Name)
		core.CheckIfError(err)

		dir.RelPath, err = core.GetRelativePath(c.Dir, dir.Path)
		core.CheckIfError(err)

		dir.Context = c.Path

		dirs = append(dirs, *dir)
	}

	return dirs
}

func (c Config) FilterDirs(
	cwdFlag bool,
	allDirsFlag bool,
	dirPathsFlag []string,
	dirsFlag []string,
	tagsFlag []string,
) []Dir {
	var finalDirs []Dir
	if allDirsFlag {
		finalDirs = c.DirList
	} else {
		var dirPaths []Dir
		if len(dirPathsFlag) > 0 {
			dirPaths = c.GetDirsByPath(dirPathsFlag)
		}

		var tagDirs []Dir
		if len(tagsFlag) > 0 {
			tagDirs = c.GetDirsByTags(tagsFlag)
		}

		var dirs []Dir
		if len(dirsFlag) > 0 {
			dirs = c.GetDirs(dirsFlag)
		}

		var cwdDir Dir
		if cwdFlag {
			cwdDir = c.GetCwdDir()
		}

		finalDirs = GetUnionDirs(dirPaths, tagDirs, dirs, cwdDir)
	}

	return finalDirs
}

// DirList must have all paths to match. For instance, if --paths frontend,backend
// is passed, then a dir must have both paths.
func (c Config) GetDirsByPath(drs []string) []Dir {
	if len(drs) == 0 {
		return c.DirList
	}

	var dirs []Dir
	for _, dir := range c.DirList {

		// Variable use to check that all dirs are matched
		var numMatched int = 0
		for _, d := range drs {
			if strings.Contains(dir.RelPath, d) {
				numMatched = numMatched + 1
			}
		}

		if numMatched == len(drs) {
			dirs = append(dirs, dir)
		}
	}

	return dirs
}

func (c Config) GetDirs(flagDir []string) []Dir {
	var dirs []Dir

	for _, v := range flagDir {
		for _, d := range c.DirList {
			if v == d.Name {
				dirs = append(dirs, d)
			}
		}
	}

	return dirs
}

func (c Config) GetDir(name string) (*Dir, error) {
	for _, dir := range c.DirList {
		if name == dir.Name {
			return &dir, nil
		}
	}

	return nil, &core.DirNotFound{Name: name}
}

func (c Config) GetCwdDir() Dir {
	cwd, err := os.Getwd()
	core.CheckIfError(err)

	var dir Dir
	parts := strings.Split(cwd, string(os.PathSeparator))

out:
	for i := len(parts) - 1; i >= 0; i-- {
		p := strings.Join(parts[0:i+1], string(os.PathSeparator))

		for _, pro := range c.DirList {
			if p == pro.Path {
				dir = pro
				break out
			}
		}
	}

	return dir
}

func GetUnionDirs(a []Dir, b []Dir, c []Dir, d Dir) []Dir {
	drs := []Dir{}

	for _, dir := range a {
		if !DirInSlice(dir.Path, drs) {
			drs = append(drs, dir)
		}
	}

	for _, dir := range b {
		if !DirInSlice(dir.Path, drs) {
			drs = append(drs, dir)
		}
	}

	for _, dir := range c {
		if !DirInSlice(dir.Path, drs) {
			drs = append(drs, dir)
		}
	}

	if d.Name != "" && !DirInSlice(d.Name, drs) {
		drs = append(drs, d)
	}

	dirs := []Dir{}
	dirs = append(dirs, drs...)

	return dirs
}

func DirInSlice(name string, list []Dir) bool {
	for _, d := range list {
		if d.Name == name {
			return true
		}
	}
	return false
}

func (c Config) GetDirNames() []string {
	names := []string{}
	for _, dir := range c.DirList {
		names = append(names, dir.Name)
	}

	return names
}

/**
 * For each project path, get all the enumerations of dirnames. Skip absolute/relative paths
 * outside of mani.yaml
 * Example:
 * Input:
 *   - /frontend/tools/project-a
 *   - /frontend/tools/project-b
 *   - /frontend/tools/node/project-c
 *   - /backend/project-d
 *
 * Output:
 *   - /frontend
 *   - /frontend/tools
 *   - /frontend/tools/node
 *   - /backend
 */
func (c Config) GetDirPaths() []string {
	dirs := []string{}
	for _, dir := range c.DirList {
		// Ignore dirs outside of mani.yaml directory
		if strings.Contains(dir.Path, c.Dir) {
			ps := strings.Split(filepath.Dir(dir.RelPath), string(os.PathSeparator))
			for i := 1; i <= len(ps); i++ {
				p := filepath.Join(ps[0:i]...)

				if p != "." && !core.StringInSlice(p, dirs) {
					dirs = append(dirs, p)
				}
			}
		}
	}

	return dirs
}

func GetIntersectDirs(a []Dir, b []Dir) []Dir {
	dirs := []Dir{}

	for _, pa := range a {
		for _, pb := range b {
			if pa.Name == pb.Name {
				dirs = append(dirs, pa)
			}
		}
	}

	return dirs
}

func (c Config) GetDirsByName(names []string) []Dir {
	if len(names) == 0 {
		return c.DirList
	}

	var filtered []Dir
	var found []string
	for _, name := range names {
		if core.StringInSlice(name, found) {
			continue
		}

		for _, dir := range c.DirList {
			if name == dir.Name {
				filtered = append(filtered, dir)
				found = append(found, name)
			}
		}
	}

	return filtered
}

// DirList must have all tags to match. For instance, if --tags frontend,backend
// is passed, then a dir must have both tags.
func (c Config) GetDirsByTags(tags []string) []Dir {
	if len(tags) == 0 {
		return c.DirList
	}

	var dirs []Dir
	for _, dir := range c.DirList {
		// Variable use to check that all tags are matched
		var numMatched int = 0
		for _, tag := range tags {
			for _, dirTag := range dir.Tags {
				if dirTag == tag {
					numMatched = numMatched + 1
				}
			}
		}

		if numMatched == len(tags) {
			dirs = append(dirs, dir)
		}
	}

	return dirs
}

func (c Config) GetDirsTree(drs []string, tags []string) []core.TreeNode {
	dirPaths := c.GetDirsByPath(drs)
	dirTags := c.GetDirsByTags(tags)
	dirs := GetIntersectDirs(dirPaths, dirTags)

	var tree []core.TreeNode
	var paths = []string{}
	for _, p := range dirs {
		if p.RelPath != "." {
			paths = append(paths, p.RelPath)
		}
	}

	for i := range paths {
		tree = core.AddToTree(tree, strings.Split(paths[i], string(os.PathSeparator)))
	}

	return tree
}


<|start_filename|>cmd/tree.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func treeCmd(config *dao.Config, configErr *error) *cobra.Command {
	var treeFlags core.TreeFlags

	cmd := cobra.Command{
		Aliases: []string{"t", "tree"},
		Use:     "tree <projects|dirs>",
		Short:   "List dirs, projects in a tree-like format",
		Long:    "List dirs, projects in a tree-like format.",
		Example: `  # example
  mani tree projects`,
	}
	cmd.AddCommand(
		treeProjectsCmd(config, configErr, &treeFlags),
		treeDirsCmd(config, configErr, &treeFlags),
	)
	cmd.PersistentFlags().StringVar(&treeFlags.Theme, "theme", "default", "Specify theme")
	err := cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		names := config.GetThemeNames()

		return names, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.PersistentFlags().StringVarP(&treeFlags.Output, "output", "o", "tree", "Output tree|markdown|html")
	err = cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		valid := []string{"tree", "markdown", "html"}
		return valid, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.PersistentFlags().StringSliceVarP(&treeFlags.Tags, "tags", "t", []string{}, "filter entity by their tag")
	err = cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetTags()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}


<|start_filename|>cmd/tree_dirs.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func treeDirsCmd(config *dao.Config, configErr *error, treeFlags *core.TreeFlags) *cobra.Command {
	var dirFlags core.DirFlags

	cmd := cobra.Command{
		Aliases: []string{"dir", "dr", "r"},
		Use:     "dirs [flags]",
		Short:   "list dirs in a tree-like format",
		Long:    "list dirs in a tree-like format.",
		Example: `  # example
  mani tree dirs`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			runTreeDirs(config, treeFlags, &dirFlags)
		},
	}

	cmd.Flags().StringSliceVarP(&dirFlags.Paths, "paths", "p", []string{}, "filter dirs by their path")
	err := cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetProjectPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func runTreeDirs(
	config *dao.Config,
	treeFlags *core.TreeFlags,
	dirFlags *core.DirFlags,
) {
	tree := config.GetDirsTree(dirFlags.Paths, treeFlags.Tags)
	dao.PrintTree(config, treeFlags, tree)
}


<|start_filename|>test/integration/describe_test.go<|end_filename|>
package integration

import (
	"testing"
)

var describeTests = []TemplateTest{
	// Projects
	{
		TestName:   "Describe 0 projects when there's 0 projects",
		InputFiles: []string{"mani-empty/mani.yaml"},
		TestCmd:    "mani describe projects",
		Golden:     "describe/projects-empty",
		WantErr:    false,
	},
	{
		TestName:   "Describe 0 projects on non-existent tag",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects --tags lala",
		Golden:     "describe/projects-with-1-tag-non-existing-empty",
		WantErr:    false,
	},
	{
		TestName:   "Describe 0 projects on 2 non-matching tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects --tags frontend,tmux",
		Golden:     "describe/projects-with-2-tags-empty",
		WantErr:    false,
	},
	{
		TestName:   "Describe all projects",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects",
		Golden:     "describe/projects",
		WantErr:    false,
	},
	{
		TestName:   "Describe projects matching 1 tag",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects --tags frontend",
		Golden:     "describe/projects-with-1-tag",
		WantErr:    false,
	},
	{
		TestName:   "Describe projects matching multiple tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects --tags misc,frontend",
		Golden:     "describe/projects-with-2-tags",
		WantErr:    false,
	},
	{
		TestName:   "Describe 1 project",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe projects pinto",
		Golden:     "describe/projects-1-args",
		WantErr:    false,
	},

	// Tasks
	{
		TestName:   "Describe 0 tasks when no tasks exists ",
		InputFiles: []string{"mani-no-tasks/mani.yaml"},
		TestCmd:    "mani describe tasks",
		Golden:     "describe/tasks-empty",
		WantErr:    false,
	},
	{
		TestName:   "Describe all tasks",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe tasks",
		Golden:     "describe/tasks",
		WantErr:    false,
	},
	{
		TestName:   "Describe 1 tasks",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani describe tasks status",
		Golden:     "describe/tasks-1-arg",
		WantErr:    false,
	},
}

func TestDescribeCmd(t *testing.T) {
	for _, tt := range describeTests {
		t.Run(tt.TestName, func(t *testing.T) {
			Run(t, tt)
		})
	}
}


<|start_filename|>cmd/tree_projects.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func treeProjectsCmd(config *dao.Config, configErr *error, treeFlags *core.TreeFlags) *cobra.Command {
	var projectFlags core.ProjectFlags

	cmd := cobra.Command{
		Aliases: []string{"project", "proj", "p"},
		Use:     "projects [flags]",
		Short:   "list projects in a tree-like format",
		Long:    "list projects in a tree-like format.",
		Example: `  # example
  mani tree projects`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			runTreeProjects(config, treeFlags, &projectFlags)
		},
	}

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "p", []string{}, "filter projects by their path")
	err := cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetProjectPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func runTreeProjects(
	config *dao.Config,
	treeFlags *core.TreeFlags,
	projectFlags *core.ProjectFlags,
) {
	tree := config.GetProjectsTree(projectFlags.Paths, treeFlags.Tags)
	dao.PrintTree(config, treeFlags, tree)
}


<|start_filename|>cmd/describe_dirs.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func describeDirsCmd(config *dao.Config, configErr *error) *cobra.Command {
	var dirFlags core.DirFlags

	cmd := cobra.Command{
		Aliases: []string{"dir", "drs", "d"},
		Use:     "dirs [dirs] [flags]",
		Short:   "Describe dirs",
		Long:    "Describe dirs.",
		Example: `  # Describe dirs
  mani describe dirs

  # Describe dirs that have tag frontend
  mani describe dirs --tags frontend`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			describeDirs(config, args, dirFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			names := config.GetDirNames()
			return names, cobra.ShellCompDirectiveNoFileComp
		},
	}

	cmd.Flags().StringSliceVarP(&dirFlags.Tags, "tags", "t", []string{}, "filter dirs by their tag")
	err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetTags()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&dirFlags.Paths, "paths", "p", []string{}, "filter dirs by their path")
	err = cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetDirPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().BoolVarP(&dirFlags.Edit, "edit", "e", false, "Edit dir")

	return &cmd
}

func describeDirs(
	config *dao.Config,
	args []string,
	dirFlags core.DirFlags,
) {
	if dirFlags.Edit {
		if len(args) > 0 {
			config.EditDir(args[0])
		} else {
			config.EditDir("")
		}
	} else {
		allDirs := false
		if len(args) == 0 &&
			len(dirFlags.Paths) == 0 &&
			len(dirFlags.Tags) == 0 {
			allDirs = true
		}

		dirs := config.FilterDirs(false, allDirs, dirFlags.Paths, args, dirFlags.Tags)
		dao.PrintDirBlocks(dirs)
	}
}


<|start_filename|>cmd/edit_dir.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func editDir(config *dao.Config, configErr *error) *cobra.Command {
	cmd := cobra.Command{
		Use:   "dir",
		Short: "Edit mani dir",
		Long:  `Edit mani dir`,

		Example: `  # Edit a dir called mani
  mani edit dir mani

  # Edit dir in specific mani config
  mani edit --config path/to/mani/config`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			runEditDir(args, *config)
		},
		Args: cobra.MaximumNArgs(1),
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil || len(args) == 1 {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			values := config.GetDirNames()
			return values, cobra.ShellCompDirectiveNoFileComp
		},
	}

	return &cmd
}

func runEditDir(args []string, config dao.Config) {
	if len(args) > 0 {
		config.EditDir(args[0])
	} else {
		config.EditDir("")
	}
}


<|start_filename|>cmd/sync.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func syncCmd(config *dao.Config, configErr *error) *cobra.Command {
	var syncFlags core.SyncFlags

	cmd := cobra.Command{
		Use:   "sync",
		Short: "Clone repositories and add them to gitignore",
		Long: `Clone repositories and add them to gitignore.
In-case you need to enter credentials before cloning, run the command with the parallel flag.`,
		Example: `  # Clone repositories one at a time
  mani sync

  # Clone repositories one by one
  mani sync --parallel=false`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			runSync(config, syncFlags)
		},
	}

	cmd.Flags().BoolVarP(&syncFlags.Parallel, "parallel", "p", true, "Clone projects in parallel")

	return &cmd
}

func runSync(config *dao.Config, syncFlags core.SyncFlags) {
	config.SyncDirs(config.Dir, syncFlags.Parallel)
	config.SyncProjects(config.Dir, syncFlags.Parallel)
}


<|start_filename|>cmd/describe_projects.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func describeProjectsCmd(config *dao.Config, configErr *error) *cobra.Command {
	var projectFlags core.ProjectFlags

	cmd := cobra.Command{
		Aliases: []string{"project", "proj"},
		Use:     "projects [projects] [flags]",
		Short:   "Describe projects",
		Long:    "Describe projects.",
		Example: `  # Describe projects
  mani describe projects

  # Describe projects that have tag frontend
  mani describe projects --tags frontend`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			describeProjects(config, args, projectFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			projectNames := config.GetProjectNames()
			return projectNames, cobra.ShellCompDirectiveNoFileComp
		},
	}

	cmd.Flags().StringSliceVarP(&projectFlags.Tags, "tags", "t", []string{}, "filter projects by their tag")
	err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetTags()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "p", []string{}, "filter projects by their path")
	err = cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetProjectPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().BoolVarP(&projectFlags.Edit, "edit", "e", false, "Edit project")

	return &cmd
}

func describeProjects(
	config *dao.Config,
	args []string,
	projectFlags core.ProjectFlags,
) {
	if projectFlags.Edit {
		if len(args) > 0 {
			config.EditProject(args[0])
		} else {
			config.EditProject("")
		}
	} else {
		allProjects := false
		if len(args) == 0 &&
			len(projectFlags.Paths) == 0 &&
			len(projectFlags.Tags) == 0 {
			allProjects = true
		}

		projects := config.FilterProjects(false, allProjects, projectFlags.Paths, args, projectFlags.Tags)
		dao.PrintProjectBlocks(projects)
	}
}


<|start_filename|>core/dao/entity.go<|end_filename|>
package dao

type Entity struct {
	Name string
	Path string
	Type string
}

type EntityList struct {
	Type     string
	Entities []Entity
}

func (e EntityList) GetLongestNameLength() int {
	max := 0
	for _, entity := range e.Entities {
		nameLength := len(entity.Name)
		if nameLength > max {
			max = nameLength
		}
	}

	return max
}


<|start_filename|>core/dao/table_client.go<|end_filename|>
package dao

import (
	"os"
	"os/exec"
	"strings"

	"github.com/jedib0t/go-pretty/v6/table"

	core "github.com/alajmo/mani/core"
)

func RunTable(
	config Config,
	cmdStr string,
	envList []string,
	shell string,
	entity Entity,
	dryRun bool,
) (string, error) {
	entityPath, err := core.GetAbsolutePath(config.Path, entity.Path, entity.Name)
	if err != nil {
		return "", &core.FailedToParsePath{Name: entityPath}
	}
	if _, err := os.Stat(entityPath); os.IsNotExist(err) {
		return "", &core.PathDoesNotExist{Path: entityPath}
	}

	defaultArguments := getDefaultArguments(config.Path, config.Dir, entity)
	shellProgram, commandStr := formatShellString(shell, cmdStr)

	// Execute Command
	cmd := exec.Command(shellProgram, commandStr...)
	cmd.Dir = entityPath

	var output string
	if dryRun {
		for _, arg := range defaultArguments {
			env := strings.SplitN(arg, "=", 2)
			os.Setenv(env[0], env[1])
		}

		for _, arg := range envList {
			env := strings.SplitN(arg, "=", 2)
			os.Setenv(env[0], env[1])
		}

		output = os.ExpandEnv(cmdStr)
	} else {
		cmd.Env = append(os.Environ(), defaultArguments...)
		cmd.Env = append(cmd.Env, envList...)

		output, err := cmd.CombinedOutput()

		return string(output), err
	}

	return output, nil
}

func printTable(tableType string, output string, data core.TableOutput) {
	switch tableType {
	case "ascii":
		core.ManiList.Box = core.StyleBoxASCII
	default:
		core.ManiList.Box = core.StyleBoxLight
	}

	t := table.NewWriter()
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(core.ManiList)

	t.AppendHeader(data.Headers)

	for _, row := range data.Rows {
		t.AppendRow(row)
		t.AppendSeparator()
	}

	switch output {
	case "markdown":
		t.RenderMarkdown()
	case "html":
		t.RenderHTML()
	default:
		t.Render()
	}
}


<|start_filename|>core/dao/print_info.go<|end_filename|>
package dao

import (
	"fmt"
	"os/exec"
)

func PrintInfo(config *Config) {
	if config.Path != "" {
		fmt.Printf("config: %s\n", config.Path)
	}

	fmt.Printf("mani version %s\n", Version)
	cmd := exec.Command("git", "--version")
	stdout, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println("git not installed")
	} else {
		fmt.Println(string(stdout))
	}
}


<|start_filename|>test/integration/list_test.go<|end_filename|>
package integration

import (
	"testing"
)

var listTests = []TemplateTest{
	// Projects
	{
		TestName:   "List 0 projects",
		InputFiles: []string{"mani-empty/mani.yaml"},
		TestCmd:    "mani list projects",
		Golden:     "list/projects-empty",
		WantErr:    false,
	},
	{
		TestName:   "List 0 projects on non-existent tag",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --tags lala",
		Golden:     "list/projects-with-1-tag-non-existing-empty",
		WantErr:    false,
	},
	{
		TestName:   "List 0 projects on 2 non-matching tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --tags frontend,tmux",
		Golden:     "list/projects-with-2-tags-empty",
		WantErr:    false,
	},
	{
		TestName:   "List multiple projects",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects",
		Golden:     "list/projects",
		WantErr:    false,
	},
	{
		TestName:   "List only project names and no description/tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --output table --no-headers --no-borders --headers name",
		Golden:     "list/project-names",
		WantErr:    false,
	},
	{
		TestName:   "List projects matching 1 tag",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --tags frontend",
		Golden:     "list/projects-with-1-tag",
		WantErr:    false,
	},
	{
		TestName:   "List projects matching multiple tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --tags misc,frontend",
		Golden:     "list/projects-with-2-tags",
		WantErr:    false,
	},
	{
		TestName:   "List two projects",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects pinto dashgrid",
		Golden:     "list/projects-2-args",
		WantErr:    false,
	},
	{
		TestName:   "List projects matching 1 dir",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --paths frontend",
		Golden:     "list/projects-with-1-paths",
		WantErr:    false,
	},
	{
		TestName:   "List 0 projects with no matching paths",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list projects --paths hello",
		Golden:     "list/projects-0-paths",
		WantErr:    false,
	},

	// Tags
	{
		TestName:   "List all tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list tags",
		Golden:     "list/tags",
		WantErr:    false,
	},
	{
		TestName:   "List two tags",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list tags frontend misc",
		Golden:     "list/tags-2-args",
		WantErr:    false,
	},

	// Tasks
	{
		TestName:   "List 0 tasks when no tasks exists ",
		InputFiles: []string{"mani-no-tasks/mani.yaml"},
		TestCmd:    "mani list tasks",
		Golden:     "list/tasks-empty",
		WantErr:    false,
	},
	{
		TestName:   "List all tasks",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list tasks",
		Golden:     "list/tasks",
		WantErr:    false,
	},
	{
		TestName:   "List two args",
		InputFiles: []string{"mani-advanced/mani.yaml"},
		TestCmd:    "mani list tasks fetch status",
		Golden:     "list/tasks-2-args",
		WantErr:    false,
	},
}

func TestListCmd(t *testing.T) {
	for _, tt := range listTests {
		t.Run(tt.TestName, func(t *testing.T) {
			Run(t, tt)
		})
	}
}


<|start_filename|>cmd/exec.go<|end_filename|>
package cmd

import (
	"fmt"
	"strings"

	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func execCmd(config *dao.Config, configErr *error) *cobra.Command {
	var runFlags core.RunFlags

	cmd := cobra.Command{
		Use:   "exec <command>",
		Short: "Execute arbitrary commands",
		Long: `Execute arbitrary commands.

Single quote your command if you don't want the file globbing and environments variables expansion to take place
before the command gets executed in each directory.`,

		Example: `  # List files in all projects
  mani exec ls --all-projects

  # List all git files that have markdown suffix
  mani exec 'git ls-files | grep -e ".md"' --all-projects`,
		Args: cobra.MinimumNArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			execute(args, config, runFlags)
		},
	}

	cmd.Flags().BoolVar(&runFlags.DryRun, "dry-run", false, "don't execute any command, just print the output of the command to see what will be executed")
	cmd.Flags().BoolVar(&runFlags.Parallel, "parallel", false, "Run tasks in parallel")
	cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "list", "Output list|table|markdown|html")
	err := cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		valid := []string{"table", "markdown", "html"}
		return valid, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().BoolVarP(&runFlags.Cwd, "cwd", "k", false, "current working directory")

	cmd.Flags().BoolVar(&runFlags.AllProjects, "all-projects", false, "target all projects")
	cmd.Flags().BoolVar(&runFlags.AllDirs, "all-dirs", false, "target all dirs")

	cmd.Flags().StringSliceVarP(&runFlags.Projects, "projects", "p", []string{}, "target projects by their name")
	err = cmd.RegisterFlagCompletionFunc("projects", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		projects := config.GetProjectNames()
		return projects, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&runFlags.Dirs, "dirs", "d", []string{}, "target directories by their name")
	err = cmd.RegisterFlagCompletionFunc("dirs", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		dirs := config.GetDirNames()
		return dirs, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&runFlags.Paths, "paths", "g", []string{}, "target directories by their path")
	err = cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		projectPaths := config.GetProjectPaths()
		dirPaths := config.GetDirPaths()

		options := append(projectPaths, dirPaths...)

		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&runFlags.Tags, "tags", "t", []string{}, "target entities by their tag")
	err = cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		tags := config.GetTags()
		return tags, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func execute(
	args []string,
	config *dao.Config,
	runFlags core.RunFlags,
) {
	projectEntities, dirEntities := config.GetEntities(runFlags)

	if len(projectEntities) == 0 && len(dirEntities) == 0 {
		fmt.Println("No targets")
	} else {
		cmd := strings.Join(args[0:], " ")
		if len(projectEntities) > 0 {
			entityList := dao.EntityList{
				Type:     "Project",
				Entities: projectEntities,
			}

			dao.RunExec(cmd, entityList, config, &runFlags)
		}

		if len(dirEntities) > 0 {
			entityList := dao.EntityList{
				Type:     "Directory",
				Entities: dirEntities,
			}
			dao.RunExec(cmd, entityList, config, &runFlags)
		}
	}
}


<|start_filename|>_site/sidebars.js<|end_filename|>
/**
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */

const sidebars = {
  // By default, Docusaurus generates a sidebar from the docs folder structure
  // tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
  // But you can create a sidebar manually
  sidebar: [
    {
      type: 'doc',
      id: 'introduction',
    },

    {
      type: 'category',
      label: 'Getting Started',
      collapsible: false,
      items: ['installation', 'shell-completion', 'usage'],
    },

    {
      type: 'category',
      label: 'Examples',
      collapsible: false,
      items: ['example', 'commands'],
    },

    {
      type: 'doc',
      id: 'config',
    },

    {
      type: 'doc',
      id: 'changelog',
    },

    {
      type: 'category',
      label: 'Development',
      collapsible: false,
      items: ['development', 'contributing'],
    },
  ],
};

module.exports = sidebars;


<|start_filename|>core/dao/task.go<|end_filename|>
package dao

import (
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/theckman/yacspin"
	"gopkg.in/yaml.v3"

	core "github.com/alajmo/mani/core"
)

var (
	build_mode = "dev"
)

type Command struct {
	Name    string `yaml:"name"`
	Desc    string `yaml:"desc"`
	EnvList []string
	Shell   string `yaml:"shell"`
	Cmd     string `yaml:"cmd"`
	Task    string `yaml:"task"`

	Env yaml.Node `yaml:"env"`
}

type Target struct {
	AllProjects bool  `yaml:"all_projects"`
	AllDirs     bool	  `yaml:"all_dirs"`
	Projects []string `yaml:"projects"`
	Dirs     []string `yaml:"dirs"`
	Paths    []string `yaml:"paths"`
	Tags     []string `yaml:"tags"`
	Cwd      bool     `yaml:"cwd"`
}

type Task struct {
	Context string
	Output  string `yaml:"output"`
	ThemeData Theme

	Target    Target
	Parallel  bool `yaml:"parallel"`
	IgnoreError     bool `yaml:"ignore_error"`

	Name     string    `yaml:"name"`
	Desc     string    `yaml:"desc"`
	EnvList  []string
	Shell    string `yaml:"shell"`
	Cmd      string `yaml:"cmd"`
	Commands []Command

	Env      yaml.Node `yaml:"env"`
	Theme   yaml.Node `yaml:"theme"`
}

func (t *Task) ParseTheme(config Config) {
	var err error
	if len(t.Theme.Content) > 0 {
		// Theme value
		theme := &Theme{}
		err = t.Theme.Decode(theme)
		core.CheckIfError(err)

		t.ThemeData = *theme
	} else if t.Theme.Value != "" {
		// Theme reference
		theme, err := config.GetTheme(t.Theme.Value)
		core.CheckIfError(err)

		t.ThemeData = *theme
	} else {
		// Default theme
		theme, err := config.GetTheme(DEFAULT_THEME.Name)
		core.CheckIfError(err)

		t.ThemeData = *theme
	}
}

func (t *Task) ParseTask(config Config) {
	var err error
	if t.Shell == "" {
		t.Shell = DEFAULT_SHELL
	}

	for j, cmd := range t.Commands {
		if cmd.Task != "" {
			cmdRef, err := config.GetCommand(cmd.Task)
			core.CheckIfError(err)

			t.Commands[j] = *cmdRef
		}

		if t.Commands[j].Shell == "" {
			t.Commands[j].Shell = DEFAULT_SHELL
		}
	}

	if len(t.Theme.Content) > 0 {
		// Theme value
		theme := &Theme{}
		err = t.Theme.Decode(theme)
		core.CheckIfError(err)

		t.ThemeData = *theme
	} else if t.Theme.Value != "" {
		// Theme reference
		theme, err := config.GetTheme(t.Theme.Value)
		core.CheckIfError(err)

		t.ThemeData = *theme
	} else {
		// Default theme
		theme, err := config.GetTheme(DEFAULT_THEME.Name)
		core.CheckIfError(err)

		t.ThemeData = *theme
	}

	if t.Output == "" {
		t.Output = "table"
	}
}

func (t *Task) ParseOutput(config Config) {
}

func formatShellString(shell string, command string) (string, []string) {
	shellProgram := strings.SplitN(shell, " ", 2)
	return shellProgram[0], append(shellProgram[1:], command)
}

func TaskSpinner() (yacspin.Spinner, error) {
	var cfg yacspin.Config

	// NOTE: Don't print the spinner in tests since it causes
	// golden files to produce different results.
	if build_mode == "TEST" {
		cfg = yacspin.Config{
			Frequency:       100 * time.Millisecond,
			CharSet:         yacspin.CharSets[9],
			SuffixAutoColon: false,
			Writer:          io.Discard,
		}
	} else {
		cfg = yacspin.Config{
			Frequency:       100 * time.Millisecond,
			CharSet:         yacspin.CharSets[9],
			SuffixAutoColon: false,
		}
	}

	spinner, err := yacspin.New(cfg)

	return *spinner, err
}

func (c Command) GetValue(key string) string {
	switch key {
	case "Name", "name":
		return c.Name
	case "Desc", "desc":
		return c.Desc
	case "Command", "command", "Cmd", "cmd":
		return c.Cmd
	}

	return ""
}

func (t Task) GetValue(key string) string {
	switch key {
	case "Name", "name":
		return t.Name
	case "Desc", "desc", "Description", "description":
		return t.Desc
	case "Command", "command":
		return t.Cmd
	}

	return ""
}

func (c *Config) GetTaskList() []Task {
	var tasks []Task
	count := len(c.Tasks.Content)

	var err error
	for i := 0; i < count; i += 2 {
		task := &Task{}

		if c.Tasks.Content[i+1].Kind == 8 {
			task.Cmd = c.Tasks.Content[i+1].Value
		} else {
			err = c.Tasks.Content[i+1].Decode(task)
			core.CheckIfError(err)
		}

		// Add context to each task
		task.Name = c.Tasks.Content[i].Value
		task.Context = c.Path

		tasks = append(tasks, *task)
	}

	return tasks
}

func GetEnvList(env yaml.Node, userEnv []string, parentEnv []string, configEnv []string) []string {
	pEnv, err := core.EvaluateEnv(parentEnv)
	core.CheckIfError(err)

	cmdEnv, err := core.EvaluateEnv(GetEnv(env))
	core.CheckIfError(err)

	globalEnv, err := core.EvaluateEnv(configEnv)
	core.CheckIfError(err)

	envList := core.MergeEnv(userEnv, cmdEnv, pEnv, globalEnv)

	return envList
}

func (c Config) GetTaskEntities(task *Task, runFlags core.RunFlags) ([]Entity, []Entity) {
	var projects []Project
	// If any runtime target flags are used, disregard task targets
	if len(runFlags.Projects) > 0 || len(runFlags.Paths) > 0 || len(runFlags.Tags) > 0 || runFlags.Cwd || runFlags.AllProjects {
		projects = c.FilterProjects(runFlags.Cwd, runFlags.AllProjects, runFlags.Paths, runFlags.Projects, runFlags.Tags)
	} else {
		projects = c.FilterProjects(task.Target.Cwd, task.Target.AllProjects, task.Target.Paths, task.Target.Projects, task.Target.Tags)
	}

	var projectEntities []Entity
	for i := range projects {
		var entity Entity
		entity.Name = projects[i].Name
		entity.Path = projects[i].Path
		entity.Type = "project"

		projectEntities = append(projectEntities, entity)
	}

	var dirs []Dir
	// If any runtime target flags are used, disregard task targets
	if len(runFlags.Dirs) > 0 || len(runFlags.Paths) > 0 || len(runFlags.Tags) > 0 || runFlags.Cwd || runFlags.AllDirs {
		dirs = c.FilterDirs(runFlags.Cwd, runFlags.AllDirs, runFlags.Paths, runFlags.Dirs, runFlags.Tags)
	} else {
		dirs = c.FilterDirs(task.Target.Cwd, task.Target.AllDirs, task.Target.Paths, task.Target.Dirs, task.Target.Tags)
	}

	var dirEntities []Entity
	for i := range dirs {
		var entity Entity
		entity.Name = dirs[i].Name
		entity.Path = dirs[i].Path
		entity.Type = "directory"

		dirEntities = append(dirEntities, entity)
	}

	return projectEntities, dirEntities
}

func (c Config) GetEntities(runFlags core.RunFlags) ([]Entity, []Entity) {
	projects := c.FilterProjects(runFlags.Cwd, runFlags.AllProjects, runFlags.Paths, runFlags.Projects, runFlags.Tags)
	var projectEntities []Entity
	for i := range projects {
		var entity Entity
		entity.Name = projects[i].Name
		entity.Path = projects[i].Path
		entity.Type = "project"

		projectEntities = append(projectEntities, entity)
	}

	dirs := c.FilterDirs(runFlags.Cwd, runFlags.AllDirs, runFlags.Paths, runFlags.Dirs, runFlags.Tags)
	var dirEntities []Entity
	for i := range dirs {
		var entity Entity
		entity.Name = dirs[i].Name
		entity.Path = dirs[i].Path
		entity.Type = "directory"

		dirEntities = append(dirEntities, entity)
	}

	return projectEntities, dirEntities
}

func getDefaultArguments(configPath string, configDir string, entity Entity) []string {
	// Default arguments
	maniConfigPath := fmt.Sprintf("MANI_CONFIG_PATH=%s", configPath)
	maniConfigDir := fmt.Sprintf("MANI_CONFIG_DIR=%s", configDir)
	projectNameEnv := fmt.Sprintf("MANI_PROJECT_NAME=%s", entity.Name)
	projectPathEnv := fmt.Sprintf("MANI_PROJECT_PATH=%s", entity.Path)

	defaultArguments := []string{maniConfigPath, maniConfigDir, projectNameEnv, projectPathEnv}

	return defaultArguments
}

func GetEnv(node yaml.Node) []string {
	var envs []string
	count := len(node.Content)

	for i := 0; i < count; i += 2 {
		env := fmt.Sprintf("%v=%v", node.Content[i].Value, node.Content[i+1].Value)
		envs = append(envs, env)
	}

	return envs
}

func (c Config) GetTasksByNames(names []string) []Task {
	if len(names) == 0 {
		return c.TaskList
	}

	var filteredTasks []Task
	var foundTasks []string
	for _, name := range names {
		if core.StringInSlice(name, foundTasks) {
			continue
		}

		for _, task := range c.TaskList {
			if name == task.Name {
				filteredTasks = append(filteredTasks, task)
				foundTasks = append(foundTasks, name)
			}
		}
	}

	return filteredTasks
}

func (c Config) GetTaskNames() []string {
	taskNames := []string{}
	for _, task := range c.TaskList {
		taskNames = append(taskNames, task.Name)
	}

	return taskNames
}

func (c Config) GetTask(task string) (*Task, error) {
	for _, cmd := range c.TaskList {
		if task == cmd.Name {
			return &cmd, nil
		}
	}

	return nil, &core.TaskNotFound{Name: task}
}

func (c Config) GetCommand(task string) (*Command, error) {
	for _, cmd := range c.TaskList {
		if task == cmd.Name {
			cmdRef := &Command{
				Name:    cmd.Name,
				Desc:    cmd.Desc,
				EnvList: cmd.EnvList,
				Shell:   cmd.Shell,
				Cmd:     cmd.Cmd,
			}

			return cmdRef, nil
		}
	}

	return nil, &core.TaskNotFound{Name: task}
}


<|start_filename|>cmd/list_dirs.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func listDirsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command {
	var dirFlags core.DirFlags

	cmd := cobra.Command{
		Aliases: []string{"dir", "dr", "d"},
		Use:     "dirs [flags]",
		Short:   "List dirs",
		Long:    "List dirs",
		Example: `  # List dirs
  mani list dirs`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			listDirs(config, args, listFlags, &dirFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			names := config.GetDirNames()
			return names, cobra.ShellCompDirectiveNoFileComp
		},
	}

	cmd.Flags().StringSliceVarP(&dirFlags.Tags, "tags", "t", []string{}, "filter dirs by their tag")
	err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetTags()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&dirFlags.Paths, "paths", "p", []string{}, "filter dirs by their path")
	err = cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetDirPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVar(&dirFlags.Headers, "headers", []string{"name", "tags", "description"}, "Specify headers, defaults to name, tags, description")
	err = cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if err != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		validHeaders := []string{"name", "path", "relpath", "description", "url", "tags"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listDirs(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	dirFlags *core.DirFlags,
) {
	allDirs := false
	if len(args) == 0 &&
		len(dirFlags.Paths) == 0 &&
		len(dirFlags.Tags) == 0 {
		allDirs = true
	}

	dirs := config.FilterDirs(false, allDirs, dirFlags.Paths, args, dirFlags.Tags)
	dao.PrintDirs(config, dirs, *listFlags, *dirFlags)
}


<|start_filename|>cmd/edit.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func editCmd(config *dao.Config, configErr *error) *cobra.Command {
	cmd := cobra.Command{
		Aliases: []string{"e", "ed"},
		Use:     "edit",
		Short:   "Edit mani config",
		Long:    `Edit mani config`,

		Example: `  # Edit current context
  mani edit

  # Edit specific mani config
  edit edit --config path/to/mani/config`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			runEdit(args, *config)
		},
	}

	cmd.AddCommand(
		editDir(config, configErr),
		editTask(config, configErr),
		editProject(config, configErr),
	)

	return &cmd
}

func runEdit(args []string, config dao.Config) {
	config.EditConfig()
}


<|start_filename|>cmd/list_projects.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func listProjectsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command {
	var projectFlags core.ProjectFlags

	cmd := cobra.Command{
		Aliases: []string{"project", "proj", "pr"},
		Use:     "projects [flags]",
		Short:   "List projects",
		Long:    "List projects",
		Example: `  # List projects
  mani list projects`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			listProjects(config, args, listFlags, &projectFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			projectNames := config.GetProjectNames()
			return projectNames, cobra.ShellCompDirectiveNoFileComp
		},
	}

	cmd.Flags().StringSliceVarP(&projectFlags.Tags, "tags", "t", []string{}, "filter projects by their tag")
	err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetTags()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "p", []string{}, "filter projects by their path")
	err = cmd.RegisterFlagCompletionFunc("paths", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		options := config.GetProjectPaths()
		return options, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVar(&projectFlags.Headers, "headers", []string{"name", "tags", "description"}, "Specify headers, defaults to name, tags, description")
	err = cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if err != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		validHeaders := []string{"name", "path", "relpath", "description", "url", "tags"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listProjects(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	projectFlags *core.ProjectFlags,
) {
	allProjects := false
	if len(args) == 0 &&
		len(projectFlags.Paths) == 0 &&
		len(projectFlags.Tags) == 0 {
		allProjects = true
	}

	projects := config.FilterProjects(false, allProjects, projectFlags.Paths, args, projectFlags.Tags)

	dao.PrintProjects(config, projects, *listFlags, *projectFlags)
}


<|start_filename|>cmd/list_tags.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func listTagsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command {
	var tagFlags core.TagFlags

	cmd := cobra.Command{
		Aliases: []string{"tag", "tags"},
		Use:     "tags [flags]",
		Short:   "List tags",
		Long:    "List tags.",
		Example: `  # List tags
  mani list tags`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			listTags(config, args, listFlags, &tagFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			tags := config.GetTags()
			return tags, cobra.ShellCompDirectiveNoFileComp
		},
	}

	cmd.Flags().StringSliceVar(&tagFlags.Headers, "headers", []string{"name", "projects", "directories"}, "Specify headers, defaults to name, description")
	err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		validHeaders := []string{"tag", "projects", "directories"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listTags(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	tagFlags *core.TagFlags,
) {
	allTags := config.GetTags()
	if len(args) > 0 {
		args = core.Intersection(args, allTags)
		m := config.GetTagAssocations(args)
		dao.PrintTags(config, args, m, *listFlags, *tagFlags)
	} else {
		m := config.GetTagAssocations(allTags)
		dao.PrintTags(config, allTags, m, *listFlags, *tagFlags)
	}
}


<|start_filename|>cmd/root.go<|end_filename|>
package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core/dao"
)

const (
	appName      = "mani"
	shortAppDesc = "mani is a tool used to manage multiple repositories"
	longAppDesc  = `mani is a tool used to manage multiple repositories`
)

var (
	config     dao.Config
	configErr  error
	configFile string
	rootCmd    = &cobra.Command{
		Use:   appName,
		Short: shortAppDesc,
		Long:  longAppDesc,
	}
)

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(initConfig)
	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "config file (by default it checks current and all parent directories for mani.yaml|yml)")

	rootCmd.AddCommand(
		versionCmd(),
		completionCmd(),
		initCmd(),
		execCmd(&config, &configErr),
		runCmd(&config, &configErr),
		listCmd(&config, &configErr),
		treeCmd(&config, &configErr),
		describeCmd(&config, &configErr),
		syncCmd(&config, &configErr),
		editCmd(&config, &configErr),
	)
}

func initConfig() {
	config, configErr = dao.ReadConfig(configFile)
}


<|start_filename|>core/dao/tags.go<|end_filename|>
package dao

import (
	"strings"

	"github.com/alajmo/mani/core"
)

type TagAssocations struct {
	Name     string
	Projects []string
	Dirs     []string
}

func (t TagAssocations) GetValue(key string) string {
	switch key {
	case "Name", "name":
		return t.Name
	case "Projects", "projects":
		return strings.Join(t.Projects, "\n")
	case "Dirs", "dirs", "directories":
		return strings.Join(t.Dirs, "\n")
	}

	return ""
}

func (c Config) GetTagsByProject(projectNames []string) []string {
	tags := []string{}
	for _, project := range c.ProjectList {
		if core.StringInSlice(project.Name, projectNames) {
			tags = append(tags, project.Tags...)
		}
	}

	return tags
}

func (c Config) GetTagsByDir(names []string) []string {
	tags := []string{}
	for _, dir := range c.DirList {
		if core.StringInSlice(dir.Name, names) {
			tags = append(tags, dir.Tags...)
		}
	}

	return tags
}

func (c Config) GetTags() []string {
	tags := []string{}
	for _, project := range c.ProjectList {
		for _, tag := range project.Tags {
			if !core.StringInSlice(tag, tags) {
				tags = append(tags, tag)
			}
		}
	}

	for _, dir := range c.DirList {
		for _, tag := range dir.Tags {
			if !core.StringInSlice(tag, tags) {
				tags = append(tags, tag)
			}
		}
	}

	return tags
}

func (c Config) GetTagAssocations(tags []string) map[string]TagAssocations {
	m := make(map[string]TagAssocations)

	for _, tag := range tags {
		projects := c.GetProjectsByTags([]string{tag})
		var projectNames []string
		for _, p := range projects {
			projectNames = append(projectNames, p.Name)
		}

		dirs := c.GetDirsByTags([]string{tag})
		var dirNames []string
		for _, d := range dirs {
			dirNames = append(dirNames, d.Name)
		}

		m[tag] = TagAssocations{
			Name:     tag,
			Projects: projectNames,
			Dirs:     dirNames,
		}
	}

	return m
}


<|start_filename|>core/flags.go<|end_filename|>
package core

import (
	"github.com/jedib0t/go-pretty/v6/list"
	"github.com/jedib0t/go-pretty/v6/table"
	"github.com/jedib0t/go-pretty/v6/text"
)

type TableOutput struct {
	Headers table.Row
	Rows    []table.Row
}

// CMD Flags

type ListFlags struct {
	NoHeaders bool
	NoBorders bool
	Output    string
	Theme     string
}

type DirFlags struct {
	Tags    []string
	Paths   []string
	Headers []string
	Edit    bool
}

type ProjectFlags struct {
	Tags    []string
	Paths   []string
	Headers []string
	Edit    bool
}

type TagFlags struct {
	Headers []string
}

type TaskFlags struct {
	Headers []string
	Edit    bool
}

type TreeFlags struct {
	Tags   []string
	Output string
	Theme  string
}

type RunFlags struct {
	Edit     bool
	Parallel bool
	DryRun   bool
	Describe bool
	Cwd      bool

	AllProjects bool
	AllDirs     bool
	Projects    []string
	Dirs        []string
	Paths       []string
	Tags        []string

	Output string
}

type SyncFlags struct {
	Parallel bool
}

type InitFlags struct {
	AutoDiscovery bool
}

// STYLES

var StyleBoxLight = table.BoxStyle{
	BottomLeft:       "└",
	BottomRight:      "┘",
	BottomSeparator:  "┴",
	EmptySeparator:   text.RepeatAndTrim(" ", text.RuneCount("┼")),
	Left:             "│",
	LeftSeparator:    "├",
	MiddleHorizontal: "─",
	MiddleSeparator:  "┼",
	MiddleVertical:   "│",
	PaddingLeft:      " ",
	PaddingRight:     " ",
	PageSeparator:    "\n",
	Right:            "│",
	RightSeparator:   "┤",
	TopLeft:          "┌",
	TopRight:         "┐",
	TopSeparator:     "┬",
	UnfinishedRow:    " ≈",
}

var StyleBoxASCII = table.BoxStyle{
	BottomLeft:       "+",
	BottomRight:      "+",
	BottomSeparator:  "+",
	EmptySeparator:   text.RepeatAndTrim(" ", text.RuneCount("+")),
	Left:             "|",
	LeftSeparator:    "+",
	MiddleHorizontal: "-",
	MiddleSeparator:  "+",
	MiddleVertical:   "|",
	PaddingLeft:      " ",
	PaddingRight:     " ",
	PageSeparator:    "\n",
	Right:            "|",
	RightSeparator:   "+",
	TopLeft:          "+",
	TopRight:         "+",
	TopSeparator:     "+",
	UnfinishedRow:    " ~",
}

var StyleNoBorders = table.BoxStyle{
	PaddingLeft:  "",
	PaddingRight: " ",
}

var ManiList = table.Style{
	Name: "table",

	Box: StyleBoxLight,

	Color: table.ColorOptions{
		// Header: text.Colors{ text.Bold },
	},

	Format: table.FormatOptions{
		Header: text.FormatDefault,
		Row:    text.FormatDefault,
		Footer: text.FormatUpper,
	},

	Options: table.Options{
		DrawBorder:      true,
		SeparateColumns: true,
		SeparateFooter:  false,
		SeparateHeader:  true,
		SeparateRows:    false,
	},
}

var TreeStyle list.Style


<|start_filename|>cmd/list.go<|end_filename|>
package cmd

import (
	"github.com/spf13/cobra"

	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
)

func listCmd(config *dao.Config, configErr *error) *cobra.Command {
	var listFlags core.ListFlags

	cmd := cobra.Command{
		Aliases: []string{"l", "ls"},
		Use:     "list <projects|tasks|tags>",
		Short:   "List projects, tasks and tags",
		Long:    "List projects, tasks and tags.",
		Example: `  # List projects
  mani list projects

  # List tasks
  mani list tasks`,
	}

	cmd.AddCommand(
		listProjectsCmd(config, configErr, &listFlags),
		listDirsCmd(config, configErr, &listFlags),
		listTasksCmd(config, configErr, &listFlags),
		listTagsCmd(config, configErr, &listFlags),
	)

	cmd.PersistentFlags().BoolVar(&listFlags.NoHeaders, "no-headers", false, "Remove table headers")
	cmd.PersistentFlags().BoolVar(&listFlags.NoBorders, "no-borders", false, "Remove table borders")
	cmd.PersistentFlags().StringVar(&listFlags.Theme, "theme", "default", "Specify theme")
	err := cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		names := config.GetThemeNames()

		return names, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.PersistentFlags().StringVarP(&listFlags.Output, "output", "o", "table", "Output table|markdown|html")
	err = cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		valid := []string{"table", "markdown", "html"}
		return valid, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}


<|start_filename|>core/dao/print_dir.go<|end_filename|>
package dao

import (
	"fmt"
	"github.com/jedib0t/go-pretty/v6/table"
	"os"

	"github.com/alajmo/mani/core"
)

func PrintDirs(
	config *Config,
	dirs []Dir,
	listFlags core.ListFlags,
	dirFlags core.DirFlags,
) {
	theme, err := config.GetTheme(listFlags.Theme)
	core.CheckIfError(err)

	// Table Style
	switch theme.Table {
	case "ascii":
		core.ManiList.Box = core.StyleBoxASCII
	default:
		core.ManiList.Box = core.StyleBoxLight
	}

	t := table.NewWriter()
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(core.ManiList)

	var headers []interface{}
	for _, h := range dirFlags.Headers {
		headers = append(headers, h)
	}

	if !listFlags.NoHeaders {
		t.AppendHeader(headers)
	}

	for _, dir := range dirs {
		var row []interface{}
		for _, h := range headers {
			value := dir.GetValue(fmt.Sprintf("%v", h))
			row = append(row, value)
		}

		t.AppendRow(row)
	}

	if listFlags.NoBorders {
		t.Style().Box = core.StyleNoBorders
		t.Style().Options.SeparateHeader = false
		t.Style().Options.DrawBorder = false
	}

	switch listFlags.Output {
	case "markdown":
		t.RenderMarkdown()
	case "html":
		t.RenderHTML()
	default:
		t.Render()
	}
}

func PrintDirBlocks(dirs []Dir) {
	t := table.NewWriter()
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(core.ManiList)

	for _, dir := range dirs {
		t.AppendRows([]table.Row{
			{"Name: ", dir.Name},
			{"Path: ", dir.RelPath},
			{"Desc: ", dir.Desc},
			{"Tags: ", dir.GetValue("Tags")},
		})

		t.AppendSeparator()
		t.AppendRow(table.Row{})
		t.AppendSeparator()
	}

	t.Style().Box = core.StyleNoBorders
	t.Style().Options.SeparateHeader = false
	t.Style().Options.DrawBorder = false

	t.Render()
}


<|start_filename|>core/dao/text_client.go<|end_filename|>
package dao

import (
	"fmt"
	"os"
	"os/exec"
	"strings"

	core "github.com/alajmo/mani/core"
)

func RunText(
	cmdStr string,
	envList []string,
	config Config,
	shell string,
	entity Entity,
	dryRun bool,
) error {
	entityPath, err := core.GetAbsolutePath(config.Path, entity.Path, entity.Name)
	if err != nil {
		return &core.FailedToParsePath{Name: entityPath}
	}
	if _, err := os.Stat(entityPath); os.IsNotExist(err) {
		return &core.PathDoesNotExist{Path: entityPath}
	}

	defaultArguments := getDefaultArguments(config.Path, config.Dir, entity)
	shellProgram, commandStr := formatShellString(shell, cmdStr)

	// Execute Command
	cmd := exec.Command(shellProgram, commandStr...)
	cmd.Dir = entityPath

	if dryRun {
		for _, arg := range defaultArguments {
			env := strings.SplitN(arg, "=", 2)
			os.Setenv(env[0], env[1])
		}

		for _, arg := range envList {
			env := strings.SplitN(arg, "=", 2)
			os.Setenv(env[0], env[1])
		}

		fmt.Println(os.ExpandEnv(cmdStr))
	} else {
		cmd.Env = append(os.Environ(), defaultArguments...)
		cmd.Env = append(cmd.Env, envList...)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err = cmd.Run()

		return err
	}

	return nil
}
