Full Code of alajmo/mani for AI

main cce639f8e72d cached
330 files
635.9 KB
194.6k tokens
583 symbols
1 requests
Download .txt
Showing preview only (720K chars total). Download the full file or copy to clipboard to get everything.
Repository: alajmo/mani
Branch: main
Commit: cce639f8e72d
Files: 330
Total size: 635.9 KB

Directory structure:
gitextract_1sykag_x/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── BUG_REPORT.md
│   │   └── FEATURE_REQUEST.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── agents/
│   │   └── default.agent.md
│   ├── copilot-instructions.md
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── benchmarks/
│   └── .gitignore
├── cmd/
│   ├── check.go
│   ├── completion.go
│   ├── describe.go
│   ├── describe_projects.go
│   ├── describe_tasks.go
│   ├── edit.go
│   ├── edit_project.go
│   ├── edit_task.go
│   ├── exec.go
│   ├── gen.go
│   ├── gen_docs.go
│   ├── init.go
│   ├── list.go
│   ├── list_projects.go
│   ├── list_tags.go
│   ├── list_tasks.go
│   ├── root.go
│   ├── run.go
│   ├── sync.go
│   └── tui.go
├── core/
│   ├── config.man
│   ├── dao/
│   │   ├── benchmark_test.go
│   │   ├── common.go
│   │   ├── common_test.go
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── import.go
│   │   ├── project.go
│   │   ├── project_test.go
│   │   ├── spec.go
│   │   ├── spec_test.go
│   │   ├── tag.go
│   │   ├── tag_expr.go
│   │   ├── tag_expr_test.go
│   │   ├── target.go
│   │   ├── target_test.go
│   │   ├── task.go
│   │   ├── task_test.go
│   │   ├── theme.go
│   │   ├── theme_block.go
│   │   ├── theme_stream.go
│   │   ├── theme_table.go
│   │   ├── theme_tree.go
│   │   ├── theme_tui.go
│   │   └── utils_test.go
│   ├── errors.go
│   ├── exec/
│   │   ├── client.go
│   │   ├── clone.go
│   │   ├── exec.go
│   │   ├── table.go
│   │   ├── text.go
│   │   ├── unix.go
│   │   └── windows.go
│   ├── flags.go
│   ├── man.go
│   ├── man_gen.go
│   ├── mani.1
│   ├── prefixer.go
│   ├── prefixer_benchmark_test.go
│   ├── print/
│   │   ├── lib.go
│   │   ├── print_block.go
│   │   ├── print_table.go
│   │   ├── print_tree.go
│   │   └── table.go
│   ├── sizedwaitgroup.go
│   ├── tui/
│   │   ├── components/
│   │   │   ├── tui_button.go
│   │   │   ├── tui_checkbox.go
│   │   │   ├── tui_filter.go
│   │   │   ├── tui_list.go
│   │   │   ├── tui_modal.go
│   │   │   ├── tui_output.go
│   │   │   ├── tui_search.go
│   │   │   ├── tui_table.go
│   │   │   ├── tui_text.go
│   │   │   ├── tui_textarea.go
│   │   │   ├── tui_toggle_text.go
│   │   │   └── tui_tree.go
│   │   ├── misc/
│   │   │   ├── tui_event.go
│   │   │   ├── tui_focus.go
│   │   │   ├── tui_global.go
│   │   │   ├── tui_theme.go
│   │   │   ├── tui_utils.go
│   │   │   └── tui_writer.go
│   │   ├── pages/
│   │   │   ├── tui_exec.go
│   │   │   ├── tui_project.go
│   │   │   ├── tui_run.go
│   │   │   └── tui_task.go
│   │   ├── pages.go
│   │   ├── tui.go
│   │   ├── tui_input.go
│   │   ├── views/
│   │   │   ├── tui_help.go
│   │   │   ├── tui_project_view.go
│   │   │   ├── tui_shortcut_info.go
│   │   │   ├── tui_spec_view.go
│   │   │   └── tui_task_view.go
│   │   └── watcher.go
│   └── utils.go
├── docs/
│   ├── changelog.md
│   ├── commands.md
│   ├── config.md
│   ├── contributing.md
│   ├── development.md
│   ├── error-handling.md
│   ├── examples.md
│   ├── filtering-projects.md
│   ├── installation.md
│   ├── introduction.md
│   ├── man-pages.md
│   ├── output.md
│   ├── project-background.md
│   ├── roadmap.md
│   ├── shell-completion.mdx
│   ├── usage.md
│   └── variables.md
├── examples/
│   ├── .gitignore
│   ├── README.md
│   └── mani.yaml
├── go.mod
├── go.sum
├── install.sh
├── main.go
├── res/
│   ├── demo.md
│   ├── demo.vhs
│   └── mani.yaml
├── scripts/
│   └── release.sh
└── test/
    ├── README.md
    ├── fixtures/
    │   ├── mani-advanced/
    │   │   ├── .gitignore
    │   │   └── mani.yaml
    │   ├── mani-empty/
    │   │   └── mani.yaml
    │   └── mani-no-tasks/
    │       └── mani.yaml
    ├── images/
    │   ├── alpine.exec.Dockerfile
    │   └── alpine.test.Dockerfile
    ├── integration/
    │   ├── describe_test.go
    │   ├── exec_test.go
    │   ├── golden/
    │   │   ├── describe/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── exec/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-6/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── init/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── dashgrid/
    │   │   │   │   │   └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   ├── nameless/
    │   │   │   │   │   └── empty
    │   │   │   │   ├── nested/
    │   │   │   │   │   └── template-generator/
    │   │   │   │   │       └── empty
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-2/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── list/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-10/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-11/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-12/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-13/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-14/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-15/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-16/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-17/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── run/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-10/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-11/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── sync/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-1/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   └── version/
    │   │       ├── golden-0/
    │   │       │   └── stdout.golden
    │   │       └── golden-1/
    │   │           ├── mani.yaml
    │   │           └── stdout.golden
    │   ├── init_test.go
    │   ├── list_test.go
    │   ├── main_test.go
    │   ├── run_test.go
    │   ├── sync_test.go
    │   └── version_test.go
    ├── playground/
    │   ├── .gitignore
    │   ├── imports/
    │   │   ├── many-projects.yaml
    │   │   ├── projects.yaml
    │   │   ├── specs.yaml
    │   │   ├── targets.yaml
    │   │   ├── tasks.yaml
    │   │   └── themes.yaml
    │   └── mani.yaml
    └── scripts/
        ├── exec
        ├── git
        └── test

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git


================================================
FILE: .github/FUNDING.yml
================================================
custom: ["https://paypal.me/samiralajmovic", "https://www.buymeacoffee.com/alajmo"]


================================================
FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.md
================================================
---
name: Bug report
about: Report a bug
title: ''
labels: 'bug'
assignees: ''
---

- [ ] I have the latest version of mani
- [ ] I have searched through the existing issues

## Info

- OS
  - [ ] Linux
  - [ ] Mac OS X
  - [ ] Windows
  - [ ] other

- Shell
  - [ ] Bash
  - [ ] Zsh
  - [ ] Fish
  - [ ] Powershell
  - [ ] other

<!-- run `mani --version` -->
- Version:

## Problem / Steps to reproduce

<!-- Provide project and task definitions -->

<!-- How do you invoke the `mani` CLI -->


================================================
FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
================================================
---
name: Feature request
about: Suggest an feature for this project
title: ''
labels: 'enhancement'
assignees: ''
---

## Is your feature request related to a problem? Please describe

## Describe the solution you'd like

## Additional context


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
### What's Changed

A description of the issue/feature; reference the issue number (if one exists). The Pull Request should not include fixes for issues other than the main issue/feature request.

### Technical Description

Any specific technical detail that may provide additional context to aid code review.

> Before opening a Pull Request you should read and agreed to the Contributor Code of Conduct (see `CONTRIBUTING.md`\)


================================================
FILE: .github/agents/default.agent.md
================================================
---
name: default
description: add features and fix bugs
---

# Copilot Instructions for mani

This document provides guidance for GitHub Copilot when working on the `mani` repository. `mani` is a CLI tool written in Go that helps manage multiple repositories. It's useful for microservices, multi-project systems, or any collection of repositories.

## Project Overview

- **Language**: Go 1.25+
- **CLI Framework**: [Cobra](https://github.com/spf13/cobra)
- **Configuration**: YAML-based configuration files (`mani.yaml`)
- **Key Features**: Repository management, task running across multiple repos, TUI interface

## Repository Structure

```
mani/
├── cmd/              # CLI commands (Cobra commands)
├── core/             # Core business logic
│   ├── dao/          # Data access objects, config parsing
│   ├── exec/         # Command execution logic
│   ├── print/        # Output formatting
│   └── tui/          # Terminal UI components
├── docs/             # Documentation
├── examples/         # Example configurations
├── test/             # Test fixtures and integration tests
│   ├── fixtures/     # Test data
│   ├── integration/  # Integration test files
│   └── scripts/      # Test scripts
└── main.go           # Entry point
```

## Development Commands

Build commands can be found in Makefile.

## Coding Standards

### Go Code Style

1. **Follow standard Go conventions**:
   - Use `gofmt` for formatting
   - Follow [Effective Go](https://go.dev/doc/effective_go) guidelines
   - Use meaningful variable and function names

2. **Error handling**:
   - Always check and handle errors explicitly
   - Use the custom error types in `core/errors.go` for consistent error messages
   - Return errors to callers rather than panicking

3. **Package structure**:
   - Keep `cmd/` for CLI command definitions only
   - Business logic goes in `core/`
   - Data structures and parsing go in `core/dao/`

## Adding Features

When adding a new feature:

1. **Understand the existing patterns**:
   - Review similar features in the codebase
   - Follow the established code organization

2. **Implementation steps**:
   - Add data structures in `core/dao/` if needed
   - Implement business logic in `core/`
   - Add CLI command in `cmd/` if needed
   - Add tests (unit and/or integration)

3. **Configuration changes**:
   - Update `mani.yaml` schema if adding new config options
   - Update documentation in `docs/config.md`
   - Consider backward compatibility

4. **Documentation**:
   - Update relevant docs in `docs/`
   - Update README.md if needed
   - Add examples if appropriate

## Fixing Bugs

When fixing a bug:

1. **Reproduce the issue**:
   - Create a minimal test case
   - Understand the root cause

2. **Fix process**:
   - Make the minimal change needed
   - Add a test that would have caught the bug
   - Verify the fix doesn't break existing functionality

3. **Verification**:
   - Run `make test-unit` to ensure all tests pass
   - Run `make lint` to check code quality
   - Test manually with real scenarios

## Platform Considerations

- **Windows support**: The code handles Windows-specific shell behavior
  - Default shell on Windows is `powershell -NoProfile`
  - Default shell on Unix is `bash -c`
- **Cross-platform paths**: Use `filepath.Join()` for paths

## Key Dependencies

- `github.com/spf13/cobra` - CLI framework
- `gopkg.in/yaml.v3` - YAML parsing
- `github.com/gdamore/tcell/v2` - Terminal handling for TUI
- `github.com/rivo/tview` - TUI components
- `github.com/jedib0t/go-pretty/v6` - Table formatting

## Configuration File Structure

The main configuration file is `mani.yaml`:

```yaml
# Global settings
shell: bash -c
sync_remotes: false
sync_gitignore: true

# Environment variables
env:
  KEY: value

# Project definitions
projects:
  project-name:
    path: ./relative/path
    url: git@github.com:user/repo.git
    tags: [tag1, tag2]

# Task definitions
tasks:
  task-name:
    desc: Task description
    cmd: echo "command"
    # Or multi-command:
    commands:
      - task: other-task
      - cmd: echo "inline command"
```

## Pull Request Guidelines

1. Reference the issue number if one exists
2. Provide a clear description of changes
3. Ensure all tests pass (`make test`)
4. Ensure code is formatted (`make gofmt`)
5. Ensure linter passes (`make lint`)
6. Update documentation if needed


================================================
FILE: .github/copilot-instructions.md
================================================
# Copilot Instructions for mani

This document provides guidance for GitHub Copilot when working on the `mani` repository. `mani` is a CLI tool written in Go that helps manage multiple repositories. It's useful for microservices, multi-project systems, or any collection of repositories.

## Project Overview

- **Language**: Go 1.25+
- **CLI Framework**: [Cobra](https://github.com/spf13/cobra)
- **Configuration**: YAML-based configuration files (`mani.yaml`)
- **Key Features**: Repository management, task running across multiple repos, TUI interface

## Repository Structure

```
mani/
├── cmd/              # CLI commands (Cobra commands)
├── core/             # Core business logic
│   ├── dao/          # Data access objects, config parsing
│   ├── exec/         # Command execution logic
│   ├── print/        # Output formatting
│   └── tui/          # Terminal UI components
├── docs/             # Documentation
├── examples/         # Example configurations
├── test/             # Test fixtures and integration tests
│   ├── fixtures/     # Test data
│   ├── integration/  # Integration test files
│   └── scripts/      # Test scripts
└── main.go           # Entry point
```

## Development Commands

Build commands can be found in Makefile.

## Coding Standards

### Go Code Style

1. **Follow standard Go conventions**:
   - Use `gofmt` for formatting
   - Follow [Effective Go](https://go.dev/doc/effective_go) guidelines
   - Use meaningful variable and function names

2. **Error handling**:
   - Always check and handle errors explicitly
   - Use the custom error types in `core/errors.go` for consistent error messages
   - Return errors to callers rather than panicking

3. **Package structure**:
   - Keep `cmd/` for CLI command definitions only
   - Business logic goes in `core/`
   - Data structures and parsing go in `core/dao/`

## Adding Features

When adding a new feature:

1. **Understand the existing patterns**:
   - Review similar features in the codebase
   - Follow the established code organization

2. **Implementation steps**:
   - Add data structures in `core/dao/` if needed
   - Implement business logic in `core/`
   - Add CLI command in `cmd/` if needed
   - Add tests (unit and/or integration)

3. **Configuration changes**:
   - Update `mani.yaml` schema if adding new config options
   - Update documentation in `docs/config.md`
   - Consider backward compatibility

4. **Documentation**:
   - Update relevant docs in `docs/`
   - Update README.md if needed
   - Add examples if appropriate

## Fixing Bugs

When fixing a bug:

1. **Reproduce the issue**:
   - Create a minimal test case
   - Understand the root cause

2. **Fix process**:
   - Make the minimal change needed
   - Add a test that would have caught the bug
   - Verify the fix doesn't break existing functionality

3. **Verification**:
   - Run `make test-unit` to ensure all tests pass
   - Run `make lint` to check code quality
   - Test manually with real scenarios

## Platform Considerations

- **Windows support**: The code handles Windows-specific shell behavior
  - Default shell on Windows is `powershell -NoProfile`
  - Default shell on Unix is `bash -c`
- **Cross-platform paths**: Use `filepath.Join()` for paths

## Key Dependencies

- `github.com/spf13/cobra` - CLI framework
- `gopkg.in/yaml.v3` - YAML parsing
- `github.com/gdamore/tcell/v2` - Terminal handling for TUI
- `github.com/rivo/tview` - TUI components
- `github.com/jedib0t/go-pretty/v6` - Table formatting

## Configuration File Structure

The main configuration file is `mani.yaml`:

```yaml
# Global settings
shell: bash -c
sync_remotes: false
sync_gitignore: true

# Environment variables
env:
  KEY: value

# Project definitions
projects:
  project-name:
    path: ./relative/path
    url: git@github.com:user/repo.git
    tags: [tag1, tag2]

# Task definitions
tasks:
  task-name:
    desc: Task description
    cmd: echo "command"
    # Or multi-command:
    commands:
      - task: other-task
      - cmd: echo "inline command"
```

## Pull Request Guidelines

1. Reference the issue number if one exists
2. Provide a clear description of changes
3. Ensure all tests pass (`make test`)
4. Ensure code is formatted (`make gofmt`)
5. Ensure linter passes (`make lint`)
6. Update documentation if needed


================================================
FILE: .github/workflows/release.yml
================================================
name: release

on:
  push:
    tags:
      - "v*"

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "1.25.5"

      - name: Create release notes
        run: ./scripts/release.sh

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          distribution: goreleaser
          version: latest
          args: release --release-notes=release-changelog.md
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.GH_PAT }}


================================================
FILE: .github/workflows/test.yml
================================================
name: test

on:
  pull_request:
    branches: [main]
    paths-ignore:
      - "**.md"
jobs:
  test:
    name: test
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
        go: ["1.25.5"]

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "1.25.5"

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v9
        with:
          version: v2.7.1

      - name: Get dependencies
        run: go get -v -t -d ./...

      - name: Test
        run: make test

      - name: Build
        run: make build


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

dist
target
test/tmp
examples/frontend
release-changelog.md

.netlify
.tags

assets
.config

res/go


================================================
FILE: .golangci.yaml
================================================
version: "2"

linters:
  settings:
    errcheck:
      exclude-functions:
        - fmt.Fprintf
        - fmt.Fprint
        - (io.Closer).Close


================================================
FILE: .goreleaser.yaml
================================================
version: 2
project_name: mani

before:
  hooks:
    - go mod download

builds:
  - binary: mani
    id: mani
    ldflags: -s -w -X github.com/alajmo/mani/cmd.version={{ .Version }} -X github.com/alajmo/mani/cmd.commit={{ .ShortCommit }} -X github.com/alajmo/mani/cmd.date={{ .Date }}
    env:
      - CGO_ENABLED=0
    goos:
      - darwin
      - linux
      - windows
      - freebsd
      - netbsd
      - openbsd
    goarch:
      - amd64
      - '386'
      - arm
      - arm64
    goarm:
      - '7'

    ignore:
      - goos: freebsd
        goarch: arm
      - goos: freebsd
        goarch: arm64

      - goos: openbsd
        goarch: arm
      - goos: openbsd
        goarch: arm64

      - goos: darwin
        goarch: arm
      - goos: darwin
        goarch: '386'

      - goos: windows
        goarch: arm
      - goos: windows
        goarch: arm64

archives:
  - id: 'mani'
    builds: ['mani']
    format: tar.gz
    format_overrides:
      - goos: windows
        format: zip
    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
    files:
      - LICENSE
      - src: 'core/mani.1'
        dst: '.'
        strip_parent: true

brews:
  - name: mani
    description: 'CLI tool to help you manage multiple repositories'
    homepage: 'https://manicli.com'
    license: 'MIT'
    repository:
      owner: alajmo
      name: homebrew-mani
      token: '{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}'
    directory: Formula

checksum:
  name_template: 'checksums.txt'

snapshot:
  version_template: '{{ .Tag }}'


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

mani is a CLI tool written in Go that helps manage multiple repositories. It's useful for microservices, multi-project systems, or any collection of repositories where you need a central place to pull repositories and run commands across them.

## Build & Development Commands

```bash
make build              # Build for current platform (output: dist/mani)
make build-all          # Cross-platform builds using goreleaser
make test               # Run unit + integration tests
make test-unit          # Run unit tests only (go test ./core/dao/...)
make test-integration   # Run integration tests (requires Docker)
make update-golden-files # Update golden test outputs
make gofmt              # Format Go code
make lint               # Run golangci-lint and deadcode
make tidy               # Update dependencies
```

## Architecture

**Entry point flow:**
```
main.go -> cmd.Execute() (cmd/root.go) -> Cobra command routing
```

**Core layers:**
- `cmd/` - Cobra CLI command definitions (exec, run, sync, list, describe, tui, etc.)
- `core/dao/` - Data structures, YAML config parsing, project/task/spec definitions
- `core/exec/` - Task execution engine, SSH/exec clients, cloning logic
- `core/print/` - Output formatters (table, stream, tree, html, markdown)
- `core/tui/` - Terminal UI using tview/tcell

**Configuration:** YAML-based (`mani.yaml`) with projects, tasks, specs, and themes.

## Key Patterns

- CLI commands in `cmd/` delegate to business logic in `core/`
- Custom error types in `core/errors.go` - use `core.CheckIfError()` for consistency
- Config lookup: `mani.yaml`, `mani.yml`, `.mani.yaml`, `.mani.yml`
- Shell handling: bash on Unix, powershell on Windows
- Use `filepath.Join()` for cross-platform paths

## Testing

- Unit tests in `core/dao/*_test.go`
- Integration tests in `test/integration/` using Docker and golden files
- Golden files in `test/integration/golden/` serve as expected outputs
- Test script: `./test/scripts/test` (supports --debug, --build, --run, --update, --count, --clean)

## Adding Features

1. Add data structures in `core/dao/` if needed
2. Implement business logic in `core/`
3. Add CLI command in `cmd/` if needed
4. Update config schema in `docs/config.md` if adding new options
5. Add tests (unit and/or integration)


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2020-2021 Samir Alajmovic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: Makefile
================================================
NAME    := mani
PACKAGE := github.com/alajmo/$(NAME)
DATE    := $(shell date +"%Y %B %d")
GIT     := $(shell [ -d .git ] && git rev-parse --short HEAD)
VERSION := v0.31.2

default: build

tidy:
	go get -u && go mod tidy

gofmt:
	go fmt ./cmd/***.go
	go fmt ./core/***.go
	go fmt ./core/dao/***.go
	go fmt ./core/exec/***.go
	go fmt ./core/print/***.go
	go fmt ./core/tui/***.go
	go fmt ./test/integration/***.go

lint:
	golangci-lint run ./...
	deadcode .

test:
	# Unit tests
	go test -v ./core/dao/***

	# Integration tests
	./test/scripts/test --build --count 5 --clean

test-unit:
	go test -v ./core/dao/***

bench:
	go test -bench=. -benchmem ./core/dao/... ./core/...

bench-save:
	@mkdir -p benchmarks
	go test -bench=. -benchmem -count=6 ./core/dao/... ./core/... > benchmarks/bench-$(shell date +%Y%m%d-%H%M%S).txt 2>&1

bench-compare:
	@if [ -n "$(OLD)" ] && [ -n "$(NEW)" ]; then benchstat $(OLD) $(NEW); fi

test-integration:
	./test/scripts/test --count 5 --build --clean

update-golden-files:
	./test/scripts/test --build --update

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

build-all:
	goreleaser release --snapshot --clean

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

gen-man:
	go run -ldflags="-X 'github.com/alajmo/mani/cmd.buildMode=man' -X '${PACKAGE}/cmd.version=${VERSION}' -X '${PACKAGE}/cmd.commit=${GIT}' -X '${PACKAGE}/cmd.date=${DATE}'" ./main.go gen-docs

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

clean:
	$(RM) -r dist target

.PHONY: tidy gofmt lint test test-unit test-integration update-golden-files build build-all build-test gen-man release clean bench bench-save bench-compare


================================================
FILE: README.md
================================================
<h1 align="center"><code>mani</code></h1>

<div align="center">
  <a href="https://github.com/alajmo/mani/releases">
    <img src="https://img.shields.io/github/release-pre/alajmo/mani.svg" alt="version">
  </a>

  <a href="https://github.com/alajmo/mani/actions">
    <img src="https://github.com/alajmo/mani/workflows/release/badge.svg" alt="build status">
  </a>

  <a href="https://img.shields.io/badge/license-MIT-green">
    <img src="https://img.shields.io/badge/license-MIT-green" alt="license">
  </a>

  <a href="https://goreportcard.com/report/github.com/alajmo/mani">
    <img src="https://goreportcard.com/badge/github.com/alajmo/mani" alt="Go Report Card">
  </a>

  <a href="https://pkg.go.dev/github.com/alajmo/mani">
    <img src="https://pkg.go.dev/badge/github.com/alajmo/mani.svg" alt="reference">
  </a>
</div>

<br>

`mani` lets you manage multiple repositories and run commands across them.

![demo](res/demo.gif)

Interested in managing your servers in a similar way? Checkout [sake](https://github.com/alajmo/sake)!

## Table of Contents

- [Sponsors](#sponsors)
- [Installation](#installation)
  - [Building From Source](#building-from-source)
- [Usage](#usage)
  - [Initialize Mani](#initialize-mani)
  - [Example Commands](#example-commands)
  - [Documentation](#documentation)
- [License](#license)

## Sponsors

Mani is an MIT-licensed open source project with ongoing development. If you'd like to support their efforts, check out [Tabify](https://chromewebstore.google.com/detail/tabify/bokfkclamoepkmhjncgkdldmhfpgfdmo) - a Chrome extension that enhances your browsing experience with powerful window and tab management, focus-improving site blocking, and numerous features to optimize your browser workflow.

## Installation

`mani` is available on Linux and Mac, with partial support for Windows.

<details>
<summary><b>Binaries</b></summary>

Download from the [release](https://github.com/alajmo/mani/releases) page.
</details>

<details>
<summary><b>cURL</b> (Linux & macOS)</summary>

```sh
curl -sfL https://raw.githubusercontent.com/alajmo/mani/main/install.sh | sh
```
</details>

<details>
<summary><b>Homebrew</b></summary>

```sh
brew tap alajmo/mani
brew install mani
```
</details>

<details>
<summary><b>MacPorts</b></summary>

```sh
sudo port install mani
```
</details>

<details>
<summary><b>Arch</b> (AUR)</summary>

```sh
yay -S mani
```
</details>

<details>
<summary><b>Nix</b></summary>

```sh
nix-env -iA nixos.mani
```
</details>

<details>
<summary><b>Go</b></summary>

```sh
go get -u github.com/alajmo/mani
```
</details>

<details>
<summary><b>Building From Source</b></summary>

1. Clone the repo
2. Build and run the executable
    ```sh
    make build && ./dist/mani
    ```
</details>

Auto-completion is available via `mani completion bash|zsh|fish|powershell` and man page via `mani gen`.

## Usage

### Initialize Mani

Run the following command inside a directory containing your `git` repositories:

```sh
mani init
```

This will generate:

- `mani.yaml`: Contains projects and custom tasks. Any subdirectory that has a `.git` directory will be included (add the flag `--auto-discovery=false` to turn off this feature)
- `.gitignore`: (only when inside a git repo) Includes the projects specified in `mani.yaml` file. To opt out, use `mani init --sync-gitignore=false`.

It can be helpful to initialize the `mani` repository as a git repository so that anyone can easily download the `mani` repository and run `mani sync` to clone all repositories and get the same project setup as you.

### Example Commands

```bash
# List all projects
mani list projects

# Run git status across all projects
mani exec --all git status

# Run git status across all projects in parallel with output in table format
mani exec --all --parallel --output table git status
```

### Documentation

Checkout the following to learn more about mani:

- [Examples](examples)
- [Config](docs/config.md)
- [Commands](docs/commands.md)
- Documentation
  - [Filtering Projects](docs/filtering-projects.md)
  - [Variables](docs/variables.md)
  - [Output](docs/output.md)
- [Changelog](/docs/changelog.md)
- [Roadmap](/docs/roadmap.md)
- [Project Background](docs/project-background.md)
- [Contributing](docs/contributing.md)

## [License](LICENSE)

The MIT License (MIT)

Copyright (c) 2020-2021 Samir Alajmovic


================================================
FILE: benchmarks/.gitignore
================================================
# Ignore benchmark output files (they can be large and change frequently)
# Use `make bench-save` to generate new results
*.txt


================================================
FILE: cmd/check.go
================================================
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"

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

func checkCmd(configErr *error) *cobra.Command {
	cmd := cobra.Command{
		Use:   "check",
		Short: "Validate config",
		Long:  `Validate config.`,
		Example: `  # Validate config
  mani check`,
		Args: cobra.NoArgs,
		Run: func(cmd *cobra.Command, args []string) {
			if *configErr != nil {
				fmt.Printf("Found configuration errors:\n\n")
				core.Exit(*configErr)
			}

			fmt.Println("Config Valid")
		},
		DisableAutoGenTag: true,
	}

	return &cmd
}


================================================
FILE: cmd/completion.go
================================================
package cmd

import (
	"os"

	"github.com/spf13/cobra"

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

func completionCmd() *cobra.Command {
	cmd := cobra.Command{
		Use:   "completion <bash|zsh|fish|powershell>",
		Short: "Generate completion script",
		Long: `To load completions:
Bash:

  $ source <(mani completion bash)

  # To load completions for each session, execute once:
  # Linux:
  $ mani completion bash > /etc/bash_completion.d/mani
  # macOS:
  $ mani completion bash > /usr/local/etc/bash_completion.d/mani

Zsh:

  # If shell completion is not already enabled in your environment,
  # you will need to enable it.  You can execute the following once:

  $ echo "autoload -U compinit; compinit" >> ~/.zshrc

  # To load completions for each session, execute once:
  $ mani completion zsh > "${fpath[1]}/_mani"

  # You will need to start a new shell for this setup to take effect.

fish:

  $ mani completion fish | source

  # To load completions for each session, execute once:
  $ mani completion fish > ~/.config/fish/completions/mani.fish

PowerShell:

  PS> mani completion powershell | Out-String | Invoke-Expression

  # To load completions for every new session, run:
  PS> mani completion powershell > mani.ps1
  # and source this file from your PowerShell profile.
		`,
		DisableFlagsInUseLine: true,
		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
		Run:                   generateCompletion,
		DisableAutoGenTag:     true,
	}

	return &cmd
}

func generateCompletion(cmd *cobra.Command, args []string) {
	switch args[0] {
	case "bash":
		err := cmd.Root().GenBashCompletion(os.Stdout)
		core.CheckIfError(err)
	case "zsh":
		err := cmd.Root().GenZshCompletion(os.Stdout)
		core.CheckIfError(err)
	case "fish":
		err := cmd.Root().GenFishCompletion(os.Stdout, true)
		core.CheckIfError(err)
	case "powershell":
		err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
		core.CheckIfError(err)
	}
}


================================================
FILE: cmd/describe.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

func describeCmd(config *dao.Config, configErr *error) *cobra.Command {
	var describeFlags core.DescribeFlags

	cmd := cobra.Command{
		Aliases: []string{"desc"},
		Use:     "describe",
		Short:   "Describe projects and tasks",
		Long:    "Describe projects and tasks.",
		Example: `  # Describe all projects
  mani describe projects

  # Describe all tasks
  mani describe tasks`,
		DisableAutoGenTag: true,
	}

	cmd.AddCommand(
		describeProjectsCmd(config, configErr, &describeFlags),
		describeTasksCmd(config, configErr, &describeFlags),
	)

	cmd.PersistentFlags().StringVar(&describeFlags.Theme, "theme", "default", "set 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)

	return &cmd
}


================================================
FILE: cmd/describe_projects.go
================================================
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"

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

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

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

  # Describe projects by name
  mani describe projects <project>

  # Describe projects by tags
  mani describe projects --tags <tag>

  # Describe projects by paths
  mani describe projects --paths <path>

	# Describe projects matching a tag expression
	mani run <task> --tags-expr '<tag-1> || <tag-2>'`,

		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)

			setProjectFlags.All = cmd.Flags().Changed("all")
			setProjectFlags.Cwd = cmd.Flags().Changed("cwd")
			setProjectFlags.Target = cmd.Flags().Changed("target")

			describeProjects(config, args, &projectFlags, &setProjectFlags, describeFlags)
		},
		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
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVarP(&projectFlags.All, "all", "a", true, "select all projects")
	cmd.Flags().BoolVarP(&projectFlags.Cwd, "cwd", "k", false, "select current working directory")

	cmd.Flags().StringSliceVarP(&projectFlags.Tags, "tags", "t", []string{}, "filter projects by tags")
	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().StringVarP(&projectFlags.TagsExpr, "tags-expr", "E", "", "target projects by tags expression")
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "d", []string{}, "filter projects by paths")
	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().StringVarP(&projectFlags.Target, "target", "T", "", "target projects by target name")
	err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetTargetNames()
		return values, 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,
	setProjectFlags *core.SetProjectFlags,
	describeFlags *core.DescribeFlags,
) {
	if projectFlags.Edit {
		if len(args) > 0 {
			err := config.EditProject(args[0])
			core.CheckIfError(err)
		} else {
			err := config.EditProject("")
			core.CheckIfError(err)
		}
	} else {

		projectFlags.Projects = args
		if !setProjectFlags.All {
			// If no flags are set, use all and empty default target (but not the modified one by user)
			// If target is set, use the defaults from that target and respect other flags
			isNoFiltersSet := len(projectFlags.Projects) == 0 &&
				len(projectFlags.Paths) == 0 &&
				len(projectFlags.Tags) == 0 &&
				projectFlags.TagsExpr == "" &&
				!setProjectFlags.Cwd &&
				!setProjectFlags.Target
			projectFlags.All = isNoFiltersSet
		}
		projects, err := config.GetFilteredProjects(projectFlags)
		core.CheckIfError(err)

		if len(projects) == 0 {
			fmt.Println("No matching projects found")
		} else {
			theme, err := config.GetTheme(describeFlags.Theme)
			core.CheckIfError(err)

			output := print.PrintProjectBlocks(projects, true, theme.Block, print.GookitFormatter{})
			fmt.Print(output)
		}
	}
}


================================================
FILE: cmd/describe_tasks.go
================================================
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"

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

func describeTasksCmd(config *dao.Config, configErr *error, describeFlags *core.DescribeFlags) *cobra.Command {
	var taskFlags core.TaskFlags

	cmd := cobra.Command{
		Aliases: []string{"task", "tsk"},
		Use:     "tasks [tasks]",
		Short:   "Describe tasks",
		Long:    "Describe tasks.",
		Example: `  # Describe all tasks
  mani describe tasks

  # Describe task <task>
  mani describe task <task>`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			describe(config, args, taskFlags, describeFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			values := config.GetTaskNames()
			return values, cobra.ShellCompDirectiveNoFileComp
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVarP(&taskFlags.Edit, "edit", "e", false, "edit task")

	return &cmd
}

func describe(
	config *dao.Config,
	args []string,
	taskFlags core.TaskFlags,
	describeFlags *core.DescribeFlags,
) {
	if taskFlags.Edit {
		if len(args) > 0 {
			err := config.EditTask(args[0])
			core.CheckIfError(err)
		} else {
			err := config.EditTask("")
			core.CheckIfError(err)
		}
	} else {
		tasks, err := config.GetTasksByNames(args)
		core.CheckIfError(err)

		if len(tasks) == 0 {
			fmt.Println("No tasks")
		} else {
			dao.ParseTasksEnv(tasks)

			theme, err := config.GetTheme(describeFlags.Theme)
			core.CheckIfError(err)

			out := print.PrintTaskBlock(tasks, true, theme.Block, print.GookitFormatter{})
			fmt.Print(out)
		}
	}
}


================================================
FILE: cmd/edit.go
================================================
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:   "Open up mani config file",
		Long:    "Open up mani config file in $EDITOR.",

		Example: `  # Edit current context
  mani edit`,
		Run: func(cmd *cobra.Command, args []string) {
			err := *configErr
			switch e := err.(type) {
			case *core.ConfigNotFound:
				core.CheckIfError(e)
			default:
				runEdit(*config)
			}
		},
		DisableAutoGenTag: true,
	}

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

	return &cmd
}

func runEdit(config dao.Config) {
	err := config.EditConfig()
	core.CheckIfError(err)
}


================================================
FILE: cmd/edit_project.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

func editProject(config *dao.Config, configErr *error) *cobra.Command {
	cmd := cobra.Command{
		Aliases: []string{"projects", "proj", "pr"},
		Use:     "project [project]",
		Short:   "Edit mani project",
		Long:    `Edit mani project in $EDITOR.`,

		Example: `  # Edit projects
  mani edit project

  # Edit project <project>
  mani edit project <project>`,
		Run: func(cmd *cobra.Command, args []string) {
			err := *configErr
			switch e := err.(type) {
			case *core.ConfigNotFound:
				core.CheckIfError(e)
			default:
				runEditProject(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.GetProjectNames()
			return values, cobra.ShellCompDirectiveNoFileComp
		},
		DisableAutoGenTag: true,
	}

	return &cmd
}

func runEditProject(args []string, config dao.Config) {
	if len(args) > 0 {
		err := config.EditProject(args[0])
		core.CheckIfError(err)
	} else {
		err := config.EditProject("")
		core.CheckIfError(err)
	}
}


================================================
FILE: cmd/edit_task.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

func editTask(config *dao.Config, configErr *error) *cobra.Command {
	cmd := cobra.Command{
		Aliases: []string{"tasks", "tsks", "tsk"},
		Use:     "task [task]",
		Short:   "Edit mani task",
		Long:    `Edit mani task in $EDITOR.`,

		Example: `  # Edit tasks
  mani edit task

  # Edit task <task>
  mani edit task <task>`,
		Run: func(cmd *cobra.Command, args []string) {
			err := *configErr
			switch e := err.(type) {
			case *core.ConfigNotFound:
				core.CheckIfError(e)
			default:
				runEditTask(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.GetTaskNames()
			return values, cobra.ShellCompDirectiveNoFileComp
		},
		DisableAutoGenTag: true,
	}

	return &cmd
}

func runEditTask(args []string, config dao.Config) {
	if len(args) > 0 {
		err := config.EditTask(args[0])
		core.CheckIfError(err)
	} else {
		err := config.EditTask("")
		core.CheckIfError(err)
	}
}


================================================
FILE: cmd/exec.go
================================================
package cmd

import (
	"strings"

	"github.com/spf13/cobra"

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

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

	cmd := cobra.Command{
		Use:   "exec <command>",
		Short: "Execute arbitrary commands",
		Long: `Execute arbitrary commands.
Use single quotes around your command to prevent file globbing and 
environment variable expansion from occurring before the command is 
executed in each directory.`,

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

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

			// This is necessary since cobra doesn't support pointers for bools
			// (that would allow us to use nil as default value)
			setRunFlags.TTY = cmd.Flags().Changed("tty")
			setRunFlags.Parallel = cmd.Flags().Changed("parallel")
			setRunFlags.OmitEmptyRows = cmd.Flags().Changed("omit-empty-rows")
			setRunFlags.OmitEmptyColumns = cmd.Flags().Changed("omit-empty-columns")
			setRunFlags.IgnoreErrors = cmd.Flags().Changed("ignore-errors")
			setRunFlags.IgnoreNonExisting = cmd.Flags().Changed("ignore-non-existing")
			setRunFlags.Forks = cmd.Flags().Changed("forks")
			setRunFlags.Cwd = cmd.Flags().Changed("cwd")
			setRunFlags.All = cmd.Flags().Changed("all")

			if setRunFlags.Forks {
				forks, err := cmd.Flags().GetUint32("forks")
				core.CheckIfError(err)
				if forks == 0 {
					core.Exit(&core.ZeroNotAllowed{Name: "forks"})
				}
				runFlags.Forks = forks
			}

			execute(args, config, &runFlags, &setRunFlags)
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace current process")
	cmd.Flags().BoolVar(&runFlags.DryRun, "dry-run", false, "print commands without executing them")
	cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "s", false, "hide progress when running tasks")
	cmd.Flags().BoolVar(&runFlags.IgnoreNonExisting, "ignore-non-existing", false, "ignore non-existing projects")
	cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "ignore errors")
	cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "omit empty rows in table output")
	cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "omit empty columns in table output")
	cmd.Flags().BoolVar(&runFlags.Parallel, "parallel", false, "run tasks in parallel across projects")
	cmd.Flags().Uint32P("forks", "f", 4, "maximum number of concurrent processes")
	cmd.Flags().BoolVarP(&runFlags.Cwd, "cwd", "k", false, "use current working directory")
	cmd.Flags().BoolVarP(&runFlags.All, "all", "a", false, "target all projects")

	cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set output format [stream|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().StringVarP(&runFlags.Spec, "spec", "J", "", "set spec")
	err = cmd.RegisterFlagCompletionFunc("spec", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetSpecNames()
		return values, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&runFlags.Projects, "projects", "p", []string{}, "select projects by 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.Paths, "paths", "d", []string{}, "select projects by 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().StringSliceVarP(&runFlags.Tags, "tags", "t", []string{}, "select projects by 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)

	cmd.Flags().StringVarP(&runFlags.TagsExpr, "tags-expr", "E", "", "select projects by tags expression")
	core.CheckIfError(err)

	cmd.Flags().StringVarP(&runFlags.Target, "target", "T", "", "target projects by target name")
	err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetTargetNames()
		return values, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.PersistentFlags().StringVar(&runFlags.Theme, "theme", "", "set 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)

	return &cmd
}

func execute(
	args []string,
	config *dao.Config,
	runFlags *core.RunFlags,
	setRunFlags *core.SetRunFlags,
) {
	cmd := strings.Join(args[0:], " ")
	var tasks []dao.Task

	tasks, projects, err := dao.ParseCmd(cmd, runFlags, setRunFlags, config)
	core.CheckIfError(err)

	target := exec.Exec{Projects: projects, Tasks: tasks, Config: *config}
	err = target.Run([]string{}, runFlags, setRunFlags)
	core.CheckIfError(err)
}


================================================
FILE: cmd/gen.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

func genCmd() *cobra.Command {
	dir := ""
	cmd := cobra.Command{
		Use:                   "gen",
		Short:                 "Generate man page",
		DisableFlagsInUseLine: true,
		Run: func(cmd *cobra.Command, args []string) {
			err := core.GenManPages(dir)
			core.CheckIfError(err)
		},

		DisableAutoGenTag: true,
	}

	cmd.Flags().StringVarP(&dir, "dir", "d", "./", "directory to save manpage to")
	err := cmd.RegisterFlagCompletionFunc("dir", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		return nil, cobra.ShellCompDirectiveFilterDirs
	})
	core.CheckIfError(err)

	return &cmd
}


================================================
FILE: cmd/gen_docs.go
================================================
// This source will generate
//   - core/mani.1
//   - docs/commands.md
//
// and is not included in the final build.

package cmd

import (
	"github.com/spf13/cobra"

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

func genDocsCmd(longAppDesc string) *cobra.Command {
	cmd := cobra.Command{
		Use:                   "gen-docs",
		Short:                 "Generate man and markdown pages",
		DisableFlagsInUseLine: true,
		Run: func(cmd *cobra.Command, args []string) {
			err := core.CreateManPage(
				longAppDesc,
				version,
				date,
				rootCmd,
				runCmd(&config, &configErr),
				execCmd(&config, &configErr),
				initCmd(),
				syncCmd(&config, &configErr),
				editCmd(&config, &configErr),
				listCmd(&config, &configErr),
				describeCmd(&config, &configErr),
				tuiCmd(&config, &configErr),
				checkCmd(&configErr),
				genCmd(),
			)
			core.CheckIfError(err)
		},

		DisableAutoGenTag: true,
	}

	return &cmd
}


================================================
FILE: cmd/init.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

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 new mani repository by generating a mani.yaml configuration file 
and a .gitignore file in the current directory.`,

		Example: `  # Initialize with default settings
  mani init

  # Initialize without auto-discovering projects
  mani init --auto-discovery=false

  # Initialize without updating .gitignore
  mani init --sync-gitignore=false`,

		Args: cobra.MaximumNArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			foundProjects, err := dao.InitMani(args, initFlags)
			core.CheckIfError(err)

			if initFlags.AutoDiscovery {
				exec.PrintProjectInit(foundProjects)
			}
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVar(&initFlags.AutoDiscovery, "auto-discovery", true, "automatically discover and add Git repositories to mani.yaml")
	cmd.Flags().BoolVarP(&initFlags.SyncGitignore, "sync-gitignore", "g", true, "synchronize .gitignore file")

	return &cmd
}


================================================
FILE: cmd/list.go
================================================
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{"ls", "l"},
		Use:     "list",
		Short:   "List projects, tasks and tags",
		Long:    "List projects, tasks and tags.",
		Example: `  # List all projects
  mani list projects

  # List all tasks
  mani list tasks

  # List all tags
  mani list tags`,
		DisableAutoGenTag: true,
	}

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

	cmd.PersistentFlags().StringVar(&listFlags.Theme, "theme", "default", "set 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", "set output format [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
}


================================================
FILE: cmd/list_projects.go
================================================
package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"

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

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

	cmd := cobra.Command{
		Aliases: []string{"project", "proj", "pr"},
		Use:     "projects [projects]",
		Short:   "List projects",
		Long:    "List projects.",
		Example: `  # List all projects
  mani list projects

  # List projects by name
  mani list projects <project>

  # List projects by tags
  mani list projects --tags <tag>

  # List projects by paths
  mani list projects --paths <path>

	# List projects matching a tag expression
	mani run <task> --tags-expr '<tag-1> || <tag-2>'`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)

			setProjectFlags.All = cmd.Flags().Changed("all")
			setProjectFlags.Cwd = cmd.Flags().Changed("cwd")
			setProjectFlags.Target = cmd.Flags().Changed("target")

			listProjects(config, args, listFlags, &projectFlags, &setProjectFlags)
		},
		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
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVar(&listFlags.Tree, "tree", false, "display output in tree format")

	cmd.Flags().BoolVarP(&projectFlags.All, "all", "a", true, "select all projects")
	cmd.Flags().BoolVarP(&projectFlags.Cwd, "cwd", "k", false, "select current working directory")

	cmd.Flags().StringSliceVarP(&projectFlags.Tags, "tags", "t", []string{}, "select projects by tags")
	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().StringVarP(&projectFlags.TagsExpr, "tags-expr", "E", "", "select projects by tags expression")
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "d", []string{}, "select projects by paths")
	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().StringVarP(&projectFlags.Target, "target", "T", "", "select projects by target name")
	err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetTargetNames()
		return values, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringSliceVar(&projectFlags.Headers, "headers", []string{"project", "tag", "description"}, "specify columns to display [project, path, relpath, description, url, tag, worktree]")
	err = cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if err != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		validHeaders := []string{"project", "path", "relpath", "description", "url", "tag", "worktree"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listProjects(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	projectFlags *core.ProjectFlags,
	setProjectFlags *core.SetProjectFlags,
) {
	theme, err := config.GetTheme(listFlags.Theme)
	core.CheckIfError(err)

	if listFlags.Tree {
		tree, err := config.GetProjectsTree(projectFlags.Paths, projectFlags.Tags)
		core.CheckIfError(err)
		print.PrintTree(config, *theme, listFlags, tree)
		return
	}

	projectFlags.Projects = args
	// If flag All is not set and no other filters are applied set All to true.
	if !setProjectFlags.All {
		isNoFiltersSet := len(projectFlags.Projects) == 0 &&
			len(projectFlags.Paths) == 0 &&
			len(projectFlags.Tags) == 0 &&
			projectFlags.TagsExpr == "" &&
			!setProjectFlags.Cwd &&
			!setProjectFlags.Target
		projectFlags.All = isNoFiltersSet
	}
	projects, err := config.GetFilteredProjects(projectFlags)
	core.CheckIfError(err)

	if len(projects) == 0 {
		fmt.Println("No matching projects found")
	} else {
		theme.Table.Border.Rows = core.Ptr(false)
		theme.Table.Header.Format = core.Ptr("t")

		options := print.PrintTableOptions{
			Output:           listFlags.Output,
			Theme:            *theme,
			Tree:             listFlags.Tree,
			AutoWrap:         true,
			OmitEmptyRows:    false,
			OmitEmptyColumns: true,
			Color:            *theme.Color,
		}

		fmt.Println()
		print.PrintTable(projects, options, projectFlags.Headers, []string{}, os.Stdout)
		fmt.Println()
	}
}


================================================
FILE: cmd/list_tags.go
================================================
package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"

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

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

	cmd := cobra.Command{
		Aliases: []string{"tag"},
		Use:     "tags [tags]",
		Short:   "List tags",
		Long:    "List tags.",
		Example: `  # List all 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
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().StringSliceVar(&tagFlags.Headers, "headers", []string{"tag", "project"}, "specify columns to display [project, tag]")
	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", "project"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listTags(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	tagFlags *core.TagFlags,
) {
	theme, err := config.GetTheme(listFlags.Theme)
	core.CheckIfError(err)

	theme.Table.Border.Rows = core.Ptr(false)
	theme.Table.Header.Format = core.Ptr("t")

	options := print.PrintTableOptions{
		Output:           listFlags.Output,
		Theme:            *theme,
		Tree:             listFlags.Tree,
		AutoWrap:         true,
		OmitEmptyRows:    false,
		OmitEmptyColumns: true,
		Color:            *theme.Color,
	}

	allTags := config.GetTags()

	if len(args) > 0 {
		foundTags := core.Intersection(args, allTags)

		// Could not find one of the provided tags
		if len(foundTags) != len(args) {
			core.CheckIfError(&core.TagNotFound{Tags: args})
		}

		tags, err := config.GetTagAssocations(foundTags)
		core.CheckIfError(err)

		if len(tags) == 0 {
			fmt.Println("No tags")
		} else {
			fmt.Println()
			print.PrintTable(tags, options, tagFlags.Headers, []string{}, os.Stdout)
			fmt.Println()
		}
	} else {
		tags, err := config.GetTagAssocations(allTags)
		core.CheckIfError(err)
		if len(tags) == 0 {
			fmt.Println("No tags")
		} else {
			fmt.Println("")
			print.PrintTable(tags, options, tagFlags.Headers, []string{}, os.Stdout)
			fmt.Println("")
		}
	}
}


================================================
FILE: cmd/list_tasks.go
================================================
package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"

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

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

	cmd := cobra.Command{
		Aliases: []string{"task", "tsk", "tsks"},
		Use:     "tasks [tasks]",
		Short:   "List tasks",
		Long:    "List tasks.",
		Example: `  # List all tasks
  mani list tasks

  # List tasks by name
  mani list task <task>`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			listTasks(config, args, listFlags, &taskFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			values := config.GetTaskNames()
			return values, cobra.ShellCompDirectiveNoFileComp
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().StringSliceVar(&taskFlags.Headers, "headers", []string{"task", "description"}, "specify columns to display [task, description, target, spec]")
	err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}

		validHeaders := []string{"task", "description", "target", "spec"}
		return validHeaders, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	return &cmd
}

func listTasks(
	config *dao.Config,
	args []string,
	listFlags *core.ListFlags,
	taskFlags *core.TaskFlags,
) {
	tasks, err := config.GetTasksByNames(args)
	core.CheckIfError(err)

	theme, err := config.GetTheme(listFlags.Theme)
	core.CheckIfError(err)

	if len(tasks) == 0 {
		fmt.Println("No tasks")
	} else {
		theme.Table.Border.Rows = core.Ptr(false)
		theme.Table.Header.Format = core.Ptr("t")

		options := print.PrintTableOptions{
			Output:           listFlags.Output,
			Theme:            *theme,
			Tree:             listFlags.Tree,
			AutoWrap:         true,
			OmitEmptyRows:    false,
			OmitEmptyColumns: true,
			Color:            *theme.Color,
		}

		fmt.Println()
		print.PrintTable(tasks, options, taskFlags.Headers, []string{}, os.Stdout)
		fmt.Println()
	}
}


================================================
FILE: cmd/root.go
================================================
package cmd

import (
	"fmt"
	"os"
	"runtime"

	"github.com/spf13/cobra"

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

const (
	appName      = "mani"
	shortAppDesc = "repositories manager and task runner"
)

var (
	config         dao.Config
	configErr      error
	configFilepath string
	userConfigPath string
	color          bool
	buildMode      = ""
	version        = "dev"
	commit         = "none"
	date           = "n/a"
	rootCmd        = &cobra.Command{
		Use:     appName,
		Short:   shortAppDesc,
		Version: version,
	}
)

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		// When user input's wrong command or flag
		os.Exit(1)
	}
}

func init() {
	// Modify default shell in-case we're on windows
	if runtime.GOOS == "windows" {
		dao.DEFAULT_SHELL = "powershell -NoProfile"
		dao.DEFAULT_SHELL_PROGRAM = "powershell"
	}

	cobra.OnInitialize(initConfig)

	rootCmd.PersistentFlags().StringVarP(&configFilepath, "config", "c", "", "specify config")
	rootCmd.PersistentFlags().StringVarP(&userConfigPath, "user-config", "u", "", "specify user config")
	rootCmd.PersistentFlags().BoolVar(&color, "color", true, "enable color")

	rootCmd.AddCommand(
		completionCmd(),
		genCmd(),
		initCmd(),
		execCmd(&config, &configErr),
		runCmd(&config, &configErr),
		listCmd(&config, &configErr),
		describeCmd(&config, &configErr),
		syncCmd(&config, &configErr),
		editCmd(&config, &configErr),
		checkCmd(&configErr),
		tuiCmd(&config, &configErr),
	)

	rootCmd.SetVersionTemplate(fmt.Sprintf("Version: %-10s\nCommit: %-10s\nDate: %-10s\n", version, commit, date))

	// Add custom help template with footer
	defaultHelpTemplate := rootCmd.HelpTemplate()
	rootCmd.SetHelpTemplate(defaultHelpTemplate + `
Documentation: https://manicli.com
Issues:        https://github.com/alajmo/mani/issues
`)

	if buildMode == "man" {
		rootCmd.AddCommand(genDocsCmd("manage multiple repositories and run commands across them"))
	}

	rootCmd.DisableAutoGenTag = true
}

func initConfig() {
	config, configErr = dao.ReadConfig(configFilepath, userConfigPath, color)
}


================================================
FILE: cmd/run.go
================================================
package cmd

import (
	"strings"

	"github.com/spf13/cobra"

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

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

	cmd := cobra.Command{
		Use:   "run <task>",
		Short: "Run tasks",
		Long: `Run tasks.

The tasks are specified in a mani.yaml file along with the projects you can target.`,

		Example: `  # Execute task for all projects
  mani run <task> --all

  # Execute a task in parallel with a maximum of 8 concurrent processes
  mani run <task> --projects <project> --parallel --forks 8

  # Execute task for a specific projects
  mani run <task> --projects <project>

  # Execute a task for projects with specific tags
  mani run <task> --tags <tag>

  # Execute a task for projects matching specific paths
  mani run <task> --paths <path>

  # Execute a task for all projects matching a tag expression
  mani run <task> --tags-expr 'active || git' <tag>

  # Execute a task with environment variables from shell
  mani run <task> key=value`,

		DisableFlagsInUseLine: true,
		Args:                  cobra.MinimumNArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)

			// This is necessary since cobra doesn't support pointers for bools
			// (that would allow us to use nil as default value)
			setRunFlags.TTY = cmd.Flags().Changed("tty")
			setRunFlags.Cwd = cmd.Flags().Changed("cwd")
			setRunFlags.All = cmd.Flags().Changed("all")

			setRunFlags.Parallel = cmd.Flags().Changed("parallel")
			setRunFlags.OmitEmptyRows = cmd.Flags().Changed("omit-empty-rows")
			setRunFlags.OmitEmptyColumns = cmd.Flags().Changed("omit-empty-columns")
			setRunFlags.IgnoreErrors = cmd.Flags().Changed("ignore-errors")
			setRunFlags.IgnoreNonExisting = cmd.Flags().Changed("ignore-non-existing")
			setRunFlags.Forks = cmd.Flags().Changed("forks")

			if setRunFlags.Forks {
				forks, err := cmd.Flags().GetUint32("forks")
				core.CheckIfError(err)
				if forks == 0 {
					core.Exit(&core.ZeroNotAllowed{Name: "forks"})
				}
				runFlags.Forks = forks
			}

			run(args, config, &runFlags, &setRunFlags)
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			if *configErr != nil {
				return []string{}, cobra.ShellCompDirectiveDefault
			}

			return config.GetTaskNameAndDesc(), cobra.ShellCompDirectiveNoFileComp
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace current process")
	cmd.Flags().BoolVar(&runFlags.Describe, "describe", false, "display task information")
	cmd.Flags().BoolVar(&runFlags.DryRun, "dry-run", false, "display the task without execution")
	cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "s", false, "hide progress output during task execution")
	cmd.Flags().BoolVar(&runFlags.IgnoreNonExisting, "ignore-non-existing", false, "skip non-existing projects")
	cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "continue execution despite errors")
	cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "hide empty rows in table output")
	cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "hide empty columns in table output")
	cmd.Flags().BoolVar(&runFlags.Parallel, "parallel", false, "execute tasks in parallel across projects")
	cmd.Flags().BoolVarP(&runFlags.Edit, "edit", "e", false, "edit task")
	cmd.Flags().Uint32P("forks", "f", 4, "maximum number of concurrent processes")

	cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set output format [stream|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{"stream", "table", "html", "markdown"}
		return valid, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.Flags().StringVarP(&runFlags.Spec, "spec", "J", "", "set spec")
	err = cmd.RegisterFlagCompletionFunc("spec", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetSpecNames()
		return values, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

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

	cmd.Flags().BoolVarP(&runFlags.All, "all", "a", false, "select all projects")

	cmd.Flags().StringSliceVarP(&runFlags.Projects, "projects", "p", []string{}, "select projects by 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.Paths, "paths", "d", []string{}, "select projects by 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().StringSliceVarP(&runFlags.Tags, "tags", "t", []string{}, "select projects by 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)

	cmd.Flags().StringVarP(&runFlags.TagsExpr, "tags-expr", "E", "", "select projects by tags expression")
	core.CheckIfError(err)

	cmd.Flags().StringVarP(&runFlags.Target, "target", "T", "", "select projects by target name")
	err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if *configErr != nil {
			return []string{}, cobra.ShellCompDirectiveDefault
		}
		values := config.GetTargetNames()
		return values, cobra.ShellCompDirectiveDefault
	})
	core.CheckIfError(err)

	cmd.PersistentFlags().StringVar(&runFlags.Theme, "theme", "", "set 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)

	return &cmd
}

func run(
	args []string,
	config *dao.Config,
	runFlags *core.RunFlags,
	setRunFlags *core.SetRunFlags,
) {
	var taskNames []string
	var userArgs []string
	// Separate user arguments from task names
	for _, arg := range args {
		if strings.Contains(arg, "=") {
			userArgs = append(userArgs, arg)
		} else {
			taskNames = append(taskNames, arg)
		}
	}

	if runFlags.Edit {
		if len(args) > 0 {
			_ = config.EditTask(taskNames[0])
			return
		} else {
			_ = config.EditTask("")
			return
		}
	}

	var tasks []dao.Task
	var projects []dao.Project
	var err error
	if len(taskNames) == 1 {
		tasks, projects, err = dao.ParseSingleTask(taskNames[0], runFlags, setRunFlags, config)
	} else {
		tasks, projects, err = dao.ParseManyTasks(taskNames, runFlags, setRunFlags, config)
	}
	core.CheckIfError(err)

	target := exec.Exec{Projects: projects, Tasks: tasks, Config: *config}
	err = target.Run(userArgs, runFlags, setRunFlags)
	core.CheckIfError(err)
}


================================================
FILE: cmd/sync.go
================================================
package cmd

import (
	"github.com/spf13/cobra"

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

func syncCmd(config *dao.Config, configErr *error) *cobra.Command {
	var projectFlags core.ProjectFlags
	var syncFlags = core.SyncFlags{Forks: 4}
	var setSyncFlags core.SetSyncFlags

	cmd := cobra.Command{
		Use:     "sync",
		Aliases: []string{"clone"},
		Short:   "Clone repositories and update .gitignore",
		Long: `Clone repositories and update .gitignore file.
For repositories requiring authentication, disable parallel cloning to enter
credentials for each repository individually.`,
		Example: `  # Clone repositories one at a time
  mani sync

  # Clone repositories in parallel
  mani sync --parallel

  # Disable updating .gitignore file
  mani sync --sync-gitignore=false

  # Sync project remotes. This will modify the projects .git state
  mani sync --sync-remotes

  # Clone repositories even if project sync field is set to false
  mani sync --ignore-sync-state

  # Display sync status
  mani sync --status`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)

			// This is necessary since cobra doesn't support pointers for bools
			// (that would allow us to use nil as default value)
			setSyncFlags.Parallel = cmd.Flags().Changed("parallel")
			setSyncFlags.SyncGitignore = cmd.Flags().Changed("sync-gitignore")
			setSyncFlags.SyncRemotes = cmd.Flags().Changed("sync-remotes")
			setSyncFlags.RemoveOrphanedWorktrees = cmd.Flags().Changed("remove-orphaned-worktrees")
			setSyncFlags.Forks = cmd.Flags().Changed("forks")

			if setSyncFlags.Forks {
				forks, err := cmd.Flags().GetUint32("forks")
				core.CheckIfError(err)
				if forks == 0 {
					core.Exit(&core.ZeroNotAllowed{Name: "forks"})
				}
				syncFlags.Forks = forks
			}

			runSync(config, args, projectFlags, syncFlags, setSyncFlags)
		},
		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
		},
		DisableAutoGenTag: true,
	}

	cmd.Flags().BoolVarP(&syncFlags.SyncRemotes, "sync-remotes", "r", false, "update git remote state")
	cmd.Flags().BoolVarP(&syncFlags.RemoveOrphanedWorktrees, "remove-orphaned-worktrees", "w", false, "remove git worktrees not in config")
	cmd.Flags().BoolVarP(&syncFlags.SyncGitignore, "sync-gitignore", "g", true, "sync gitignore")
	cmd.Flags().BoolVar(&syncFlags.IgnoreSyncState, "ignore-sync-state", false, "sync project even if the project's sync field is set to false")
	cmd.Flags().BoolVarP(&syncFlags.Parallel, "parallel", "p", false, "clone projects in parallel")
	cmd.Flags().BoolVarP(&syncFlags.Status, "status", "s", false, "display status only")
	cmd.Flags().Uint32P("forks", "f", 4, "maximum number of concurrent processes")

	// Targets
	cmd.Flags().StringSliceVarP(&projectFlags.Tags, "tags", "t", []string{}, "clone projects by tags")
	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().StringVarP(&projectFlags.TagsExpr, "tags-expr", "E", "", "clone projects by tag expression")
	core.CheckIfError(err)

	cmd.Flags().StringSliceVarP(&projectFlags.Paths, "paths", "d", []string{}, "clone projects by 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 runSync(
	config *dao.Config,
	args []string,
	projectFlags core.ProjectFlags,
	syncFlags core.SyncFlags,
	setSyncFlags core.SetSyncFlags,
) {

	// If no flag is set for targetting projects, then assume all projects
	var allProjects bool
	if len(args) == 0 &&
		projectFlags.TagsExpr == "" &&
		len(projectFlags.Paths) == 0 &&
		len(projectFlags.Tags) == 0 {
		allProjects = true
	}

	projects, err := config.FilterProjects(false, allProjects, args, projectFlags.Paths, projectFlags.Tags, projectFlags.TagsExpr)
	core.CheckIfError(err)

	if !syncFlags.Status {
		if setSyncFlags.SyncRemotes {
			config.SyncRemotes = &syncFlags.SyncRemotes
		}

		if setSyncFlags.RemoveOrphanedWorktrees {
			config.RemoveOrphanedWorktrees = &syncFlags.RemoveOrphanedWorktrees
		}

		if setSyncFlags.SyncGitignore {
			config.SyncGitignore = &syncFlags.SyncGitignore
		}

		if *config.SyncGitignore {
			err := exec.UpdateGitignoreIfExists(config)
			core.CheckIfError(err)
		}

		err = exec.CloneRepos(config, projects, syncFlags)
		core.CheckIfError(err)
	}

	err = exec.PrintProjectStatus(config, projects)
	core.CheckIfError(err)
}


================================================
FILE: cmd/tui.go
================================================
package cmd

import (
	"github.com/alajmo/mani/core"
	"github.com/alajmo/mani/core/dao"
	"github.com/alajmo/mani/core/tui"
	"github.com/spf13/cobra"
)

func tuiCmd(config *dao.Config, configErr *error) *cobra.Command {
	var tuiFlags core.TUIFlags

	cmd := cobra.Command{
		Use:     "tui",
		Aliases: []string{"gui"},
		Short:   "TUI",
		Long:    `Run TUI`,
		Example: `  # Open tui
  mani tui`,
		Run: func(cmd *cobra.Command, args []string) {
			core.CheckIfError(*configErr)
			reloadChanged := cmd.Flags().Changed("reload-on-change")
			reload := config.ReloadTUI
			if reloadChanged {
				reload = &tuiFlags.Reload
			}

			tui.RunTui(config, tuiFlags.Theme, *reload)
		},
		DisableAutoGenTag: true,
	}

	cmd.PersistentFlags().StringVar(&tuiFlags.Theme, "theme", "default", "set 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.Flags().BoolVarP(&tuiFlags.Reload, "reload-on-change", "r", false, "reload mani on config change")

	return &cmd
}


================================================
FILE: core/config.man
================================================
.SH CONFIG

The mani.yaml config is based on the following concepts:

.RS 2
.IP "\(bu" 2
\fBprojects\fR are directories, which may be git repositories, in which case they have an URL attribute
.PD 0
.IP "\(bu" 2
\fBtasks\fR are shell commands that you write and then run for selected \fBprojects\fR
.IP "\(bu" 2
\fBspecs\fR are configs that alter \fBtask\fR execution and output
.PD 0
.IP "\(bu" 2
\fBtargets\fR are configs that provide shorthand filtering of \fBprojects\fR when executing tasks
.PD 0
.IP "\(bu" 2
\fBenv\fR are environment variables that can be defined globally, per project and per task
.PD 0
.RE

\fBSpecs\fR, \fBtargets\fR and \fBthemes\fR use a \fBdefault\fR object by default that the user can override to modify execution of mani commands.

Check the files and environment section to see how the config file is loaded.

Below is a config file detailing all of the available options and their defaults.

.RS 4
 # Import projects/tasks/env/specs/themes/targets from other configs
 import:
   - ./some-dir/mani.yaml

 # Shell used for commands
 # If you use any other program than bash, zsh, sh, node, and python
 # then you have to provide the command flag if you want the command-line string evaluted
 # For instance: bash -c
 shell: bash

 # If set to true, mani will override the URL of any existing remote
 # and remove remotes not found in the config
 sync_remotes: false

 # Determines whether the .gitignore should be updated when syncing projects
 sync_gitignore: true

 # When running the TUI, specifies whether it should reload when the mani config is changed
 reload_tui_on_change: false

 # List of Projects
 projects:
   # Project name [required]
   pinto:
     # Determines if the project should be synchronized during 'mani sync'
     sync: true

     # Project path relative to the config file
     # Defaults to project name if not specified
     path: frontend/pinto

     # Repository URL
     url: git@github.com:alajmo/pinto

     # Project description
     desc: A vim theme editor

     # Custom clone command
     # Defaults to "git clone URL PATH"
     clone: git clone git@github.com:alajmo/pinto --branch main

     # Branch to use as primary HEAD when cloning
     # Defaults to repository's primary HEAD
     branch:

     # When true, clones only the specified branch or primary HEAD
     single_branch: false

     # Project tags
     tags: [dev]

     # Remote repositories
     # Key is the remote name, value is the URL
     remotes:
       foo: https://github.com/bar

     # Project-specific environment variables
     env:
       # Simple string value
       branch: main

       # Shell command substitution
       date: $(date -u +"%Y-%m-%dT%H:%M:%S%Z")

 # List of Specs
 specs:
   default:
     # Output format for task results
     # Options: stream, table, html, markdown
     output: stream

     # Enable parallel task execution
     parallel: false

     # Maximum number of concurrent tasks when running in parallel
     forks: 4

     # When true, continues execution if a command fails in a multi-command task
     ignore_errors: false

     # When true, skips project entries in the config that don't exist
     # on the filesystem without throwing an error
     ignore_non_existing: false

     # Hide projects with no command output
     omit_empty_rows: false

     # Hide columns with no data
     omit_empty_columns: false

     # Clear screen before task execution (TUI only)
     clear_output: true

 # List of targets
 targets:
   default:
     # Select all projects
     all: false

     # Select project in current working directory
     cwd: false

     # Select projects by name
     projects: []

     # Select projects by path
     paths: []

     # Select projects by tag
     tags: []

     # Select projects by tag expression
     tags_expr: ""

 # Environment variables available to all tasks
 env:
   # Simple string value
   AUTHOR: "alajmo"

   # Shell command substitution
   DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z")

 # List of tasks
 tasks:
   # Command name [required]
   simple-2: echo "hello world"

   # Command name [required]
   simple-1:
     cmd: |
       echo "hello world"
     desc: simple command 1

   # Command name [required]
   advanced-command:
     # Task description
     desc: complex task

     # Task theme
     theme: default

     # Shell interpreter
     shell: bash

     # Task-specific environment variables
     env:
       # Static value
       branch: main

       # Dynamic shell command output
       num_lines: $(ls -1 | wc -l)

     # Can reference predefined spec:
     # spec: custom_spec
     # or define inline:
     spec:
       output: table
       parallel: true
       forks: 4
       ignore_errors: false
       ignore_non_existing: true
       omit_empty_rows: true
       omit_empty_columns: true

     # Can reference predefined target:
     # target: custom_target
     # or define inline:
     target:
       all: true
       cwd: false
       projects: [pinto]
       paths: [frontend]
       tags: [dev]
       tags_expr: (prod || dev) && !test

     # Single multi-line command
     cmd: |
       echo complex
       echo command

     # Multiple commands
     commands:
       # Node.js command example
       - name: node-example
 	       shell: node
         cmd: console.log("hello world from node.js");

       # Reference to another task
       - task: simple-1

 # List of themes
 # Styling Options:
 #   Fg (foreground color): Empty string (""), hex color, or named color from W3C standard
 #   Bg (background color): Empty string (""), hex color, or named color from W3C standard
 #   Format: Empty string (""), "lower", "title", "upper"
 #   Attribute: Empty string (""), "bold", "italic", "underline"
 #   Alignment: Empty string (""), "left", "center", "right"
 themes:
   # Theme name [required]
   default:
     # Stream Output Configuration
     stream:
       # Include project name prefix for each line
       prefix: true

       # Colors to alternate between for each project prefix
       prefix_colors: ["#d787ff", "#00af5f", "#d75f5f", "#5f87d7", "#00af87", "#5f00ff"]

       # Add a header before each project
       header: true

       # String value that appears before the project name in the header
       header_prefix: "TASK"

       # Fill remaining spaces with a character after the prefix
       header_char: "*"

     # Table Output Configuration
     table:
       # Table style
       # Available options: ascii, light, bold, double, rounded
       style: ascii

       # Border options for table output
       border:
         around: false  # Border around the table
         columns: true  # Vertical border between columns
         header: true   # Horizontal border between headers and rows
         rows: false    # Horizontal border between rows

       header:
         fg: "#d787ff"
         attr: bold
         format: ""

       title_column:
         fg: "#5f87d7"
         attr: bold
         format: ""

     # Tree View Configuration
     tree:
       # Tree style
       # Available options: ascii, light, bold, double, rounded, bullet-square, bullet-circle, bullet-star
       style: ascii

     # Block Display Configuration
     block:
       key:
         fg: "#5f87d7"
       separator:
         fg: "#5f87d7"
       value:
         fg:
       value_true:
         fg: "#00af5f"
       value_false:
         fg: "#d75f5f"

      # TUI Configuration
      tui:
        default:
          fg:
          bg:
          attr:

        border:
          fg:
        border_focus:
          fg: "#d787ff"

        title:
          fg:
          bg:
          attr:
          align: center
        title_active:
          fg: "#000000"
          bg: "#d787ff"
          attr:
          align: center

        button:
          fg:
          bg:
          attr:
          format:
        button_active:
          fg: "#080808"
          bg: "#d787ff"
          attr:
          format:

        table_header:
          fg: "#d787ff"
          bg:
          attr: bold
          align: left
          format:

        item:
          fg:
          bg:
          attr:
        item_focused:
          fg: "#ffffff"
          bg: "#262626"
          attr:
        item_selected:
          fg: "#5f87d7"
          bg:
          attr:
        item_dir:
          fg: "#d787ff"
          bg:
          attr:
        item_ref:
          fg: "#d787ff"
          bg:
          attr:

        search_label:
          fg: "#d7d75f"
          bg:
          attr: bold
        search_text:
          fg:
          bg:
          attr:

        filter_label:
          fg: "#d7d75f"
          bg:
          attr: bold
        filter_text:
          fg:
          bg:
          attr:

        shortcut_label:
          fg: "#00af5f"
          bg:
          attr:
        shortcut_text:
          fg:
          bg:
          attr:
.RE


.SH EXAMPLES

.TP
Initialize mani
.B samir@hal-9000 ~ $ mani init

.nf
Initialized mani repository in /tmp
- Created mani.yaml
- Created .gitignore

Following projects were added to mani.yaml

 Project  | Path
----------+------------
 test     | .
 pinto    | dev/pinto
.fi

.TP
Clone projects
.B samir@hal-9000 ~ $ mani sync --parallel --forks 8
.nf
pinto | Cloning into '/tmp/dev/pinto'...

 Project  | Synced
----------+--------
 test     | ✓
 pinto    | ✓
.fi

.TP
List all projects
.B samir@hal-9000 ~ $ mani list projects
.nf
 Project
---------
 test
 pinto
.fi

.TP
List all projects with output set to tree
.nf
.B samir@hal-9000 ~ $ mani list projects --tree
    ── dev
       └─ pinto
.fi

.nf

.TP
List all tags
.B samir@hal-9000 ~ $ mani list tags
.nf
 Tag | Project
-----+---------
 dev | pinto
.fi

.TP
List all tasks
.nf
.B samir@hal-9000 ~ $ mani list tasks
 Task             | Description
------------------+------------------
 simple-1         | simple command 1
 simple-2         |
 advanced-command | complex task
.fi

.TP
Describe a task
.nf
.B samir@hal-9000 ~ $ mani describe tasks advanced-command
Name: advanced-command
Description: complex task
Theme: default
Target:
    All: true
    Cwd: false
    Projects: pinto
    Paths: frontend
    Tags: dev
    TagsExpr: ""
Spec:
    Output: table
    Parallel: true
    Forks: 4
    IgnoreErrors: false
    IgnoreNonExisting: false
    OmitEmptyRows: false
    OmitEmptyColumns: false
Env:
    branch: dev
    num_lines: 2
Cmd:
    echo advanced
    echo command
Commands:
     - simple-1
     - simple-2
     - cmd
.fi

.TP
Run a task for all projects with tag 'dev'
.nf
.B samir@hal-9000 ~ $ mani run simple-1 --tags dev
 Project | Simple-1
---------+-------------
 pinto   | hello world
.fi

.TP
Run a task for all projects matching tags expression 'dev && !prod'
.nf
.B samir@hal-9000 ~ $ mani run simple-1 --tags-expr '(dev && !prod)'
 Project | Simple-1
---------+-------------
 pinto   | hello world
.fi

.TP
Run ad-hoc command for all projects
.nf
.B samir@hal-9000 ~ $ mani exec 'echo 123' --all
 Project | Output
---------+--------
 archive | 123
 pinto   | 123
.fi

.SH FILTERING PROJECTS
Projects can be filtered when managing projects (sync, list, describe) or running tasks. 
Filters can be specified through CLI flags or target configurations. 
The filtering is inclusive, meaning projects must satisfy all specified filters to be included in the results.

.PP
Available options:
.RS 2
.IP "\(bu" 2
cwd: include only the project under the current working directory, ignoring all other filters
.IP "\(bu" 2
all: include all projects
.IP "\(bu" 2
projects: Filter by project names
.IP "\(bu" 2
paths: Filter by project paths
.IP "\(bu" 2
tags: Filter by project tags
.IP "\(bu" 2
tags_expr: Filter using tag logic expressions
.IP "\(bu" 2
target: Filter using target
.RE

.PP

For \fBmani sync/list/describe\fR:
.RS 2
.IP "\(bu" 2
No filters: Targets all projects
.IP "\(bu" 2
Multiple filters: Select intersection of projects/paths/tags/tags_expr/target filters
.RE

For \fBmani run/exec\fR:
.RS 2
.IP "1." 4
Runtime flags (highest priority)
.IP "2." 4
Target flag configuration (\fB--target\fR)
.IP "3." 4
Task's default target data (lowest priority)
.RE

The default target is named `default` and can be overridden by defining a target named `default` in the config. This only applies for sub-commands `run` and `exec`.

.SH TAGS EXPRESSION

Tag expressions allow filtering projects using boolean operations on their tags. 
The expression is evaluated for each project's tags to determine if the project should be included.

.PP
Operators (in precedence order):

.RS 2
.IP "\(bu" 2
(): Parentheses for grouping
.PD 0
.IP "\(bu" 2
!: NOT operator (logical negation)
.PD 0
.IP "\(bu" 2
&&: AND operator (logical conjunction)
.PD 0
.IP "\(bu" 2
||: OR operator (logical disjunction)
.RE

.PP
For example, the expression:

  \fB(main && (dev || prod)) && !test\fR

.PP
requires the projects to pass these conditions:

.RS 2
.IP "\(bu" 2
Must have "main" tag
.PD 0
.IP "\(bu" 2
Must have either "dev" OR "prod" tag
.IP "\(bu" 2
Must NOT have "test" tag
.PD 0
.RE

.SH FILES

When running a command,
.B mani
will check the current directory and all parent directories for the following files: mani.yaml, mani.yml, .mani.yaml, .mani.yml.

Additionally, it will import (if found) a config file from:

.RS 2
.IP "\(bu" 2
Linux: \fB$XDG_CONFIG_HOME/mani/config.yaml\fR or \fB$HOME/.config/mani/config.yaml\fR if \fB$XDG_CONFIG_HOME\fR is not set.
.IP "\(bu" 2
Darwin: \fB$HOME/Library/Application Support/mani/config.yaml\fR
.IP "\(bu" 2
Windows: \fB%AppData%\mani\fR
.RE

Both the config and user config can be specified via flags or environments variables.

.SH
ENVIRONMENT

.TP
.B MANI_CONFIG
Override config file path

.TP
.B MANI_USER_CONFIG
Override user config file path

.TP
.B NO_COLOR
If this env variable is set (regardless of value) then all colors will be disabled

.SH BUGS

See GitHub Issues:
.UR https://github.com/alajmo/mani/issues
.ME .

.SH AUTHOR

.B mani
was written by Samir Alajmovic
.MT alajmovic.samir@gmail.com
.ME .
For updates and more information go to
.UR https://\:www.manicli.com
manicli.com
.UE .


================================================
FILE: core/dao/benchmark_test.go
================================================
package dao

import (
	"fmt"
	"testing"
)

// Helper to create a config with N projects, M tasks, and default specs/themes/targets
func createBenchmarkConfig(numProjects, numTasks int) Config {
	config := Config{}

	// Create projects
	config.ProjectList = make([]Project, numProjects)
	for i := 0; i < numProjects; i++ {
		config.ProjectList[i] = Project{
			Name:    fmt.Sprintf("project-%d", i),
			Path:    fmt.Sprintf("/path/to/project-%d", i),
			RelPath: fmt.Sprintf("project-%d", i),
			Tags:    []string{"tag1", "tag2"},
		}
	}

	// Create tasks
	config.TaskList = make([]Task, numTasks)
	for i := 0; i < numTasks; i++ {
		config.TaskList[i] = Task{
			Name: fmt.Sprintf("task-%d", i),
			Cmd:  fmt.Sprintf("echo task %d", i),
		}
	}

	// Create specs
	config.SpecList = []Spec{
		{Name: "default", Output: "stream", Forks: 4},
		{Name: "parallel", Output: "stream", Parallel: true, Forks: 8},
	}

	// Create themes
	config.ThemeList = []Theme{
		{Name: "default"},
		{Name: "custom"},
	}

	// Create targets
	config.TargetList = []Target{
		{Name: "default", All: true},
		{Name: "frontend", Tags: []string{"frontend"}},
	}

	return config
}

// Lookup_GetProject: Find project by name (O(n) linear search)
func BenchmarkLookup_GetProject(b *testing.B) {
	sizes := []int{10, 50, 100, 500}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			// Look up a project in the middle
			targetName := fmt.Sprintf("project-%d", size/2)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProject(targetName)
			}
		})
	}
}

// Lookup_GetTask: Find task by name (O(n) linear search)
func BenchmarkLookup_GetTask(b *testing.B) {
	sizes := []int{10, 50, 100, 500}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("tasks_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(10, size)
			// Look up a task in the middle
			targetName := fmt.Sprintf("task-%d", size/2)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetTask(targetName)
			}
		})
	}
}

// Lookup_GetSpec: Find spec by name
func BenchmarkLookup_GetSpec(b *testing.B) {
	config := createBenchmarkConfig(10, 10)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = config.GetSpec("default")
	}
}

// Lookup_GetTheme: Find theme by name
func BenchmarkLookup_GetTheme(b *testing.B) {
	config := createBenchmarkConfig(10, 10)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = config.GetTheme("default")
	}
}

// Lookup_GetTarget: Find target by name
func BenchmarkLookup_GetTarget(b *testing.B) {
	config := createBenchmarkConfig(10, 10)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _ = config.GetTarget("default")
	}
}

// Filter_ByName: Filter projects by name list
func BenchmarkFilter_ByName(b *testing.B) {
	sizes := []int{10, 50, 100}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			// Look up 5 projects
			names := []string{
				fmt.Sprintf("project-%d", size/5),
				fmt.Sprintf("project-%d", size/4),
				fmt.Sprintf("project-%d", size/3),
				fmt.Sprintf("project-%d", size/2),
				fmt.Sprintf("project-%d", size-1),
			}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByName(names)
			}
		})
	}
}

// Filter_ByTags: Filter projects by tags
func BenchmarkFilter_ByTags(b *testing.B) {
	sizes := []int{10, 50, 100, 500}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			tags := []string{"tag1"}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByTags(tags)
			}
		})
	}
}

// Filter_ByPath: Filter by path patterns (simple, *, **)
func BenchmarkFilter_ByPath(b *testing.B) {
	sizes := []int{10, 50, 100}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d_simple", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			paths := []string{"project-1"}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByPath(paths)
			}
		})

		b.Run(fmt.Sprintf("projects_%d_glob", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			paths := []string{"project-*"}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByPath(paths)
			}
		})

		b.Run(fmt.Sprintf("projects_%d_doubleglob", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			paths := []string{"**/project-*"}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByPath(paths)
			}
		})
	}
}

// Filter_Combined: FilterProjects with multiple criteria
func BenchmarkFilter_Combined(b *testing.B) {
	sizes := []int{10, 50, 100}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d_all", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.FilterProjects(false, true, nil, nil, nil, "")
			}
		})

		b.Run(fmt.Sprintf("projects_%d_bytags", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.FilterProjects(false, false, nil, nil, []string{"tag1"}, "")
			}
		})
	}
}

// Util_ConfigLoad: Simulates config loading (ParseTask lookups)
func BenchmarkUtil_ConfigLoad(b *testing.B) {
	taskCounts := []int{10, 25, 50, 100}

	for _, numTasks := range taskCounts {
		b.Run(fmt.Sprintf("tasks_%d", numTasks), func(b *testing.B) {
			config := createBenchmarkConfig(50, numTasks)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				// Simulate what happens during config load:
				// Each task calls GetTheme, GetSpec, GetTarget
				for j := 0; j < numTasks; j++ {
					_, _ = config.GetTheme("default")
					_, _ = config.GetSpec("default")
					_, _ = config.GetTarget("default")
				}
			}
		})
	}
}

// Lookup_GetCommand: Find task and convert to command
func BenchmarkLookup_GetCommand(b *testing.B) {
	sizes := []int{10, 50, 100, 500}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("tasks_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(10, size)
			targetName := fmt.Sprintf("task-%d", size/2)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetCommand(targetName)
			}
		})
	}
}

// Filter_ByTagsExpr: Filter using tag expressions (&&, ||, !)
func BenchmarkFilter_ByTagsExpr(b *testing.B) {
	sizes := []int{10, 50, 100}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d_simple", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByTagsExpr("tag1")
			}
		})

		b.Run(fmt.Sprintf("projects_%d_and", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByTagsExpr("tag1 && tag2")
			}
		})

		b.Run(fmt.Sprintf("projects_%d_or", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByTagsExpr("tag1 || tag2")
			}
		})

		b.Run(fmt.Sprintf("projects_%d_complex", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = config.GetProjectsByTagsExpr("(tag1 && tag2) || !tag3")
			}
		})
	}
}

// Util_GetCwdProject: Find project matching current directory
func BenchmarkUtil_GetCwdProject(b *testing.B) {
	sizes := []int{10, 50, 100, 500}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				// This will search through all projects
				// In real usage, it matches against cwd
				_, _ = config.GetCwdProject()
			}
		})
	}
}

// Filter_Intersect: Intersection of project lists
func BenchmarkFilter_Intersect(b *testing.B) {
	sizes := []int{10, 50, 100}

	for _, size := range sizes {
		b.Run(fmt.Sprintf("projects_%d", size), func(b *testing.B) {
			config := createBenchmarkConfig(size, 10)
			// Create two overlapping project lists
			list1 := config.ProjectList[:size/2]
			list2 := config.ProjectList[size/4:]

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_ = config.GetIntersectProjects(list1, list2)
			}
		})
	}
}


================================================
FILE: core/dao/common.go
================================================
package dao

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

	"github.com/gookit/color"
	"gopkg.in/yaml.v3"

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

// Resource Errors

type ResourceErrors[T any] struct {
	Resource *T
	Errors   []error
}

type Resource interface {
	GetContext() string
	GetContextLine() int
}

func FormatErrors(re Resource, errs []error) error {
	var msg = ""
	partsRe := regexp.MustCompile(`line (\d*): (.*)`)

	context := re.GetContext()

	var errPrefix = color.FgRed.Sprintf("error")
	var ptrPrefix = color.FgBlue.Sprintf("-->")
	for _, err := range errs {
		match := partsRe.FindStringSubmatch(err.Error())
		// In-case matching fails, return unformatted error
		if len(match) != 3 {
			contextLine := re.GetContextLine()

			if contextLine == -1 {
				msg = fmt.Sprintf("%s%s: %s\n  %s %s\n\n", msg, errPrefix, err, ptrPrefix, context)
			} else {
				msg = fmt.Sprintf("%s%s: %s\n  %s %s:%d\n\n", msg, errPrefix, err, ptrPrefix, context, contextLine)
			}
		} else {
			msg = fmt.Sprintf("%s%s: %s\n  %s %s:%s\n\n", msg, errPrefix, match[2], ptrPrefix, context, match[1])
		}
	}

	if msg != "" {
		return &core.ConfigErr{Msg: msg}
	}

	return nil
}

// ENV

func ParseNodeEnv(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 EvaluateEnv(envList []string) ([]string, error) {
	var envs []string

	for _, arg := range envList {
		kv := strings.SplitN(arg, "=", 2)

		if val, hasPrefix := strings.CutPrefix(kv[1], "$("); hasPrefix {
			if cmdStr, hasSuffix := strings.CutSuffix(val, ")"); hasSuffix {
				cmd := exec.Command("sh", "-c", cmdStr)
				cmd.Env = os.Environ()
				out, err := cmd.CombinedOutput()
				if err != nil {
					return envs, &core.ConfigEnvFailed{Name: kv[0], Err: string(out)}
				}

				envs = append(envs, fmt.Sprintf("%v=%v", kv[0], string(out)))
				continue
			}
		}
		envs = append(envs, fmt.Sprintf("%v=%v", kv[0], kv[1]))
	}

	return envs, nil
}

// MergeEnvs Merges environment variables.
// Priority is from highest to lowest (1st env takes precedence over the last entry).
func MergeEnvs(envs ...[]string) []string {
	var mergedEnvs []string
	args := make(map[string]bool)

	for _, part := range envs {
		for _, elem := range part {
			elem = strings.TrimSuffix(elem, "\n")

			kv := strings.SplitN(elem, "=", 2)
			_, ok := args[kv[0]]

			if !ok {
				mergedEnvs = append(mergedEnvs, elem)
				args[kv[0]] = true
			}
		}
	}

	return mergedEnvs
}


================================================
FILE: core/dao/common_test.go
================================================
package dao

import (
	"testing"

	"gopkg.in/yaml.v3"
)

func TestEnv_ParseNodeEnv(t *testing.T) {
	tests := []struct {
		name     string
		input    yaml.Node
		expected []string
	}{
		{
			name: "basic env variables",
			input: yaml.Node{
				Content: []*yaml.Node{
					{Value: "KEY1"},
					{Value: "value1"},
					{Value: "KEY2"},
					{Value: "value2"},
				},
			},
			expected: []string{
				"KEY1=value1",
				"KEY2=value2",
			},
		},
		{
			name: "empty env",
			input: yaml.Node{
				Content: []*yaml.Node{},
			},
			expected: []string{},
		},
		{
			name: "env with special characters",
			input: yaml.Node{
				Content: []*yaml.Node{
					{Value: "PATH"},
					{Value: "/usr/bin:/bin"},
					{Value: "URL"},
					{Value: "http://example.com"},
				},
			},
			expected: []string{
				"PATH=/usr/bin:/bin",
				"URL=http://example.com",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ParseNodeEnv(tt.input)
			if !equalStringSlices(result, tt.expected) {
				t.Errorf("expected %v, got %v", tt.expected, result)
			}
		})
	}
}

func TestEnv_MergeEnvs(t *testing.T) {
	tests := []struct {
		name     string
		inputs   [][]string
		expected []string
	}{
		{
			name: "basic merge",
			inputs: [][]string{
				{"KEY1=value1", "KEY2=value2"},
				{"KEY3=value3"},
			},
			expected: []string{
				"KEY1=value1",
				"KEY2=value2",
				"KEY3=value3",
			},
		},
		{
			name: "override priority",
			inputs: [][]string{
				{"KEY1=override"},
				{"KEY1=original", "KEY2=value2"},
			},
			expected: []string{
				"KEY1=override",
				"KEY2=value2",
			},
		},
		{
			name: "empty inputs",
			inputs: [][]string{
				{},
				{},
			},
			expected: []string{},
		},
		{
			name: "with newline characters",
			inputs: [][]string{
				{"KEY1=value1\n", "KEY2=value2\n"},
				{"KEY3=value3\n"},
			},
			expected: []string{
				"KEY1=value1",
				"KEY2=value2",
				"KEY3=value3",
			},
		},
		{
			name: "complex values",
			inputs: [][]string{
				{"PATH=/usr/bin:/bin", "URL=http://example.com"},
				{"DEBUG=true", "PATH=/custom/path"},
			},
			expected: []string{
				"PATH=/usr/bin:/bin",
				"URL=http://example.com",
				"DEBUG=true",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := MergeEnvs(tt.inputs...)
			if !equalStringSlices(result, tt.expected) {
				t.Errorf("expected %v, got %v", tt.expected, result)
			}
		})
	}
}


================================================
FILE: core/dao/config.go
================================================
package dao

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"text/template"

	"github.com/alajmo/mani/core"
	"github.com/gookit/color"
	"gopkg.in/yaml.v3"
)

var (
	DEFAULT_SHELL         = "bash -c"
	DEFAULT_SHELL_PROGRAM = "bash"
	ACCEPTABLE_FILE_NAMES = []string{"mani.yaml", "mani.yml", ".mani.yaml", ".mani.yml"}

	DEFAULT_THEME = Theme{
		Name:   "default",
		Stream: DefaultStream,
		Table:  DefaultTable,
		Tree:   DefaultTree,
		TUI:    DefaultTUI,
		Block:  DefaultBlock,
		Color:  core.Ptr(true),
	}

	DEFAULT_TARGET = Target{
		Name: "default",

		All: false,
		Cwd: false,

		Projects: []string{},
		Paths:    []string{},
		Tags:     []string{},

		TagsExpr: "",
	}

	DEFAULT_SPEC = Spec{
		Name:   "default",
		Output: "stream",

		Parallel: false,
		Forks:    4,

		IgnoreErrors:      false,
		IgnoreNonExisting: false,

		OmitEmptyRows:    false,
		OmitEmptyColumns: false,

		ClearOutput: true,
	}
)

type Config struct {
	// Internal
	EnvList        []string  `yaml:"-"`
	ImportData     []Import  `yaml:"-"`
	ThemeList      []Theme   `yaml:"-"`
	SpecList       []Spec    `yaml:"-"`
	TargetList     []Target  `yaml:"-"`
	ProjectList    []Project `yaml:"-"`
	TaskList       []Task    `yaml:"-"`
	Path           string    `yaml:"-"`
	Dir            string    `yaml:"-"`
	UserConfigFile *string   `yaml:"-"`
	ConfigPaths    []string  `yaml:"-"`
	Color          bool      `yaml:"-"`

	Shell                   string `yaml:"shell"`
	SyncRemotes             *bool  `yaml:"sync_remotes"`
	SyncGitignore           *bool  `yaml:"sync_gitignore"`
	RemoveOrphanedWorktrees *bool  `yaml:"remove_orphaned_worktrees"`
	ReloadTUI               *bool  `yaml:"reload_tui_on_change"`

	// Intermediate
	Env      yaml.Node `yaml:"env"`
	Import   yaml.Node `yaml:"import"`
	Themes   yaml.Node `yaml:"themes"`
	Specs    yaml.Node `yaml:"specs"`
	Targets  yaml.Node `yaml:"targets"`
	Projects yaml.Node `yaml:"projects"`
	Tasks    yaml.Node `yaml:"tasks"`
}

func (c *Config) GetContext() string {
	return c.Path
}

func (c *Config) GetContextLine() int {
	return -1
}

// Returns the config env list as a string splice in the form [key=value, key1=$(echo 123)]
func (c Config) GetEnvList() []string {
	var envs []string
	count := len(c.Env.Content)
	for i := 0; i < count; i += 2 {
		env := fmt.Sprintf("%v=%v", c.Env.Content[i].Value, c.Env.Content[i+1].Value)
		envs = append(envs, env)
	}

	return envs
}

func getUserConfigFile(userConfigPath string) *string {
	// Flag
	if userConfigPath != "" {
		if _, err := os.Stat(userConfigPath); err == nil {
			return &userConfigPath
		}
	}

	// Env
	val, present := os.LookupEnv("MANI_USER_CONFIG")
	if present {
		return &val
	}

	// Default
	defaultUserConfigDir, _ := os.UserConfigDir()
	defaultUserConfigPath := filepath.Join(defaultUserConfigDir, "mani", "config.yaml")
	if _, err := os.Stat(defaultUserConfigPath); err == nil {
		return &defaultUserConfigPath
	}

	return nil
}

// Function to read Mani configs.
func ReadConfig(configFilepath string, userConfigPath string, colorFlag bool) (Config, error) {
	color := CheckUserColor(colorFlag)
	var configPath string

	userConfigFile := getUserConfigFile(userConfigPath)

	// Try to find config file in current directory and all parents
	if configFilepath != "" {
		filename, err := filepath.Abs(configFilepath)
		if err != nil {
			return Config{}, err
		}

		configPath = filename
	} else {
		wd, err := os.Getwd()
		if err != nil {
			return Config{}, err
		}

		// Check first cwd and all parent directories, then if not found,
		// check if env variable MANI_CONFIG is set, and if not found
		// return no config found
		filename, err := core.FindFileInParentDirs(wd, ACCEPTABLE_FILE_NAMES)
		if err != nil {
			val, present := os.LookupEnv("MANI_CONFIG")
			if present {
				filename = val
			} else {
				return Config{}, err
			}
		}

		filename, err = core.ResolveTildePath(filename)
		if err != nil {
			return Config{}, err
		}

		filename, err = filepath.Abs(filename)
		if err != nil {
			return Config{}, err
		}

		configPath = filename
	}

	dat, err := os.ReadFile(configPath)
	if err != nil {
		return Config{}, err
	}

	// Found config, now try to read it
	var config Config

	config.Path = configPath
	config.Dir = filepath.Dir(configPath)
	config.UserConfigFile = userConfigFile
	config.Color = color

	err = yaml.Unmarshal(dat, &config)
	if err != nil {
		re := ResourceErrors[Config]{Resource: &config, Errors: []error{err}}
		return config, FormatErrors(re.Resource, re.Errors)
	}

	// Set default shell command
	if config.Shell == "" {
		config.Shell = DEFAULT_SHELL
	} else {
		config.Shell = core.FormatShell(config.Shell)
	}

	// Set Sync Gitignore
	if config.SyncGitignore == nil {
		config.SyncGitignore = core.Ptr(true)
	}

	// Set Reload TUI
	if config.ReloadTUI == nil {
		config.ReloadTUI = core.Ptr(false)
	}

	// Set Sync Remote
	if config.SyncRemotes == nil {
		config.SyncRemotes = core.Ptr(false)
	}

	// Set Remove Orphan Worktrees
	if config.RemoveOrphanedWorktrees == nil {
		config.RemoveOrphanedWorktrees = core.Ptr(false)
	}

	configResources, err := config.importConfigs()
	if err != nil {
		return config, err
	}

	config.TaskList = configResources.Tasks
	config.ProjectList = configResources.Projects
	config.ThemeList = configResources.Themes
	config.SpecList = configResources.Specs
	config.TargetList = configResources.Targets
	config.EnvList = configResources.Envs

	config.CheckConfigNoColor()

	for _, configPath := range configResources.Imports {
		config.ConfigPaths = append(config.ConfigPaths, configPath.Path)
	}

	// Set default theme if it's not set already
	_, err = config.GetTheme(DEFAULT_THEME.Name)
	if err != nil {
		config.ThemeList = append(config.ThemeList, DEFAULT_THEME)
	}

	// Set default spec if it's not set already
	_, err = config.GetSpec(DEFAULT_SPEC.Name)
	if err != nil {
		config.SpecList = append(config.SpecList, DEFAULT_SPEC)
	}

	// Set default target if it's not set already
	_, err = config.GetTarget(DEFAULT_TARGET.Name)
	if err != nil {
		config.TargetList = append(config.TargetList, DEFAULT_TARGET)
	}

	// Parse all tasks
	taskErrors := make([]ResourceErrors[Task], len(configResources.Tasks))
	for i := range configResources.Tasks {
		taskErrors[i].Resource = &configResources.Tasks[i]
		configResources.Tasks[i].ParseTask(config, &taskErrors[i])
	}

	var configErr = ""
	for _, taskError := range taskErrors {
		if len(taskError.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(taskError.Resource, taskError.Errors))
		}
	}

	if configErr != "" {
		return config, &core.ConfigErr{Msg: configErr}
	}

	return config, nil
}

// Open mani config in editor
func (c Config) EditConfig() error {
	return openEditor(c.Path, -1)
}

func openEditor(path string, lineNr int) error {
	editor := os.Getenv("EDITOR")
	var args []string

	if lineNr > 0 {
		switch editor {
		case "nvim":
			args = []string{fmt.Sprintf("+%v", lineNr), path}
		case "vim":
			args = []string{fmt.Sprintf("+%v", lineNr), path}
		case "vi":
			args = []string{fmt.Sprintf("+%v", lineNr), path}
		case "emacs":
			args = []string{fmt.Sprintf("+%v", lineNr), path}
		case "nano":
			args = []string{fmt.Sprintf("+%v", lineNr), path}
		case "code": // visual studio code
			args = []string{"--goto", fmt.Sprintf("%s:%v", path, lineNr)}
		case "idea": // Intellij
			args = []string{"--line", fmt.Sprintf("%v", lineNr), path}
		case "subl": // Sublime
			args = []string{fmt.Sprintf("%s:%v", path, lineNr)}
		case "atom":
			args = []string{fmt.Sprintf("%s:%v", path, lineNr)}
		case "notepad-plus-plus":
			args = []string{"-n", fmt.Sprintf("%v", lineNr), path}
		default:
			args = []string{path}
		}
	} else {
		args = []string{path}
	}

	cmd := exec.Command(editor, args...)
	cmd.Env = os.Environ()
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	err := cmd.Run()
	if err != nil {
		return err
	}

	return nil
}

// Open mani config in editor and optionally go to line matching the task name
func (c Config) EditTask(name string) error {
	configPath := c.Path
	if name != "" {
		task, err := c.GetTask(name)
		if err != nil {
			return err
		}
		configPath = task.context
	}

	dat, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}

	type ConfigTmp struct {
		Tasks yaml.Node
	}

	var configTmp ConfigTmp
	err = yaml.Unmarshal([]byte(dat), &configTmp)
	if err != nil {
		return err
	}

	lineNr := 0
	if name == "" {
		lineNr = configTmp.Tasks.Line - 1
	} else {
		for _, task := range configTmp.Tasks.Content {
			if task.Value == name {
				lineNr = task.Line
				break
			}
		}
	}

	return openEditor(configPath, lineNr)
}

// Open mani config in editor and optionally go to line matching the project name
func (c Config) EditProject(name string) error {
	configPath := c.Path
	if name != "" {
		project, err := c.GetProject(name)
		if err != nil {
			return err
		}
		configPath = project.context
	}

	dat, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}

	type ConfigTmp struct {
		Projects yaml.Node
	}

	var configTmp ConfigTmp
	err = yaml.Unmarshal([]byte(dat), &configTmp)
	if err != nil {
		return err
	}

	lineNr := 0
	if name == "" {
		lineNr = configTmp.Projects.Line - 1
	} else {
		for _, project := range configTmp.Projects.Content {
			if project.Value == name {
				lineNr = project.Line
				break
			}
		}
	}

	return openEditor(configPath, lineNr)
}

func InitMani(args []string, initFlags core.InitFlags) ([]Project, error) {
	// Choose to initialize mani in a different directory
	// 1. absolute or
	// 2. relative or
	// 3. working directory
	var configDir string
	if len(args) > 0 && filepath.IsAbs(args[0]) {
		// absolute path
		configDir = args[0]
	} else if len(args) > 0 {
		// relative path
		wd, err := os.Getwd()
		if err != nil {
			return []Project{}, err
		}
		configDir = filepath.Join(wd, args[0])
	} else {
		// working directory
		wd, err := os.Getwd()
		if err != nil {
			return []Project{}, err
		}
		configDir = wd
	}

	err := os.MkdirAll(configDir, os.ModePerm)
	if err != nil {
		return []Project{}, err
	}

	configPath := filepath.Join(configDir, "mani.yaml")
	if _, err := os.Stat(configPath); err == nil {
		return []Project{}, &core.AlreadyManiDirectory{Dir: configDir}
	}

	// Check if current directory is a git repository
	gitDir := filepath.Join(configDir, ".git")
	isGitRepo := false
	if _, err := os.Stat(gitDir); err == nil {
		isGitRepo = true
	}

	var projects []Project

	// Only add root directory as project if it IS a git repository
	if isGitRepo {
		url, err := core.GetWdRemoteURL(configDir)
		if err != nil {
			return []Project{}, err
		}
		rootName := filepath.Base(configDir)
		rootPath := "."
		rootURL := url
		rootProject := Project{Name: rootName, Path: rootPath, URL: rootURL}

		// Discover worktrees for root project
		if initFlags.AutoDiscovery {
			worktrees, _ := core.GetWorktreeList(configDir)
			for wtPath, branch := range worktrees {
				if branch == "" {
					continue
				}
				wtRelPath, _ := filepath.Rel(configDir, wtPath)
				rootProject.WorktreeList = append(rootProject.WorktreeList, Worktree{
					Path:   wtRelPath,
					Branch: branch,
				})
			}
		}

		projects = []Project{rootProject}
	}

	if initFlags.AutoDiscovery {
		prs, err := FindVCSystems(configDir)
		if err != nil {
			return []Project{}, err
		}
		RenameDuplicates(prs)

		projects = append(projects, prs...)
	}

	funcMap := template.FuncMap{
		"projectItem": func(name string, path string, url string, worktrees []Worktree) string {
			var txt = name + ":"

			if name != path {
				txt = txt + "\n    path: " + path
			}

			if url != "" {
				txt = txt + "\n    url: " + url
			}

			if len(worktrees) > 0 {
				txt = txt + "\n    worktrees:"
				for _, wt := range worktrees {
					txt = txt + "\n      - path: " + wt.Path
					txt = txt + "\n        branch: " + wt.Branch
				}
			}

			return txt
		},
	}

	tmpl, err := template.New("init").Funcs(funcMap).Parse(`projects:
  {{- range .}}
  {{ (projectItem .Name .Path .URL .WorktreeList) }}
  {{ end }}
tasks:
  hello:
    desc: Print Hello World
    cmd: echo "Hello World"
`,
	)
	if err != nil {
		return []Project{}, err
	}

	// Create mani.yaml
	f, err := os.Create(configPath)
	if err != nil {
		return []Project{}, err
	}

	err = tmpl.Execute(f, projects)
	if err != nil {
		return []Project{}, err
	}

	err = f.Close()
	if err != nil {
		return []Project{}, err
	}

	// Update gitignore file only if inside a git repository
	hasURL := false
	for _, project := range projects {
		if project.URL != "" {
			hasURL = true
			break
		}
	}

	if isGitRepo && hasURL && initFlags.SyncGitignore {
		// Add gitignore file
		gitignoreFilepath := filepath.Join(configDir, ".gitignore")
		if _, err := os.Stat(gitignoreFilepath); os.IsNotExist(err) {
			err := os.WriteFile(gitignoreFilepath, []byte(""), 0644)
			if err != nil {
				return []Project{}, err
			}
		}

		var projectNames []string
		for _, project := range projects {
			if project.URL == "" {
				continue
			}

			if project.Path == "." {
				continue
			}

			projectNames = append(projectNames, project.Path)
		}

		// Add projects to gitignore file
		err = UpdateProjectsToGitignore(projectNames, gitignoreFilepath)
		if err != nil {
			return []Project{}, err
		}
	}

	fmt.Println("\nInitialized mani repository in", configDir)
	fmt.Println("- Created mani.yaml")

	if isGitRepo && hasURL && initFlags.SyncGitignore {
		fmt.Println("- Created .gitignore")
	}

	return projects, nil
}

func RenameDuplicates(projects []Project) {
	projectNamesCount := make(map[string]int)
	// Find duplicate names
	for _, p := range projects {
		projectNamesCount[p.Name] += 1
	}

	// Rename duplicate projects
	for i, p := range projects {
		if projectNamesCount[p.Name] > 1 {
			projects[i].Name = p.Path
		}
	}
}

func CheckUserColor(colorFlag bool) bool {
	_, present := os.LookupEnv("NO_COLOR")
	if present || !colorFlag {
		color.Disable()
		return false
	}

	return true
}

func (c *Config) CheckConfigNoColor() {
	for _, env := range c.EnvList {
		name := strings.Split(env, "=")[0]
		if name == "NO_COLOR" {
			color.Disable()
		}
	}
}


================================================
FILE: core/dao/config_test.go
================================================
package dao

import (
	"testing"
)

func TestConfig_DuplicateProjectName(t *testing.T) {
	originalProjects := []Project{
		{Name: "project-a", Path: "sub-1/project-a"},
		{Name: "project-a", Path: "sub-2/project-a"},
		{Name: "project-b", Path: "sub-3/project-b"},
	}

	var projects []Project
	projects = append(projects, originalProjects...)
	RenameDuplicates(projects)

	if projects[0].Name != originalProjects[0].Path {
		t.Fatalf(`Wanted: %q, Found: %q`, projects[0].Path, originalProjects[0].Name)
	}

	if projects[1].Name != originalProjects[1].Path {
		t.Fatalf(`Wanted: %q, Found: %q`, projects[1].Path, originalProjects[1].Name)
	}

	if originalProjects[2].Name != projects[2].Name {
		t.Fatalf(`Wanted: %q, Found: %q`, projects[2].Name, originalProjects[2].Name)
	}
}


================================================
FILE: core/dao/import.go
================================================
package dao

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/alajmo/mani/core"
	"github.com/gookit/color"
	"gopkg.in/yaml.v3"
)

type Import struct {
	Path string

	context     string
	contextLine int
}

func (i *Import) GetContext() string {
	return i.context
}

func (i *Import) GetContextLine() int {
	return i.contextLine
}

// Populates SpecList and creates a default spec if no default spec is set.
func (c *Config) GetImportList() ([]Import, []ResourceErrors[Import]) {
	var imports []Import
	count := len(c.Import.Content)

	importErrors := []ResourceErrors[Import]{}
	foundErrors := false
	for i := range count {
		imp := &Import{
			Path:        c.Import.Content[i].Value,
			context:     c.Path,
			contextLine: c.Import.Content[i].Line,
		}

		imports = append(imports, *imp)
	}

	if foundErrors {
		return imports, importErrors
	}

	return imports, nil
}

// Used for config imports
type ConfigResources struct {
	Imports  []Import
	Themes   []Theme
	Specs    []Spec
	Targets  []Target
	Tasks    []Task
	Projects []Project
	Envs     []string

	ThemeErrors   []ResourceErrors[Theme]
	SpecErrors    []ResourceErrors[Spec]
	TargetErrors  []ResourceErrors[Target]
	TaskErrors    []ResourceErrors[Task]
	ProjectErrors []ResourceErrors[Project]
	ImportErrors  []ResourceErrors[Import]
}

type Node struct {
	Path     string
	Imports  []Import
	Visiting bool
	Visited  bool
}

type NodeLink struct {
	A Node
	B Node
}

type FoundCyclicDependency struct {
	Cycles []NodeLink
}

func (c *FoundCyclicDependency) Error() string {
	var msg string

	var errPrefix = color.FgRed.Sprintf("error")
	var ptrPrefix = color.FgBlue.Sprintf("-->")
	msg = fmt.Sprintf("%s: %s\n", errPrefix, "Found direct or indirect circular dependency")
	for i := range c.Cycles {
		msg += fmt.Sprintf("  %s %s\n      %s\n", ptrPrefix, c.Cycles[i].A.Path, c.Cycles[i].B.Path)
	}

	return msg
}

// Given config imports, use a Depth-first-search algorithm to recursively
// check for resources (tasks, projects, dirs, themes, specs, targets).
// A struct is passed around that is populated with resources from each config.
// In case a cyclic dependency is found (a -> b and b -> a), we return early and
// with an error containing the cyclic dependency found.
//
// This is the first parsing, later on we will perform more passes where we check what commands/tasks
// are imported.
func (c Config) importConfigs() (ConfigResources, error) {
	// Main config
	ci := ConfigResources{}
	c.loadResources(&ci)

	if c.UserConfigFile != nil {
		ci.Imports = append(ci.Imports, Import{Path: *c.UserConfigFile, context: c.Path, contextLine: -1})
	}

	// Import other configs
	n := Node{
		Path:    c.Path,
		Imports: ci.Imports,
	}
	m := make(map[string]*Node)
	m[n.Path] = &n
	cycles := []NodeLink{}

	dfs(&n, m, &cycles, &ci)

	// Get errors
	configErr := concatErrors(ci, &cycles)

	if configErr != nil {
		return ci, configErr
	}

	return ci, nil
}

func concatErrors(ci ConfigResources, cycles *[]NodeLink) error {
	var configErr = ""

	if len(*cycles) > 0 {
		err := &FoundCyclicDependency{Cycles: *cycles}
		configErr = fmt.Sprintf("%s%s\n", configErr, err.Error())
	}

	for _, theme := range ci.ThemeErrors {
		if len(theme.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(theme.Resource, theme.Errors))
		}
	}

	for _, spec := range ci.SpecErrors {
		if len(spec.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(spec.Resource, spec.Errors))
		}
	}

	for _, target := range ci.TargetErrors {
		if len(target.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(target.Resource, target.Errors))
		}
	}

	for _, task := range ci.TaskErrors {
		if len(task.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(task.Resource, task.Errors))
		}
	}

	for _, project := range ci.ProjectErrors {
		if len(project.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(project.Resource, project.Errors))
		}
	}

	for _, imp := range ci.ImportErrors {
		if len(imp.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(imp.Resource, imp.Errors))
		}
	}

	if configErr != "" {
		return &core.ConfigErr{Msg: configErr}
	}

	return nil
}

func parseConfig(path string, ci *ConfigResources) ([]Import, error) {
	dat, err := os.ReadFile(path)
	if err != nil {
		return []Import{}, err
	}

	absPath, err := filepath.Abs(path)
	if err != nil {
		return []Import{}, err
	}

	// Found config, now try to read it
	var config Config
	err = yaml.Unmarshal(dat, &config)
	if err != nil {
		return []Import{}, err
	}

	config.Path = absPath
	config.Dir = filepath.Dir(absPath)
	imports := config.loadResources(ci)

	return imports, nil
}

func (c Config) loadResources(ci *ConfigResources) []Import {
	imports, importErrors := c.GetImportList()
	ci.ImportErrors = append(ci.ImportErrors, importErrors...)

	tasks, taskErrors := c.GetTaskList()
	ci.TaskErrors = append(ci.TaskErrors, taskErrors...)

	projects, projectErrors := c.GetProjectList()
	ci.ProjectErrors = append(ci.ProjectErrors, projectErrors...)

	themes, themeErrors := c.ParseThemes()
	ci.ThemeErrors = append(ci.ThemeErrors, themeErrors...)

	specs, specErrors := c.GetSpecList()
	ci.SpecErrors = append(ci.SpecErrors, specErrors...)

	targets, targetErrors := c.GetTargetList()
	ci.TargetErrors = append(ci.TargetErrors, targetErrors...)

	envs := c.GetEnvList()

	ci.Imports = append(ci.Imports, imports...)
	ci.Tasks = append(ci.Tasks, tasks...)
	ci.Projects = append(ci.Projects, projects...)
	ci.Themes = append(ci.Themes, themes...)
	ci.Specs = append(ci.Specs, specs...)
	ci.Targets = append(ci.Targets, targets...)
	ci.Envs = append(ci.Envs, envs...)

	return imports
}

func dfs(n *Node, m map[string]*Node, cycles *[]NodeLink, ci *ConfigResources) {
	n.Visiting = true

	for i := range n.Imports {
		p, err := core.GetAbsolutePath(filepath.Dir(n.Path), n.Imports[i].Path, "")
		if err != nil {
			importError := ResourceErrors[Import]{Resource: &n.Imports[i], Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			ci.ImportErrors = append(ci.ImportErrors, importError)
			continue
		}

		// Skip visited nodes
		var nc Node
		v, exists := m[p]
		if exists {
			nc = *v
		} else {
			nc = Node{Path: p}
			m[nc.Path] = &nc
		}

		if nc.Visited {
			continue
		}

		// Found cyclic dependency
		if nc.Visiting {
			c := NodeLink{
				A: *n,
				B: nc,
			}

			*cycles = append(*cycles, c)
			break
		}

		// Import Config
		imports, err := parseConfig(nc.Path, ci)
		if err != nil {
			importError := ResourceErrors[Import]{Resource: &n.Imports[i], Errors: []error{err}}
			ci.ImportErrors = append(ci.ImportErrors, importError)
			continue
		}

		nc.Imports = imports

		dfs(&nc, m, cycles, ci)

		// err = dfs(&nc, m, cycles, ci)
		// if err != nil {
		// 	return err
		// }
	}

	n.Visiting = false
	n.Visited = true
}


================================================
FILE: core/dao/project.go
================================================
package dao

import (
	"bufio"
	"container/list"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strings"

	"gopkg.in/yaml.v3"

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

type Project struct {
	Name         string   `yaml:"name"`
	Path         string   `yaml:"path"`
	Desc         string   `yaml:"desc"`
	URL          string   `yaml:"url"`
	Clone        string   `yaml:"clone"`
	Branch       string   `yaml:"branch"`
	SingleBranch *bool    `yaml:"single_branch"`
	Sync         *bool    `yaml:"sync"`
	Tags         []string `yaml:"tags"`
	EnvList      []string `yaml:"-"`
	RemoteList   []Remote `yaml:"-"`

	Env          yaml.Node  `yaml:"env"`
	Remotes      yaml.Node  `yaml:"remotes"`
	Worktrees    yaml.Node  `yaml:"worktrees"`
	WorktreeList []Worktree `yaml:"-"`
	context      string
	contextLine  int
	RelPath      string
}

type Remote struct {
	Name string
	URL  string
}

type Worktree struct {
	Path   string `yaml:"path"`
	Branch string `yaml:"branch"`
}

func (p *Project) GetContext() string {
	return p.context
}

func (p *Project) GetContextLine() int {
	return p.contextLine
}

func (p Project) IsSingleBranch() bool {
	return p.SingleBranch != nil && *p.SingleBranch
}

func (p Project) IsSync() bool {
	return p.Sync == nil || *p.Sync
}

func (p Project) GetValue(key string, _ int) string {
	switch strings.ToLower(key) {
	case "project":
		return p.Name
	case "path":
		return p.Path
	case "relpath":
		return p.RelPath
	case "desc", "description":
		return p.Desc
	case "url":
		return p.URL
	case "tag", "tags":
		return strings.Join(p.Tags, ", ")
	case "worktree", "worktrees":
		if len(p.WorktreeList) == 0 {
			return ""
		}
		entries := make([]string, len(p.WorktreeList))
		for i, wt := range p.WorktreeList {
			entries[i] = wt.Path + ":" + wt.Branch
		}
		return strings.Join(entries, ", ")
	default:
		return ""
	}
}

func (c *Config) GetProjectList() ([]Project, []ResourceErrors[Project]) {
	var projects []Project
	count := len(c.Projects.Content)

	projectErrors := []ResourceErrors[Project]{}
	foundErrors := false
	for i := 0; i < count; i += 2 {
		project := &Project{
			context:     c.Path,
			contextLine: c.Projects.Content[i].Line,
		}

		err := c.Projects.Content[i+1].Decode(project)
		if err != nil {
			foundErrors = true
			projectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			projectErrors = append(projectErrors, projectError)
			continue
		}

		project.Name = c.Projects.Content[i].Value

		// Add absolute and relative path for each project
		project.Path, err = core.GetAbsolutePath(c.Dir, project.Path, project.Name)
		if err != nil {
			foundErrors = true
			projectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			projectErrors = append(projectErrors, projectError)
			continue
		}

		project.RelPath, err = core.GetRelativePath(c.Dir, project.Path)
		if err != nil {
			foundErrors = true
			projectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			projectErrors = append(projectErrors, projectError)
			continue
		}

		envList := []string{}
		projectEnvs, err := EvaluateEnv(ParseNodeEnv(project.Env))
		if err != nil {
			foundErrors = true
			projectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			projectErrors = append(projectErrors, projectError)
			continue
		}
		envList = append(envList, projectEnvs...)
		project.EnvList = envList

		projectRemotes := ParseRemotes(project.Remotes)
		project.RemoteList = projectRemotes

		projectWorktrees, err := ParseWorktrees(project.Worktrees)
		if err != nil {
			foundErrors = true
			projectError := ResourceErrors[Project]{Resource: project, Errors: []error{err}}
			projectErrors = append(projectErrors, projectError)
			continue
		}
		project.WorktreeList = projectWorktrees

		projects = append(projects, *project)
	}

	if foundErrors {
		return projects, projectErrors
	}

	return projects, nil
}

// GetFilteredProjects retrieves a filtered list of projects based on the provided ProjectFlags.
// It processes various filtering criteria and returns the matching projects.
//
// The function follows these steps:
// 1. If a target is specified, loads the target configuration, otherwise sets all to false
// 2. Merges any provided flag values with the target configuration
// 3. Applies all filtering criteria using FilterProjects
func (c Config) GetFilteredProjects(flags *core.ProjectFlags) ([]Project, error) {
	var err error
	var projects []Project

	target := &Target{}
	if flags.Target != "" {
		target, err = c.GetTarget(flags.Target)
		if err != nil {
			return []Project{}, err
		}
	}

	if len(flags.Projects) > 0 {
		target.Projects = flags.Projects
	}

	if len(flags.Paths) > 0 {
		target.Paths = flags.Paths
	}

	if len(flags.Tags) > 0 {
		target.Tags = flags.Tags
	}

	if flags.TagsExpr != "" {
		target.TagsExpr = flags.TagsExpr
	}

	if flags.Cwd {
		target.Cwd = flags.Cwd
	}

	if flags.All {
		target.All = flags.All
	}

	projects, err = c.FilterProjects(
		target.Cwd,
		target.All,
		target.Projects,
		target.Paths,
		target.Tags,
		target.TagsExpr,
	)
	if err != nil {
		return []Project{}, err
	}

	return projects, nil
}

// FilterProjects filters the project list based on various criteria. It supports filtering by:
// - All projects (allProjectsFlag)
// - Current working directory (cwdFlag)
// - Project names (projectsFlag)
// - Project paths (projectPathsFlag)
// - Project tags (tagsFlag)
// - Tag expressions (tagsExprFlag)
//
// Priority handling:
//   - If cwdFlag is true, the function immediately returns only the current working directory
//     project, ignoring all other filters.
//   - For all other combinations of filters, the function collects projects from each filter
//     into separate slices, then finds their intersection. If multiple
//     filters are specified, only projects that match ALL filters will be returned.
func (c Config) FilterProjects(
	cwdFlag bool,
	allProjectsFlag bool,
	projectsFlag []string,
	projectPathsFlag []string,
	tagsFlag []string,
	tagsExprFlag string,
) ([]Project, error) {
	var finalProjects []Project

	var err error
	var inputProjects [][]Project

	if cwdFlag {
		var cwdProjects []Project
		cwdProject, err := c.GetCwdProject()
		cwdProjects = append(cwdProjects, cwdProject)
		if err != nil {
			return []Project{}, err
		}

		return cwdProjects, nil
	}

	if allProjectsFlag {
		inputProjects = append(inputProjects, c.ProjectList)
	}

	if len(projectsFlag) > 0 {
		var projects []Project
		projects, err = c.GetProjectsByName(projectsFlag)
		if err != nil {
			return []Project{}, err
		}
		inputProjects = append(inputProjects, projects)
	}

	if len(projectPathsFlag) > 0 {
		var projectPaths []Project
		projectPaths, err = c.GetProjectsByPath(projectPathsFlag)
		if err != nil {
			return []Project{}, err
		}
		inputProjects = append(inputProjects, projectPaths)
	}

	if len(tagsFlag) > 0 {
		var tagProjects []Project
		tagProjects, err = c.GetProjectsByTags(tagsFlag)
		if err != nil {
			return []Project{}, err
		}
		inputProjects = append(inputProjects, tagProjects)
	}

	if tagsExprFlag != "" {
		var tagExprProjects []Project
		tagExprProjects, err = c.GetProjectsByTagsExpr(tagsExprFlag)
		if err != nil {
			return []Project{}, err
		}
		inputProjects = append(inputProjects, tagExprProjects)
	}

	finalProjects = c.GetIntersectProjects(inputProjects...)

	return finalProjects, nil
}

func (c Config) GetProject(name string) (*Project, error) {
	for _, project := range c.ProjectList {
		if name == project.Name {
			return &project, nil
		}
	}

	return nil, &core.ProjectNotFound{Name: []string{name}}
}

func (c Config) GetProjectsByName(projectNames []string) ([]Project, error) {
	var matchedProjects []Project

	foundProjectNames := make(map[string]bool)
	for _, p := range projectNames {
		foundProjectNames[p] = false
	}

	for _, v := range projectNames {
		for _, p := range c.ProjectList {
			if v == p.Name {
				foundProjectNames[p.Name] = true
				matchedProjects = append(matchedProjects, p)
			}
		}
	}

	nonExistingProjects := []string{}
	for k, v := range foundProjectNames {
		if !v {
			nonExistingProjects = append(nonExistingProjects, k)
		}
	}

	if len(nonExistingProjects) > 0 {
		return []Project{}, &core.ProjectNotFound{Name: nonExistingProjects}
	}

	return matchedProjects, nil
}

// GetProjectsByPath Projects must have all dirs to match.
// If user provides a path which does not exist, then return error containing
// all the paths it didn't find.
// Supports glob patterns:
// - '*' matches any sequence of non-separator characters
// - '**' matches any sequence of characters including separators
func (c Config) GetProjectsByPath(dirs []string) ([]Project, error) {
	if len(dirs) == 0 {
		return c.ProjectList, nil
	}

	foundDirs := make(map[string]bool)
	for _, dir := range dirs {
		foundDirs[dir] = false
	}

	projects := []Project{}
	for _, project := range c.ProjectList {
		// Variable use to check that all dirs are matched
		var numMatched = 0
		for _, dir := range dirs {

			matchPath := func(dir string, path string) (bool, error) {
				// Handle glob pattern
				if strings.Contains(dir, "*") {
					// Handle '**' glob pattern
					if strings.Contains(dir, "**") {
						// Convert the glob pattern to a regex pattern
						regexPattern := strings.ReplaceAll(dir, "**/", "<glob>")
						regexPattern = strings.ReplaceAll(regexPattern, "*", "[^/]*")
						regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
						regexPattern = strings.ReplaceAll(regexPattern, "<glob>", "(.*/)*")
						regexPattern = "^" + regexPattern + "$"

						matched, err := regexp.MatchString(regexPattern, path)

						if err != nil {
							return false, err
						}

						if matched {
							return true, nil
						}
					}

					// Handle standard glob pattern
					matched, err := filepath.Match(dir, path)

					if err != nil {
						return false, err
					}

					if matched {
						return true, nil
					}
				}

				// Try matching as a partial path
				if strings.Contains(path, dir) {
					return true, nil
				}

				return false, nil
			}

			matched, err := matchPath(dir, project.RelPath)

			if err != nil {
				return []Project{}, err
			}

			if matched {
				foundDirs[dir] = true
				numMatched++
			}
		}

		if numMatched == len(dirs) {
			projects = append(projects, project)
		}
	}

	nonExistingDirs := []string{}
	for k, v := range foundDirs {
		if !v {
			nonExistingDirs = append(nonExistingDirs, k)
		}
	}

	if len(nonExistingDirs) > 0 {
		return []Project{}, &core.DirNotFound{Dirs: nonExistingDirs}
	}

	return projects, nil
}

// GetProjectsByTags Projects must have all tags to match. For instance, if --tags frontend,backend
// is passed, then a project must have both tags.
// We only return error if the flags provided do not exist in the mani config.
func (c Config) GetProjectsByTags(tags []string) ([]Project, error) {
	if len(tags) == 0 {
		return c.ProjectList, nil
	}

	foundTags := make(map[string]bool)
	for _, tag := range tags {
		foundTags[tag] = false
	}

	// Find projects matching the flag
	var projects []Project
	for _, project := range c.ProjectList {
		// Variable use to check that all tags are matched
		var numMatched = 0
		for _, tag := range tags {
			for _, projectTag := range project.Tags {
				if projectTag == tag {
					foundTags[tag] = true
					numMatched = numMatched + 1
				}
			}
		}

		if numMatched == len(tags) {
			projects = append(projects, project)
		}
	}

	nonExistingTags := []string{}
	for k, v := range foundTags {
		if !v {
			nonExistingTags = append(nonExistingTags, k)
		}
	}

	if len(nonExistingTags) > 0 {
		return []Project{}, &core.TagNotFound{Tags: nonExistingTags}
	}

	return projects, nil
}

// GetProjectsByTagsExpr Projects must have all tags to match. For instance, if --tags frontend,backend
// is passed, then a project must have both tags.
// We only return error if the tags provided do not exist.
func (c Config) GetProjectsByTagsExpr(tagsExpr string) ([]Project, error) {
	if tagsExpr == "" {
		return c.ProjectList, nil
	}

	var projects []Project
	for _, project := range c.ProjectList {
		matches, err := evaluateExpression(&project, tagsExpr)
		if err != nil {
			return c.ProjectList, &core.TagExprInvalid{Expression: err.Error()}
		}
		if matches {
			projects = append(projects, project)
		}
	}

	return projects, nil
}

func (c Config) GetCwdProject() (Project, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return Project{}, err
	}

	var project Project
	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.ProjectList {
			if p == pro.Path {
				project = pro
				break out
			}
		}
	}

	return project, nil
}

/**
 * GetProjectPaths For each project path, get all the enumerations of dirnames.
 * 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) GetProjectPaths() []string {
	dirs := []string{}
	for _, project := range c.ProjectList {
		// Ignore projects outside of mani.yaml directory
		if strings.Contains(project.Path, c.Dir) {
			ps := strings.Split(filepath.Dir(project.RelPath), string(os.PathSeparator))
			for i := 1; i <= len(ps); i++ {
				p := filepath.Join(ps[0:i]...)

				if p != "." && !slices.Contains(dirs, p) {
					dirs = append(dirs, p)
				}
			}
		}
	}

	return dirs
}

func (c Config) GetProjectNames() []string {
	names := []string{}
	for _, project := range c.ProjectList {
		names = append(names, project.Name)
	}

	return names
}

func (c Config) GetProjectUrls() []string {
	urls := []string{}
	for _, project := range c.ProjectList {
		if project.URL != "" {
			urls = append(urls, project.URL)
		}
	}

	return urls
}

func (c Config) GetProjectsTree(dirs []string, tags []string) ([]TreeNode, error) {
	dirProjects, err := c.GetProjectsByPath(dirs)
	if err != nil {
		return []TreeNode{}, err
	}

	tagProjects, err := c.GetProjectsByTags(tags)
	if err != nil {
		return []TreeNode{}, err
	}

	projects := c.GetIntersectProjects(dirProjects, tagProjects)

	var projectPaths = []TNode{}
	for _, p := range projects {
		node := TNode{Name: p.Name, Path: p.RelPath}
		projectPaths = append(projectPaths, node)
	}

	var tree []TreeNode
	for i := range projectPaths {
		tree = AddToTree(tree, projectPaths[i])
	}

	return tree, nil
}

// IsGitWorktree checks if the given path is a git worktree (not the main repo).
//
// A worktree's .git is a FILE (not directory) containing:
// "gitdir: /path/to/main-repo/.git/worktrees/worktree-name"
func IsGitWorktree(path string) (bool, error) {
	gitPath := filepath.Join(path, ".git")
	info, err := os.Stat(gitPath)
	if err != nil {
		return false, err
	}

	// If .git is a directory, it's a regular git repo (or the main worktree)
	if info.IsDir() {
		return false, nil
	}

	// .git is a file - read its content
	content, err := os.ReadFile(gitPath)
	if err != nil {
		return false, err
	}

	// Parse "gitdir: <path>"
	contentStr := strings.TrimSpace(string(content))
	gitDir, found := strings.CutPrefix(contentStr, "gitdir: ")
	if !found {
		return false, nil
	}

	// Make gitDir absolute if it's relative
	if !filepath.IsAbs(gitDir) {
		gitDir = filepath.Join(path, gitDir)
		gitDir = filepath.Clean(gitDir)
	}

	// Check if it matches the worktree pattern: <parent-repo>/.git/worktrees/<name>
	sep := string(filepath.Separator)
	pattern := sep + ".git" + sep + "worktrees" + sep
	if strings.Contains(gitDir, pattern) {
		return true, nil
	}

	return false, nil
}

func FindVCSystems(rootPath string) ([]Project, error) {
	projects := []Project{}

	err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Is file
		if !info.IsDir() {
			return nil
		}

		if path == rootPath {
			return nil
		}

		// Is Directory and Has a Git Dir inside, add to projects and SkipDir
		gitDir := filepath.Join(path, ".git")
		if _, err := os.Stat(gitDir); !os.IsNotExist(err) {
			name := filepath.Base(path)
			relPath, _ := filepath.Rel(rootPath, path)

			// Check if this is a worktree (skip worktrees, they belong to parent)
			isWorktree, _ := IsGitWorktree(path)
			if isWorktree {
				return filepath.SkipDir
			}

			// This is a regular repository
			var project Project
			url, rErr := core.GetRemoteURL(path)
			if rErr != nil {
				project = Project{Name: name, Path: relPath}
			} else {
				project = Project{Name: name, Path: relPath, URL: url}
			}

			// Get worktrees using git worktree list (skip detached HEAD worktrees)
			worktrees, _ := core.GetWorktreeList(path)
			for wtPath, branch := range worktrees {
				if branch == "" {
					continue
				}
				// Convert absolute path to relative path from project
				wtRelPath, _ := filepath.Rel(path, wtPath)
				project.WorktreeList = append(project.WorktreeList, Worktree{
					Path:   wtRelPath,
					Branch: branch,
				})
			}

			projects = append(projects, project)
			return filepath.SkipDir
		}

		return nil
	})

	if err != nil {
		return projects, err
	}

	return projects, nil
}

func UpdateProjectsToGitignore(projectNames []string, gitignoreFilename string) (err error) {
	l := list.New()
	gitignoreFile, err := os.OpenFile(gitignoreFilename, os.O_RDWR, 0644)
	if err != nil {
		return &core.FailedToOpenFile{Name: gitignoreFilename}
	}
	defer func() {
		closeErr := gitignoreFile.Close()
		if err == nil {
			err = closeErr
		}
	}()

	scanner := bufio.NewScanner(gitignoreFile)
	for scanner.Scan() {
		line := scanner.Text()
		l.PushBack(line)
	}

	const maniComment = "# mani #"
	var insideComment = false
	var beginElement *list.Element
	var endElement *list.Element
	var next *list.Element

	// Remove all projects inside # mani #
	for e := l.Front(); e != nil; e = next {
		next = e.Next()

		if e.Value == maniComment && !insideComment {
			insideComment = true
			beginElement = e
			continue
		}

		if e.Value == maniComment {
			endElement = e
			break
		}

		if insideComment {
			l.Remove(e)
		}
	}

	// If missing start # mani #
	if beginElement == nil {
		l.PushBack(maniComment)
		beginElement = l.Back()
	}

	// If missing ending # mani #
	if endElement == nil {
		l.PushBack(maniComment)
	}

	// Insert projects within # mani # section
	for _, projectName := range projectNames {
		l.InsertAfter(projectName, beginElement)
	}

	err = gitignoreFile.Truncate(0)
	if err != nil {
		return err
	}

	_, err = gitignoreFile.Seek(0, 0)
	if err != nil {
		return err
	}

	// Write to gitignore file
	for e := l.Front(); e != nil; e = e.Next() {
		str := fmt.Sprint(e.Value)
		_, err = gitignoreFile.WriteString(str)
		if err != nil {
			return err
		}

		_, err = gitignoreFile.WriteString("\n")
		if err != nil {
			return err
		}
	}

	return nil
}

// ParseRemotes List of remotes (key: value)
func ParseRemotes(node yaml.Node) []Remote {
	var remotes []Remote
	count := len(node.Content)

	for i := 0; i < count; i += 2 {
		remote := Remote{
			Name: node.Content[i].Value,
			URL:  node.Content[i+1].Value,
		}

		remotes = append(remotes, remote)
	}

	return remotes
}

// ParseWorktrees parses worktree definitions from YAML
func ParseWorktrees(node yaml.Node) ([]Worktree, error) {
	var worktrees []Worktree

	for _, content := range node.Content {
		var wt Worktree
		if err := content.Decode(&wt); err != nil {
			return nil, err
		}
		// Path is required
		if wt.Path == "" {
			return nil, &core.WorktreePathRequired{}
		}
		// Default branch to path basename (like git does)
		if wt.Branch == "" {
			wt.Branch = filepath.Base(wt.Path)
		}
		worktrees = append(worktrees, wt)
	}

	return worktrees, nil
}

func (c Config) GetIntersectProjects(ps ...[]Project) []Project {
	counts := make(map[string]int, len(c.ProjectList))
	for _, projects := range ps {
		for _, project := range projects {
			counts[project.Name] += 1
		}
	}

	var projects []Project
	for _, p := range c.ProjectList {
		if counts[p.Name] == len(ps) && len(ps) > 0 {
			projects = append(projects, p)
		}
	}

	return projects
}

// TREE

type TNode struct {
	Name string
	Path string
}

type TreeNode struct {
	Path        string
	ProjectName string
	Children    []TreeNode
}

// AddToTree recursively builds a tree structure from path components
// root: The current level of tree nodes
// node: Node containing path and name information to be added
func AddToTree(root []TreeNode, node TNode) []TreeNode {
	// Return if path is empty or starts with separator
	items := strings.Split(node.Path, string(os.PathSeparator))
	if len(items) == 0 || items[0] == "" {
		return root
	}

	if len(items) > 0 {
		var i int
		// Search for existing node with same path at current level
		for i = 0; i < len(root); i++ {
			if root[i].Path == items[0] { // already in tree
				break
			}
		}

		// If node doesn't exist at current level, create new node
		if i == len(root) {
			root = append(root, TreeNode{
				Path:        items[0],
				ProjectName: "",
				Children:    []TreeNode{},
			})
		}

		// If this is the last component in the path (leaf node/file)
		if len(items) == 1 {
			root[i].ProjectName = node.Name // Set name for projects only
		} else {
			root[i].ProjectName = ""
			str := strings.Join(items[1:], string(os.PathSeparator))
			n := TNode{Name: node.Name, Path: str}
			root[i].Children = AddToTree(root[i].Children, n)
		}
	}

	return root
}


================================================
FILE: core/dao/project_test.go
================================================
package dao

import (
	"testing"

	"gopkg.in/yaml.v3"

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

func TestProject_GetValue(t *testing.T) {
	project := Project{
		Name:    "test-project",
		Path:    "/path/to/project",
		RelPath: "relative/path",
		Desc:    "Test description",
		URL:     "https://example.com",
		Tags:    []string{"frontend", "api"},
	}

	tests := []struct {
		name     string
		key      string
		expected string
	}{
		{
			name:     "get project name",
			key:      "Project",
			expected: "test-project",
		},
		{
			name:     "get project path",
			key:      "Path",
			expected: "/path/to/project",
		},
		{
			name:     "get relative path",
			key:      "RelPath",
			expected: "relative/path",
		},
		{
			name:     "get description",
			key:      "Desc",
			expected: "Test description",
		},
		{
			name:     "get url",
			key:      "Url",
			expected: "https://example.com",
		},
		{
			name:     "get tags",
			key:      "Tag",
			expected: "frontend, api",
		},
		{
			name:     "get invalid key",
			key:      "InvalidKey",
			expected: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := project.GetValue(tt.key, 0)
			if result != tt.expected {
				t.Errorf("expected %q, got %q", tt.expected, result)
			}
		})
	}
}

func TestProject_GetProjectsByName(t *testing.T) {
	config := Config{
		ProjectList: []Project{
			{Name: "project1", Path: "/path/1"},
			{Name: "project2", Path: "/path/2"},
			{Name: "project3", Path: "/path/3"},
		},
	}

	tests := []struct {
		name          string
		projectNames  []string
		expectError   bool
		expectedCount int
	}{
		{
			name:          "find existing projects",
			projectNames:  []string{"project1", "project2"},
			expectError:   false,
			expectedCount: 2,
		},
		{
			name:          "find non-existing project",
			projectNames:  []string{"project1", "nonexistent"},
			expectError:   true,
			expectedCount: 0,
		},
		{
			name:          "empty project names",
			projectNames:  []string{},
			expectError:   false,
			expectedCount: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			projects, err := config.GetProjectsByName(tt.projectNames)

			if tt.expectError && err == nil {
				t.Error("expected error but got none")
			}
			if !tt.expectError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if len(projects) != tt.expectedCount {
				t.Errorf("expected %d projects, got %d", tt.expectedCount, len(projects))
			}
			if err != nil && !tt.expectError {
				if _, ok := err.(*core.ProjectNotFound); !ok {
					t.Errorf("expected ProjectNotFound error, got %T", err)
				}
			}
		})
	}
}

func TestProject_GetProjectsByTags(t *testing.T) {
	config := Config{
		ProjectList: []Project{
			{Name: "project1", Tags: []string{"frontend", "react"}},
			{Name: "project2", Tags: []string{"backend", "api"}},
			{Name: "project3", Tags: []string{"frontend", "vue"}},
		},
	}

	tests := []struct {
		name          string
		tags          []string
		expectError   bool
		expectedNames []string
	}{
		{
			name:          "find projects with existing tag",
			tags:          []string{"frontend"},
			expectError:   false,
			expectedNames: []string{"project1", "project3"},
		},
		{
			name:          "find projects with multiple tags",
			tags:          []string{"frontend", "react"},
			expectError:   false,
			expectedNames: []string{"project1"},
		},
		{
			name:          "find projects with non-existing tag",
			tags:          []string{"nonexistent"},
			expectError:   true,
			expectedNames: []string{},
		},
		{
			name:          "empty tags",
			tags:          []string{},
			expectError:   false,
			expectedNames: []string{"project1", "project2", "project3"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			projects, err := config.GetProjectsByTags(tt.tags)

			if tt.expectError && err == nil {
				t.Error("expected error but got none")
			}
			if !tt.expectError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}

			gotNames := getProjectNames(projects)
			if !equalStringSlices(gotNames, tt.expectedNames) {
				t.Errorf("expected projects %v, got %v", tt.expectedNames, gotNames)
			}
		})
	}
}

func TestProject_GetProjectsByPath(t *testing.T) {
	config := Config{
		Dir: "/base",
		ProjectList: []Project{
			{Name: "project1", Path: "/base/frontend/app1", RelPath: "frontend/app1"},
			{Name: "project2", Path: "/base/backend/api", RelPath: "backend/api"},
			{Name: "project3", Path: "/base/frontend/app2", RelPath: "frontend/app2"},
			{Name: "project4", Path: "/base/frontend/nested/app3", RelPath: "frontend/nested/app3"},
		},
	}

	tests := []struct {
		name          string
		paths         []string
		expectError   bool
		expectedNames []string
	}{
		{
			name:          "find projects in frontend path",
			paths:         []string{"frontend"},
			expectError:   false,
			expectedNames: []string{"project1", "project3", "project4"},
		},
		{
			name:          "find projects with specific path",
			paths:         []string{"frontend/app1"},
			expectError:   false,
			expectedNames: []string{"project1"},
		},
		{
			name:          "find projects with single-level glob (1)",
			paths:         []string{"*/app*"},
			expectError:   false,
			expectedNames: []string{"project1", "project3"},
		},
		{
			name:          "find projects with single-level glob (2)",
			paths:         []string{"*/app?"},
			expectError:   false,
			expectedNames: []string{"project1", "project3"},
		},
		{
			name:          "find projects with double-star glob (1)",
			paths:         []string{"frontend/**/app*"},
			expectError:   false,
			expectedNames: []string{"project1", "project3", "project4"},
		},
		{
			name:          "find projects with double-star glob (2)",
			paths:         []string{"frontend/**/app?"},
			expectError:   false,
			expectedNames: []string{"project1", "project3", "project4"},
		},
		{
			name:          "find projects with double-star glob (3)",
			paths:         []string{"frontend/**/**/app?"},
			expectError:   false,
			expectedNames: []string{"project1", "project3", "project4"},
		},
		{
			name:          "find projects with double-star glob (4)",
			paths:         []string{"**/app?"},
			expectError:   false,
			expectedNames: []string{"project1", "project3", "project4"},
		},
		{
			name:          "find projects with non-existing path",
			paths:         []string{"nonexistent"},
			expectError:   true,
			expectedNames: []string{},
		},
		{
			name:          "empty paths",
			paths:         []string{},
			expectError:   false,
			expectedNames: []string{"project1", "project2", "project3", "project4"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			projects, err := config.GetProjectsByPath(tt.paths)

			if tt.expectError && err == nil {
				t.Error("expected error but got none")
			}
			if !tt.expectError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}

			gotNames := getProjectNames(projects)
			if !equalStringSlices(gotNames, tt.expectedNames) {
				t.Errorf("expected projects %v, got %v", tt.expectedNames, gotNames)
			}
		})
	}
}

func TestProject_TestAddToTree(t *testing.T) {
	tests := []struct {
		name          string
		nodes         []TNode
		expectedPaths []string
	}{
		{
			name: "simple tree",
			nodes: []TNode{
				{Name: "app1", Path: "frontend/app1"},
				{Name: "app2", Path: "frontend/app2"},
				{Name: "api", Path: "backend/api"},
			},
			expectedPaths: []string{"frontend", "backend"},
		},
		{
			name: "nested tree",
			nodes: []TNode{
				{Name: "app1", Path: "frontend/web/app1"},
				{Name: "app2", Path: "frontend/mobile/app2"},
			},
			expectedPaths: []string{"frontend"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var tree []TreeNode
			for _, node := range tt.nodes {
				tree = AddToTree(tree, node)
			}

			paths := getTreePaths(tree)
			if !equalStringSlices(paths, tt.expectedPaths) {
				t.Errorf("expected paths %v, got %v", tt.expectedPaths, paths)
			}
		})
	}
}

func TestProject_GetIntersectProjects(t *testing.T) {
	config := Config{
		ProjectList: []Project{
			{Name: "project1", Tags: []string{"frontend"}},
			{Name: "project2", Tags: []string{"backend"}},
			{Name: "project3", Tags: []string{"frontend", "api"}},
		},
	}

	tests := []struct {
		name          string
		inputs        [][]Project
		expectedNames []string
	}{
		{
			name: "intersect frontend and api projects",
			inputs: [][]Project{
				{{Name: "project1"}, {Name: "project3"}}, // frontend projects
				{{Name: "project3"}},                     // api projects
			},
			expectedNames: []string{"project3"},
		},
		{
			name: "no intersection",
			inputs: [][]Project{
				{{Name: "project1"}},
				{{Name: "project2"}},
			},
			expectedNames: []string{},
		},
		{
			name:          "empty input",
			inputs:        [][]Project{},
			expectedNames: []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := config.GetIntersectProjects(tt.inputs...)

			gotNames := getProjectNames(result)
			if !equalStringSlices(gotNames, tt.expectedNames) {
				t.Errorf("expected projects %v, got %v", tt.expectedNames, gotNames)
			}
		})
	}
}

func TestParseWorktrees(t *testing.T) {
	tests := []struct {
		name        string
		yaml        string
		expected    []Worktree
		expectError bool
	}{
		{
			name: "worktree with path and branch",
			yaml: `
- path: feature-branch
  branch: feature/awesome
`,
			expected: []Worktree{
				{Path: "feature-branch", Branch: "feature/awesome"},
			},
		},
		{
			name: "multiple worktrees",
			yaml: `
- path: feature-branch
  branch: feature/awesome
- path: staging
  branch: staging
`,
			expected: []Worktree{
				{Path: "feature-branch", Branch: "feature/awesome"},
				{Path: "staging", Branch: "staging"},
			},
		},
		{
			name: "worktree without branch defaults to path basename",
			yaml: `
- path: hotfix
`,
			expected: []Worktree{
				{Path: "hotfix", Branch: "hotfix"},
			},
		},
		{
			name: "worktree with nested path defaults branch to basename",
			yaml: `
- path: worktrees/feature
`,
			expected: []Worktree{
				{Path: "worktrees/feature", Branch: "feature"},
			},
		},
		{
			name:     "empty worktrees",
			yaml:     ``,
			expected: []Worktree{},
		},
		{
			name: "worktree without path returns error",
			yaml: `
- branch: feat
`,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var node yaml.Node
			if err := yaml.Unmarshal([]byte(tt.yaml), &node); err != nil {
				t.Fatalf("failed to parse yaml: %v", err)
			}

			// Handle empty YAML case
			if len(node.Content) == 0 {
				result, err := ParseWorktrees(yaml.Node{})
				if err != nil {
					t.Errorf("unexpected error: %v", err)
				}
				if len(result) != 0 {
					t.Errorf("expected empty worktrees, got %v", result)
				}
				return
			}

			result, err := ParseWorktrees(*node.Content[0])

			if tt.expectError {
				if err == nil {
					t.Error("expected error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if len(result) != len(tt.expected) {
				t.Errorf("expected %d worktrees, got %d", len(tt.expected), len(result))
				return
			}

			for i, wt := range result {
				if wt.Path != tt.expected[i].Path {
					t.Errorf("worktree[%d].Path: expected %q, got %q", i, tt.expected[i].Path, wt.Path)
				}
				if wt.Branch != tt.expected[i].Branch {
					t.Errorf("worktree[%d].Branch: expected %q, got %q", i, tt.expected[i].Branch, wt.Branch)
				}
			}
		})
	}
}

func TestProject_GetValue_Worktrees(t *testing.T) {
	tests := []struct {
		name     string
		project  Project
		expected string
	}{
		{
			name: "project with worktrees",
			project: Project{
				Name: "test-project",
				WorktreeList: []Worktree{
					{Path: "feature", Branch: "feature/test"},
					{Path: "staging", Branch: "staging"},
				},
			},
			expected: "feature:feature/test, staging:staging",
		},
		{
			name: "project without worktrees",
			project: Project{
				Name:         "test-project",
				WorktreeList: []Worktree{},
			},
			expected: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := tt.project.GetValue("worktrees", 0)
			if result != tt.expected {
				t.Errorf("expected %q, got %q", tt.expected, result)
			}
		})
	}
}


================================================
FILE: core/dao/spec.go
================================================
package dao

import (
	"gopkg.in/yaml.v3"

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

type Spec struct {
	Name              string `yaml:"name"`
	Output            string `yaml:"output"`
	Parallel          bool   `yaml:"parallel"`
	IgnoreErrors      bool   `yaml:"ignore_errors"`
	IgnoreNonExisting bool   `yaml:"ignore_non_existing"`
	OmitEmptyRows     bool   `yaml:"omit_empty_rows"`
	OmitEmptyColumns  bool   `yaml:"omit_empty_columns"`
	ClearOutput       bool   `yaml:"clear_output"`
	Forks             uint32 `yaml:"forks"`

	context     string
	contextLine int
}

func (s *Spec) GetContext() string {
	return s.context
}

func (s *Spec) GetContextLine() int {
	return s.contextLine
}

// Populates SpecList and creates a default spec if no default spec is set.
func (c *Config) GetSpecList() ([]Spec, []ResourceErrors[Spec]) {
	var specs []Spec
	count := len(c.Specs.Content)

	specErrors := []ResourceErrors[Spec]{}
	foundErrors := false
	for i := 0; i < count; i += 2 {
		spec := &Spec{
			Name:        c.Specs.Content[i].Value,
			context:     c.Path,
			contextLine: c.Specs.Content[i].Line,
		}

		err := c.Specs.Content[i+1].Decode(spec)
		if err != nil {
			foundErrors = true
			specError := ResourceErrors[Spec]{Resource: spec, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			specErrors = append(specErrors, specError)
			continue
		}

		switch spec.Output {
		case "", "table", "stream", "html", "markdown":
		default:
			foundErrors = true
			specError := ResourceErrors[Spec]{
				Resource: spec,
				Errors:   []error{&core.SpecOutputError{Name: spec.Name, Output: spec.Output}},
			}
			specErrors = append(specErrors, specError)
		}

		if spec.Forks == 0 {
			spec.Forks = 4
		}

		specs = append(specs, *spec)
	}

	if foundErrors {
		return specs, specErrors
	}

	return specs, nil
}

func (c Config) GetSpec(name string) (*Spec, error) {
	for _, spec := range c.SpecList {
		if name == spec.Name {
			return &spec, nil
		}
	}

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

func (c Config) GetSpecNames() []string {
	names := []string{}
	for _, spec := range c.SpecList {
		names = append(names, spec.Name)
	}

	return names
}


================================================
FILE: core/dao/spec_test.go
================================================
package dao

import (
	"reflect"
	"testing"

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

func TestSpec_GetContext(t *testing.T) {
	spec := Spec{
		Name:        "test-spec",
		context:     "/path/to/config",
		contextLine: 42,
	}

	if spec.GetContext() != "/path/to/config" {
		t.Errorf("expected context '/path/to/config', got %q", spec.GetContext())
	}

	if spec.GetContextLine() != 42 {
		t.Errorf("expected context line 42, got %d", spec.GetContextLine())
	}
}

func TestSpec_GetSpecList(t *testing.T) {
	tests := []struct {
		name          string
		config        Config
		expectedCount int
		expectError   bool
	}{
		{
			name: "empty spec list",
			config: Config{
				SpecList: []Spec{},
			},
			expectedCount: 0,
			expectError:   false,
		},
		{
			name: "valid specs",
			config: Config{
				SpecList: []Spec{
					{
						Name:     "spec1",
						Output:   "table",
						Parallel: true,
						Forks:    4,
					},
					{
						Name:   "spec2",
						Output: "stream",
						Forks:  8,
					},
				},
			},
			expectedCount: 2,
			expectError:   false,
		},
		{
			name: "spec with defaults",
			config: Config{
				SpecList: []Spec{
					{
						Name:   "default-spec",
						Output: "table",
						Forks:  4,
					},
				},
			},
			expectedCount: 1,
			expectError:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			specs := tt.config.SpecList

			if len(specs) != tt.expectedCount {
				t.Errorf("expected %d specs, got %d", tt.expectedCount, len(specs))
			}
		})
	}
}

func TestSpec_GetSpec(t *testing.T) {
	config := Config{
		SpecList: []Spec{
			{
				Name:   "spec1",
				Output: "table",
				Forks:  4,
			},
			{
				Name:   "spec2",
				Output: "stream",
				Forks:  8,
			},
		},
	}

	tests := []struct {
		name          string
		specName      string
		expectError   bool
		expectedForks uint32
	}{
		{
			name:          "existing spec",
			specName:      "spec1",
			expectError:   false,
			expectedForks: 4,
		},
		{
			name:          "non-existing spec",
			specName:      "nonexistent",
			expectError:   true,
			expectedForks: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			spec, err := config.GetSpec(tt.specName)

			if tt.expectError {
				if err == nil {
					t.Error("expected error but got none")
				}
				if _, ok := err.(*core.SpecNotFound); !ok {
					t.Errorf("expected SpecNotFound error, got %T", err)
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if spec.Forks != tt.expectedForks {
				t.Errorf("expected forks %d, got %d", tt.expectedForks, spec.Forks)
			}
		})
	}
}

func TestSpec_GetSpecNames(t *testing.T) {
	tests := []struct {
		name          string
		config        Config
		expectedNames []string
	}{
		{
			name: "multiple specs",
			config: Config{
				SpecList: []Spec{
					{Name: "spec1"},
					{Name: "spec2"},
					{Name: "spec3"},
				},
			},
			expectedNames: []string{"spec1", "spec2", "spec3"},
		},
		{
			name: "empty spec list",
			config: Config{
				SpecList: []Spec{},
			},
			expectedNames: []string{},
		},
		{
			name: "single spec",
			config: Config{
				SpecList: []Spec{
					{Name: "solo-spec"},
				},
			},
			expectedNames: []string{"solo-spec"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			names := tt.config.GetSpecNames()

			if !reflect.DeepEqual(names, tt.expectedNames) {
				t.Errorf("expected names %v, got %v", tt.expectedNames, names)
			}
		})
	}
}


================================================
FILE: core/dao/tag.go
================================================
package dao

import (
	"slices"
	"strings"
)

type Tag struct {
	Name     string
	Projects []string
}

func (t Tag) GetValue(key string, _ int) string {
	switch strings.ToLower(key) {
	case "tag":
		return t.Name
	case "project", "projects":
		return strings.Join(t.Projects, "\n")
	default:
		return ""
	}
}

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

	return tags
}

func (c Config) GetTagAssocations(tags []string) ([]Tag, error) {
	t := []Tag{}

	for _, tag := range tags {
		projects, err := c.GetProjectsByTags([]string{tag})
		if err != nil {
			return []Tag{}, err
		}

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

		t = append(t, Tag{Name: tag, Projects: projectNames})
	}

	return t, nil
}


================================================
FILE: core/dao/tag_expr.go
================================================
// Package dao for evaluating boolean tag expressions against project tags.
package dao

import (
	"fmt"
	"slices"
	"strings"
	"unicode"
)

type TokenType int

const (
	TokenTag TokenType = iota
	TokenAnd
	TokenOr
	TokenNot
	TokenLParent
	TokenRParen
	TokenEOF
)

type Position struct {
	line   int
	column int
}

type Token struct {
	Type     TokenType
	Value    string
	Position Position
}

type Lexer struct {
	input  string
	pos    int
	line   int
	column int
	tokens []Token
}

func NewLexer(input string) *Lexer {
	return &Lexer{
		input:  input,
		pos:    0,
		line:   1,
		column: 1,
		tokens: make([]Token, 0),
	}
}

func (l *Lexer) Tokenize() error {
	if strings.TrimSpace(l.input) == "" {
		return fmt.Errorf("empty expression")
	}

	for l.pos < len(l.input) {
		char := l.current()
		switch {
		case char == ' ' || char == '\t':
			l.advance()
		case char == '\n':
			l.line++
			l.column = 1
			l.advance()
		case char == '(':
			l.addToken(TokenLParent, "(")
			l.advance()
		case char == ')':
			l.addToken(TokenRParen, ")")
			l.advance()
		case char == '!':
			l.addToken(TokenNot, "!")
			l.advance()
		case l.matchOperator("&&"):
			l.addToken(TokenAnd, "&&")
			l.advance()
			l.advance()
		case l.matchOperator("||"):
			l.addToken(TokenOr, "||")
			l.advance()
			l.advance()
		case isValidTagStart(char):
			l.readTag()
		default:
			return fmt.Errorf("unexpected character: %c at line %d, column %d", char, l.line, l.column)
		}
	}

	l.addToken(TokenEOF, "")
	return nil
}

func (l *Lexer) addToken(tokenType TokenType, value string) {
	l.tokens = append(l.tokens, Token{
		Type:     tokenType,
		Value:    value,
		Position: Position{line: l.line, column: l.column},
	})
}

func (l *Lexer) advance() {
	l.pos++
	l.column++
}

func (l *Lexer) current() rune {
	if l.pos >= len(l.input) {
		return 0
	}
	return rune(l.input[l.pos])
}

func (l *Lexer) matchOperator(op string) bool {
	if l.pos+len(op) > len(l.input) {
		return false
	}
	return l.input[l.pos:l.pos+len(op)] == op
}

func (l *Lexer) readTag() {
	startPos := l.pos
	startColumn := l.column

	// First character must be a letter
	if !isValidTagStart(l.current()) {
		return
	}
	l.advance()

	// Subsequent characters can be letters, numbers, hyphens, or underscores
	for l.pos < len(l.input) && isValidTagPart(l.current()) {
		l.advance()
	}

	value := l.input[startPos:l.pos]
	l.tokens = append(l.tokens, Token{
		Type:     TokenTag,
		Value:    value,
		Position: Position{line: l.line, column: startColumn},
	})
}

func isValidTagStart(r rune) bool {
	return !isReservedChar(r) && !unicode.IsSpace(r)
}

func isValidTagPart(r rune) bool {
	return !isReservedChar(r) && !unicode.IsSpace(r)
}

func isReservedChar(r rune) bool {
	return r == '(' || r == ')' || r == '!' || r == '&' || r == '|'
}

type Parser struct {
	tokens  []Token
	pos     int
	project *Project
}

func NewParser(tokens []Token, project *Project) *Parser {
	return &Parser{
		tokens:  tokens,
		pos:     0,
		project: project,
	}
}

func (p *Parser) Parse() (bool, error) {
	if len(p.tokens) <= 1 { // Only EOF token
		return false, fmt.Errorf("empty expression")
	}

	result, err := p.parseExpression()
	if err != nil {
		return false, err
	}

	// Check if we consumed all tokens
	if p.current().Type != TokenEOF {
		pos := p.current().Position
		return false, fmt.Errorf("unexpected token at line %d, column %d", pos.line, pos.column)
	}

	return result, nil
}

func (p *Parser) parseExpression() (bool, error) {
	left, err := p.parseTerm()
	if err != nil {
		return false, err
	}

	for p.current().Type == TokenOr {
		op := p.current()
		p.pos++

		// Check for missing right operand
		if p.current().Type == TokenEOF {
			return false, fmt.Errorf("missing right operand for OR operator at line %d, column %d",
				op.Position.line, op.Position.column)
		}

		right, err := p.parseTerm()
		if err != nil {
			return false, err
		}
		left = left || right
	}

	return left, nil
}

func (p *Parser) parseTerm() (bool, error) {
	left, err := p.parseFactor()
	if err != nil {
		return false, err
	}

	for p.current().Type == TokenAnd {
		op := p.current()
		p.pos++

		// Check for missing right operand
		if p.current().Type == TokenEOF {
			return false, fmt.Errorf("missing right operand for AND operator at line %d, column %d",
				op.Position.line, op.Position.column)
		}

		right, err := p.parseFactor()
		if err != nil {
			return false, err
		}
		left = left && right
	}

	return left, nil
}

func (p *Parser) parseFactor() (bool, error) {
	token := p.current()

	switch token.Type {
	case TokenNot:
		p.pos++
		if p.current().Type == TokenEOF {
			return false, fmt.Errorf("missing operand after NOT at line %d, column %d",
				token.Position.line, token.Position.column)
		}
		val, err := p.parseFactor()
		if err != nil {
			return false, err
		}
		return !val, nil

	case TokenLParent:
		p.pos++
		// Check for empty parentheses
		if p.current().Type == TokenRParen {
			return false, fmt.Errorf("empty parentheses at line %d, column %d",
				token.Position.line, token.Position.column)
		}
		val, err := p.parseExpression()
		if err != nil {
			return false, err
		}
		if p.current().Type != TokenRParen {
			return false, fmt.Errorf("missing closing parenthesis for opening parenthesis at line %d, column %d",
				token.Position.line, token.Position.column)
		}
		p.pos++
		return val, nil

	case TokenTag:
		p.pos++
		return slices.Contains(p.project.Tags, token.Value), nil

	default:
		return false, fmt.Errorf("unexpected token at line %d, column %d: %v",
			token.Position.line, token.Position.column, token.Value)
	}
}

func (p *Parser) current() Token {
	if p.pos >= len(p.tokens) {
		return Token{Type: TokenEOF}
	}
	return p.tokens[p.pos]
}

// evaluateExpression checks if a boolean tag expression evaluates to true for a given project.
// The function supports boolean operations on project tags with full operator precedence.
//
// Operators (in precedence order):
//  1. ()  - Parentheses for grouping
//  2. !   - NOT operator (logical negation)
//  3. &&  - AND operator (logical conjunction)
//  4. ||  - OR operator (logical disjunction)
//
// Tag Expression Example:
//
// Expression: (main && (dev || prod)) && !test
//
// Requirements:
// 1. Must have "main" tag            - Mandatory
// 2. Must have "dev" OR "prod" tag   - At least one required
// 3. Must NOT have "test" tag        - Excluded if present
//
// Matches tags:
//
//	["main", "dev"]
//	["main", "prod"]
//	["main", "dev", "prod"]
//
// Does NOT match tags:
//
//	["main"]                   - missing dev/prod
//	["main", "dev", "test"]    - has test tag
//	["dev", "prod"]            - missing main
func evaluateExpression(project *Project, expression string) (bool, error) {
	lexer := NewLexer(expression)
	err := lexer.Tokenize()
	if err != nil {
		return false, fmt.Errorf("lexer error: %v", err)
	}

	parser := NewParser(lexer.tokens, project)
	return parser.Parse()
}

func validateExpression(expression string) error {
	lexer := NewLexer(expression)
	err := lexer.Tokenize()
	if err != nil {
		return fmt.Errorf("%v", err)
	}

	project := &Project{Tags: []string{}}
	parser := NewParser(lexer.tokens, project)
	_, err = parser.Parse()
	if err != nil {
		return fmt.Errorf("%v", err)
	}

	return nil
}


================================================
FILE: core/dao/tag_expr_test.go
================================================
package dao

import (
	"strings"
	"testing"
)

func TestTagExpression(t *testing.T) {
	projects := []Project{
		{
			Name: "Project A",
			Tags: []string{"active", "git", "frontend"},
		},
		{
			Name: "Project B",
			Tags: []string{"active", "sake", "backend"},
		},
	}

	// Test cases for valid expressions
	validTests := []struct {
		name     string
		expr     string
		project  string
		expected bool
	}{
		{"simple AND", "active && git", "Project A", true},
		{"simple AND false", "active && git", "Project B", false},
		{"simple OR", "git || sake", "Project A", true},
		{"simple OR", "git || sake", "Project B", true},
		{"nested AND-OR", "((active && git) || (sake && backend))", "Project A", true},
		{"nested AND-OR", "((active && git) || (sake && backend))", "Project B", true},
		{"parentheses precedence", "(active && (git || sake))", "Project A", true},
		{"parentheses precedence", "(active && (git || sake))", "Project B", true},
		{"complex expression", "((active && git) || (active && sake)) && (frontend || backend)", "Project A", true},
		{"complex expression", "((active && git) || (active && sake)) && (frontend || backend)", "Project B", true},
		{"NOT operator", "!(active && (git || sake))", "Project A", false},
		{"NOT operator", "!(active && (git || sake))", "Project B", false},
		{"triple nested", "(((active && git) || sake) && backend)", "Project A", false},
		{"triple nested", "(((active && git) || sake) && backend)", "Project B", true},
	}

	t.Run("valid expressions", func(t *testing.T) {
		for _, tt := range validTests {
			t.Run(tt.name, func(t *testing.T) {
				var proj Project
				for _, p := range projects {
					if p.Name == tt.project {
						proj = p
						break
					}
				}

				result, err := evaluateExpression(&proj, tt.expr)
				if err != nil {
					t.Errorf("unexpected error: %v", err)
				}
				if result != tt.expected {
					t.Errorf("expression %q on project %q: got %v, want %v",
						tt.expr, tt.project, result, tt.expected)
				}
			})
		}
	})

	// Test cases for invalid expressions
	invalidTests := []struct {
		name        string
		expr        string
		expectedErr string
	}{
		{"empty expression", "", "empty expression"},
		{"operator without operands", "&&", "unexpected token"},
		{"missing right operand", "tag &&", "missing right operand"},
		{"missing left operand", "&& tag", "unexpected token"},
		{"empty parentheses", "()", "empty parentheses"},
		{"unmatched parenthesis", "((tag)", "missing closing parenthesis"},
		{"missing operator", "tag tag", "unexpected token"},
		{"double operator", "tag && && tag", "unexpected token"},
		{"NOT without operand", "!", "missing operand after NOT"},
	}

	t.Run("invalid expressions", func(t *testing.T) {
		for _, tt := range invalidTests {
			t.Run(tt.name, func(t *testing.T) {
				_, err := evaluateExpression(&projects[0], tt.expr)
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tt.expectedErr)
					return
				}
				if !strings.Contains(err.Error(), tt.expectedErr) {
					t.Errorf("expected error containing %q, got %q", tt.expectedErr, err.Error())
				}
			})
		}
	})
}


================================================
FILE: core/dao/target.go
================================================
package dao

import (
	"gopkg.in/yaml.v3"

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

type Target struct {
	Name     string   `yaml:"name"`
	All      bool     `yaml:"all"`
	Projects []string `yaml:"projects"`
	Paths    []string `yaml:"paths"`
	Tags     []string `yaml:"tags"`
	TagsExpr string   `yaml:"tags_expr"`
	Cwd      bool     `yaml:"cwd"`

	context     string
	contextLine int
}

func (t *Target) GetContext() string {
	return t.context
}

func (t *Target) GetContextLine() int {
	return t.contextLine
}

// Populates TargetList and creates a default target if no default target is set.
func (c *Config) GetTargetList() ([]Target, []ResourceErrors[Target]) {
	var targets []Target
	count := len(c.Targets.Content)

	targetErrors := []ResourceErrors[Target]{}
	foundErrors := false
	for i := 0; i < count; i += 2 {
		target := &Target{
			Name:        c.Targets.Content[i].Value,
			context:     c.Path,
			contextLine: c.Targets.Content[i].Line,
		}

		err := c.Targets.Content[i+1].Decode(target)
		if err != nil {
			foundErrors = true
			targetError := ResourceErrors[Target]{Resource: target, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
			targetErrors = append(targetErrors, targetError)
			continue
		}

		if target.TagsExpr != "" {
			valid := validateExpression(target.TagsExpr)
			if valid != nil {
				foundErrors = true
				targetError := ResourceErrors[Target]{
					Resource: target,
					Errors:   []error{&core.TargetTagsExprError{Name: target.Name, Err: valid}},
				}
				targetErrors = append(targetErrors, targetError)
			}
		}

		targets = append(targets, *target)
	}

	if foundErrors {
		return targets, targetErrors
	}

	return targets, nil
}

func (c Config) GetTarget(name string) (*Target, error) {
	for _, target := range c.TargetList {
		if name == target.Name {
			return &target, nil
		}
	}

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

func (c Config) GetTargetNames() []string {
	names := []string{}
	for _, target := range c.TargetList {
		names = append(names, target.Name)
	}

	return names
}


================================================
FILE: core/dao/target_test.go
================================================
package dao

import (
	"reflect"
	"testing"

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

func TestTarget_GetContext(t *testing.T) {
	target := Target{
		Name:        "test-target",
		context:     "/path/to/config",
		contextLine: 42,
	}

	if target.GetContext() != "/path/to/config" {
		t.Errorf("expected context '/path/to/config', got %q", target.GetContext())
	}

	if target.GetContextLine() != 42 {
		t.Errorf("expected context line 42, got %d", target.GetContextLine())
	}
}

func TestTarget_GetTargetList(t *testing.T) {
	tests := []struct {
		name          string
		config        Config
		expectedCount int
		expectError   bool
	}{
		{
			name: "empty target list",
			config: Config{
				TargetList: []Target{},
			},
			expectedCount: 0,
			expectError:   false,
		},
		{
			name: "multiple valid targets",
			config: Config{
				TargetList: []Target{
					{
						Name:     "target1",
						Projects: []string{"proj1", "proj2"},
						Tags:     []string{"frontend"},
					},
					{
						Name:     "target2",
						Projects: []string{"proj3"},
						Tags:     []string{"backend"},
					},
				},
			},
			expectedCount: 2,
			expectError:   false,
		},
		{
			name: "target with all flag",
			config: Config{
				TargetList: []Target{
					{
						Name: "all-target",
						All:  true,
					},
				},
			},
			expectedCount: 1,
			expectError:   false,
		},
		{
			name: "target with paths",
			config: Config{
				TargetList: []Target{
					{
						Name:  "path-target",
						Paths: []string{"path1", "path2"},
					},
				},
			},
			expectedCount: 1,
			expectError:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			targets := tt.config.TargetList

			if len(targets) != tt.expectedCount {
				t.Errorf("expected %d targets, got %d", tt.expectedCount, len(targets))
			}
		})
	}
}

func TestTarget_GetTarget(t *testing.T) {
	config := Config{
		TargetList: []Target{
			{
				Name:     "frontend",
				Projects: []string{"web", "mobile"},
				Tags:     []string{"frontend"},
			},
			{
				Name:     "backend",
				Projects: []string{"api", "worker"},
				Tags:     []string{"backend"},
			},
		},
	}

	tests := []struct {
		name         string
		targetName   string
		expectError  bool
		expectedTags []string
	}{
		{
			name:         "existing target",
			targetName:   "frontend",
			expectError:  false,
			expectedTags: []string{"frontend"},
		},
		{
			name:         "non-existing target",
			targetName:   "nonexistent",
			expectError:  true,
			expectedTags: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			target, err := config.GetTarget(tt.targetName)

			if tt.expectError {
				if err == nil {
					t.Error("expected error but got none")
				}
				if _, ok := err.(*core.TargetNotFound); !ok {
					t.Errorf("expected TargetNotFound error, got %T", err)
				}
				return
			}

			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}

			if !reflect.DeepEqual(target.Tags, tt.expectedTags) {
				t.Errorf("expected tags %v, got %v", tt.expectedTags, target.Tags)
			}
		})
	}
}

func TestTarget_GetTargetNames(t *testing.T) {
	tests := []struct {
		name          string
		config        Config
		expectedNames []string
	}{
		{
			name: "multiple targets",
			config: Config{
				TargetList: []Target{
					{Name: "target1"},
					{Name: "target2"},
					{Name: "target3"},
				},
			},
			expectedNames: []string{"target1", "target2", "target3"},
		},
		{
			name: "empty target list",
			config: Config{
				TargetList: []Target{},
			},
			expectedNames: []string{},
		},
		{
			name: "single target",
			config: Config{
				TargetList: []Target{
					{Name: "solo-target"},
				},
			},
			expectedNames: []string{"solo-target"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			names := tt.config.GetTargetNames()

			if !reflect.DeepEqual(names, tt.expectedNames) {
				t.Errorf("expected names %v, got %v", tt.expectedNames, names)
			}
		})
	}
}


================================================
FILE: core/dao/task.go
================================================
package dao

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

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

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

var (
	buildMode = "dev"
)

type Command struct {
	Name    string    `yaml:"name"`
	Desc    string    `yaml:"desc"`
	Shell   string    `yaml:"shell"` // should be in the format: <program> <command flag>, for instance "sh -c", "node -e"
	Cmd     string    `yaml:"cmd"`   // "echo hello world", it should not include the program flag (-c,-e, .etc)
	Task    string    `yaml:"task"`
	TaskRef string    `yaml:"-"` // Keep a reference to the task
	TTY     bool      `yaml:"tty"`
	Env     yaml.Node `yaml:"env"`
	EnvList []string  `yaml:"-"`

	// Internal
	ShellProgram string   `yaml:"-"` // should be in the format: <program>, example: "sh", "node"
	CmdArg       []string `yaml:"-"` // is in the format ["-c echo hello world"] or ["-c", "echo hello world"], it includes the shell flag
}

type Task struct {
	SpecData   Spec
	TargetData Target
	ThemeData  Theme

	Name     string    `yaml:"name"`
	Desc     string    `yaml:"desc"`
	Shell    string    `yaml:"shell"`
	Cmd      string    `yaml:"cmd"`
	Commands []Command `yaml:"commands"`
	EnvList  []string  `yaml:"-"`
	TTY      bool      `yaml:"tty"`

	Env    yaml.Node `yaml:"env"`
	Spec   yaml.Node `yaml:"spec"`
	Target yaml.Node `yaml:"target"`
	Theme  yaml.Node `yaml:"theme"`

	// Internal
	ShellProgram string   `yaml:"-"` // should be in the format: <program>, example: "sh", "node"
	CmdArg       []string `yaml:"-"` // is in the format ["-c echo hello world"] or ["-c", "echo hello world"], it includes the shell flag
	context      string
	contextLine  int
}

func (t *Task) GetContext() string {
	return t.context
}

func (t *Task) GetContextLine() int {
	return t.contextLine
}

// ParseTask parses tasks and builds the correct "AST". Depending on if the data is specified inline,
// or if it is a reference to resource, it will handle them differently.
func (t *Task) ParseTask(config Config, taskErrors *ResourceErrors[Task]) {
	if t.Shell == "" {
		t.Shell = config.Shell
	} else {
		t.Shell = core.FormatShell(t.Shell)
	}

	program, cmdArgs := core.FormatShellString(t.Shell, t.Cmd)
	t.ShellProgram = program
	t.CmdArg = cmdArgs

	for j, cmd := range t.Commands {
		// Task reference
		if cmd.Task != "" {
			cmdRef, err := config.GetCommand(cmd.Task)
			if err != nil {
				taskErrors.Errors = append(taskErrors.Errors, err)
				continue
			}

			t.Commands[j] = *cmdRef
			t.Commands[j].TaskRef = cmd.Task
		}

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

		program, cmdArgs := core.FormatShellString(t.Commands[j].Shell, t.Commands[j].Cmd)
		t.Commands[j].ShellProgram = program
		t.Commands[j].CmdArg = cmdArgs
	}

	if len(t.Theme.Content) > 0 {
		// Theme value
		theme := &Theme{}
		err := t.Theme.Decode(theme)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.ThemeData = *theme
		}
	} else if t.Theme.Value != "" {
		// Theme reference
		theme, err := config.GetTheme(t.Theme.Value)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.ThemeData = *theme
		}
	} else {
		// Default theme
		theme, err := config.GetTheme(DEFAULT_THEME.Name)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.ThemeData = *theme
		}
	}

	if len(t.Spec.Content) > 0 {
		// Spec value
		spec := &Spec{}
		err := t.Spec.Decode(spec)

		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.SpecData = *spec
		}
	} else if t.Spec.Value != "" {
		// Spec reference
		spec, err := config.GetSpec(t.Spec.Value)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.SpecData = *spec
		}
	} else {
		// Default spec
		spec, err := config.GetSpec(DEFAULT_SPEC.Name)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.SpecData = *spec
		}
	}

	if len(t.Target.Content) > 0 {
		// Target value
		target := &Target{}
		err := t.Target.Decode(target)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.TargetData = *target
		}
	} else if t.Target.Value != "" {
		// Target reference
		target, err := config.GetTarget(t.Target.Value)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.TargetData = *target
		}
	} else {
		// Default target
		target, err := config.GetTarget(DEFAULT_TARGET.Name)
		if err != nil {
			taskErrors.Errors = append(taskErrors.Errors, err)
		} else {
			t.TargetData = *target
		}
	}
}

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 buildMode == "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,
			ShowCursor:      true,
		}
	}

	spinner, err := yacspin.New(cfg)

	return *spinner, err
}

func (t Task) GetValue(key string, _ int) string {
	switch strings.ToLower(key) {
	case "name", "task":
		return t.Name
	case "desc", "description":
		return t.Desc
	case "command":
		return t.Cmd
	case "spec":
		return t.SpecData.Name
	case "target":
		return t.TargetData.Name
	default:
		return ""
	}
}

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

	taskErrors := []ResourceErrors[Task]{}
	foundErrors := false
	for i := 0; i < count; i += 2 {
		task := &Task{
			Name:        c.Tasks.Content[i].Value,
			context:     c.Path,
			contextLine: c.Tasks.Content[i].Line,
		}

		// Shorthand definition: example_task: echo 123
		if c.Tasks.Content[i+1].Kind == 8 {
			task.Cmd = c.Tasks.Content[i+1].Value
		} else { // Full definition
			err := c.Tasks.Content[i+1].Decode(task)
			if err != nil {
				foundErrors = true
				taskError := ResourceErrors[Task]{Resource: task, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}
				taskErrors = append(taskErrors, taskError)
				continue
			}
		}

		tasks = append(tasks, *task)
	}

	if foundErrors {
		return tasks, taskErrors
	}

	return tasks, nil
}

func ParseTaskEnv(
	env yaml.Node,
	userEnv []string,
	parentEnv []string,
	configEnv []string,
) ([]string, error) {
	cmdEnv, err := EvaluateEnv(ParseNodeEnv(env))
	if err != nil {
		return []string{}, err
	}

	pEnv, err := EvaluateEnv(parentEnv)
	if err != nil {
		return []string{}, err
	}

	envList := MergeEnvs(userEnv, cmdEnv, pEnv, configEnv)

	return envList, nil
}

func ParseTasksEnv(tasks []Task) {
	for i := range tasks {
		envs, err := ParseTaskEnv(tasks[i].Env, []string{}, []string{}, []string{})
		core.CheckIfError(err)

		tasks[i].EnvList = envs

		for j := range tasks[i].Commands {
			envs, err = ParseTaskEnv(tasks[i].Commands[j].Env, []string{}, []string{}, []string{})
			core.CheckIfError(err)

			tasks[i].Commands[j].EnvList = envs
		}
	}
}

// GetTaskProjects retrieves a filtered list of projects for a given task, applying
// runtime flag overrides and target configurations.
//
// Behavior depends on the provided runtime flags (flags, setFlags) and task target:
//   - If runtime flags are set (Projects, Paths, Tags, etc.), they take precedence
//     and reset the task's target configuration.
//   - If a target is explicitly specified (flags.Target), it loads and applies that
//     target's configuration before applying runtime flag overrides.
//   - If no runtime flags or target are provided, the task's default target data is used.
//
// Filtering priority (highest to lowest):
//  1. Runtime flags (e.g., --projects, --tags, --cwd)
//  2. Explicit target configuration (--target)
//  3. Task's default target data (if no overrides exist)
//
// Returns:
//   - Filtered []Project based on the resolved configuration.
//   - Non-nil error if target resolution or project filtering fails.
func (c Config) GetTaskProjects(
	task *Task,
	flags *core.RunFlags,
	setFlags *core.SetRunFlags,
) ([]Project, error) {
	var err error
	var projects []Project

	// Reset target if any runtime flags are used
	if len(flags.Projects) > 0 ||
		len(flags.Paths) > 0 ||
		len(flags.Tags) > 0 ||
		flags.TagsExpr != "" ||
		flags.Target != "" ||
		setFlags.Cwd ||
		setFlags.All {
		task.TargetData = Target{}
	}

	if flags.Target != "" {
		target, err := c.GetTarget(flags.Target)
		if err != nil {
			return []Project{}, err
		}
		task.TargetData = *target
	}

	if len(flags.Projects) > 0 {
		task.TargetData.Projects = flags.Projects
	}

	if len(flags.Paths) > 0 {
		task.TargetData.Paths = flags.Paths
	}

	if len(flags.Tags) > 0 {
		task.TargetData.Tags = flags.Tags
	}

	if flags.TagsExpr != "" {
		task.TargetData.TagsExpr = flags.TagsExpr
	}

	if setFlags.Cwd {
		task.TargetData.Cwd = flags.Cwd
	}

	if setFlags.All {
		task.TargetData.All = flags.All
	}

	projects, err = c.FilterProjects(
		task.TargetData.Cwd,
		task.TargetData.All,
		task.TargetData.Projects,
		task.TargetData.Paths,
		task.TargetData.Tags,
		task.TargetData.TagsExpr,
	)
	if err != nil {
		return []Project{}, err
	}

	return projects, nil
}

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

	foundTasks := make(map[string]bool)
	for _, t := range names {
		foundTasks[t] = false
	}

	var filteredTasks []Task
	for _, name := range names {
		if foundTasks[name] {
			continue
		}

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

	nonExistingTasks := []string{}
	for k, v := range foundTasks {
		if !v {
			nonExistingTasks = append(nonExistingTasks, k)
		}
	}

	if len(nonExistingTasks) > 0 {
		return []Task{}, &core.TaskNotFound{Name: nonExistingTasks}
	}

	return filteredTasks, nil
}

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

	return taskNames
}

func (c Config) GetTaskNameAndDesc() []string {
	taskNames := []string{}
	for _, task := range c.TaskList {
		taskNames = append(taskNames, fmt.Sprintf("%s\t%s", task.Name, task.Desc))
	}

	return taskNames
}

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

	return nil, &core.TaskNotFound{Name: []string{name}}
}

func (c Config) GetCommand(taskName string) (*Command, error) {
	for _, cmd := range c.TaskList {
		if taskName == 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: []string{taskName}}
}

func (t Task) ConvertTaskToCommand() Command {
	cmd := Command{
		Name:         t.Name,
		Desc:         t.Desc,
		EnvList:      t.EnvList,
		Shell:        t.Shell,
		Cmd:          t.Cmd,
		CmdArg:       t.CmdArg,
		ShellProgram: t.ShellProgram,
	}

	return cmd
}

func ParseCmd(
	cmd string,
	runFlags *core.RunFlags,
	setFlags *core.SetRunFlags,
	config *Config,
) ([]Task, []Project, error) {
	task := Task{Name: "output", Cmd: cmd, TTY: runFlags.TTY}
	taskErrors := make([]ResourceErrors[Task], 1)
	task.ParseTask(*config, &taskErrors[0])

	var configErr = ""
	for _, taskError := range taskErrors {
		if len(taskError.Errors) > 0 {
			configErr = fmt.Sprintf("%s%s", configErr, FormatErrors(taskError.Resource, taskError.Errors))
		}
	}
	if configErr != "" {
		core.CheckIfError(errors.New(configErr))
	}

	projects, err := config.GetTaskProjects(&task, runFlags, setFlags)
	if err != nil {
		return nil, nil, err
	}
	core.CheckIfError(err)

	var tasks []Task
	for range projects {
		t := Task{}
		err := copier.Copy(&t, &task)
		core.CheckIfError(err)
		tasks = append(tasks, t)
	}

	if len(projects) == 0 {
		return nil, nil, &core.NoTargets{}
	}

	return tasks, projects, err
}

func ParseSingleTas
Download .txt
gitextract_1sykag_x/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── BUG_REPORT.md
│   │   └── FEATURE_REQUEST.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── agents/
│   │   └── default.agent.md
│   ├── copilot-instructions.md
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── benchmarks/
│   └── .gitignore
├── cmd/
│   ├── check.go
│   ├── completion.go
│   ├── describe.go
│   ├── describe_projects.go
│   ├── describe_tasks.go
│   ├── edit.go
│   ├── edit_project.go
│   ├── edit_task.go
│   ├── exec.go
│   ├── gen.go
│   ├── gen_docs.go
│   ├── init.go
│   ├── list.go
│   ├── list_projects.go
│   ├── list_tags.go
│   ├── list_tasks.go
│   ├── root.go
│   ├── run.go
│   ├── sync.go
│   └── tui.go
├── core/
│   ├── config.man
│   ├── dao/
│   │   ├── benchmark_test.go
│   │   ├── common.go
│   │   ├── common_test.go
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── import.go
│   │   ├── project.go
│   │   ├── project_test.go
│   │   ├── spec.go
│   │   ├── spec_test.go
│   │   ├── tag.go
│   │   ├── tag_expr.go
│   │   ├── tag_expr_test.go
│   │   ├── target.go
│   │   ├── target_test.go
│   │   ├── task.go
│   │   ├── task_test.go
│   │   ├── theme.go
│   │   ├── theme_block.go
│   │   ├── theme_stream.go
│   │   ├── theme_table.go
│   │   ├── theme_tree.go
│   │   ├── theme_tui.go
│   │   └── utils_test.go
│   ├── errors.go
│   ├── exec/
│   │   ├── client.go
│   │   ├── clone.go
│   │   ├── exec.go
│   │   ├── table.go
│   │   ├── text.go
│   │   ├── unix.go
│   │   └── windows.go
│   ├── flags.go
│   ├── man.go
│   ├── man_gen.go
│   ├── mani.1
│   ├── prefixer.go
│   ├── prefixer_benchmark_test.go
│   ├── print/
│   │   ├── lib.go
│   │   ├── print_block.go
│   │   ├── print_table.go
│   │   ├── print_tree.go
│   │   └── table.go
│   ├── sizedwaitgroup.go
│   ├── tui/
│   │   ├── components/
│   │   │   ├── tui_button.go
│   │   │   ├── tui_checkbox.go
│   │   │   ├── tui_filter.go
│   │   │   ├── tui_list.go
│   │   │   ├── tui_modal.go
│   │   │   ├── tui_output.go
│   │   │   ├── tui_search.go
│   │   │   ├── tui_table.go
│   │   │   ├── tui_text.go
│   │   │   ├── tui_textarea.go
│   │   │   ├── tui_toggle_text.go
│   │   │   └── tui_tree.go
│   │   ├── misc/
│   │   │   ├── tui_event.go
│   │   │   ├── tui_focus.go
│   │   │   ├── tui_global.go
│   │   │   ├── tui_theme.go
│   │   │   ├── tui_utils.go
│   │   │   └── tui_writer.go
│   │   ├── pages/
│   │   │   ├── tui_exec.go
│   │   │   ├── tui_project.go
│   │   │   ├── tui_run.go
│   │   │   └── tui_task.go
│   │   ├── pages.go
│   │   ├── tui.go
│   │   ├── tui_input.go
│   │   ├── views/
│   │   │   ├── tui_help.go
│   │   │   ├── tui_project_view.go
│   │   │   ├── tui_shortcut_info.go
│   │   │   ├── tui_spec_view.go
│   │   │   └── tui_task_view.go
│   │   └── watcher.go
│   └── utils.go
├── docs/
│   ├── changelog.md
│   ├── commands.md
│   ├── config.md
│   ├── contributing.md
│   ├── development.md
│   ├── error-handling.md
│   ├── examples.md
│   ├── filtering-projects.md
│   ├── installation.md
│   ├── introduction.md
│   ├── man-pages.md
│   ├── output.md
│   ├── project-background.md
│   ├── roadmap.md
│   ├── shell-completion.mdx
│   ├── usage.md
│   └── variables.md
├── examples/
│   ├── .gitignore
│   ├── README.md
│   └── mani.yaml
├── go.mod
├── go.sum
├── install.sh
├── main.go
├── res/
│   ├── demo.md
│   ├── demo.vhs
│   └── mani.yaml
├── scripts/
│   └── release.sh
└── test/
    ├── README.md
    ├── fixtures/
    │   ├── mani-advanced/
    │   │   ├── .gitignore
    │   │   └── mani.yaml
    │   ├── mani-empty/
    │   │   └── mani.yaml
    │   └── mani-no-tasks/
    │       └── mani.yaml
    ├── images/
    │   ├── alpine.exec.Dockerfile
    │   └── alpine.test.Dockerfile
    ├── integration/
    │   ├── describe_test.go
    │   ├── exec_test.go
    │   ├── golden/
    │   │   ├── describe/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── exec/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-6/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── init/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── dashgrid/
    │   │   │   │   │   └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   ├── nameless/
    │   │   │   │   │   └── empty
    │   │   │   │   ├── nested/
    │   │   │   │   │   └── template-generator/
    │   │   │   │   │       └── empty
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-2/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── list/
    │   │   │   ├── golden-0/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-10/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-11/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-12/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-13/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-14/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-15/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-16/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-17/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── run/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-1/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-10/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-11/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-2/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-3/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-4/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-5/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-6/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-7/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   ├── golden-8/
    │   │   │   │   ├── .gitignore
    │   │   │   │   ├── frontend/
    │   │   │   │   │   ├── dashgrid/
    │   │   │   │   │   │   └── empty
    │   │   │   │   │   └── pinto/
    │   │   │   │   │       └── empty
    │   │   │   │   ├── mani.yaml
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-9/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   ├── sync/
    │   │   │   ├── golden-0/
    │   │   │   │   └── stdout.golden
    │   │   │   └── golden-1/
    │   │   │       ├── .gitignore
    │   │   │       ├── frontend/
    │   │   │       │   ├── dashgrid/
    │   │   │       │   │   └── empty
    │   │   │       │   └── pinto/
    │   │   │       │       └── empty
    │   │   │       ├── mani.yaml
    │   │   │       └── stdout.golden
    │   │   └── version/
    │   │       ├── golden-0/
    │   │       │   └── stdout.golden
    │   │       └── golden-1/
    │   │           ├── mani.yaml
    │   │           └── stdout.golden
    │   ├── init_test.go
    │   ├── list_test.go
    │   ├── main_test.go
    │   ├── run_test.go
    │   ├── sync_test.go
    │   └── version_test.go
    ├── playground/
    │   ├── .gitignore
    │   ├── imports/
    │   │   ├── many-projects.yaml
    │   │   ├── projects.yaml
    │   │   ├── specs.yaml
    │   │   ├── targets.yaml
    │   │   ├── tasks.yaml
    │   │   └── themes.yaml
    │   └── mani.yaml
    └── scripts/
        ├── exec
        ├── git
        └── test
Download .txt
SYMBOL INDEX (583 symbols across 103 files)

FILE: cmd/check.go
  function checkCmd (line 11) | func checkCmd(configErr *error) *cobra.Command {

FILE: cmd/completion.go
  function completionCmd (line 11) | func completionCmd() *cobra.Command {
  function generateCompletion (line 63) | func generateCompletion(cmd *cobra.Command, args []string) {

FILE: cmd/describe.go
  function describeCmd (line 10) | func describeCmd(config *dao.Config, configErr *error) *cobra.Command {

FILE: cmd/describe_projects.go
  function describeProjectsCmd (line 13) | func describeProjectsCmd(
  function describeProjects (line 104) | func describeProjects(

FILE: cmd/describe_tasks.go
  function describeTasksCmd (line 13) | func describeTasksCmd(config *dao.Config, configErr *error, describeFlag...
  function describe (line 46) | func describe(

FILE: cmd/edit.go
  function editCmd (line 10) | func editCmd(config *dao.Config, configErr *error) *cobra.Command {
  function runEdit (line 39) | func runEdit(config dao.Config) {

FILE: cmd/edit_project.go
  function editProject (line 10) | func editProject(config *dao.Config, configErr *error) *cobra.Command {
  function runEditProject (line 46) | func runEditProject(args []string, config dao.Config) {

FILE: cmd/edit_task.go
  function editTask (line 10) | func editTask(config *dao.Config, configErr *error) *cobra.Command {
  function runEditTask (line 46) | func runEditTask(args []string, config dao.Config) {

FILE: cmd/exec.go
  function execCmd (line 13) | func execCmd(config *dao.Config, configErr *error) *cobra.Command {
  function execute (line 154) | func execute(

FILE: cmd/gen.go
  function genCmd (line 9) | func genCmd() *cobra.Command {

FILE: cmd/gen_docs.go
  function genDocsCmd (line 15) | func genDocsCmd(longAppDesc string) *cobra.Command {

FILE: cmd/init.go
  function initCmd (line 11) | func initCmd() *cobra.Command {

FILE: cmd/list.go
  function listCmd (line 10) | func listCmd(config *dao.Config, configErr *error) *cobra.Command {

FILE: cmd/list_projects.go
  function listProjectsCmd (line 14) | func listProjectsCmd(
  function listProjects (line 112) | func listProjects(

FILE: cmd/list_tags.go
  function listTagsCmd (line 14) | func listTagsCmd(config *dao.Config, configErr *error, listFlags *core.L...
  function listTags (line 53) | func listTags(

FILE: cmd/list_tasks.go
  function listTasksCmd (line 14) | func listTasksCmd(config *dao.Config, configErr *error, listFlags *core....
  function listTasks (line 56) | func listTasks(

FILE: cmd/root.go
  constant appName (line 14) | appName      = "mani"
  constant shortAppDesc (line 15) | shortAppDesc = "repositories manager and task runner"
  function Execute (line 35) | func Execute() {
  function init (line 42) | func init() {
  function initConfig (line 85) | func initConfig() {

FILE: cmd/run.go
  function runCmd (line 13) | func runCmd(config *dao.Config, configErr *error) *cobra.Command {
  function run (line 179) | func run(

FILE: cmd/sync.go
  function syncCmd (line 11) | func syncCmd(config *dao.Config, configErr *error) *cobra.Command {
  function runSync (line 110) | func runSync(

FILE: cmd/tui.go
  function tuiCmd (line 10) | func tuiCmd(config *dao.Config, configErr *error) *cobra.Command {

FILE: core/dao/benchmark_test.go
  function createBenchmarkConfig (line 9) | func createBenchmarkConfig(numProjects, numTasks int) Config {
  function BenchmarkLookup_GetProject (line 54) | func BenchmarkLookup_GetProject(b *testing.B) {
  function BenchmarkLookup_GetTask (line 72) | func BenchmarkLookup_GetTask(b *testing.B) {
  function BenchmarkLookup_GetSpec (line 90) | func BenchmarkLookup_GetSpec(b *testing.B) {
  function BenchmarkLookup_GetTheme (line 100) | func BenchmarkLookup_GetTheme(b *testing.B) {
  function BenchmarkLookup_GetTarget (line 110) | func BenchmarkLookup_GetTarget(b *testing.B) {
  function BenchmarkFilter_ByName (line 120) | func BenchmarkFilter_ByName(b *testing.B) {
  function BenchmarkFilter_ByTags (line 144) | func BenchmarkFilter_ByTags(b *testing.B) {
  function BenchmarkFilter_ByPath (line 161) | func BenchmarkFilter_ByPath(b *testing.B) {
  function BenchmarkFilter_Combined (line 198) | func BenchmarkFilter_Combined(b *testing.B) {
  function BenchmarkUtil_ConfigLoad (line 223) | func BenchmarkUtil_ConfigLoad(b *testing.B) {
  function BenchmarkLookup_GetCommand (line 245) | func BenchmarkLookup_GetCommand(b *testing.B) {
  function BenchmarkFilter_ByTagsExpr (line 262) | func BenchmarkFilter_ByTagsExpr(b *testing.B) {
  function BenchmarkUtil_GetCwdProject (line 305) | func BenchmarkUtil_GetCwdProject(b *testing.B) {
  function BenchmarkFilter_Intersect (line 323) | func BenchmarkFilter_Intersect(b *testing.B) {

FILE: core/dao/common.go
  type ResourceErrors (line 18) | type ResourceErrors struct
  type Resource (line 23) | type Resource interface
  function FormatErrors (line 28) | func FormatErrors(re Resource, errs []error) error {
  function ParseNodeEnv (line 61) | func ParseNodeEnv(node yaml.Node) []string {
  function EvaluateEnv (line 73) | func EvaluateEnv(envList []string) ([]string, error) {
  function MergeEnvs (line 100) | func MergeEnvs(envs ...[]string) []string {

FILE: core/dao/common_test.go
  function TestEnv_ParseNodeEnv (line 9) | func TestEnv_ParseNodeEnv(t *testing.T) {
  function TestEnv_MergeEnvs (line 64) | func TestEnv_MergeEnvs(t *testing.T) {

FILE: core/dao/config.go
  type Config (line 61) | type Config struct
    method GetContext (line 92) | func (c *Config) GetContext() string {
    method GetContextLine (line 96) | func (c *Config) GetContextLine() int {
    method GetEnvList (line 101) | func (c Config) GetEnvList() []string {
    method EditConfig (line 287) | func (c Config) EditConfig() error {
    method EditTask (line 339) | func (c Config) EditTask(name string) error {
    method EditProject (line 380) | func (c Config) EditProject(name string) error {
    method CheckConfigNoColor (line 631) | func (c *Config) CheckConfigNoColor() {
  function getUserConfigFile (line 112) | func getUserConfigFile(userConfigPath string) *string {
  function ReadConfig (line 137) | func ReadConfig(configFilepath string, userConfigPath string, colorFlag ...
  function openEditor (line 291) | func openEditor(path string, lineNr int) error {
  function InitMani (line 420) | func InitMani(args []string, initFlags core.InitFlags) ([]Project, error) {
  function RenameDuplicates (line 606) | func RenameDuplicates(projects []Project) {
  function CheckUserColor (line 621) | func CheckUserColor(colorFlag bool) bool {

FILE: core/dao/config_test.go
  function TestConfig_DuplicateProjectName (line 7) | func TestConfig_DuplicateProjectName(t *testing.T) {

FILE: core/dao/import.go
  type Import (line 13) | type Import struct
    method GetContext (line 20) | func (i *Import) GetContext() string {
    method GetContextLine (line 24) | func (i *Import) GetContextLine() int {
  method GetImportList (line 29) | func (c *Config) GetImportList() ([]Import, []ResourceErrors[Import]) {
  type ConfigResources (line 53) | type ConfigResources struct
  type Node (line 70) | type Node struct
  type NodeLink (line 77) | type NodeLink struct
  type FoundCyclicDependency (line 82) | type FoundCyclicDependency struct
    method Error (line 86) | func (c *FoundCyclicDependency) Error() string {
  method importConfigs (line 107) | func (c Config) importConfigs() (ConfigResources, error) {
  function concatErrors (line 137) | func concatErrors(ci ConfigResources, cycles *[]NodeLink) error {
  function parseConfig (line 188) | func parseConfig(path string, ci *ConfigResources) ([]Import, error) {
  method loadResources (line 213) | func (c Config) loadResources(ci *ConfigResources) []Import {
  function dfs (line 245) | func dfs(n *Node, m map[string]*Node, cycles *[]NodeLink, ci *ConfigReso...

FILE: core/dao/project.go
  type Project (line 18) | type Project struct
    method GetContext (line 50) | func (p *Project) GetContext() string {
    method GetContextLine (line 54) | func (p *Project) GetContextLine() int {
    method IsSingleBranch (line 58) | func (p Project) IsSingleBranch() bool {
    method IsSync (line 62) | func (p Project) IsSync() bool {
    method GetValue (line 66) | func (p Project) GetValue(key string, _ int) string {
  type Remote (line 40) | type Remote struct
  type Worktree (line 45) | type Worktree struct
  method GetProjectList (line 94) | func (c *Config) GetProjectList() ([]Project, []ResourceErrors[Project]) {
  method GetFilteredProjects (line 173) | func (c Config) GetFilteredProjects(flags *core.ProjectFlags) ([]Project...
  method FilterProjects (line 238) | func (c Config) FilterProjects(
  method GetProject (line 307) | func (c Config) GetProject(name string) (*Project, error) {
  method GetProjectsByName (line 317) | func (c Config) GetProjectsByName(projectNames []string) ([]Project, err...
  method GetProjectsByPath (line 354) | func (c Config) GetProjectsByPath(dirs []string) ([]Project, error) {
  method GetProjectsByTags (line 447) | func (c Config) GetProjectsByTags(tags []string) ([]Project, error) {
  method GetProjectsByTagsExpr (line 493) | func (c Config) GetProjectsByTagsExpr(tagsExpr string) ([]Project, error) {
  method GetCwdProject (line 512) | func (c Config) GetCwdProject() (Project, error) {
  method GetProjectPaths (line 550) | func (c Config) GetProjectPaths() []string {
  method GetProjectNames (line 569) | func (c Config) GetProjectNames() []string {
  method GetProjectUrls (line 578) | func (c Config) GetProjectUrls() []string {
  method GetProjectsTree (line 589) | func (c Config) GetProjectsTree(dirs []string, tags []string) ([]TreeNod...
  function IsGitWorktree (line 620) | func IsGitWorktree(path string) (bool, error) {
  function FindVCSystems (line 661) | func FindVCSystems(rootPath string) ([]Project, error) {
  function UpdateProjectsToGitignore (line 727) | func UpdateProjectsToGitignore(projectNames []string, gitignoreFilename ...
  function ParseRemotes (line 816) | func ParseRemotes(node yaml.Node) []Remote {
  function ParseWorktrees (line 833) | func ParseWorktrees(node yaml.Node) ([]Worktree, error) {
  method GetIntersectProjects (line 855) | func (c Config) GetIntersectProjects(ps ...[]Project) []Project {
  type TNode (line 875) | type TNode struct
  type TreeNode (line 880) | type TreeNode struct
  function AddToTree (line 889) | func AddToTree(root []TreeNode, node TNode) []TreeNode {

FILE: core/dao/project_test.go
  function TestProject_GetValue (line 11) | func TestProject_GetValue(t *testing.T) {
  function TestProject_GetProjectsByName (line 73) | func TestProject_GetProjectsByName(t *testing.T) {
  function TestProject_GetProjectsByTags (line 130) | func TestProject_GetProjectsByTags(t *testing.T) {
  function TestProject_GetProjectsByPath (line 190) | func TestProject_GetProjectsByPath(t *testing.T) {
  function TestProject_TestAddToTree (line 288) | func TestProject_TestAddToTree(t *testing.T) {
  function TestProject_GetIntersectProjects (line 328) | func TestProject_GetIntersectProjects(t *testing.T) {
  function TestParseWorktrees (line 377) | func TestParseWorktrees(t *testing.T) {
  function TestProject_GetValue_Worktrees (line 489) | func TestProject_GetValue_Worktrees(t *testing.T) {

FILE: core/dao/spec.go
  type Spec (line 9) | type Spec struct
    method GetContext (line 24) | func (s *Spec) GetContext() string {
    method GetContextLine (line 28) | func (s *Spec) GetContextLine() int {
  method GetSpecList (line 33) | func (c *Config) GetSpecList() ([]Spec, []ResourceErrors[Spec]) {
  method GetSpec (line 79) | func (c Config) GetSpec(name string) (*Spec, error) {
  method GetSpecNames (line 89) | func (c Config) GetSpecNames() []string {

FILE: core/dao/spec_test.go
  function TestSpec_GetContext (line 10) | func TestSpec_GetContext(t *testing.T) {
  function TestSpec_GetSpecList (line 26) | func TestSpec_GetSpecList(t *testing.T) {
  function TestSpec_GetSpec (line 88) | func TestSpec_GetSpec(t *testing.T) {
  function TestSpec_GetSpecNames (line 150) | func TestSpec_GetSpecNames(t *testing.T) {

FILE: core/dao/tag.go
  type Tag (line 8) | type Tag struct
    method GetValue (line 13) | func (t Tag) GetValue(key string, _ int) string {
  method GetTags (line 24) | func (c Config) GetTags() []string {
  method GetTagAssocations (line 37) | func (c Config) GetTagAssocations(tags []string) ([]Tag, error) {

FILE: core/dao/tag_expr.go
  type TokenType (line 11) | type TokenType
  constant TokenTag (line 14) | TokenTag TokenType = iota
  constant TokenAnd (line 15) | TokenAnd
  constant TokenOr (line 16) | TokenOr
  constant TokenNot (line 17) | TokenNot
  constant TokenLParent (line 18) | TokenLParent
  constant TokenRParen (line 19) | TokenRParen
  constant TokenEOF (line 20) | TokenEOF
  type Position (line 23) | type Position struct
  type Token (line 28) | type Token struct
  type Lexer (line 34) | type Lexer struct
    method Tokenize (line 52) | func (l *Lexer) Tokenize() error {
    method addToken (line 94) | func (l *Lexer) addToken(tokenType TokenType, value string) {
    method advance (line 102) | func (l *Lexer) advance() {
    method current (line 107) | func (l *Lexer) current() rune {
    method matchOperator (line 114) | func (l *Lexer) matchOperator(op string) bool {
    method readTag (line 121) | func (l *Lexer) readTag() {
  function NewLexer (line 42) | func NewLexer(input string) *Lexer {
  function isValidTagStart (line 144) | func isValidTagStart(r rune) bool {
  function isValidTagPart (line 148) | func isValidTagPart(r rune) bool {
  function isReservedChar (line 152) | func isReservedChar(r rune) bool {
  type Parser (line 156) | type Parser struct
    method Parse (line 170) | func (p *Parser) Parse() (bool, error) {
    method parseExpression (line 189) | func (p *Parser) parseExpression() (bool, error) {
    method parseTerm (line 215) | func (p *Parser) parseTerm() (bool, error) {
    method parseFactor (line 241) | func (p *Parser) parseFactor() (bool, error) {
    method current (line 285) | func (p *Parser) current() Token {
  function NewParser (line 162) | func NewParser(tokens []Token, project *Project) *Parser {
  function evaluateExpression (line 321) | func evaluateExpression(project *Project, expression string) (bool, erro...
  function validateExpression (line 332) | func validateExpression(expression string) error {

FILE: core/dao/tag_expr_test.go
  function TestTagExpression (line 8) | func TestTagExpression(t *testing.T) {

FILE: core/dao/target.go
  type Target (line 9) | type Target struct
    method GetContext (line 22) | func (t *Target) GetContext() string {
    method GetContextLine (line 26) | func (t *Target) GetContextLine() int {
  method GetTargetList (line 31) | func (c *Config) GetTargetList() ([]Target, []ResourceErrors[Target]) {
  method GetTarget (line 74) | func (c Config) GetTarget(name string) (*Target, error) {
  method GetTargetNames (line 84) | func (c Config) GetTargetNames() []string {

FILE: core/dao/target_test.go
  function TestTarget_GetContext (line 10) | func TestTarget_GetContext(t *testing.T) {
  function TestTarget_GetTargetList (line 26) | func TestTarget_GetTargetList(t *testing.T) {
  function TestTarget_GetTarget (line 99) | func TestTarget_GetTarget(t *testing.T) {
  function TestTarget_GetTargetNames (line 161) | func TestTarget_GetTargetNames(t *testing.T) {

FILE: core/dao/task.go
  type Command (line 21) | type Command struct
  type Task (line 37) | type Task struct
    method GetContext (line 62) | func (t *Task) GetContext() string {
    method GetContextLine (line 66) | func (t *Task) GetContextLine() int {
    method ParseTask (line 72) | func (t *Task) ParseTask(config Config, taskErrors *ResourceErrors[Tas...
    method GetValue (line 214) | func (t Task) GetValue(key string, _ int) string {
    method ConvertTaskToCommand (line 472) | func (t Task) ConvertTaskToCommand() Command {
  function TaskSpinner (line 188) | func TaskSpinner() (yacspin.Spinner, error) {
  method GetTaskList (line 231) | func (c *Config) GetTaskList() ([]Task, []ResourceErrors[Task]) {
  function ParseTaskEnv (line 267) | func ParseTaskEnv(
  function ParseTasksEnv (line 288) | func ParseTasksEnv(tasks []Task) {
  method GetTaskProjects (line 322) | func (c Config) GetTaskProjects(
  method GetTasksByNames (line 388) | func (c Config) GetTasksByNames(names []string) ([]Task, error) {
  method GetTaskNames (line 426) | func (c Config) GetTaskNames() []string {
  method GetTaskNameAndDesc (line 435) | func (c Config) GetTaskNameAndDesc() []string {
  method GetTask (line 444) | func (c Config) GetTask(name string) (*Task, error) {
  method GetCommand (line 454) | func (c Config) GetCommand(taskName string) (*Command, error) {
  function ParseCmd (line 486) | func ParseCmd(
  function ParseSingleTask (line 527) | func ParseSingleTask(
  function ParseManyTasks (line 554) | func ParseManyTasks(

FILE: core/dao/task_test.go
  function TestTask_ParseTask (line 11) | func TestTask_ParseTask(t *testing.T) {
  function TestTask_GetTaskProjects (line 97) | func TestTask_GetTaskProjects(t *testing.T) {
  function TestTask_CmdParse (line 194) | func TestTask_CmdParse(t *testing.T) {
  function TestConfig_FilterProjects (line 266) | func TestConfig_FilterProjects(t *testing.T) {

FILE: core/dao/theme.go
  type ColorOptions (line 14) | type ColorOptions struct
  type Theme (line 22) | type Theme struct
    method GetContext (line 44) | func (t *Theme) GetContext() string {
    method GetContextLine (line 48) | func (t *Theme) GetContextLine() int {
  type Row (line 35) | type Row struct
    method GetValue (line 52) | func (r Row) GetValue(_ string, i int) string {
  type TableOutput (line 39) | type TableOutput struct
  method ParseThemes (line 61) | func (c *Config) ParseThemes() ([]Theme, []ResourceErrors[Theme]) {
  method GetTheme (line 115) | func (c Config) GetTheme(name string) (*Theme, error) {
  method GetThemeNames (line 125) | func (c Config) GetThemeNames() []string {
  function MergeThemeOptions (line 136) | func MergeThemeOptions(userOption *ColorOptions, defaultOption *ColorOpt...
  function StyleFg (line 182) | func StyleFg(colr string) color.RGBColor {
  function StyleFormat (line 192) | func StyleFormat(text string, format string) string {
  function StyleString (line 207) | func StyleString(text string, opts ColorOptions, useColors bool) string {
  function convertToHex (line 247) | func convertToHex(s *string) *string {
  function convertToAttr (line 262) | func convertToAttr(attr *string) *string {
  function convertToAlign (line 280) | func convertToAlign(align *string) *string {
  function convertToFormat (line 298) | func convertToFormat(format *string) *string {

FILE: core/dao/theme_block.go
  type Block (line 39) | type Block struct
  function LoadBlockTheme (line 47) | func LoadBlockTheme(block *Block) {

FILE: core/dao/theme_stream.go
  type Stream (line 3) | type Stream struct
  function LoadStreamTheme (line 19) | func LoadStreamTheme(stream *Stream) {

FILE: core/dao/theme_table.go
  type Border (line 36) | type Border struct
  type Table (line 43) | type Table struct
  function LoadTableTheme (line 54) | func LoadTableTheme(mTable *Table) {

FILE: core/dao/theme_tree.go
  type Tree (line 7) | type Tree struct
  function LoadTreeTheme (line 15) | func LoadTreeTheme(tree *Tree) {

FILE: core/dao/theme_tui.go
  type TUI (line 139) | type TUI struct
  function LoadTUITheme (line 169) | func LoadTUITheme(tui *TUI) {

FILE: core/dao/utils_test.go
  function getProjectNames (line 10) | func getProjectNames(projects []Project) []string {
  function getTreePaths (line 19) | func getTreePaths(nodes []TreeNode) []string {
  function equalStringSlices (line 28) | func equalStringSlices(a, b []string) bool {

FILE: core/errors.go
  type ConfigEnvFailed (line 11) | type ConfigEnvFailed struct
    method Error (line 16) | func (c *ConfigEnvFailed) Error() string {
  type AlreadyManiDirectory (line 20) | type AlreadyManiDirectory struct
    method Error (line 24) | func (c *AlreadyManiDirectory) Error() string {
  type ZeroNotAllowed (line 28) | type ZeroNotAllowed struct
    method Error (line 32) | func (c *ZeroNotAllowed) Error() string {
  type FailedToOpenFile (line 36) | type FailedToOpenFile struct
    method Error (line 40) | func (f *FailedToOpenFile) Error() string {
  type FailedToParsePath (line 44) | type FailedToParsePath struct
    method Error (line 48) | func (f *FailedToParsePath) Error() string {
  type PathDoesNotExist (line 52) | type PathDoesNotExist struct
    method Error (line 56) | func (p *PathDoesNotExist) Error() string {
  type TagNotFound (line 60) | type TagNotFound struct
    method Error (line 64) | func (c *TagNotFound) Error() string {
  type DirNotFound (line 69) | type DirNotFound struct
    method Error (line 73) | func (c *DirNotFound) Error() string {
  type NoTargets (line 78) | type NoTargets struct
    method Error (line 80) | func (c *NoTargets) Error() string {
  type ProjectNotFound (line 84) | type ProjectNotFound struct
    method Error (line 88) | func (c *ProjectNotFound) Error() string {
  type TaskNotFound (line 93) | type TaskNotFound struct
    method Error (line 97) | func (c *TaskNotFound) Error() string {
  type ThemeNotFound (line 102) | type ThemeNotFound struct
    method Error (line 106) | func (c *ThemeNotFound) Error() string {
  type SpecNotFound (line 110) | type SpecNotFound struct
    method Error (line 114) | func (c *SpecNotFound) Error() string {
  type SpecOutputError (line 118) | type SpecOutputError struct
    method Error (line 123) | func (c *SpecOutputError) Error() string {
  type TargetNotFound (line 127) | type TargetNotFound struct
    method Error (line 131) | func (c *TargetNotFound) Error() string {
  type TargetTagsExprError (line 135) | type TargetTagsExprError struct
    method Error (line 140) | func (c *TargetTagsExprError) Error() string {
  type TagExprInvalid (line 144) | type TagExprInvalid struct
    method Error (line 148) | func (c *TagExprInvalid) Error() string {
  type ConfigNotFound (line 152) | type ConfigNotFound struct
    method Error (line 156) | func (f *ConfigNotFound) Error() string {
  type WorktreePathRequired (line 160) | type WorktreePathRequired struct
    method Error (line 162) | func (c *WorktreePathRequired) Error() string {
  type FailedToCreateWorktree (line 166) | type FailedToCreateWorktree struct
    method Error (line 172) | func (c *FailedToCreateWorktree) Error() string {
  type FailedToRemoveWorktree (line 176) | type FailedToRemoveWorktree struct
    method Error (line 182) | func (c *FailedToRemoveWorktree) Error() string {
  type ConfigErr (line 186) | type ConfigErr struct
    method Error (line 190) | func (f *ConfigErr) Error() string {
  function CheckIfError (line 194) | func CheckIfError(err error) {
  function Exit (line 208) | func Exit(err error) {

FILE: core/exec/client.go
  type Client (line 11) | type Client struct
    method Run (line 22) | func (c *Client) Run(shell string, env []string, cmdStr []string) error {
    method Wait (line 54) | func (c *Client) Wait() error {
    method Close (line 65) | func (c *Client) Close() error {
    method Stderr (line 69) | func (c *Client) Stderr() io.Reader {
    method Stdout (line 73) | func (c *Client) Stdout() io.Reader {
    method Prefix (line 77) | func (c *Client) Prefix() string {

FILE: core/exec/clone.go
  function getRemotes (line 16) | func getRemotes(project dao.Project) (map[string]string, error) {
  function addRemote (line 43) | func addRemote(project dao.Project, remote dao.Remote) error {
  function removeRemote (line 55) | func removeRemote(project dao.Project, name string) error {
  function updateRemote (line 65) | func updateRemote(project dao.Project, remote dao.Remote) error {
  function syncRemotes (line 77) | func syncRemotes(project dao.Project) error {
  function CreateWorktree (line 134) | func CreateWorktree(parentPath string, worktreePath string, branch strin...
  function GetWorktrees (line 150) | func GetWorktrees(parentPath string) (map[string]string, error) {
  function RemoveWorktree (line 155) | func RemoveWorktree(parentPath string, worktreePath string) error {
  function SyncWorktrees (line 166) | func SyncWorktrees(config *dao.Config, project dao.Project, removeOrphan...
  function CloneRepos (line 227) | func CloneRepos(config *dao.Config, projects []dao.Project, syncFlags co...
  function UpdateGitignoreIfExists (line 360) | func UpdateGitignoreIfExists(config *dao.Config) error {
  method SetCloneClients (line 429) | func (exec *Exec) SetCloneClients(clientCh chan Client) error {
  function PrintProjectStatus (line 453) | func PrintProjectStatus(config *dao.Config, projects []dao.Project) error {
  function PrintProjectInit (line 497) | func PrintProjectInit(projects []dao.Project) {

FILE: core/exec/exec.go
  type Exec (line 16) | type Exec struct
    method Run (line 38) | func (exec *Exec) Run(
    method RunTUI (line 87) | func (exec *Exec) RunTUI(
    method SetClients (line 131) | func (exec *Exec) SetClients(
    method ParseTask (line 187) | func (exec *Exec) ParseTask(userArgs []string, runFlags *core.RunFlags...
    method CheckTaskNoColor (line 270) | func (exec *Exec) CheckTaskNoColor() {
  type TableCmd (line 23) | type TableCmd struct

FILE: core/exec/table.go
  method Table (line 18) | func (exec *Exec) Table(runFlags *core.RunFlags) dao.TableOutput {
  method TableWork (line 110) | func (exec *Exec) TableWork(rIndex int, dryRun bool, data dao.TableOutpu...
  function RunTableCmd (line 164) | func RunTableCmd(t TableCmd, data dao.TableOutput, dataMutex *sync.RWMut...
  function initSpinner (line 216) | func initSpinner() (*yacspin.Spinner, error) {

FILE: core/exec/text.go
  method Text (line 18) | func (exec *Exec) Text(
  method TextWork (line 51) | func (exec *Exec) TextWork(
  function RunTextCmd (line 125) | func RunTextCmd(
  function printHeader (line 198) | func printHeader(stdout io.Writer, i int, numTasks int, name string, des...
  function getPrefixer (line 241) | func getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Strea...
  function calcMaxPrefixLength (line 269) | func calcMaxPrefixLength(clients []Client) int {
  function printCmd (line 281) | func printCmd(prefix string, cmd string) {

FILE: core/exec/unix.go
  function ExecTTY (line 13) | func ExecTTY(cmd string, envs []string) error {

FILE: core/exec/windows.go
  function ExecTTY (line 6) | func ExecTTY(cmd string, envs []string) error {

FILE: core/flags.go
  type TUIFlags (line 5) | type TUIFlags struct
  type ListFlags (line 10) | type ListFlags struct
  type DescribeFlags (line 16) | type DescribeFlags struct
  type SetProjectFlags (line 20) | type SetProjectFlags struct
  type ProjectFlags (line 26) | type ProjectFlags struct
  type TagFlags (line 38) | type TagFlags struct
  type TaskFlags (line 42) | type TaskFlags struct
  type RunFlags (line 47) | type RunFlags struct
  type SetRunFlags (line 73) | type SetRunFlags struct
  type SyncFlags (line 87) | type SyncFlags struct
  type SetSyncFlags (line 97) | type SetSyncFlags struct
  type InitFlags (line 105) | type InitFlags struct

FILE: core/man.go
  function GenManPages (line 13) | func GenManPages(dir string) error {

FILE: core/man_gen.go
  type genManHeaders (line 27) | type genManHeaders struct
  function CreateManPage (line 37) | func CreateManPage(desc string, version string, date string, rootCmd *co...
  function manPreamble (line 73) | func manPreamble(buf io.StringWriter, header *genManHeaders, cmd *cobra....
  function manCommand (line 94) | func manCommand(buf io.StringWriter, cmd *cobra.Command) {
  function manPrintFlags (line 122) | func manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) {
  function genMan (line 154) | func genMan(header *genManHeaders, cmd *cobra.Command, cmds ...*cobra.Co...
  function genDoc (line 195) | func genDoc(cmd *cobra.Command, cmds ...*cobra.Command) ([]byte, error) {

FILE: core/prefixer.go
  type Prefixer (line 13) | type Prefixer struct
    method Read (line 32) | func (r *Prefixer) Read(p []byte) (n int, err error) {
    method WriteTo (line 77) | func (r *Prefixer) WriteTo(w io.Writer) (n int64, err error) {
  function NewPrefixer (line 21) | func NewPrefixer(r io.Reader, prefix string) *Prefixer {

FILE: core/prefixer_benchmark_test.go
  function BenchmarkPrefixer_Read (line 12) | func BenchmarkPrefixer_Read(b *testing.B) {
  function BenchmarkPrefixer_WriteTo (line 48) | func BenchmarkPrefixer_WriteTo(b *testing.B) {
  function BenchmarkPrefixer_PrefixLen (line 75) | func BenchmarkPrefixer_PrefixLen(b *testing.B) {
  function BenchmarkPrefixer_Allocs (line 103) | func BenchmarkPrefixer_Allocs(b *testing.B) {

FILE: core/print/lib.go
  function GetMaxTextWidth (line 9) | func GetMaxTextWidth(text string) int {
  function GetTextDimensions (line 23) | func GetTextDimensions(text string) (int, int) {

FILE: core/print/print_block.go
  function PrintProjectBlocks (line 16) | func PrintProjectBlocks(projects []dao.Project, colorize bool, block dao...
  function PrintTaskBlock (line 79) | func PrintTaskBlock(tasks []dao.Task, colorize bool, block dao.Block, f ...
  type Formatter (line 142) | type Formatter interface
  function printKeyValue (line 146) | func printKeyValue(
  function printCmd (line 173) | func printCmd(cmd string) string {
  function printEnv (line 183) | func printEnv(env []string, block dao.Block) string {
  function trueOrFalse (line 196) | func trueOrFalse(value bool) dao.ColorOptions {
  type TviewFormatter (line 203) | type TviewFormatter struct
    method Format (line 206) | func (t TviewFormatter) Format(
  type GookitFormatter (line 204) | type GookitFormatter struct
    method Format (line 221) | func (g GookitFormatter) Format(

FILE: core/print/print_table.go
  type Items (line 10) | type Items interface
  type PrintTableOptions (line 14) | type PrintTableOptions struct
  function PrintTable (line 24) | func PrintTable[T Items](

FILE: core/print/print_tree.go
  function PrintTree (line 13) | func PrintTree(config *dao.Config, theme dao.Theme, listFlags *core.List...
  function printTreeNodes (line 53) | func printTreeNodes(l list.Writer, tree []dao.TreeNode, depth int) {
  function printTree (line 68) | func printTree(content string) {

FILE: core/print/table.go
  function CreateTable (line 13) | func CreateTable[T Items](
  function FormatTable (line 63) | func FormatTable(theme dao.Theme) table.Style {
  function RenderTable (line 77) | func RenderTable(t table.Writer, output string) {
  function calcColumnWidths (line 88) | func calcColumnWidths[T Items](

FILE: core/sizedwaitgroup.go
  type SizedWaitGroup (line 15) | type SizedWaitGroup struct
    method Add (line 45) | func (s *SizedWaitGroup) Add() {
    method AddWithContext (line 57) | func (s *SizedWaitGroup) AddWithContext(ctx context.Context) error {
    method Done (line 70) | func (s *SizedWaitGroup) Done() {
    method Wait (line 77) | func (s *SizedWaitGroup) Wait() {
  function NewSizedWaitGroup (line 25) | func NewSizedWaitGroup(limit uint32) SizedWaitGroup {

FILE: core/tui/components/tui_button.go
  function CreateButton (line 10) | func CreateButton(label string) *tview.Button {
  function SetActiveButtonStyle (line 17) | func SetActiveButtonStyle(button *tview.Button) {
  function SetInactiveButtonStyle (line 31) | func SetInactiveButtonStyle(button *tview.Button) {

FILE: core/tui/components/tui_checkbox.go
  function Checkbox (line 8) | func Checkbox(label string, checked *bool, onFocus func(), onBlur func()...

FILE: core/tui/components/tui_filter.go
  function CreateFilter (line 9) | func CreateFilter() *tview.InputField {
  function ShowFilter (line 18) | func ShowFilter(filter *tview.InputField, text string) {
  function CloseFilter (line 24) | func CloseFilter(filter *tview.InputField) {
  function InitFilter (line 29) | func InitFilter(filter *tview.InputField, text string) {

FILE: core/tui/components/tui_list.go
  type TList (line 10) | type TList struct
    method Create (line 25) | func (l *TList) Create() {
    method Update (line 177) | func (l *TList) Update(items []string) {
    method SetItemSelect (line 184) | func (l *TList) SetItemSelect(i int, item string) {
    method ClearFilter (line 194) | func (l *TList) ClearFilter() {
    method applyFilter (line 199) | func (l *TList) applyFilter() {
    method getItemText (line 203) | func (l *TList) getItemText(item string) string {

FILE: core/tui/components/tui_modal.go
  function OpenModal (line 15) | func OpenModal(pageTitle string, title string, contentPane *tview.Flex, ...
  function OpenTextModal (line 59) | func OpenTextModal(pageTitle string, textColor string, textNoColor strin...
  function CloseModal (line 103) | func CloseModal() {
  function IsModalOpen (line 113) | func IsModalOpen() bool {

FILE: core/tui/components/tui_output.go
  function CreateOutputView (line 8) | func CreateOutputView(title string) (*tview.TextView, *misc.ThreadSafeWr...

FILE: core/tui/components/tui_search.go
  function CreateSearch (line 11) | func CreateSearch() *tview.InputField {
  function ShowSearch (line 19) | func ShowSearch() {
  function EmptySearch (line 25) | func EmptySearch() {
  function SearchInTable (line 30) | func SearchInTable(table *tview.Table, query string, lastFoundRow, lastF...
  function SearchInTree (line 66) | func SearchInTree(tree *TTree, query string, lastFoundIndex *int, direct...
  function SearchInList (line 99) | func SearchInList(list *tview.List, query string, lastFoundIndex *int, d...

FILE: core/tui/components/tui_table.go
  type TTable (line 13) | type TTable struct
    method Create (line 32) | func (t *TTable) Create() {
    method CreateTableHeader (line 192) | func (t *TTable) CreateTableHeader(header string) *tview.TableCell {
    method Update (line 201) | func (t *TTable) Update(headers []string, rows [][]string) {
    method UpdateRowStyle (line 224) | func (t *TTable) UpdateRowStyle() {
    method ToggleSelectCurrentRow (line 230) | func (t *TTable) ToggleSelectCurrentRow(name string) {
    method SetRowSelect (line 242) | func (t *TTable) SetRowSelect(row int) {
    method ClearFilter (line 284) | func (t *TTable) ClearFilter() {
    method applyFilter (line 289) | func (t *TTable) applyFilter() {

FILE: core/tui/components/tui_text.go
  function CreateText (line 9) | func CreateText(title string) *tview.TextView {

FILE: core/tui/components/tui_textarea.go
  function CreateTextArea (line 8) | func CreateTextArea(title string) *tview.TextArea {

FILE: core/tui/components/tui_toggle_text.go
  type TToggleText (line 9) | type TToggleText struct
    method Create (line 20) | func (t *TToggleText) Create() {

FILE: core/tui/components/tui_tree.go
  type TTree (line 10) | type TTree struct
    method Create (line 40) | func (t *TTree) Create() {
    method UpdateProjects (line 241) | func (t *TTree) UpdateProjects(paths []dao.TNode) {
    method UpdateProjectsStyle (line 255) | func (t *TTree) UpdateProjectsStyle() {
    method BuildProjectTree (line 261) | func (t *TTree) BuildProjectTree(node *tview.TreeNode, tnode dao.TreeN...
    method UpdateTasks (line 300) | func (t *TTree) UpdateTasks(nodes []TNode) {
    method UpdateTasksStyle (line 340) | func (t *TTree) UpdateTasksStyle() {
    method ToggleSelectCurrentNode (line 365) | func (t *TTree) ToggleSelectCurrentNode(id string) {
    method setNodeSelect (line 375) | func (t *TTree) setNodeSelect(node *TNode) {
    method FocusFirst (line 411) | func (t *TTree) FocusFirst() {
    method FocusLast (line 415) | func (t *TTree) FocusLast() {
    method ClearFilter (line 428) | func (t *TTree) ClearFilter() {
    method applyFilter (line 433) | func (t *TTree) applyFilter() {
    method getVisibleNodes (line 437) | func (t *TTree) getVisibleNodes() []*tview.TreeNode {
    method findNodeIndex (line 458) | func (t *TTree) findNodeIndex(nodes []*tview.TreeNode, target *tview.T...
  type TNode (line 31) | type TNode struct

FILE: core/tui/misc/tui_event.go
  type Event (line 7) | type Event struct
  type EventListener (line 12) | type EventListener
  type EventEmitter (line 14) | type EventEmitter struct
    method Subscribe (line 25) | func (ee *EventEmitter) Subscribe(eventName string, listener EventList...
    method Publish (line 31) | func (ee *EventEmitter) Publish(event Event) {
    method PublishAndWait (line 41) | func (ee *EventEmitter) PublishAndWait(event Event) {
  function NewEventEmitter (line 19) | func NewEventEmitter() *EventEmitter {

FILE: core/tui/misc/tui_focus.go
  type TItem (line 8) | type TItem struct
  function FocusNext (line 13) | func FocusNext(elements []*TItem) *tview.Primitive {
  function FocusPrevious (line 51) | func FocusPrevious(elements []*TItem) *tview.Primitive {
  function FocusPage (line 89) | func FocusPage(event *tcell.EventKey, focusable []*TItem) {
  function FocusPreviousPage (line 96) | func FocusPreviousPage() {
  function GetTUIItem (line 100) | func GetTUIItem(primitive tview.Primitive, box *tview.Box) *TItem {

FILE: core/tui/misc/tui_theme.go
  type StyleOption (line 47) | type StyleOption struct
  function LoadStyles (line 62) | func LoadStyles(tui *dao.TUI) {
  function initStyle (line 99) | func initStyle(opts *dao.ColorOptions) StyleOption {
  function Colorize (line 121) | func Colorize(value string, opts dao.ColorOptions) string {
  function ColorizeTitle (line 125) | func ColorizeTitle(value string, opts dao.ColorOptions) string {
  function getAttr (line 129) | func getAttr(attrStr string) tcell.AttrMask {
  function getAlign (line 147) | func getAlign(alignStr *string) int {
  function PadString (line 169) | func PadString(name string) string {

FILE: core/tui/misc/tui_utils.go
  function SetActive (line 10) | func SetActive(box *tview.Box, title string, active bool) {
  function GetTexztModalSize (line 30) | func GetTexztModalSize(text string) (int, int) {

FILE: core/tui/misc/tui_writer.go
  type ThreadSafeWriter (line 11) | type ThreadSafeWriter struct
    method Write (line 24) | func (w *ThreadSafeWriter) Write(p []byte) (n int, err error) {
  function NewThreadSafeWriter (line 17) | func NewThreadSafeWriter(view *tview.TextView) *ThreadSafeWriter {

FILE: core/tui/pages.go
  function createPages (line 12) | func createPages(
  function createNav (line 45) | func createNav() *tview.Flex {
  function SwitchToPage (line 100) | func SwitchToPage(pageName string) {
  function setupStyles (line 138) | func setupStyles() {

FILE: core/tui/pages/tui_exec.go
  type TExecPage (line 16) | type TExecPage struct
    method createSelectPage (line 134) | func (e *TExecPage) createSelectPage(
    method createOutputPage (line 180) | func (e *TExecPage) createOutputPage(
    method updateSelectFocusable (line 194) | func (e *TExecPage) updateSelectFocusable(
    method updateStreamFocusable (line 238) | func (e *TExecPage) updateStreamFocusable(
    method switchView (line 249) | func (e *TExecPage) switchView(
    method switchBeforeRun (line 268) | func (e *TExecPage) switchBeforeRun(
    method runCmd (line 283) | func (e *TExecPage) runCmd(
  function CreateExecPage (line 20) | func CreateExecPage(

FILE: core/tui/pages/tui_project.go
  type TProjectPage (line 12) | type TProjectPage struct
    method createProjectPage (line 96) | func (p *TProjectPage) createProjectPage(projectData *views.TProject) ...
    method updateProjectFocusable (line 132) | func (p *TProjectPage) updateProjectFocusable(
  function CreateProjectsPage (line 16) | func CreateProjectsPage(

FILE: core/tui/pages/tui_run.go
  type TRunPage (line 15) | type TRunPage struct
    method createSelectPage (line 133) | func (r *TRunPage) createSelectPage(
    method createOutputPage (line 212) | func (r *TRunPage) createOutputPage(
    method updateRunFocusable (line 224) | func (r *TRunPage) updateRunFocusable(
    method updateStreamFocusable (line 279) | func (r *TRunPage) updateStreamFocusable(streamView *tview.TextView) [...
    method switchView (line 286) | func (r *TRunPage) switchView(
    method switchBeforeRun (line 305) | func (r *TRunPage) switchBeforeRun(
    method runTasks (line 319) | func (r *TRunPage) runTasks(
  function CreateRunPage (line 19) | func CreateRunPage(

FILE: core/tui/pages/tui_task.go
  type TTaskPage (line 12) | type TTaskPage struct
    method createTaskPage (line 77) | func (taskPage *TTaskPage) createTaskPage(taskData *views.TTask) *tvie...
    method updateTaskFocusable (line 112) | func (taskPage *TTaskPage) updateTaskFocusable(
  function CreateTasksPage (line 16) | func CreateTasksPage(tasks []dao.Task) *tview.Flex {

FILE: core/tui/tui.go
  function RunTui (line 12) | func RunTui(config *dao.Config, themeName string, reload bool) {
  type App (line 24) | type App struct
    method Run (line 37) | func (app *App) Run() error {
    method Reload (line 41) | func (app *App) Reload() {
    method setupApp (line 52) | func (app *App) setupApp(config *dao.Config, themeName string) {
  function NewApp (line 28) | func NewApp(config *dao.Config, themeName string) *App {

FILE: core/tui/tui_input.go
  function HandleInput (line 12) | func HandleInput(app *App) {
  function handleSearchInput (line 151) | func handleSearchInput(_ *tcell.EventKey, searchDirection int, lastFound...

FILE: core/tui/views/tui_help.go
  function ShowHelpModal (line 13) | func ShowHelpModal() {
  function shortcutRow (line 19) | func shortcutRow(shortcut string, description string) (*tview.TableCell,...
  function titleRow (line 40) | func titleRow(title string) (*tview.TableCell, *tview.TableCell) {
  function createShortcutsTable (line 55) | func createShortcutsTable() (*tview.Flex, *tview.Table) {

FILE: core/tui/views/tui_project_view.go
  type TProject (line 15) | type TProject struct
    method CreateProjectsTable (line 158) | func (p *TProject) CreateProjectsTable(
    method CreateProjectsTree (line 202) | func (p *TProject) CreateProjectsTree(
    method CreateProjectsTagsList (line 245) | func (p *TProject) CreateProjectsTagsList(title string) *components.TL...
    method CreateProjectsPathsList (line 277) | func (p *TProject) CreateProjectsPathsList(title string) *components.T...
    method getTableRows (line 309) | func (p *TProject) getTableRows() [][]string {
    method getTreeHierarchy (line 320) | func (p *TProject) getTreeHierarchy() []dao.TNode {
    method toggleSelectProject (line 330) | func (p *TProject) toggleSelectProject(name string) {
    method filterProjects (line 336) | func (p *TProject) filterProjects() {
    method filterTags (line 379) | func (p *TProject) filterTags() {
    method filterPaths (line 390) | func (p *TProject) filterPaths() {
    method selectAllProjects (line 401) | func (p *TProject) selectAllProjects() {
    method selectAllTags (line 409) | func (p *TProject) selectAllTags() {
    method selectAllPaths (line 416) | func (p *TProject) selectAllPaths() {
    method unselectAllProjects (line 423) | func (p *TProject) unselectAllProjects() {
    method unselectAllTags (line 431) | func (p *TProject) unselectAllTags() {
    method unselectAllPaths (line 438) | func (p *TProject) unselectAllPaths() {
    method showProjectDescModal (line 445) | func (p *TProject) showProjectDescModal(name string) {
    method editProject (line 455) | func (p *TProject) editProject(projectName string) {
  function CreateProjectsData (line 49) | func CreateProjectsData(

FILE: core/tui/views/tui_shortcut_info.go
  type Shortcut (line 11) | type Shortcut struct
  function getShortcutInfo (line 16) | func getShortcutInfo(shortcuts []Shortcut) string {
  function CreateRunInfoVIew (line 28) | func CreateRunInfoVIew() *tview.TextView {
  function CreateExecInfoView (line 45) | func CreateExecInfoView() *tview.TextView {
  function CreateProjectInfoView (line 62) | func CreateProjectInfoView() *tview.TextView {
  function CreateTaskInfoView (line 76) | func CreateTaskInfoView() *tview.TextView {

FILE: core/tui/views/tui_spec_view.go
  type TSpec (line 12) | type TSpec struct
    method AddCheckbox (line 138) | func (spec *TSpec) AddCheckbox(title string, checked *bool) *tview.Che...
  function CreateSpecView (line 26) | func CreateSpecView() *TSpec {

FILE: core/tui/views/tui_task_view.go
  type TTask (line 14) | type TTask struct
    method CreateTasksTable (line 88) | func (t *TTask) CreateTasksTable(
    method CreateTasksTree (line 133) | func (t *TTask) CreateTasksTree(
    method getTableRows (line 177) | func (t *TTask) getTableRows() [][]string {
    method getTreeHierarchy (line 188) | func (t *TTask) getTreeHierarchy() []components.TNode {
    method toggleSelectTask (line 228) | func (t *TTask) toggleSelectTask(name string) {
    method filterTasks (line 234) | func (t *TTask) filterTasks() {
    method selectAllTasks (line 256) | func (t *TTask) selectAllTasks() {
    method unselectAllTasks (line 264) | func (t *TTask) unselectAllTasks() {
    method showTaskDescModal (line 272) | func (t *TTask) showTaskDescModal(name string) {
    method editTask (line 283) | func (t *TTask) editTask(taskName string) {
  function CreateTasksData (line 34) | func CreateTasksData(

FILE: core/tui/watcher.go
  function WatchFiles (line 11) | func WatchFiles(app *App, files ...string) {

FILE: core/utils.go
  constant ANSI (line 16) | ANSI = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)...
  function Strip (line 20) | func Strip(str string) string {
  function Intersection (line 24) | func Intersection(a []string, b []string) []string {
  function GetWdRemoteURL (line 35) | func GetWdRemoteURL(path string) (string, error) {
  function GetRemoteURL (line 44) | func GetRemoteURL(path string) (string, error) {
  function GetWorktreeList (line 57) | func GetWorktreeList(repoPath string) (map[string]string, error) {
  function FindFileInParentDirs (line 85) | func FindFileInParentDirs(path string, files []string) (string, error) {
  function GetRelativePath (line 101) | func GetRelativePath(configDir string, path string) (string, error) {
  function GetAbsolutePath (line 116) | func GetAbsolutePath(configDir string, path string, name string) (string...
  function ResolveTildePath (line 151) | func ResolveTildePath(path string) (string, error) {
  function FormatShell (line 173) | func FormatShell(shell string) string {
  function FormatShellString (line 198) | func FormatShellString(shell string, command string) (string, []string) {
  function Ptr (line 205) | func Ptr[T any](t T) *T {
  function StringsToErrors (line 209) | func StringsToErrors(str []string) []error {
  function DebugPrint (line 218) | func DebugPrint(data any) {

FILE: main.go
  function main (line 7) | func main() {

FILE: test/integration/describe_test.go
  function TestDescribe (line 8) | func TestDescribe(t *testing.T) {

FILE: test/integration/exec_test.go
  function TestExec (line 8) | func TestExec(t *testing.T) {

FILE: test/integration/init_test.go
  function TestInit (line 8) | func TestInit(t *testing.T) {

FILE: test/integration/list_test.go
  function TestList (line 8) | func TestList(t *testing.T) {

FILE: test/integration/main_test.go
  type TemplateTest (line 35) | type TemplateTest struct
    method GoldenOutput (line 45) | func (tt TemplateTest) GoldenOutput(output []byte) []byte {
  type TestFile (line 57) | type TestFile struct
    method Dir (line 67) | func (tf *TestFile) Dir() string {
    method path (line 77) | func (tf *TestFile) path() string {
    method Write (line 87) | func (tf *TestFile) Write(content string) {
  function NewGoldenFile (line 63) | func NewGoldenFile(t *testing.T, name string) *TestFile {
  function clearGolden (line 100) | func clearGolden(goldenDir string) {
  function clearTmp (line 107) | func clearTmp() {
  function diff (line 115) | func diff(expected, actual any) []string {
  function TestMain (line 122) | func TestMain(m *testing.M) {
  function printDirectoryContent (line 139) | func printDirectoryContent(dir string) {
  function countFilesAndFolders (line 158) | func countFilesAndFolders(dir string) int {
  function Run (line 182) | func Run(t *testing.T, tt TemplateTest) {

FILE: test/integration/run_test.go
  function TestRun (line 8) | func TestRun(t *testing.T) {

FILE: test/integration/sync_test.go
  function TestSync (line 8) | func TestSync(t *testing.T) {

FILE: test/integration/version_test.go
  function TestVersion (line 8) | func TestVersion(t *testing.T) {
Condensed preview — 330 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (734K chars).
[
  {
    "path": ".dockerignore",
    "chars": 5,
    "preview": ".git\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 84,
    "preview": "custom: [\"https://paypal.me/samiralajmovic\", \"https://www.buymeacoffee.com/alajmo\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/BUG_REPORT.md",
    "chars": 495,
    "preview": "---\nname: Bug report\nabout: Report a bug\ntitle: ''\nlabels: 'bug'\nassignees: ''\n---\n\n- [ ] I have the latest version of m"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.md",
    "chars": 258,
    "preview": "---\r\nname: Feature request\r\nabout: Suggest an feature for this project\r\ntitle: ''\r\nlabels: 'enhancement'\r\nassignees: ''\r"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 439,
    "preview": "### What's Changed\r\n\r\nA description of the issue/feature; reference the issue number (if one exists). The Pull Request s"
  },
  {
    "path": ".github/agents/default.agent.md",
    "chars": 4362,
    "preview": "---\nname: default\ndescription: add features and fix bugs\n---\n\n# Copilot Instructions for mani\n\nThis document provides gu"
  },
  {
    "path": ".github/copilot-instructions.md",
    "chars": 4300,
    "preview": "# Copilot Instructions for mani\n\nThis document provides guidance for GitHub Copilot when working on the `mani` repositor"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 674,
    "preview": "name: release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name:"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 669,
    "preview": "name: test\n\non:\n  pull_request:\n    branches: [main]\n    paths-ignore:\n      - \"**.md\"\njobs:\n  test:\n    name: test\n    "
  },
  {
    "path": ".gitignore",
    "chars": 368,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
  },
  {
    "path": ".golangci.yaml",
    "chars": 145,
    "preview": "version: \"2\"\n\nlinters:\n  settings:\n    errcheck:\n      exclude-functions:\n        - fmt.Fprintf\n        - fmt.Fprint\n   "
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 1541,
    "preview": "version: 2\nproject_name: mani\n\nbefore:\n  hooks:\n    - go mod download\n\nbuilds:\n  - binary: mani\n    id: mani\n    ldflags"
  },
  {
    "path": "CLAUDE.md",
    "chars": 2417,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "LICENSE",
    "chars": 1087,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2020-2021 Samir Alajmovic\n\nPermission is hereby granted, free of charge, to any per"
  },
  {
    "path": "Makefile",
    "chars": 1918,
    "preview": "NAME    := mani\nPACKAGE := github.com/alajmo/$(NAME)\nDATE    := $(shell date +\"%Y %B %d\")\nGIT     := $(shell [ -d .git ]"
  },
  {
    "path": "README.md",
    "chars": 4354,
    "preview": "<h1 align=\"center\"><code>mani</code></h1>\n\n<div align=\"center\">\n  <a href=\"https://github.com/alajmo/mani/releases\">\n   "
  },
  {
    "path": "benchmarks/.gitignore",
    "chars": 128,
    "preview": "# Ignore benchmark output files (they can be large and change frequently)\n# Use `make bench-save` to generate new result"
  },
  {
    "path": "cmd/check.go",
    "chars": 546,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc checkCmd(configErr *erro"
  },
  {
    "path": "cmd/completion.go",
    "chars": 2025,
    "preview": "package cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc completionCmd() *cobra.Co"
  },
  {
    "path": "cmd/describe.go",
    "chars": 1108,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfu"
  },
  {
    "path": "cmd/describe_projects.go",
    "chars": 4545,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/da"
  },
  {
    "path": "cmd/describe_tasks.go",
    "chars": 1777,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/da"
  },
  {
    "path": "cmd/edit.go",
    "chars": 819,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfu"
  },
  {
    "path": "cmd/edit_project.go",
    "chars": 1296,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfu"
  },
  {
    "path": "cmd/edit_task.go",
    "chars": 1246,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfu"
  },
  {
    "path": "cmd/exec.go",
    "chars": 6519,
    "preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/cor"
  },
  {
    "path": "cmd/gen.go",
    "chars": 718,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc genCmd() *cobra.Command {\n\tdir :"
  },
  {
    "path": "cmd/gen_docs.go",
    "chars": 916,
    "preview": "// This source will generate\n//   - core/mani.1\n//   - docs/commands.md\n//\n// and is not included in the final build.\n\np"
  },
  {
    "path": "cmd/init.go",
    "chars": 1228,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"git"
  },
  {
    "path": "cmd/list.go",
    "chars": 1621,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfu"
  },
  {
    "path": "cmd/list_projects.go",
    "chars": 5307,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/c"
  },
  {
    "path": "cmd/list_tags.go",
    "chars": 2714,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/c"
  },
  {
    "path": "cmd/list_tasks.go",
    "chars": 2349,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/c"
  },
  {
    "path": "cmd/root.go",
    "chars": 2055,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nconst (\n"
  },
  {
    "path": "cmd/run.go",
    "chars": 7950,
    "preview": "package cmd\n\nimport (\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/cor"
  },
  {
    "path": "cmd/sync.go",
    "chars": 5167,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"git"
  },
  {
    "path": "cmd/tui.go",
    "chars": 1253,
    "preview": "package cmd\n\nimport (\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tu"
  },
  {
    "path": "core/config.man",
    "chars": 14122,
    "preview": ".SH CONFIG\n\nThe mani.yaml config is based on the following concepts:\n\n.RS 2\n.IP \"\\(bu\" 2\n\\fBprojects\\fR are directories,"
  },
  {
    "path": "core/dao/benchmark_test.go",
    "chars": 8455,
    "preview": "package dao\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\n// Helper to create a config with N projects, M tasks, and default specs/them"
  },
  {
    "path": "core/dao/common.go",
    "chars": 2594,
    "preview": "package dao\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gookit/color\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"gi"
  },
  {
    "path": "core/dao/common_test.go",
    "chars": 2432,
    "preview": "package dao\n\nimport (\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestEnv_ParseNodeEnv(t *testing.T) {\n\ttests := []struct {\n"
  },
  {
    "path": "core/dao/config.go",
    "chars": 14161,
    "preview": "package dao\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/alajmo/mani/cor"
  },
  {
    "path": "core/dao/config_test.go",
    "chars": 778,
    "preview": "package dao\n\nimport (\n\t\"testing\"\n)\n\nfunc TestConfig_DuplicateProjectName(t *testing.T) {\n\toriginalProjects := []Project{"
  },
  {
    "path": "core/dao/import.go",
    "chars": 6885,
    "preview": "package dao\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/gookit/color\"\n\t\"gopkg.i"
  },
  {
    "path": "core/dao/project.go",
    "chars": 21725,
    "preview": "package dao\n\nimport (\n\t\"bufio\"\n\t\"container/list\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"gopkg.i"
  },
  {
    "path": "core/dao/project_test.go",
    "chars": 12409,
    "preview": "package dao\n\nimport (\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc TestProject_GetValue(t *te"
  },
  {
    "path": "core/dao/spec.go",
    "chars": 2156,
    "preview": "package dao\n\nimport (\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\ntype Spec struct {\n\tName              strin"
  },
  {
    "path": "core/dao/spec_test.go",
    "chars": 3498,
    "preview": "package dao\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc TestSpec_GetContext(t *testing.T) {\n"
  },
  {
    "path": "core/dao/tag.go",
    "chars": 936,
    "preview": "package dao\n\nimport (\n\t\"slices\"\n\t\"strings\"\n)\n\ntype Tag struct {\n\tName     string\n\tProjects []string\n}\n\nfunc (t Tag) GetV"
  },
  {
    "path": "core/dao/tag_expr.go",
    "chars": 7265,
    "preview": "// Package dao for evaluating boolean tag expressions against project tags.\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"str"
  },
  {
    "path": "core/dao/tag_expr_test.go",
    "chars": 3127,
    "preview": "package dao\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTagExpression(t *testing.T) {\n\tprojects := []Project{\n\t\t{\n\t\t\tNam"
  },
  {
    "path": "core/dao/target.go",
    "chars": 2045,
    "preview": "package dao\n\nimport (\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\ntype Target struct {\n\tName     string   `ya"
  },
  {
    "path": "core/dao/target_test.go",
    "chars": 3991,
    "preview": "package dao\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc TestTarget_GetContext(t *testing.T) "
  },
  {
    "path": "core/dao/task.go",
    "chars": 13748,
    "preview": "package dao\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/theckman/yacsp"
  },
  {
    "path": "core/dao/task_test.go",
    "chars": 10144,
    "preview": "package dao\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc TestTask_ParseTask(t *testin"
  },
  {
    "path": "core/dao/theme.go",
    "chars": 6080,
    "preview": "package dao\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github"
  },
  {
    "path": "core/dao/theme_block.go",
    "chars": 1672,
    "preview": "package dao\n\nimport (\n\t\"github.com/alajmo/mani/core\"\n)\n\nvar DefaultBlock = Block{\n\tKey: &ColorOptions{\n\t\tFg:     core.Pt"
  },
  {
    "path": "core/dao/theme_stream.go",
    "chars": 706,
    "preview": "package dao\n\ntype Stream struct {\n\tPrefix       bool     `yaml:\"prefix\"`\n\tPrefixColors []string `yaml:\"prefix_colors\"`\n\t"
  },
  {
    "path": "core/dao/theme_table.go",
    "chars": 2166,
    "preview": "package dao\n\nimport (\n\t\"strings\"\n\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nvar Defaul"
  },
  {
    "path": "core/dao/theme_tree.go",
    "chars": 672,
    "preview": "package dao\n\nimport (\n\t\"strings\"\n)\n\ntype Tree struct {\n\tStyle string `yaml:\"style\"`\n}\n\nvar DefaultTree = Tree{\n\tStyle: \""
  },
  {
    "path": "core/dao/theme_tui.go",
    "chars": 5241,
    "preview": "package dao\n\nimport (\n\t\"github.com/alajmo/mani/core\"\n)\n\n// DefaultTUI  Not all attributes are used, but no clean way to "
  },
  {
    "path": "core/dao/utils_test.go",
    "chars": 697,
    "preview": "package dao\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n)\n\n// Helper functions\n\nfunc getProjectNames(projects []Project) []string {\n\tna"
  },
  {
    "path": "core/errors.go",
    "chars": 4371,
    "preview": "package core\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gookit/color\"\n)\n\ntype ConfigEnvFailed struct {\n\tName string"
  },
  {
    "path": "core/exec/client.go",
    "chars": 1127,
    "preview": "package exec\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n)\n\n// Client is a wrapper over the SSH connection/sessions.\ntype Cl"
  },
  {
    "path": "core/exec/clone.go",
    "chars": 12941,
    "preview": "package exec\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/"
  },
  {
    "path": "core/exec/exec.go",
    "chars": 6730,
    "preview": "package exec\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gookit/color\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"githu"
  },
  {
    "path": "core/exec/table.go",
    "chars": 4966,
    "preview": "package exec\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/theckman/yacspin\"\n\n\t\"git"
  },
  {
    "path": "core/exec/text.go",
    "chars": 6121,
    "preview": "package exec\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"golang.org/x/term\"\n\n\t\"github.com/gookit/color\"\n\n\t\"git"
  },
  {
    "path": "core/exec/unix.go",
    "chars": 480,
    "preview": "//go:build !windows\n// +build !windows\n\npackage exec\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc ExecTT"
  },
  {
    "path": "core/exec/windows.go",
    "chars": 114,
    "preview": "//go:build windows\n// +build windows\n\npackage exec\n\nfunc ExecTTY(cmd string, envs []string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "core/flags.go",
    "chars": 1668,
    "preview": "package core\n\n// CMD Flags\n\ntype TUIFlags struct {\n\tTheme  string\n\tReload bool\n}\n\ntype ListFlags struct {\n\tOutput string"
  },
  {
    "path": "core/man.go",
    "chars": 304,
    "preview": "package core\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n//go:embed mani.1\nvar ConfigMan []byte\n\nfunc GenManPa"
  },
  {
    "path": "core/man_gen.go",
    "chars": 5515,
    "preview": "// This source will generate\n//   - core/mani.1\n//   - docs/commands.md\n//\n// and is not included in the final build.\n\np"
  },
  {
    "path": "core/mani.1",
    "chars": 21065,
    "preview": ".TH \"MANI\" \"1\" \"2025 December 05\" \"v0.31.2\" \"Mani Manual\" \"mani\"\n.SH NAME\nmani - repositories manager and task runner\n\n."
  },
  {
    "path": "core/prefixer.go",
    "chars": 2794,
    "preview": "// Source: https://github.com/goware/prefixer\n// Author: goware\npackage core\n\nimport (\n\t\"bufio\"\n\t\"io\"\n)\n\n// Prefixer imp"
  },
  {
    "path": "core/prefixer_benchmark_test.go",
    "chars": 2912,
    "preview": "package core\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// Prefixer_Read: Read() with varying line counts "
  },
  {
    "path": "core/print/lib.go",
    "chars": 700,
    "preview": "package print\n\nimport (\n\t\"bufio\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nfunc GetMaxTextWidth(text string) int {\n\tscanner := bufio"
  },
  {
    "path": "core/print/print_block.go",
    "chars": 7567,
    "preview": "package print\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nvar FORMATTER Forma"
  },
  {
    "path": "core/print/print_table.go",
    "chars": 1598,
    "preview": "package print\n\nimport (\n\t\"io\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n)\n\ntype Items"
  },
  {
    "path": "core/print/print_tree.go",
    "chars": 1496,
    "preview": "package print\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/jedib0t/go-pretty/v6/list\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"git"
  },
  {
    "path": "core/print/table.go",
    "chars": 3191,
    "preview": "package print\n\nimport (\n\t\"io\"\n\n\t\"github.com/jedib0t/go-pretty/v6/table\"\n\t\"github.com/jedib0t/go-pretty/v6/text\"\n\t\"golang"
  },
  {
    "path": "core/sizedwaitgroup.go",
    "chars": 2019,
    "preview": "// Source: https://github.com/remeh/sizedwaitgroup/blob/master/sizedwaitgroup.go\n// Author: Rémy Mathieu\n\npackage core\n\n"
  },
  {
    "path": "core/tui/components/tui_button.go",
    "chars": 1322,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/gda"
  },
  {
    "path": "core/tui/components/tui_checkbox.go",
    "chars": 1391,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc Checkbox(label str"
  },
  {
    "path": "core/tui/components/tui_filter.go",
    "chars": 756,
    "preview": "package components\n\nimport (\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\nfunc CreateFilter() *t"
  },
  {
    "path": "core/tui/components/tui_list.go",
    "chars": 4859,
    "preview": "package components\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/tui/"
  },
  {
    "path": "core/tui/components/tui_modal.go",
    "chars": 3232,
    "preview": "package components\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\t\"golang.org/x/term\"\n\n\t"
  },
  {
    "path": "core/tui/components/tui_output.go",
    "chars": 296,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc CreateOutputView(t"
  },
  {
    "path": "core/tui/components/tui_search.go",
    "chars": 2793,
    "preview": "package components\n\nimport (\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\nfunc Creat"
  },
  {
    "path": "core/tui/components/tui_table.go",
    "chars": 7067,
    "preview": "package components\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/ma"
  },
  {
    "path": "core/tui/components/tui_text.go",
    "chars": 1362,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tv"
  },
  {
    "path": "core/tui/components/tui_textarea.go",
    "chars": 701,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc CreateTextArea(tit"
  },
  {
    "path": "core/tui/components/tui_toggle_text.go",
    "chars": 1400,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tv"
  },
  {
    "path": "core/tui/components/tui_tree.go",
    "chars": 12074,
    "preview": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/gda"
  },
  {
    "path": "core/tui/misc/tui_event.go",
    "chars": 1010,
    "preview": "package misc\n\nimport (\n\t\"sync\"\n)\n\ntype Event struct {\n\tName string\n\tData interface{}\n}\n\ntype EventListener func(Event)\n\n"
  },
  {
    "path": "core/tui/misc/tui_focus.go",
    "chars": 2377,
    "preview": "package misc\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype TItem struct {\n\tPrimitive tview.P"
  },
  {
    "path": "core/tui/misc/tui_global.go",
    "chars": 670,
    "preview": "package misc\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/rivo/tview\"\n)\n\nvar Config *dao.Config\nvar ThemeNa"
  },
  {
    "path": "core/tui/misc/tui_theme.go",
    "chars": 3541,
    "preview": "package misc\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com"
  },
  {
    "path": "core/tui/misc/tui_utils.go",
    "chars": 1422,
    "preview": "package misc\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n\t\"github.com/rivo/tview\"\n"
  },
  {
    "path": "core/tui/misc/tui_writer.go",
    "chars": 601,
    "preview": "package misc\n\nimport (\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/rivo/tview\"\n)\n\n// ThreadSafeWriter wraps a tview.ANSIWriter to make i"
  },
  {
    "path": "core/tui/pages/tui_exec.go",
    "chars": 9743,
    "preview": "package pages\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.co"
  },
  {
    "path": "core/tui/pages/tui_project.go",
    "chars": 4436,
    "preview": "package pages\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/m"
  },
  {
    "path": "core/tui/pages/tui_run.go",
    "chars": 10921,
    "preview": "package pages\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github"
  },
  {
    "path": "core/tui/pages/tui_task.go",
    "chars": 3277,
    "preview": "package pages\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"gi"
  },
  {
    "path": "core/tui/pages.go",
    "chars": 4530,
    "preview": "package tui\n\nimport (\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alaj"
  },
  {
    "path": "core/tui/tui.go",
    "chars": 1565,
    "preview": "package tui\n\nimport (\n\t\"os\"\n\n\t\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/"
  },
  {
    "path": "core/tui/tui_input.go",
    "chars": 4092,
    "preview": "package tui\n\nimport (\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/tui/compone"
  },
  {
    "path": "core/tui/views/tui_help.go",
    "chars": 4070,
    "preview": "package views\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\""
  },
  {
    "path": "core/tui/views/tui_project_view.go",
    "chars": 11198,
    "preview": "package views\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/ala"
  },
  {
    "path": "core/tui/views/tui_shortcut_info.go",
    "chars": 2103,
    "preview": "package views\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\ntype Shor"
  },
  {
    "path": "core/tui/views/tui_spec_view.go",
    "chars": 3651,
    "preview": "package views\n\nimport (\n\t\"os\"\n\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"g"
  },
  {
    "path": "core/tui/views/tui_task_view.go",
    "chars": 6518,
    "preview": "package views\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n\t\"git"
  },
  {
    "path": "core/tui/watcher.go",
    "chars": 1191,
    "preview": "package tui\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n)\n\nfunc WatchFiles(app *App, files"
  },
  {
    "path": "core/utils.go",
    "chars": 5297,
    "preview": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices"
  },
  {
    "path": "docs/changelog.md",
    "chars": 11024,
    "preview": "# Changelog\n\n## Unreleased\n\n### Features\n\n- Added Git worktree support for projects [#119](https://github.com/alajmo/man"
  },
  {
    "path": "docs/commands.md",
    "chars": 11968,
    "preview": "# Commands\n\n## mani\n\nrepositories manager and task runner\n\n### Synopsis\n\nmani is a CLI tool that helps you manage multip"
  },
  {
    "path": "docs/config.md",
    "chars": 9021,
    "preview": "# Config\n\nThe mani.yaml config is based on the following concepts:\n\n- **projects** are directories, which may be git rep"
  },
  {
    "path": "docs/contributing.md",
    "chars": 157,
    "preview": "# Contributing\n\nAll contributions are welcome, be it [filing bugs](https://github.com/alajmo/mani/issues), feature sugge"
  },
  {
    "path": "docs/development.md",
    "chars": 1791,
    "preview": "# Development\n\n## Build instructions\n\n### Prerequisites\n\n- [go 1.25 or above](https://golang.org/doc/install)\n- [gorelea"
  },
  {
    "path": "docs/error-handling.md",
    "chars": 1525,
    "preview": "# Error Handling\n\n## Ignoring Task Errors\n\nIf you wish to continue task execution even if an error is encountered, use t"
  },
  {
    "path": "docs/examples.md",
    "chars": 4713,
    "preview": "# Examples\n\nThis is an example of how to use `mani`. Save the following content to a file named `mani.yaml` and run `man"
  },
  {
    "path": "docs/filtering-projects.md",
    "chars": 2072,
    "preview": "# Filtering Projects\n\nProjects can be filtered when managing projects (sync, list, describe) or running tasks. Filters c"
  },
  {
    "path": "docs/installation.md",
    "chars": 711,
    "preview": "# Installation\n\n`mani` is available on Linux and Mac, with partial support for Windows.\n\n* Binaries are available on the"
  },
  {
    "path": "docs/introduction.md",
    "chars": 2196,
    "preview": "---\nslug: /\n---\n\n# Introduction\n\n`mani` is a CLI tool that helps you manage multiple repositories. It's useful when you "
  },
  {
    "path": "docs/man-pages.md",
    "chars": 210,
    "preview": "# Man Page\n\nMan page generation is available via:\n\n```bash\n$ mani gen\nCreated mani.1\n\n# Or specify a different directory"
  },
  {
    "path": "docs/output.md",
    "chars": 1670,
    "preview": "# Output Format\n\n`mani` supports different output formats for tasks. By default it will use `stream` output, but it's po"
  },
  {
    "path": "docs/project-background.md",
    "chars": 5078,
    "preview": "# Project Background\n\nThis document contains a little bit of everything:\n\n- Background to `mani` and core design decisio"
  },
  {
    "path": "docs/roadmap.md",
    "chars": 356,
    "preview": "# Roadmap\n\n`mani` is under active development. Before **v1.0.0**, I want to finish the following tasks:\n\n- [ ] Bring cha"
  },
  {
    "path": "docs/shell-completion.mdx",
    "chars": 700,
    "preview": "# Shell Completion\n\nShell completion is available for `bash`, `zsh`, `fish` and `powershell`.\n\nimport Tabs from '@theme/"
  },
  {
    "path": "docs/usage.md",
    "chars": 1113,
    "preview": "# Usage\n\n## Initialize Mani\n\nRun the following command inside a directory containing your `git` repositories:\n\n```bash\nm"
  },
  {
    "path": "docs/variables.md",
    "chars": 657,
    "preview": "# Variables\n\n`mani` supports setting variables for both projects and tasks. Variables can be either strings or commands "
  },
  {
    "path": "examples/.gitignore",
    "chars": 52,
    "preview": "# mani #\ntemplate-generator\nfrontend/pinto\n# mani #\n"
  },
  {
    "path": "examples/README.md",
    "chars": 4597,
    "preview": "# Examples\n\nThis is an example of how you can use `mani`. Simply save the following content to a file named `mani.yaml` "
  },
  {
    "path": "examples/mani.yaml",
    "chars": 1390,
    "preview": "projects:\n  example:\n    path: .\n    desc: A mani example\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com"
  },
  {
    "path": "go.mod",
    "chars": 1318,
    "preview": "module github.com/alajmo/mani\n\ngo 1.25.5\n\nrequire (\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/gdamore/tcell/v2 v2"
  },
  {
    "path": "go.sum",
    "chars": 10298,
    "preview": "github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=\ngithub.com/clipperhouse/uax29/v2"
  },
  {
    "path": "install.sh",
    "chars": 1900,
    "preview": "#!/bin/sh\n# Credit to https://github.com/ducaale/xh for this install script.\n\nset -e\n\nif [ \"$(uname -s)\" = \"Darwin\" ] &&"
  },
  {
    "path": "main.go",
    "chars": 87,
    "preview": "package main\n\nimport (\n\t\"github.com/alajmo/mani/cmd\"\n)\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "res/demo.md",
    "chars": 194,
    "preview": "# Demo\n\nTo generate a `demo.gif` use [vhs](https://github.com/charmbracelet/vhs).\n\nRequires:\n\n- ffmpeg\n- ttyd\n\n```\n# Sta"
  },
  {
    "path": "res/demo.vhs",
    "chars": 334,
    "preview": "Output demo.gif\n\nSet FontSize 28\nSet Width 1920\nSet Height 1080\n\nSleep 500ms\nType \"mani sync\"\nEnter\nSleep 2000ms\n\nType \""
  },
  {
    "path": "res/mani.yaml",
    "chars": 910,
    "preview": "reload_tui_on_change: true\nsync_remotes: true\n\nprojects:\n  projects:\n    path: .\n\n  mani:\n    path: go/mani\n    url: htt"
  },
  {
    "path": "scripts/release.sh",
    "chars": 156,
    "preview": "#!/bin/bash\n\nset -euo pipefail\n\n# Get latest version changes only\nsed '0,/## v/d;/## v/Q' docs/changelog.md | tail -n +2"
  },
  {
    "path": "test/README.md",
    "chars": 1813,
    "preview": "# Test\n\n`mani` currently only has integration tests, which require `docker` to run. This is because `mani` mainly intera"
  },
  {
    "path": "test/fixtures/mani-advanced/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/fixtures/mani-advanced/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/fixtures/mani-empty/mani.yaml",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/fixtures/mani-no-tasks/mani.yaml",
    "chars": 346,
    "preview": "projects:\n  example:\n    path: .\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-rep"
  },
  {
    "path": "test/images/alpine.exec.Dockerfile",
    "chars": 1182,
    "preview": "FROM alpine:3.18.0 as build\n\nENV XDG_CACHE_HOME=/tmp/.cache\nENV GOPATH=${HOME}/go\nENV GO111MODULE=on\nENV PATH=\"/usr/loca"
  },
  {
    "path": "test/images/alpine.test.Dockerfile",
    "chars": 570,
    "preview": "FROM alpine:3.21.0\n\nENV GOCACHE=/go/cache\nENV GO111MODULE=on\nENV PATH=\"/usr/local/go/bin:${PATH}\"\nENV USER=\"test\"\nENV HO"
  },
  {
    "path": "test/integration/describe_test.go",
    "chars": 2128,
    "preview": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestDescribe(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t// P"
  },
  {
    "path": "test/integration/exec_test.go",
    "chars": 1885,
    "preview": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestExec(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t{\n\t\t\tTes"
  },
  {
    "path": "test/integration/golden/describe/golden-0/mani.yaml",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/describe/golden-0/stdout.golden",
    "chars": 134,
    "preview": "Index: 0\nName: Describe 0 projects when there's 0 projects\nWantErr: false\nCmd:\nmani describe projects\n\n---\nNo matching p"
  },
  {
    "path": "test/integration/golden/describe/golden-1/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-1/stdout.golden",
    "chars": 145,
    "preview": "Index: 1\nName: Describe 0 projects on non-existent tag\nWantErr: true\nCmd:\nmani describe projects --tags lala\n\n---\nerror:"
  },
  {
    "path": "test/integration/golden/describe/golden-2/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-2/stdout.golden",
    "chars": 153,
    "preview": "Index: 2\nName: Describe 0 projects on 2 non-matching tags\nWantErr: false\nCmd:\nmani describe projects --tags frontend,cli"
  },
  {
    "path": "test/integration/golden/describe/golden-3/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-3/stdout.golden",
    "chars": 550,
    "preview": "Index: 3\nName: Describe all projects\nWantErr: false\nCmd:\nmani describe projects\n\n---\n\nname: example\nsync: true\npath: .\nu"
  },
  {
    "path": "test/integration/golden/describe/golden-4/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-4/stdout.golden",
    "chars": 368,
    "preview": "Index: 4\nName: Describe projects matching 1 tag\nWantErr: false\nCmd:\nmani describe projects --tags frontend\n\n---\n\nname: p"
  },
  {
    "path": "test/integration/golden/describe/golden-5/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-5/stdout.golden",
    "chars": 259,
    "preview": "Index: 5\nName: Describe projects matching multiple tags\nWantErr: false\nCmd:\nmani describe projects --tags misc,frontend\n"
  },
  {
    "path": "test/integration/golden/describe/golden-6/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-6/stdout.golden",
    "chars": 207,
    "preview": "Index: 6\nName: Describe 1 project\nWantErr: false\nCmd:\nmani describe projects pinto\n\n---\n\nname: pinto\nsync: true\npath: fr"
  },
  {
    "path": "test/integration/golden/describe/golden-7/mani.yaml",
    "chars": 346,
    "preview": "projects:\n  example:\n    path: .\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-rep"
  },
  {
    "path": "test/integration/golden/describe/golden-7/stdout.golden",
    "chars": 108,
    "preview": "Index: 7\nName: Describe 0 tasks when no tasks exists \nWantErr: false\nCmd:\nmani describe tasks\n\n---\nNo tasks\n"
  },
  {
    "path": "test/integration/golden/describe/golden-8/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-8/stdout.golden",
    "chars": 3145,
    "preview": "Index: 8\nName: Describe all tasks\nWantErr: false\nCmd:\nmani describe tasks\n\n---\n\nname: fetch\ndescription: Fetch git\ntheme"
  },
  {
    "path": "test/integration/golden/describe/golden-9/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/describe/golden-9/stdout.golden",
    "chars": 370,
    "preview": "Index: 9\nName: Describe 1 tasks\nWantErr: false\nCmd:\nmani describe tasks status\n\n---\n\nname: status\ndescription: \ntheme: d"
  },
  {
    "path": "test/integration/golden/exec/golden-0/stdout.golden",
    "chars": 268,
    "preview": "Index: 0\nName: Should fail to exec when no configuration file found\nWantErr: true\nCmd:\nmani exec --all -o table ls\n\n\n---"
  },
  {
    "path": "test/integration/golden/exec/golden-1/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-1/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-1/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-1/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/exec/golden-1/stdout.golden",
    "chars": 388,
    "preview": "Index: 1\nName: Should exec in zero projects\nWantErr: true\nCmd:\nmani sync\nmani exec -o table ls\n\n\n---\n\nProject [pinto]\n\n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-2/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-2/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-2/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-2/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/exec/golden-2/stdout.golden",
    "chars": 824,
    "preview": "Index: 2\nName: Should exec in all projects\nWantErr: false\nCmd:\nmani sync\nmani exec --all -o table ls\n\n\n---\n\nProject [pin"
  },
  {
    "path": "test/integration/golden/exec/golden-3/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-3/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-3/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-3/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/exec/golden-3/stdout.golden",
    "chars": 444,
    "preview": "Index: 3\nName: Should exec when filtered on project name\nWantErr: false\nCmd:\nmani sync\nmani exec -o table --projects pin"
  },
  {
    "path": "test/integration/golden/exec/golden-4/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-4/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-4/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-4/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/exec/golden-4/stdout.golden",
    "chars": 478,
    "preview": "Index: 4\nName: Should exec when filtered on tags\nWantErr: false\nCmd:\nmani sync\nmani exec -o table --tags frontend ls\n\n\n-"
  },
  {
    "path": "test/integration/golden/exec/golden-5/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-5/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-5/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-5/mani.yaml",
    "chars": 1268,
    "preview": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: ["
  },
  {
    "path": "test/integration/golden/exec/golden-5/stdout.golden",
    "chars": 639,
    "preview": "Index: 5\nName: Should exec when filtered on cwd\nWantErr: false\nCmd:\nmani sync\ncd template-generator\nmani exec -o table -"
  },
  {
    "path": "test/integration/golden/exec/golden-6/.gitignore",
    "chars": 107,
    "preview": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/exec/golden-6/frontend/dashgrid/empty",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/integration/golden/exec/golden-6/frontend/pinto/empty",
    "chars": 0,
    "preview": ""
  }
]

// ... and 130 more files (download for full content)

About this extraction

This page contains the full source code of the alajmo/mani GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 330 files (635.9 KB), approximately 194.6k tokens, and a symbol index with 583 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!