[
  {
    "path": ".dockerignore",
    "content": ".git\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: [\"https://paypal.me/samiralajmovic\", \"https://www.buymeacoffee.com/alajmo\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/BUG_REPORT.md",
    "content": "---\nname: Bug report\nabout: Report a bug\ntitle: ''\nlabels: 'bug'\nassignees: ''\n---\n\n- [ ] I have the latest version of mani\n- [ ] I have searched through the existing issues\n\n## Info\n\n- OS\n  - [ ] Linux\n  - [ ] Mac OS X\n  - [ ] Windows\n  - [ ] other\n\n- Shell\n  - [ ] Bash\n  - [ ] Zsh\n  - [ ] Fish\n  - [ ] Powershell\n  - [ ] other\n\n<!-- run `mani --version` -->\n- Version:\n\n## Problem / Steps to reproduce\n\n<!-- Provide project and task definitions -->\n\n<!-- How do you invoke the `mani` CLI -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.md",
    "content": "---\r\nname: Feature request\r\nabout: Suggest an feature for this project\r\ntitle: ''\r\nlabels: 'enhancement'\r\nassignees: ''\r\n---\r\n\r\n## Is your feature request related to a problem? Please describe\r\n\r\n## Describe the solution you'd like\r\n\r\n## Additional context\r\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "### What's Changed\r\n\r\nA 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.\r\n\r\n### Technical Description\r\n\r\nAny specific technical detail that may provide additional context to aid code review.\r\n\r\n> Before opening a Pull Request you should read and agreed to the Contributor Code of Conduct (see `CONTRIBUTING.md`\\)\r\n"
  },
  {
    "path": ".github/agents/default.agent.md",
    "content": "---\nname: default\ndescription: add features and fix bugs\n---\n\n# Copilot Instructions for mani\n\nThis 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.\n\n## Project Overview\n\n- **Language**: Go 1.25+\n- **CLI Framework**: [Cobra](https://github.com/spf13/cobra)\n- **Configuration**: YAML-based configuration files (`mani.yaml`)\n- **Key Features**: Repository management, task running across multiple repos, TUI interface\n\n## Repository Structure\n\n```\nmani/\n├── cmd/              # CLI commands (Cobra commands)\n├── core/             # Core business logic\n│   ├── dao/          # Data access objects, config parsing\n│   ├── exec/         # Command execution logic\n│   ├── print/        # Output formatting\n│   └── tui/          # Terminal UI components\n├── docs/             # Documentation\n├── examples/         # Example configurations\n├── test/             # Test fixtures and integration tests\n│   ├── fixtures/     # Test data\n│   ├── integration/  # Integration test files\n│   └── scripts/      # Test scripts\n└── main.go           # Entry point\n```\n\n## Development Commands\n\nBuild commands can be found in Makefile.\n\n## Coding Standards\n\n### Go Code Style\n\n1. **Follow standard Go conventions**:\n   - Use `gofmt` for formatting\n   - Follow [Effective Go](https://go.dev/doc/effective_go) guidelines\n   - Use meaningful variable and function names\n\n2. **Error handling**:\n   - Always check and handle errors explicitly\n   - Use the custom error types in `core/errors.go` for consistent error messages\n   - Return errors to callers rather than panicking\n\n3. **Package structure**:\n   - Keep `cmd/` for CLI command definitions only\n   - Business logic goes in `core/`\n   - Data structures and parsing go in `core/dao/`\n\n## Adding Features\n\nWhen adding a new feature:\n\n1. **Understand the existing patterns**:\n   - Review similar features in the codebase\n   - Follow the established code organization\n\n2. **Implementation steps**:\n   - Add data structures in `core/dao/` if needed\n   - Implement business logic in `core/`\n   - Add CLI command in `cmd/` if needed\n   - Add tests (unit and/or integration)\n\n3. **Configuration changes**:\n   - Update `mani.yaml` schema if adding new config options\n   - Update documentation in `docs/config.md`\n   - Consider backward compatibility\n\n4. **Documentation**:\n   - Update relevant docs in `docs/`\n   - Update README.md if needed\n   - Add examples if appropriate\n\n## Fixing Bugs\n\nWhen fixing a bug:\n\n1. **Reproduce the issue**:\n   - Create a minimal test case\n   - Understand the root cause\n\n2. **Fix process**:\n   - Make the minimal change needed\n   - Add a test that would have caught the bug\n   - Verify the fix doesn't break existing functionality\n\n3. **Verification**:\n   - Run `make test-unit` to ensure all tests pass\n   - Run `make lint` to check code quality\n   - Test manually with real scenarios\n\n## Platform Considerations\n\n- **Windows support**: The code handles Windows-specific shell behavior\n  - Default shell on Windows is `powershell -NoProfile`\n  - Default shell on Unix is `bash -c`\n- **Cross-platform paths**: Use `filepath.Join()` for paths\n\n## Key Dependencies\n\n- `github.com/spf13/cobra` - CLI framework\n- `gopkg.in/yaml.v3` - YAML parsing\n- `github.com/gdamore/tcell/v2` - Terminal handling for TUI\n- `github.com/rivo/tview` - TUI components\n- `github.com/jedib0t/go-pretty/v6` - Table formatting\n\n## Configuration File Structure\n\nThe main configuration file is `mani.yaml`:\n\n```yaml\n# Global settings\nshell: bash -c\nsync_remotes: false\nsync_gitignore: true\n\n# Environment variables\nenv:\n  KEY: value\n\n# Project definitions\nprojects:\n  project-name:\n    path: ./relative/path\n    url: git@github.com:user/repo.git\n    tags: [tag1, tag2]\n\n# Task definitions\ntasks:\n  task-name:\n    desc: Task description\n    cmd: echo \"command\"\n    # Or multi-command:\n    commands:\n      - task: other-task\n      - cmd: echo \"inline command\"\n```\n\n## Pull Request Guidelines\n\n1. Reference the issue number if one exists\n2. Provide a clear description of changes\n3. Ensure all tests pass (`make test`)\n4. Ensure code is formatted (`make gofmt`)\n5. Ensure linter passes (`make lint`)\n6. Update documentation if needed\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Copilot Instructions for mani\n\nThis 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.\n\n## Project Overview\n\n- **Language**: Go 1.25+\n- **CLI Framework**: [Cobra](https://github.com/spf13/cobra)\n- **Configuration**: YAML-based configuration files (`mani.yaml`)\n- **Key Features**: Repository management, task running across multiple repos, TUI interface\n\n## Repository Structure\n\n```\nmani/\n├── cmd/              # CLI commands (Cobra commands)\n├── core/             # Core business logic\n│   ├── dao/          # Data access objects, config parsing\n│   ├── exec/         # Command execution logic\n│   ├── print/        # Output formatting\n│   └── tui/          # Terminal UI components\n├── docs/             # Documentation\n├── examples/         # Example configurations\n├── test/             # Test fixtures and integration tests\n│   ├── fixtures/     # Test data\n│   ├── integration/  # Integration test files\n│   └── scripts/      # Test scripts\n└── main.go           # Entry point\n```\n\n## Development Commands\n\nBuild commands can be found in Makefile.\n\n## Coding Standards\n\n### Go Code Style\n\n1. **Follow standard Go conventions**:\n   - Use `gofmt` for formatting\n   - Follow [Effective Go](https://go.dev/doc/effective_go) guidelines\n   - Use meaningful variable and function names\n\n2. **Error handling**:\n   - Always check and handle errors explicitly\n   - Use the custom error types in `core/errors.go` for consistent error messages\n   - Return errors to callers rather than panicking\n\n3. **Package structure**:\n   - Keep `cmd/` for CLI command definitions only\n   - Business logic goes in `core/`\n   - Data structures and parsing go in `core/dao/`\n\n## Adding Features\n\nWhen adding a new feature:\n\n1. **Understand the existing patterns**:\n   - Review similar features in the codebase\n   - Follow the established code organization\n\n2. **Implementation steps**:\n   - Add data structures in `core/dao/` if needed\n   - Implement business logic in `core/`\n   - Add CLI command in `cmd/` if needed\n   - Add tests (unit and/or integration)\n\n3. **Configuration changes**:\n   - Update `mani.yaml` schema if adding new config options\n   - Update documentation in `docs/config.md`\n   - Consider backward compatibility\n\n4. **Documentation**:\n   - Update relevant docs in `docs/`\n   - Update README.md if needed\n   - Add examples if appropriate\n\n## Fixing Bugs\n\nWhen fixing a bug:\n\n1. **Reproduce the issue**:\n   - Create a minimal test case\n   - Understand the root cause\n\n2. **Fix process**:\n   - Make the minimal change needed\n   - Add a test that would have caught the bug\n   - Verify the fix doesn't break existing functionality\n\n3. **Verification**:\n   - Run `make test-unit` to ensure all tests pass\n   - Run `make lint` to check code quality\n   - Test manually with real scenarios\n\n## Platform Considerations\n\n- **Windows support**: The code handles Windows-specific shell behavior\n  - Default shell on Windows is `powershell -NoProfile`\n  - Default shell on Unix is `bash -c`\n- **Cross-platform paths**: Use `filepath.Join()` for paths\n\n## Key Dependencies\n\n- `github.com/spf13/cobra` - CLI framework\n- `gopkg.in/yaml.v3` - YAML parsing\n- `github.com/gdamore/tcell/v2` - Terminal handling for TUI\n- `github.com/rivo/tview` - TUI components\n- `github.com/jedib0t/go-pretty/v6` - Table formatting\n\n## Configuration File Structure\n\nThe main configuration file is `mani.yaml`:\n\n```yaml\n# Global settings\nshell: bash -c\nsync_remotes: false\nsync_gitignore: true\n\n# Environment variables\nenv:\n  KEY: value\n\n# Project definitions\nprojects:\n  project-name:\n    path: ./relative/path\n    url: git@github.com:user/repo.git\n    tags: [tag1, tag2]\n\n# Task definitions\ntasks:\n  task-name:\n    desc: Task description\n    cmd: echo \"command\"\n    # Or multi-command:\n    commands:\n      - task: other-task\n      - cmd: echo \"inline command\"\n```\n\n## Pull Request Guidelines\n\n1. Reference the issue number if one exists\n2. Provide a clear description of changes\n3. Ensure all tests pass (`make test`)\n4. Ensure code is formatted (`make gofmt`)\n5. Ensure linter passes (`make lint`)\n6. Update documentation if needed\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"1.25.5\"\n\n      - name: Create release notes\n        run: ./scripts/release.sh\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --release-notes=release-changelog.md\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  pull_request:\n    branches: [main]\n    paths-ignore:\n      - \"**.md\"\njobs:\n  test:\n    name: test\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        go: [\"1.25.5\"]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"1.25.5\"\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.7.1\n\n      - name: Get dependencies\n        run: go get -v -t -d ./...\n\n      - name: Test\n        run: make test\n\n      - name: Build\n        run: make build\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\nvendor/\n\ndist\ntarget\ntest/tmp\nexamples/frontend\nrelease-changelog.md\n\n.netlify\n.tags\n\nassets\n.config\n\nres/go\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\n\nlinters:\n  settings:\n    errcheck:\n      exclude-functions:\n        - fmt.Fprintf\n        - fmt.Fprint\n        - (io.Closer).Close\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\nproject_name: mani\n\nbefore:\n  hooks:\n    - go mod download\n\nbuilds:\n  - binary: mani\n    id: mani\n    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 }}\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - darwin\n      - linux\n      - windows\n      - freebsd\n      - netbsd\n      - openbsd\n    goarch:\n      - amd64\n      - '386'\n      - arm\n      - arm64\n    goarm:\n      - '7'\n\n    ignore:\n      - goos: freebsd\n        goarch: arm\n      - goos: freebsd\n        goarch: arm64\n\n      - goos: openbsd\n        goarch: arm\n      - goos: openbsd\n        goarch: arm64\n\n      - goos: darwin\n        goarch: arm\n      - goos: darwin\n        goarch: '386'\n\n      - goos: windows\n        goarch: arm\n      - goos: windows\n        goarch: arm64\n\narchives:\n  - id: 'mani'\n    builds: ['mani']\n    format: tar.gz\n    format_overrides:\n      - goos: windows\n        format: zip\n    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'\n    files:\n      - LICENSE\n      - src: 'core/mani.1'\n        dst: '.'\n        strip_parent: true\n\nbrews:\n  - name: mani\n    description: 'CLI tool to help you manage multiple repositories'\n    homepage: 'https://manicli.com'\n    license: 'MIT'\n    repository:\n      owner: alajmo\n      name: homebrew-mani\n      token: '{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}'\n    directory: Formula\n\nchecksum:\n  name_template: 'checksums.txt'\n\nsnapshot:\n  version_template: '{{ .Tag }}'\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nmani 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.\n\n## Build & Development Commands\n\n```bash\nmake build              # Build for current platform (output: dist/mani)\nmake build-all          # Cross-platform builds using goreleaser\nmake test               # Run unit + integration tests\nmake test-unit          # Run unit tests only (go test ./core/dao/...)\nmake test-integration   # Run integration tests (requires Docker)\nmake update-golden-files # Update golden test outputs\nmake gofmt              # Format Go code\nmake lint               # Run golangci-lint and deadcode\nmake tidy               # Update dependencies\n```\n\n## Architecture\n\n**Entry point flow:**\n```\nmain.go -> cmd.Execute() (cmd/root.go) -> Cobra command routing\n```\n\n**Core layers:**\n- `cmd/` - Cobra CLI command definitions (exec, run, sync, list, describe, tui, etc.)\n- `core/dao/` - Data structures, YAML config parsing, project/task/spec definitions\n- `core/exec/` - Task execution engine, SSH/exec clients, cloning logic\n- `core/print/` - Output formatters (table, stream, tree, html, markdown)\n- `core/tui/` - Terminal UI using tview/tcell\n\n**Configuration:** YAML-based (`mani.yaml`) with projects, tasks, specs, and themes.\n\n## Key Patterns\n\n- CLI commands in `cmd/` delegate to business logic in `core/`\n- Custom error types in `core/errors.go` - use `core.CheckIfError()` for consistency\n- Config lookup: `mani.yaml`, `mani.yml`, `.mani.yaml`, `.mani.yml`\n- Shell handling: bash on Unix, powershell on Windows\n- Use `filepath.Join()` for cross-platform paths\n\n## Testing\n\n- Unit tests in `core/dao/*_test.go`\n- Integration tests in `test/integration/` using Docker and golden files\n- Golden files in `test/integration/golden/` serve as expected outputs\n- Test script: `./test/scripts/test` (supports --debug, --build, --run, --update, --count, --clean)\n\n## Adding Features\n\n1. Add data structures in `core/dao/` if needed\n2. Implement business logic in `core/`\n3. Add CLI command in `cmd/` if needed\n4. Update config schema in `docs/config.md` if adding new options\n5. Add tests (unit and/or integration)\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020-2021 Samir Alajmovic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "NAME    := mani\nPACKAGE := github.com/alajmo/$(NAME)\nDATE    := $(shell date +\"%Y %B %d\")\nGIT     := $(shell [ -d .git ] && git rev-parse --short HEAD)\nVERSION := v0.31.2\n\ndefault: build\n\ntidy:\n\tgo get -u && go mod tidy\n\ngofmt:\n\tgo fmt ./cmd/***.go\n\tgo fmt ./core/***.go\n\tgo fmt ./core/dao/***.go\n\tgo fmt ./core/exec/***.go\n\tgo fmt ./core/print/***.go\n\tgo fmt ./core/tui/***.go\n\tgo fmt ./test/integration/***.go\n\nlint:\n\tgolangci-lint run ./...\n\tdeadcode .\n\ntest:\n\t# Unit tests\n\tgo test -v ./core/dao/***\n\n\t# Integration tests\n\t./test/scripts/test --build --count 5 --clean\n\ntest-unit:\n\tgo test -v ./core/dao/***\n\nbench:\n\tgo test -bench=. -benchmem ./core/dao/... ./core/...\n\nbench-save:\n\t@mkdir -p benchmarks\n\tgo test -bench=. -benchmem -count=6 ./core/dao/... ./core/... > benchmarks/bench-$(shell date +%Y%m%d-%H%M%S).txt 2>&1\n\nbench-compare:\n\t@if [ -n \"$(OLD)\" ] && [ -n \"$(NEW)\" ]; then benchstat $(OLD) $(NEW); fi\n\ntest-integration:\n\t./test/scripts/test --count 5 --build --clean\n\nupdate-golden-files:\n\t./test/scripts/test --build --update\n\nbuild:\n\tCGO_ENABLED=0 go build \\\n\t-ldflags \"-w -X '${PACKAGE}/cmd.version=${VERSION}' -X '${PACKAGE}/core/tui.version=${VERSION}' -X '${PACKAGE}/cmd.commit=${GIT}' -X '${PACKAGE}/cmd.date=${DATE}'\" \\\n\t-a -tags netgo -o dist/${NAME} main.go\n\nbuild-all:\n\tgoreleaser release --snapshot --clean\n\nbuild-test:\n\tCGO_ENABLED=0 go build \\\n\t-ldflags \"-X '${PACKAGE}/core/dao.build_mode=TEST'\" \\\n\t-a -tags netgo -o dist/${NAME} main.go\n\ngen-man:\n\tgo 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\n\nrelease:\n\tgit tag ${VERSION} && git push origin ${VERSION}\n\nclean:\n\t$(RM) -r dist target\n\n.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\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\"><code>mani</code></h1>\n\n<div align=\"center\">\n  <a href=\"https://github.com/alajmo/mani/releases\">\n    <img src=\"https://img.shields.io/github/release-pre/alajmo/mani.svg\" alt=\"version\">\n  </a>\n\n  <a href=\"https://github.com/alajmo/mani/actions\">\n    <img src=\"https://github.com/alajmo/mani/workflows/release/badge.svg\" alt=\"build status\">\n  </a>\n\n  <a href=\"https://img.shields.io/badge/license-MIT-green\">\n    <img src=\"https://img.shields.io/badge/license-MIT-green\" alt=\"license\">\n  </a>\n\n  <a href=\"https://goreportcard.com/report/github.com/alajmo/mani\">\n    <img src=\"https://goreportcard.com/badge/github.com/alajmo/mani\" alt=\"Go Report Card\">\n  </a>\n\n  <a href=\"https://pkg.go.dev/github.com/alajmo/mani\">\n    <img src=\"https://pkg.go.dev/badge/github.com/alajmo/mani.svg\" alt=\"reference\">\n  </a>\n</div>\n\n<br>\n\n`mani` lets you manage multiple repositories and run commands across them.\n\n![demo](res/demo.gif)\n\nInterested in managing your servers in a similar way? Checkout [sake](https://github.com/alajmo/sake)!\n\n## Table of Contents\n\n- [Sponsors](#sponsors)\n- [Installation](#installation)\n  - [Building From Source](#building-from-source)\n- [Usage](#usage)\n  - [Initialize Mani](#initialize-mani)\n  - [Example Commands](#example-commands)\n  - [Documentation](#documentation)\n- [License](#license)\n\n## Sponsors\n\nMani 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.\n\n## Installation\n\n`mani` is available on Linux and Mac, with partial support for Windows.\n\n<details>\n<summary><b>Binaries</b></summary>\n\nDownload from the [release](https://github.com/alajmo/mani/releases) page.\n</details>\n\n<details>\n<summary><b>cURL</b> (Linux & macOS)</summary>\n\n```sh\ncurl -sfL https://raw.githubusercontent.com/alajmo/mani/main/install.sh | sh\n```\n</details>\n\n<details>\n<summary><b>Homebrew</b></summary>\n\n```sh\nbrew tap alajmo/mani\nbrew install mani\n```\n</details>\n\n<details>\n<summary><b>MacPorts</b></summary>\n\n```sh\nsudo port install mani\n```\n</details>\n\n<details>\n<summary><b>Arch</b> (AUR)</summary>\n\n```sh\nyay -S mani\n```\n</details>\n\n<details>\n<summary><b>Nix</b></summary>\n\n```sh\nnix-env -iA nixos.mani\n```\n</details>\n\n<details>\n<summary><b>Go</b></summary>\n\n```sh\ngo get -u github.com/alajmo/mani\n```\n</details>\n\n<details>\n<summary><b>Building From Source</b></summary>\n\n1. Clone the repo\n2. Build and run the executable\n    ```sh\n    make build && ./dist/mani\n    ```\n</details>\n\nAuto-completion is available via `mani completion bash|zsh|fish|powershell` and man page via `mani gen`.\n\n## Usage\n\n### Initialize Mani\n\nRun the following command inside a directory containing your `git` repositories:\n\n```sh\nmani init\n```\n\nThis will generate:\n\n- `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)\n- `.gitignore`: (only when inside a git repo) Includes the projects specified in `mani.yaml` file. To opt out, use `mani init --sync-gitignore=false`.\n\nIt 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.\n\n### Example Commands\n\n```bash\n# List all projects\nmani list projects\n\n# Run git status across all projects\nmani exec --all git status\n\n# Run git status across all projects in parallel with output in table format\nmani exec --all --parallel --output table git status\n```\n\n### Documentation\n\nCheckout the following to learn more about mani:\n\n- [Examples](examples)\n- [Config](docs/config.md)\n- [Commands](docs/commands.md)\n- Documentation\n  - [Filtering Projects](docs/filtering-projects.md)\n  - [Variables](docs/variables.md)\n  - [Output](docs/output.md)\n- [Changelog](/docs/changelog.md)\n- [Roadmap](/docs/roadmap.md)\n- [Project Background](docs/project-background.md)\n- [Contributing](docs/contributing.md)\n\n## [License](LICENSE)\n\nThe MIT License (MIT)\n\nCopyright (c) 2020-2021 Samir Alajmovic\n"
  },
  {
    "path": "benchmarks/.gitignore",
    "content": "# Ignore benchmark output files (they can be large and change frequently)\n# Use `make bench-save` to generate new results\n*.txt\n"
  },
  {
    "path": "cmd/check.go",
    "content": "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 *error) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"check\",\n\t\tShort: \"Validate config\",\n\t\tLong:  `Validate config.`,\n\t\tExample: `  # Validate config\n  mani check`,\n\t\tArgs: cobra.NoArgs,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif *configErr != nil {\n\t\t\t\tfmt.Printf(\"Found configuration errors:\\n\\n\")\n\t\t\t\tcore.Exit(*configErr)\n\t\t\t}\n\n\t\t\tfmt.Println(\"Config Valid\")\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/completion.go",
    "content": "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.Command {\n\tcmd := cobra.Command{\n\t\tUse:   \"completion <bash|zsh|fish|powershell>\",\n\t\tShort: \"Generate completion script\",\n\t\tLong: `To load completions:\nBash:\n\n  $ source <(mani completion bash)\n\n  # To load completions for each session, execute once:\n  # Linux:\n  $ mani completion bash > /etc/bash_completion.d/mani\n  # macOS:\n  $ mani completion bash > /usr/local/etc/bash_completion.d/mani\n\nZsh:\n\n  # If shell completion is not already enabled in your environment,\n  # you will need to enable it.  You can execute the following once:\n\n  $ echo \"autoload -U compinit; compinit\" >> ~/.zshrc\n\n  # To load completions for each session, execute once:\n  $ mani completion zsh > \"${fpath[1]}/_mani\"\n\n  # You will need to start a new shell for this setup to take effect.\n\nfish:\n\n  $ mani completion fish | source\n\n  # To load completions for each session, execute once:\n  $ mani completion fish > ~/.config/fish/completions/mani.fish\n\nPowerShell:\n\n  PS> mani completion powershell | Out-String | Invoke-Expression\n\n  # To load completions for every new session, run:\n  PS> mani completion powershell > mani.ps1\n  # and source this file from your PowerShell profile.\n\t\t`,\n\t\tDisableFlagsInUseLine: true,\n\t\tValidArgs:             []string{\"bash\", \"zsh\", \"fish\", \"powershell\"},\n\t\tArgs:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),\n\t\tRun:                   generateCompletion,\n\t\tDisableAutoGenTag:     true,\n\t}\n\n\treturn &cmd\n}\n\nfunc generateCompletion(cmd *cobra.Command, args []string) {\n\tswitch args[0] {\n\tcase \"bash\":\n\t\terr := cmd.Root().GenBashCompletion(os.Stdout)\n\t\tcore.CheckIfError(err)\n\tcase \"zsh\":\n\t\terr := cmd.Root().GenZshCompletion(os.Stdout)\n\t\tcore.CheckIfError(err)\n\tcase \"fish\":\n\t\terr := cmd.Root().GenFishCompletion(os.Stdout, true)\n\t\tcore.CheckIfError(err)\n\tcase \"powershell\":\n\t\terr := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)\n\t\tcore.CheckIfError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/describe.go",
    "content": "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\nfunc describeCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar describeFlags core.DescribeFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"desc\"},\n\t\tUse:     \"describe\",\n\t\tShort:   \"Describe projects and tasks\",\n\t\tLong:    \"Describe projects and tasks.\",\n\t\tExample: `  # Describe all projects\n  mani describe projects\n\n  # Describe all tasks\n  mani describe tasks`,\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.AddCommand(\n\t\tdescribeProjectsCmd(config, configErr, &describeFlags),\n\t\tdescribeTasksCmd(config, configErr, &describeFlags),\n\t)\n\n\tcmd.PersistentFlags().StringVar(&describeFlags.Theme, \"theme\", \"default\", \"set theme\")\n\terr := cmd.RegisterFlagCompletionFunc(\"theme\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tnames := config.GetThemeNames()\n\t\treturn names, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/describe_projects.go",
    "content": "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/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\nfunc describeProjectsCmd(\n\tconfig *dao.Config,\n\tconfigErr *error,\n\tdescribeFlags *core.DescribeFlags,\n) *cobra.Command {\n\tvar projectFlags core.ProjectFlags\n\tvar setProjectFlags core.SetProjectFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"project\", \"prj\"},\n\t\tUse:     \"projects [projects]\",\n\t\tShort:   \"Describe projects\",\n\t\tLong:    \"Describe projects.\",\n\t\tExample: `  # Describe all projects\n  mani describe projects\n\n  # Describe projects by name\n  mani describe projects <project>\n\n  # Describe projects by tags\n  mani describe projects --tags <tag>\n\n  # Describe projects by paths\n  mani describe projects --paths <path>\n\n\t# Describe projects matching a tag expression\n\tmani run <task> --tags-expr '<tag-1> || <tag-2>'`,\n\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\n\t\t\tsetProjectFlags.All = cmd.Flags().Changed(\"all\")\n\t\t\tsetProjectFlags.Cwd = cmd.Flags().Changed(\"cwd\")\n\t\t\tsetProjectFlags.Target = cmd.Flags().Changed(\"target\")\n\n\t\t\tdescribeProjects(config, args, &projectFlags, &setProjectFlags, describeFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tprojectNames := config.GetProjectNames()\n\t\t\treturn projectNames, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVarP(&projectFlags.All, \"all\", \"a\", true, \"select all projects\")\n\tcmd.Flags().BoolVarP(&projectFlags.Cwd, \"cwd\", \"k\", false, \"select current working directory\")\n\n\tcmd.Flags().StringSliceVarP(&projectFlags.Tags, \"tags\", \"t\", []string{}, \"filter projects by tags\")\n\terr := cmd.RegisterFlagCompletionFunc(\"tags\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\toptions := config.GetTags()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&projectFlags.TagsExpr, \"tags-expr\", \"E\", \"\", \"target projects by tags expression\")\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&projectFlags.Paths, \"paths\", \"d\", []string{}, \"filter projects by paths\")\n\terr = cmd.RegisterFlagCompletionFunc(\"paths\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\toptions := config.GetProjectPaths()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&projectFlags.Target, \"target\", \"T\", \"\", \"target projects by target name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"target\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetTargetNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().BoolVarP(&projectFlags.Edit, \"edit\", \"e\", false, \"edit project\")\n\n\treturn &cmd\n}\n\nfunc describeProjects(\n\tconfig *dao.Config,\n\targs []string,\n\tprojectFlags *core.ProjectFlags,\n\tsetProjectFlags *core.SetProjectFlags,\n\tdescribeFlags *core.DescribeFlags,\n) {\n\tif projectFlags.Edit {\n\t\tif len(args) > 0 {\n\t\t\terr := config.EditProject(args[0])\n\t\t\tcore.CheckIfError(err)\n\t\t} else {\n\t\t\terr := config.EditProject(\"\")\n\t\t\tcore.CheckIfError(err)\n\t\t}\n\t} else {\n\n\t\tprojectFlags.Projects = args\n\t\tif !setProjectFlags.All {\n\t\t\t// If no flags are set, use all and empty default target (but not the modified one by user)\n\t\t\t// If target is set, use the defaults from that target and respect other flags\n\t\t\tisNoFiltersSet := len(projectFlags.Projects) == 0 &&\n\t\t\t\tlen(projectFlags.Paths) == 0 &&\n\t\t\t\tlen(projectFlags.Tags) == 0 &&\n\t\t\t\tprojectFlags.TagsExpr == \"\" &&\n\t\t\t\t!setProjectFlags.Cwd &&\n\t\t\t\t!setProjectFlags.Target\n\t\t\tprojectFlags.All = isNoFiltersSet\n\t\t}\n\t\tprojects, err := config.GetFilteredProjects(projectFlags)\n\t\tcore.CheckIfError(err)\n\n\t\tif len(projects) == 0 {\n\t\t\tfmt.Println(\"No matching projects found\")\n\t\t} else {\n\t\t\ttheme, err := config.GetTheme(describeFlags.Theme)\n\t\t\tcore.CheckIfError(err)\n\n\t\t\toutput := print.PrintProjectBlocks(projects, true, theme.Block, print.GookitFormatter{})\n\t\t\tfmt.Print(output)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/describe_tasks.go",
    "content": "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/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\nfunc describeTasksCmd(config *dao.Config, configErr *error, describeFlags *core.DescribeFlags) *cobra.Command {\n\tvar taskFlags core.TaskFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"task\", \"tsk\"},\n\t\tUse:     \"tasks [tasks]\",\n\t\tShort:   \"Describe tasks\",\n\t\tLong:    \"Describe tasks.\",\n\t\tExample: `  # Describe all tasks\n  mani describe tasks\n\n  # Describe task <task>\n  mani describe task <task>`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\t\t\tdescribe(config, args, taskFlags, describeFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tvalues := config.GetTaskNames()\n\t\t\treturn values, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVarP(&taskFlags.Edit, \"edit\", \"e\", false, \"edit task\")\n\n\treturn &cmd\n}\n\nfunc describe(\n\tconfig *dao.Config,\n\targs []string,\n\ttaskFlags core.TaskFlags,\n\tdescribeFlags *core.DescribeFlags,\n) {\n\tif taskFlags.Edit {\n\t\tif len(args) > 0 {\n\t\t\terr := config.EditTask(args[0])\n\t\t\tcore.CheckIfError(err)\n\t\t} else {\n\t\t\terr := config.EditTask(\"\")\n\t\t\tcore.CheckIfError(err)\n\t\t}\n\t} else {\n\t\ttasks, err := config.GetTasksByNames(args)\n\t\tcore.CheckIfError(err)\n\n\t\tif len(tasks) == 0 {\n\t\t\tfmt.Println(\"No tasks\")\n\t\t} else {\n\t\t\tdao.ParseTasksEnv(tasks)\n\n\t\t\ttheme, err := config.GetTheme(describeFlags.Theme)\n\t\t\tcore.CheckIfError(err)\n\n\t\t\tout := print.PrintTaskBlock(tasks, true, theme.Block, print.GookitFormatter{})\n\t\t\tfmt.Print(out)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/edit.go",
    "content": "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\nfunc editCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"e\", \"ed\"},\n\t\tUse:     \"edit\",\n\t\tShort:   \"Open up mani config file\",\n\t\tLong:    \"Open up mani config file in $EDITOR.\",\n\n\t\tExample: `  # Edit current context\n  mani edit`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\terr := *configErr\n\t\t\tswitch e := err.(type) {\n\t\t\tcase *core.ConfigNotFound:\n\t\t\t\tcore.CheckIfError(e)\n\t\t\tdefault:\n\t\t\t\trunEdit(*config)\n\t\t\t}\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.AddCommand(\n\t\teditTask(config, configErr),\n\t\teditProject(config, configErr),\n\t)\n\n\treturn &cmd\n}\n\nfunc runEdit(config dao.Config) {\n\terr := config.EditConfig()\n\tcore.CheckIfError(err)\n}\n"
  },
  {
    "path": "cmd/edit_project.go",
    "content": "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\nfunc editProject(config *dao.Config, configErr *error) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"projects\", \"proj\", \"pr\"},\n\t\tUse:     \"project [project]\",\n\t\tShort:   \"Edit mani project\",\n\t\tLong:    `Edit mani project in $EDITOR.`,\n\n\t\tExample: `  # Edit projects\n  mani edit project\n\n  # Edit project <project>\n  mani edit project <project>`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\terr := *configErr\n\t\t\tswitch e := err.(type) {\n\t\t\tcase *core.ConfigNotFound:\n\t\t\t\tcore.CheckIfError(e)\n\t\t\tdefault:\n\t\t\t\trunEditProject(args, *config)\n\t\t\t}\n\t\t},\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil || len(args) == 1 {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tvalues := config.GetProjectNames()\n\t\t\treturn values, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\treturn &cmd\n}\n\nfunc runEditProject(args []string, config dao.Config) {\n\tif len(args) > 0 {\n\t\terr := config.EditProject(args[0])\n\t\tcore.CheckIfError(err)\n\t} else {\n\t\terr := config.EditProject(\"\")\n\t\tcore.CheckIfError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/edit_task.go",
    "content": "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\nfunc editTask(config *dao.Config, configErr *error) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"tasks\", \"tsks\", \"tsk\"},\n\t\tUse:     \"task [task]\",\n\t\tShort:   \"Edit mani task\",\n\t\tLong:    `Edit mani task in $EDITOR.`,\n\n\t\tExample: `  # Edit tasks\n  mani edit task\n\n  # Edit task <task>\n  mani edit task <task>`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\terr := *configErr\n\t\t\tswitch e := err.(type) {\n\t\t\tcase *core.ConfigNotFound:\n\t\t\t\tcore.CheckIfError(e)\n\t\t\tdefault:\n\t\t\t\trunEditTask(args, *config)\n\t\t\t}\n\t\t},\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil || len(args) == 1 {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tvalues := config.GetTaskNames()\n\t\t\treturn values, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\treturn &cmd\n}\n\nfunc runEditTask(args []string, config dao.Config) {\n\tif len(args) > 0 {\n\t\terr := config.EditTask(args[0])\n\t\tcore.CheckIfError(err)\n\t} else {\n\t\terr := config.EditTask(\"\")\n\t\tcore.CheckIfError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/exec.go",
    "content": "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/core/dao\"\n\t\"github.com/alajmo/mani/core/exec\"\n)\n\nfunc execCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar runFlags core.RunFlags\n\tvar setRunFlags core.SetRunFlags\n\n\tcmd := cobra.Command{\n\t\tUse:   \"exec <command>\",\n\t\tShort: \"Execute arbitrary commands\",\n\t\tLong: `Execute arbitrary commands.\nUse single quotes around your command to prevent file globbing and \nenvironment variable expansion from occurring before the command is \nexecuted in each directory.`,\n\n\t\tExample: `  # List files in all projects\n  mani exec --all ls\n\n  # List git files with markdown suffix in all projects\n  mani exec --all 'git ls-files | grep -e \".md\"'`,\n\t\tArgs: cobra.MinimumNArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\n\t\t\t// This is necessary since cobra doesn't support pointers for bools\n\t\t\t// (that would allow us to use nil as default value)\n\t\t\tsetRunFlags.TTY = cmd.Flags().Changed(\"tty\")\n\t\t\tsetRunFlags.Parallel = cmd.Flags().Changed(\"parallel\")\n\t\t\tsetRunFlags.OmitEmptyRows = cmd.Flags().Changed(\"omit-empty-rows\")\n\t\t\tsetRunFlags.OmitEmptyColumns = cmd.Flags().Changed(\"omit-empty-columns\")\n\t\t\tsetRunFlags.IgnoreErrors = cmd.Flags().Changed(\"ignore-errors\")\n\t\t\tsetRunFlags.IgnoreNonExisting = cmd.Flags().Changed(\"ignore-non-existing\")\n\t\t\tsetRunFlags.Forks = cmd.Flags().Changed(\"forks\")\n\t\t\tsetRunFlags.Cwd = cmd.Flags().Changed(\"cwd\")\n\t\t\tsetRunFlags.All = cmd.Flags().Changed(\"all\")\n\n\t\t\tif setRunFlags.Forks {\n\t\t\t\tforks, err := cmd.Flags().GetUint32(\"forks\")\n\t\t\t\tcore.CheckIfError(err)\n\t\t\t\tif forks == 0 {\n\t\t\t\t\tcore.Exit(&core.ZeroNotAllowed{Name: \"forks\"})\n\t\t\t\t}\n\t\t\t\trunFlags.Forks = forks\n\t\t\t}\n\n\t\t\texecute(args, config, &runFlags, &setRunFlags)\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVar(&runFlags.TTY, \"tty\", false, \"replace current process\")\n\tcmd.Flags().BoolVar(&runFlags.DryRun, \"dry-run\", false, \"print commands without executing them\")\n\tcmd.Flags().BoolVarP(&runFlags.Silent, \"silent\", \"s\", false, \"hide progress when running tasks\")\n\tcmd.Flags().BoolVar(&runFlags.IgnoreNonExisting, \"ignore-non-existing\", false, \"ignore non-existing projects\")\n\tcmd.Flags().BoolVar(&runFlags.IgnoreErrors, \"ignore-errors\", false, \"ignore errors\")\n\tcmd.Flags().BoolVar(&runFlags.OmitEmptyRows, \"omit-empty-rows\", false, \"omit empty rows in table output\")\n\tcmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, \"omit-empty-columns\", false, \"omit empty columns in table output\")\n\tcmd.Flags().BoolVar(&runFlags.Parallel, \"parallel\", false, \"run tasks in parallel across projects\")\n\tcmd.Flags().Uint32P(\"forks\", \"f\", 4, \"maximum number of concurrent processes\")\n\tcmd.Flags().BoolVarP(&runFlags.Cwd, \"cwd\", \"k\", false, \"use current working directory\")\n\tcmd.Flags().BoolVarP(&runFlags.All, \"all\", \"a\", false, \"target all projects\")\n\n\tcmd.Flags().StringVarP(&runFlags.Output, \"output\", \"o\", \"\", \"set output format [stream|table|markdown|html]\")\n\terr := cmd.RegisterFlagCompletionFunc(\"output\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalid := []string{\"table\", \"markdown\", \"html\"}\n\t\treturn valid, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.Spec, \"spec\", \"J\", \"\", \"set spec\")\n\terr = cmd.RegisterFlagCompletionFunc(\"spec\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetSpecNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Projects, \"projects\", \"p\", []string{}, \"select projects by name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"projects\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tprojects := config.GetProjectNames()\n\t\treturn projects, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Paths, \"paths\", \"d\", []string{}, \"select projects by path\")\n\terr = cmd.RegisterFlagCompletionFunc(\"paths\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\toptions := config.GetProjectPaths()\n\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Tags, \"tags\", \"t\", []string{}, \"select projects by tag\")\n\terr = cmd.RegisterFlagCompletionFunc(\"tags\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\ttags := config.GetTags()\n\t\treturn tags, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.TagsExpr, \"tags-expr\", \"E\", \"\", \"select projects by tags expression\")\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.Target, \"target\", \"T\", \"\", \"target projects by target name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"target\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetTargetNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.PersistentFlags().StringVar(&runFlags.Theme, \"theme\", \"\", \"set theme\")\n\terr = cmd.RegisterFlagCompletionFunc(\"theme\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tnames := config.GetThemeNames()\n\n\t\treturn names, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc execute(\n\targs []string,\n\tconfig *dao.Config,\n\trunFlags *core.RunFlags,\n\tsetRunFlags *core.SetRunFlags,\n) {\n\tcmd := strings.Join(args[0:], \" \")\n\tvar tasks []dao.Task\n\n\ttasks, projects, err := dao.ParseCmd(cmd, runFlags, setRunFlags, config)\n\tcore.CheckIfError(err)\n\n\ttarget := exec.Exec{Projects: projects, Tasks: tasks, Config: *config}\n\terr = target.Run([]string{}, runFlags, setRunFlags)\n\tcore.CheckIfError(err)\n}\n"
  },
  {
    "path": "cmd/gen.go",
    "content": "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 := \"\"\n\tcmd := cobra.Command{\n\t\tUse:                   \"gen\",\n\t\tShort:                 \"Generate man page\",\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\terr := core.GenManPages(dir)\n\t\t\tcore.CheckIfError(err)\n\t\t},\n\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().StringVarP(&dir, \"dir\", \"d\", \"./\", \"directory to save manpage to\")\n\terr := cmd.RegisterFlagCompletionFunc(\"dir\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\treturn nil, cobra.ShellCompDirectiveFilterDirs\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/gen_docs.go",
    "content": "// This source will generate\n//   - core/mani.1\n//   - docs/commands.md\n//\n// and is not included in the final build.\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\nfunc genDocsCmd(longAppDesc string) *cobra.Command {\n\tcmd := cobra.Command{\n\t\tUse:                   \"gen-docs\",\n\t\tShort:                 \"Generate man and markdown pages\",\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\terr := core.CreateManPage(\n\t\t\t\tlongAppDesc,\n\t\t\t\tversion,\n\t\t\t\tdate,\n\t\t\t\trootCmd,\n\t\t\t\trunCmd(&config, &configErr),\n\t\t\t\texecCmd(&config, &configErr),\n\t\t\t\tinitCmd(),\n\t\t\t\tsyncCmd(&config, &configErr),\n\t\t\t\teditCmd(&config, &configErr),\n\t\t\t\tlistCmd(&config, &configErr),\n\t\t\t\tdescribeCmd(&config, &configErr),\n\t\t\t\ttuiCmd(&config, &configErr),\n\t\t\t\tcheckCmd(&configErr),\n\t\t\t\tgenCmd(),\n\t\t\t)\n\t\t\tcore.CheckIfError(err)\n\t\t},\n\n\t\tDisableAutoGenTag: true,\n\t}\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/init.go",
    "content": "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\"github.com/alajmo/mani/core/exec\"\n)\n\nfunc initCmd() *cobra.Command {\n\tvar initFlags core.InitFlags\n\n\tcmd := cobra.Command{\n\t\tUse:   \"init\",\n\t\tShort: \"Initialize a mani repository\",\n\t\tLong: `Initialize a mani repository.\n\nCreates a new mani repository by generating a mani.yaml configuration file \nand a .gitignore file in the current directory.`,\n\n\t\tExample: `  # Initialize with default settings\n  mani init\n\n  # Initialize without auto-discovering projects\n  mani init --auto-discovery=false\n\n  # Initialize without updating .gitignore\n  mani init --sync-gitignore=false`,\n\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tfoundProjects, err := dao.InitMani(args, initFlags)\n\t\t\tcore.CheckIfError(err)\n\n\t\t\tif initFlags.AutoDiscovery {\n\t\t\t\texec.PrintProjectInit(foundProjects)\n\t\t\t}\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVar(&initFlags.AutoDiscovery, \"auto-discovery\", true, \"automatically discover and add Git repositories to mani.yaml\")\n\tcmd.Flags().BoolVarP(&initFlags.SyncGitignore, \"sync-gitignore\", \"g\", true, \"synchronize .gitignore file\")\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/list.go",
    "content": "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\nfunc listCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar listFlags core.ListFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"ls\", \"l\"},\n\t\tUse:     \"list\",\n\t\tShort:   \"List projects, tasks and tags\",\n\t\tLong:    \"List projects, tasks and tags.\",\n\t\tExample: `  # List all projects\n  mani list projects\n\n  # List all tasks\n  mani list tasks\n\n  # List all tags\n  mani list tags`,\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.AddCommand(\n\t\tlistProjectsCmd(config, configErr, &listFlags),\n\t\tlistTasksCmd(config, configErr, &listFlags),\n\t\tlistTagsCmd(config, configErr, &listFlags),\n\t)\n\n\tcmd.PersistentFlags().StringVar(&listFlags.Theme, \"theme\", \"default\", \"set theme\")\n\terr := cmd.RegisterFlagCompletionFunc(\"theme\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tnames := config.GetThemeNames()\n\t\treturn names, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.PersistentFlags().StringVarP(&listFlags.Output, \"output\", \"o\", \"table\", \"set output format [table|markdown|html]\")\n\terr = cmd.RegisterFlagCompletionFunc(\"output\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tvalid := []string{\"table\", \"markdown\", \"html\"}\n\t\treturn valid, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "cmd/list_projects.go",
    "content": "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/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\nfunc listProjectsCmd(\n\tconfig *dao.Config,\n\tconfigErr *error,\n\tlistFlags *core.ListFlags,\n) *cobra.Command {\n\tvar projectFlags core.ProjectFlags\n\tvar setProjectFlags core.SetProjectFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"project\", \"proj\", \"pr\"},\n\t\tUse:     \"projects [projects]\",\n\t\tShort:   \"List projects\",\n\t\tLong:    \"List projects.\",\n\t\tExample: `  # List all projects\n  mani list projects\n\n  # List projects by name\n  mani list projects <project>\n\n  # List projects by tags\n  mani list projects --tags <tag>\n\n  # List projects by paths\n  mani list projects --paths <path>\n\n\t# List projects matching a tag expression\n\tmani run <task> --tags-expr '<tag-1> || <tag-2>'`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\n\t\t\tsetProjectFlags.All = cmd.Flags().Changed(\"all\")\n\t\t\tsetProjectFlags.Cwd = cmd.Flags().Changed(\"cwd\")\n\t\t\tsetProjectFlags.Target = cmd.Flags().Changed(\"target\")\n\n\t\t\tlistProjects(config, args, listFlags, &projectFlags, &setProjectFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tprojectNames := config.GetProjectNames()\n\t\t\treturn projectNames, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVar(&listFlags.Tree, \"tree\", false, \"display output in tree format\")\n\n\tcmd.Flags().BoolVarP(&projectFlags.All, \"all\", \"a\", true, \"select all projects\")\n\tcmd.Flags().BoolVarP(&projectFlags.Cwd, \"cwd\", \"k\", false, \"select current working directory\")\n\n\tcmd.Flags().StringSliceVarP(&projectFlags.Tags, \"tags\", \"t\", []string{}, \"select projects by tags\")\n\terr := cmd.RegisterFlagCompletionFunc(\"tags\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\toptions := config.GetTags()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&projectFlags.TagsExpr, \"tags-expr\", \"E\", \"\", \"select projects by tags expression\")\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&projectFlags.Paths, \"paths\", \"d\", []string{}, \"select projects by paths\")\n\terr = cmd.RegisterFlagCompletionFunc(\"paths\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\toptions := config.GetProjectPaths()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&projectFlags.Target, \"target\", \"T\", \"\", \"select projects by target name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"target\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetTargetNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVar(&projectFlags.Headers, \"headers\", []string{\"project\", \"tag\", \"description\"}, \"specify columns to display [project, path, relpath, description, url, tag, worktree]\")\n\terr = cmd.RegisterFlagCompletionFunc(\"headers\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif err != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalidHeaders := []string{\"project\", \"path\", \"relpath\", \"description\", \"url\", \"tag\", \"worktree\"}\n\t\treturn validHeaders, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc listProjects(\n\tconfig *dao.Config,\n\targs []string,\n\tlistFlags *core.ListFlags,\n\tprojectFlags *core.ProjectFlags,\n\tsetProjectFlags *core.SetProjectFlags,\n) {\n\ttheme, err := config.GetTheme(listFlags.Theme)\n\tcore.CheckIfError(err)\n\n\tif listFlags.Tree {\n\t\ttree, err := config.GetProjectsTree(projectFlags.Paths, projectFlags.Tags)\n\t\tcore.CheckIfError(err)\n\t\tprint.PrintTree(config, *theme, listFlags, tree)\n\t\treturn\n\t}\n\n\tprojectFlags.Projects = args\n\t// If flag All is not set and no other filters are applied set All to true.\n\tif !setProjectFlags.All {\n\t\tisNoFiltersSet := len(projectFlags.Projects) == 0 &&\n\t\t\tlen(projectFlags.Paths) == 0 &&\n\t\t\tlen(projectFlags.Tags) == 0 &&\n\t\t\tprojectFlags.TagsExpr == \"\" &&\n\t\t\t!setProjectFlags.Cwd &&\n\t\t\t!setProjectFlags.Target\n\t\tprojectFlags.All = isNoFiltersSet\n\t}\n\tprojects, err := config.GetFilteredProjects(projectFlags)\n\tcore.CheckIfError(err)\n\n\tif len(projects) == 0 {\n\t\tfmt.Println(\"No matching projects found\")\n\t} else {\n\t\ttheme.Table.Border.Rows = core.Ptr(false)\n\t\ttheme.Table.Header.Format = core.Ptr(\"t\")\n\n\t\toptions := print.PrintTableOptions{\n\t\t\tOutput:           listFlags.Output,\n\t\t\tTheme:            *theme,\n\t\t\tTree:             listFlags.Tree,\n\t\t\tAutoWrap:         true,\n\t\t\tOmitEmptyRows:    false,\n\t\t\tOmitEmptyColumns: true,\n\t\t\tColor:            *theme.Color,\n\t\t}\n\n\t\tfmt.Println()\n\t\tprint.PrintTable(projects, options, projectFlags.Headers, []string{}, os.Stdout)\n\t\tfmt.Println()\n\t}\n}\n"
  },
  {
    "path": "cmd/list_tags.go",
    "content": "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/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\nfunc listTagsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command {\n\tvar tagFlags core.TagFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"tag\"},\n\t\tUse:     \"tags [tags]\",\n\t\tShort:   \"List tags\",\n\t\tLong:    \"List tags.\",\n\t\tExample: `  # List all tags\n  mani list tags`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\t\t\tlistTags(config, args, listFlags, &tagFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\ttags := config.GetTags()\n\t\t\treturn tags, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().StringSliceVar(&tagFlags.Headers, \"headers\", []string{\"tag\", \"project\"}, \"specify columns to display [project, tag]\")\n\terr := cmd.RegisterFlagCompletionFunc(\"headers\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tvalidHeaders := []string{\"tag\", \"project\"}\n\t\treturn validHeaders, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc listTags(\n\tconfig *dao.Config,\n\targs []string,\n\tlistFlags *core.ListFlags,\n\ttagFlags *core.TagFlags,\n) {\n\ttheme, err := config.GetTheme(listFlags.Theme)\n\tcore.CheckIfError(err)\n\n\ttheme.Table.Border.Rows = core.Ptr(false)\n\ttheme.Table.Header.Format = core.Ptr(\"t\")\n\n\toptions := print.PrintTableOptions{\n\t\tOutput:           listFlags.Output,\n\t\tTheme:            *theme,\n\t\tTree:             listFlags.Tree,\n\t\tAutoWrap:         true,\n\t\tOmitEmptyRows:    false,\n\t\tOmitEmptyColumns: true,\n\t\tColor:            *theme.Color,\n\t}\n\n\tallTags := config.GetTags()\n\n\tif len(args) > 0 {\n\t\tfoundTags := core.Intersection(args, allTags)\n\n\t\t// Could not find one of the provided tags\n\t\tif len(foundTags) != len(args) {\n\t\t\tcore.CheckIfError(&core.TagNotFound{Tags: args})\n\t\t}\n\n\t\ttags, err := config.GetTagAssocations(foundTags)\n\t\tcore.CheckIfError(err)\n\n\t\tif len(tags) == 0 {\n\t\t\tfmt.Println(\"No tags\")\n\t\t} else {\n\t\t\tfmt.Println()\n\t\t\tprint.PrintTable(tags, options, tagFlags.Headers, []string{}, os.Stdout)\n\t\t\tfmt.Println()\n\t\t}\n\t} else {\n\t\ttags, err := config.GetTagAssocations(allTags)\n\t\tcore.CheckIfError(err)\n\t\tif len(tags) == 0 {\n\t\t\tfmt.Println(\"No tags\")\n\t\t} else {\n\t\t\tfmt.Println(\"\")\n\t\t\tprint.PrintTable(tags, options, tagFlags.Headers, []string{}, os.Stdout)\n\t\t\tfmt.Println(\"\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/list_tasks.go",
    "content": "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/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\nfunc listTasksCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command {\n\tvar taskFlags core.TaskFlags\n\n\tcmd := cobra.Command{\n\t\tAliases: []string{\"task\", \"tsk\", \"tsks\"},\n\t\tUse:     \"tasks [tasks]\",\n\t\tShort:   \"List tasks\",\n\t\tLong:    \"List tasks.\",\n\t\tExample: `  # List all tasks\n  mani list tasks\n\n  # List tasks by name\n  mani list task <task>`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\t\t\tlistTasks(config, args, listFlags, &taskFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tvalues := config.GetTaskNames()\n\t\t\treturn values, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().StringSliceVar(&taskFlags.Headers, \"headers\", []string{\"task\", \"description\"}, \"specify columns to display [task, description, target, spec]\")\n\terr := cmd.RegisterFlagCompletionFunc(\"headers\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tvalidHeaders := []string{\"task\", \"description\", \"target\", \"spec\"}\n\t\treturn validHeaders, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc listTasks(\n\tconfig *dao.Config,\n\targs []string,\n\tlistFlags *core.ListFlags,\n\ttaskFlags *core.TaskFlags,\n) {\n\ttasks, err := config.GetTasksByNames(args)\n\tcore.CheckIfError(err)\n\n\ttheme, err := config.GetTheme(listFlags.Theme)\n\tcore.CheckIfError(err)\n\n\tif len(tasks) == 0 {\n\t\tfmt.Println(\"No tasks\")\n\t} else {\n\t\ttheme.Table.Border.Rows = core.Ptr(false)\n\t\ttheme.Table.Header.Format = core.Ptr(\"t\")\n\n\t\toptions := print.PrintTableOptions{\n\t\t\tOutput:           listFlags.Output,\n\t\t\tTheme:            *theme,\n\t\t\tTree:             listFlags.Tree,\n\t\t\tAutoWrap:         true,\n\t\t\tOmitEmptyRows:    false,\n\t\t\tOmitEmptyColumns: true,\n\t\t\tColor:            *theme.Color,\n\t\t}\n\n\t\tfmt.Println()\n\t\tprint.PrintTable(tasks, options, taskFlags.Headers, []string{}, os.Stdout)\n\t\tfmt.Println()\n\t}\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "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\tappName      = \"mani\"\n\tshortAppDesc = \"repositories manager and task runner\"\n)\n\nvar (\n\tconfig         dao.Config\n\tconfigErr      error\n\tconfigFilepath string\n\tuserConfigPath string\n\tcolor          bool\n\tbuildMode      = \"\"\n\tversion        = \"dev\"\n\tcommit         = \"none\"\n\tdate           = \"n/a\"\n\trootCmd        = &cobra.Command{\n\t\tUse:     appName,\n\t\tShort:   shortAppDesc,\n\t\tVersion: version,\n\t}\n)\n\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\t// When user input's wrong command or flag\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\t// Modify default shell in-case we're on windows\n\tif runtime.GOOS == \"windows\" {\n\t\tdao.DEFAULT_SHELL = \"powershell -NoProfile\"\n\t\tdao.DEFAULT_SHELL_PROGRAM = \"powershell\"\n\t}\n\n\tcobra.OnInitialize(initConfig)\n\n\trootCmd.PersistentFlags().StringVarP(&configFilepath, \"config\", \"c\", \"\", \"specify config\")\n\trootCmd.PersistentFlags().StringVarP(&userConfigPath, \"user-config\", \"u\", \"\", \"specify user config\")\n\trootCmd.PersistentFlags().BoolVar(&color, \"color\", true, \"enable color\")\n\n\trootCmd.AddCommand(\n\t\tcompletionCmd(),\n\t\tgenCmd(),\n\t\tinitCmd(),\n\t\texecCmd(&config, &configErr),\n\t\trunCmd(&config, &configErr),\n\t\tlistCmd(&config, &configErr),\n\t\tdescribeCmd(&config, &configErr),\n\t\tsyncCmd(&config, &configErr),\n\t\teditCmd(&config, &configErr),\n\t\tcheckCmd(&configErr),\n\t\ttuiCmd(&config, &configErr),\n\t)\n\n\trootCmd.SetVersionTemplate(fmt.Sprintf(\"Version: %-10s\\nCommit: %-10s\\nDate: %-10s\\n\", version, commit, date))\n\n\t// Add custom help template with footer\n\tdefaultHelpTemplate := rootCmd.HelpTemplate()\n\trootCmd.SetHelpTemplate(defaultHelpTemplate + `\nDocumentation: https://manicli.com\nIssues:        https://github.com/alajmo/mani/issues\n`)\n\n\tif buildMode == \"man\" {\n\t\trootCmd.AddCommand(genDocsCmd(\"manage multiple repositories and run commands across them\"))\n\t}\n\n\trootCmd.DisableAutoGenTag = true\n}\n\nfunc initConfig() {\n\tconfig, configErr = dao.ReadConfig(configFilepath, userConfigPath, color)\n}\n"
  },
  {
    "path": "cmd/run.go",
    "content": "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/core/dao\"\n\t\"github.com/alajmo/mani/core/exec\"\n)\n\nfunc runCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar runFlags core.RunFlags\n\tvar setRunFlags core.SetRunFlags\n\n\tcmd := cobra.Command{\n\t\tUse:   \"run <task>\",\n\t\tShort: \"Run tasks\",\n\t\tLong: `Run tasks.\n\nThe tasks are specified in a mani.yaml file along with the projects you can target.`,\n\n\t\tExample: `  # Execute task for all projects\n  mani run <task> --all\n\n  # Execute a task in parallel with a maximum of 8 concurrent processes\n  mani run <task> --projects <project> --parallel --forks 8\n\n  # Execute task for a specific projects\n  mani run <task> --projects <project>\n\n  # Execute a task for projects with specific tags\n  mani run <task> --tags <tag>\n\n  # Execute a task for projects matching specific paths\n  mani run <task> --paths <path>\n\n  # Execute a task for all projects matching a tag expression\n  mani run <task> --tags-expr 'active || git' <tag>\n\n  # Execute a task with environment variables from shell\n  mani run <task> key=value`,\n\n\t\tDisableFlagsInUseLine: true,\n\t\tArgs:                  cobra.MinimumNArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\n\t\t\t// This is necessary since cobra doesn't support pointers for bools\n\t\t\t// (that would allow us to use nil as default value)\n\t\t\tsetRunFlags.TTY = cmd.Flags().Changed(\"tty\")\n\t\t\tsetRunFlags.Cwd = cmd.Flags().Changed(\"cwd\")\n\t\t\tsetRunFlags.All = cmd.Flags().Changed(\"all\")\n\n\t\t\tsetRunFlags.Parallel = cmd.Flags().Changed(\"parallel\")\n\t\t\tsetRunFlags.OmitEmptyRows = cmd.Flags().Changed(\"omit-empty-rows\")\n\t\t\tsetRunFlags.OmitEmptyColumns = cmd.Flags().Changed(\"omit-empty-columns\")\n\t\t\tsetRunFlags.IgnoreErrors = cmd.Flags().Changed(\"ignore-errors\")\n\t\t\tsetRunFlags.IgnoreNonExisting = cmd.Flags().Changed(\"ignore-non-existing\")\n\t\t\tsetRunFlags.Forks = cmd.Flags().Changed(\"forks\")\n\n\t\t\tif setRunFlags.Forks {\n\t\t\t\tforks, err := cmd.Flags().GetUint32(\"forks\")\n\t\t\t\tcore.CheckIfError(err)\n\t\t\t\tif forks == 0 {\n\t\t\t\t\tcore.Exit(&core.ZeroNotAllowed{Name: \"forks\"})\n\t\t\t\t}\n\t\t\t\trunFlags.Forks = forks\n\t\t\t}\n\n\t\t\trun(args, config, &runFlags, &setRunFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\treturn config.GetTaskNameAndDesc(), cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVar(&runFlags.TTY, \"tty\", false, \"replace current process\")\n\tcmd.Flags().BoolVar(&runFlags.Describe, \"describe\", false, \"display task information\")\n\tcmd.Flags().BoolVar(&runFlags.DryRun, \"dry-run\", false, \"display the task without execution\")\n\tcmd.Flags().BoolVarP(&runFlags.Silent, \"silent\", \"s\", false, \"hide progress output during task execution\")\n\tcmd.Flags().BoolVar(&runFlags.IgnoreNonExisting, \"ignore-non-existing\", false, \"skip non-existing projects\")\n\tcmd.Flags().BoolVar(&runFlags.IgnoreErrors, \"ignore-errors\", false, \"continue execution despite errors\")\n\tcmd.Flags().BoolVar(&runFlags.OmitEmptyRows, \"omit-empty-rows\", false, \"hide empty rows in table output\")\n\tcmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, \"omit-empty-columns\", false, \"hide empty columns in table output\")\n\tcmd.Flags().BoolVar(&runFlags.Parallel, \"parallel\", false, \"execute tasks in parallel across projects\")\n\tcmd.Flags().BoolVarP(&runFlags.Edit, \"edit\", \"e\", false, \"edit task\")\n\tcmd.Flags().Uint32P(\"forks\", \"f\", 4, \"maximum number of concurrent processes\")\n\n\tcmd.Flags().StringVarP(&runFlags.Output, \"output\", \"o\", \"\", \"set output format [stream|table|markdown|html]\")\n\terr := cmd.RegisterFlagCompletionFunc(\"output\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tvalid := []string{\"stream\", \"table\", \"html\", \"markdown\"}\n\t\treturn valid, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.Spec, \"spec\", \"J\", \"\", \"set spec\")\n\terr = cmd.RegisterFlagCompletionFunc(\"spec\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetSpecNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().BoolVarP(&runFlags.Cwd, \"cwd\", \"k\", false, \"select current working directory\")\n\n\tcmd.Flags().BoolVarP(&runFlags.All, \"all\", \"a\", false, \"select all projects\")\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Projects, \"projects\", \"p\", []string{}, \"select projects by name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"projects\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tprojects := config.GetProjectNames()\n\t\treturn projects, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Paths, \"paths\", \"d\", []string{}, \"select projects by path\")\n\terr = cmd.RegisterFlagCompletionFunc(\"paths\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\toptions := config.GetProjectPaths()\n\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&runFlags.Tags, \"tags\", \"t\", []string{}, \"select projects by tag\")\n\terr = cmd.RegisterFlagCompletionFunc(\"tags\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\ttags := config.GetTags()\n\t\treturn tags, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.TagsExpr, \"tags-expr\", \"E\", \"\", \"select projects by tags expression\")\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&runFlags.Target, \"target\", \"T\", \"\", \"select projects by target name\")\n\terr = cmd.RegisterFlagCompletionFunc(\"target\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tvalues := config.GetTargetNames()\n\t\treturn values, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.PersistentFlags().StringVar(&runFlags.Theme, \"theme\", \"\", \"set theme\")\n\terr = cmd.RegisterFlagCompletionFunc(\"theme\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\t\tnames := config.GetThemeNames()\n\n\t\treturn names, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc run(\n\targs []string,\n\tconfig *dao.Config,\n\trunFlags *core.RunFlags,\n\tsetRunFlags *core.SetRunFlags,\n) {\n\tvar taskNames []string\n\tvar userArgs []string\n\t// Separate user arguments from task names\n\tfor _, arg := range args {\n\t\tif strings.Contains(arg, \"=\") {\n\t\t\tuserArgs = append(userArgs, arg)\n\t\t} else {\n\t\t\ttaskNames = append(taskNames, arg)\n\t\t}\n\t}\n\n\tif runFlags.Edit {\n\t\tif len(args) > 0 {\n\t\t\t_ = config.EditTask(taskNames[0])\n\t\t\treturn\n\t\t} else {\n\t\t\t_ = config.EditTask(\"\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar tasks []dao.Task\n\tvar projects []dao.Project\n\tvar err error\n\tif len(taskNames) == 1 {\n\t\ttasks, projects, err = dao.ParseSingleTask(taskNames[0], runFlags, setRunFlags, config)\n\t} else {\n\t\ttasks, projects, err = dao.ParseManyTasks(taskNames, runFlags, setRunFlags, config)\n\t}\n\tcore.CheckIfError(err)\n\n\ttarget := exec.Exec{Projects: projects, Tasks: tasks, Config: *config}\n\terr = target.Run(userArgs, runFlags, setRunFlags)\n\tcore.CheckIfError(err)\n}\n"
  },
  {
    "path": "cmd/sync.go",
    "content": "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\"github.com/alajmo/mani/core/exec\"\n)\n\nfunc syncCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar projectFlags core.ProjectFlags\n\tvar syncFlags = core.SyncFlags{Forks: 4}\n\tvar setSyncFlags core.SetSyncFlags\n\n\tcmd := cobra.Command{\n\t\tUse:     \"sync\",\n\t\tAliases: []string{\"clone\"},\n\t\tShort:   \"Clone repositories and update .gitignore\",\n\t\tLong: `Clone repositories and update .gitignore file.\nFor repositories requiring authentication, disable parallel cloning to enter\ncredentials for each repository individually.`,\n\t\tExample: `  # Clone repositories one at a time\n  mani sync\n\n  # Clone repositories in parallel\n  mani sync --parallel\n\n  # Disable updating .gitignore file\n  mani sync --sync-gitignore=false\n\n  # Sync project remotes. This will modify the projects .git state\n  mani sync --sync-remotes\n\n  # Clone repositories even if project sync field is set to false\n  mani sync --ignore-sync-state\n\n  # Display sync status\n  mani sync --status`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\n\t\t\t// This is necessary since cobra doesn't support pointers for bools\n\t\t\t// (that would allow us to use nil as default value)\n\t\t\tsetSyncFlags.Parallel = cmd.Flags().Changed(\"parallel\")\n\t\t\tsetSyncFlags.SyncGitignore = cmd.Flags().Changed(\"sync-gitignore\")\n\t\t\tsetSyncFlags.SyncRemotes = cmd.Flags().Changed(\"sync-remotes\")\n\t\t\tsetSyncFlags.RemoveOrphanedWorktrees = cmd.Flags().Changed(\"remove-orphaned-worktrees\")\n\t\t\tsetSyncFlags.Forks = cmd.Flags().Changed(\"forks\")\n\n\t\t\tif setSyncFlags.Forks {\n\t\t\t\tforks, err := cmd.Flags().GetUint32(\"forks\")\n\t\t\t\tcore.CheckIfError(err)\n\t\t\t\tif forks == 0 {\n\t\t\t\t\tcore.Exit(&core.ZeroNotAllowed{Name: \"forks\"})\n\t\t\t\t}\n\t\t\t\tsyncFlags.Forks = forks\n\t\t\t}\n\n\t\t\trunSync(config, args, projectFlags, syncFlags, setSyncFlags)\n\t\t},\n\t\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\tif *configErr != nil {\n\t\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t\t}\n\n\t\t\tprojectNames := config.GetProjectNames()\n\t\t\treturn projectNames, cobra.ShellCompDirectiveNoFileComp\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.Flags().BoolVarP(&syncFlags.SyncRemotes, \"sync-remotes\", \"r\", false, \"update git remote state\")\n\tcmd.Flags().BoolVarP(&syncFlags.RemoveOrphanedWorktrees, \"remove-orphaned-worktrees\", \"w\", false, \"remove git worktrees not in config\")\n\tcmd.Flags().BoolVarP(&syncFlags.SyncGitignore, \"sync-gitignore\", \"g\", true, \"sync gitignore\")\n\tcmd.Flags().BoolVar(&syncFlags.IgnoreSyncState, \"ignore-sync-state\", false, \"sync project even if the project's sync field is set to false\")\n\tcmd.Flags().BoolVarP(&syncFlags.Parallel, \"parallel\", \"p\", false, \"clone projects in parallel\")\n\tcmd.Flags().BoolVarP(&syncFlags.Status, \"status\", \"s\", false, \"display status only\")\n\tcmd.Flags().Uint32P(\"forks\", \"f\", 4, \"maximum number of concurrent processes\")\n\n\t// Targets\n\tcmd.Flags().StringSliceVarP(&projectFlags.Tags, \"tags\", \"t\", []string{}, \"clone projects by tags\")\n\terr := cmd.RegisterFlagCompletionFunc(\"tags\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\toptions := config.GetTags()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringVarP(&projectFlags.TagsExpr, \"tags-expr\", \"E\", \"\", \"clone projects by tag expression\")\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().StringSliceVarP(&projectFlags.Paths, \"paths\", \"d\", []string{}, \"clone projects by path\")\n\terr = cmd.RegisterFlagCompletionFunc(\"paths\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\toptions := config.GetProjectPaths()\n\t\treturn options, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\treturn &cmd\n}\n\nfunc runSync(\n\tconfig *dao.Config,\n\targs []string,\n\tprojectFlags core.ProjectFlags,\n\tsyncFlags core.SyncFlags,\n\tsetSyncFlags core.SetSyncFlags,\n) {\n\n\t// If no flag is set for targetting projects, then assume all projects\n\tvar allProjects bool\n\tif len(args) == 0 &&\n\t\tprojectFlags.TagsExpr == \"\" &&\n\t\tlen(projectFlags.Paths) == 0 &&\n\t\tlen(projectFlags.Tags) == 0 {\n\t\tallProjects = true\n\t}\n\n\tprojects, err := config.FilterProjects(false, allProjects, args, projectFlags.Paths, projectFlags.Tags, projectFlags.TagsExpr)\n\tcore.CheckIfError(err)\n\n\tif !syncFlags.Status {\n\t\tif setSyncFlags.SyncRemotes {\n\t\t\tconfig.SyncRemotes = &syncFlags.SyncRemotes\n\t\t}\n\n\t\tif setSyncFlags.RemoveOrphanedWorktrees {\n\t\t\tconfig.RemoveOrphanedWorktrees = &syncFlags.RemoveOrphanedWorktrees\n\t\t}\n\n\t\tif setSyncFlags.SyncGitignore {\n\t\t\tconfig.SyncGitignore = &syncFlags.SyncGitignore\n\t\t}\n\n\t\tif *config.SyncGitignore {\n\t\t\terr := exec.UpdateGitignoreIfExists(config)\n\t\t\tcore.CheckIfError(err)\n\t\t}\n\n\t\terr = exec.CloneRepos(config, projects, syncFlags)\n\t\tcore.CheckIfError(err)\n\t}\n\n\terr = exec.PrintProjectStatus(config, projects)\n\tcore.CheckIfError(err)\n}\n"
  },
  {
    "path": "cmd/tui.go",
    "content": "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/tui\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc tuiCmd(config *dao.Config, configErr *error) *cobra.Command {\n\tvar tuiFlags core.TUIFlags\n\n\tcmd := cobra.Command{\n\t\tUse:     \"tui\",\n\t\tAliases: []string{\"gui\"},\n\t\tShort:   \"TUI\",\n\t\tLong:    `Run TUI`,\n\t\tExample: `  # Open tui\n  mani tui`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tcore.CheckIfError(*configErr)\n\t\t\treloadChanged := cmd.Flags().Changed(\"reload-on-change\")\n\t\t\treload := config.ReloadTUI\n\t\t\tif reloadChanged {\n\t\t\t\treload = &tuiFlags.Reload\n\t\t\t}\n\n\t\t\ttui.RunTui(config, tuiFlags.Theme, *reload)\n\t\t},\n\t\tDisableAutoGenTag: true,\n\t}\n\n\tcmd.PersistentFlags().StringVar(&tuiFlags.Theme, \"theme\", \"default\", \"set theme\")\n\terr := cmd.RegisterFlagCompletionFunc(\"theme\", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif *configErr != nil {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\tnames := config.GetThemeNames()\n\n\t\treturn names, cobra.ShellCompDirectiveDefault\n\t})\n\tcore.CheckIfError(err)\n\n\tcmd.Flags().BoolVarP(&tuiFlags.Reload, \"reload-on-change\", \"r\", false, \"reload mani on config change\")\n\n\treturn &cmd\n}\n"
  },
  {
    "path": "core/config.man",
    "content": ".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, which may be git repositories, in which case they have an URL attribute\n.PD 0\n.IP \"\\(bu\" 2\n\\fBtasks\\fR are shell commands that you write and then run for selected \\fBprojects\\fR\n.IP \"\\(bu\" 2\n\\fBspecs\\fR are configs that alter \\fBtask\\fR execution and output\n.PD 0\n.IP \"\\(bu\" 2\n\\fBtargets\\fR are configs that provide shorthand filtering of \\fBprojects\\fR when executing tasks\n.PD 0\n.IP \"\\(bu\" 2\n\\fBenv\\fR are environment variables that can be defined globally, per project and per task\n.PD 0\n.RE\n\n\\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.\n\nCheck the files and environment section to see how the config file is loaded.\n\nBelow is a config file detailing all of the available options and their defaults.\n\n.RS 4\n # Import projects/tasks/env/specs/themes/targets from other configs\n import:\n   - ./some-dir/mani.yaml\n\n # Shell used for commands\n # If you use any other program than bash, zsh, sh, node, and python\n # then you have to provide the command flag if you want the command-line string evaluted\n # For instance: bash -c\n shell: bash\n\n # If set to true, mani will override the URL of any existing remote\n # and remove remotes not found in the config\n sync_remotes: false\n\n # Determines whether the .gitignore should be updated when syncing projects\n sync_gitignore: true\n\n # When running the TUI, specifies whether it should reload when the mani config is changed\n reload_tui_on_change: false\n\n # List of Projects\n projects:\n   # Project name [required]\n   pinto:\n     # Determines if the project should be synchronized during 'mani sync'\n     sync: true\n\n     # Project path relative to the config file\n     # Defaults to project name if not specified\n     path: frontend/pinto\n\n     # Repository URL\n     url: git@github.com:alajmo/pinto\n\n     # Project description\n     desc: A vim theme editor\n\n     # Custom clone command\n     # Defaults to \"git clone URL PATH\"\n     clone: git clone git@github.com:alajmo/pinto --branch main\n\n     # Branch to use as primary HEAD when cloning\n     # Defaults to repository's primary HEAD\n     branch:\n\n     # When true, clones only the specified branch or primary HEAD\n     single_branch: false\n\n     # Project tags\n     tags: [dev]\n\n     # Remote repositories\n     # Key is the remote name, value is the URL\n     remotes:\n       foo: https://github.com/bar\n\n     # Project-specific environment variables\n     env:\n       # Simple string value\n       branch: main\n\n       # Shell command substitution\n       date: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n # List of Specs\n specs:\n   default:\n     # Output format for task results\n     # Options: stream, table, html, markdown\n     output: stream\n\n     # Enable parallel task execution\n     parallel: false\n\n     # Maximum number of concurrent tasks when running in parallel\n     forks: 4\n\n     # When true, continues execution if a command fails in a multi-command task\n     ignore_errors: false\n\n     # When true, skips project entries in the config that don't exist\n     # on the filesystem without throwing an error\n     ignore_non_existing: false\n\n     # Hide projects with no command output\n     omit_empty_rows: false\n\n     # Hide columns with no data\n     omit_empty_columns: false\n\n     # Clear screen before task execution (TUI only)\n     clear_output: true\n\n # List of targets\n targets:\n   default:\n     # Select all projects\n     all: false\n\n     # Select project in current working directory\n     cwd: false\n\n     # Select projects by name\n     projects: []\n\n     # Select projects by path\n     paths: []\n\n     # Select projects by tag\n     tags: []\n\n     # Select projects by tag expression\n     tags_expr: \"\"\n\n # Environment variables available to all tasks\n env:\n   # Simple string value\n   AUTHOR: \"alajmo\"\n\n   # Shell command substitution\n   DATE: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n # List of tasks\n tasks:\n   # Command name [required]\n   simple-2: echo \"hello world\"\n\n   # Command name [required]\n   simple-1:\n     cmd: |\n       echo \"hello world\"\n     desc: simple command 1\n\n   # Command name [required]\n   advanced-command:\n     # Task description\n     desc: complex task\n\n     # Task theme\n     theme: default\n\n     # Shell interpreter\n     shell: bash\n\n     # Task-specific environment variables\n     env:\n       # Static value\n       branch: main\n\n       # Dynamic shell command output\n       num_lines: $(ls -1 | wc -l)\n\n     # Can reference predefined spec:\n     # spec: custom_spec\n     # or define inline:\n     spec:\n       output: table\n       parallel: true\n       forks: 4\n       ignore_errors: false\n       ignore_non_existing: true\n       omit_empty_rows: true\n       omit_empty_columns: true\n\n     # Can reference predefined target:\n     # target: custom_target\n     # or define inline:\n     target:\n       all: true\n       cwd: false\n       projects: [pinto]\n       paths: [frontend]\n       tags: [dev]\n       tags_expr: (prod || dev) && !test\n\n     # Single multi-line command\n     cmd: |\n       echo complex\n       echo command\n\n     # Multiple commands\n     commands:\n       # Node.js command example\n       - name: node-example\n \t       shell: node\n         cmd: console.log(\"hello world from node.js\");\n\n       # Reference to another task\n       - task: simple-1\n\n # List of themes\n # Styling Options:\n #   Fg (foreground color): Empty string (\"\"), hex color, or named color from W3C standard\n #   Bg (background color): Empty string (\"\"), hex color, or named color from W3C standard\n #   Format: Empty string (\"\"), \"lower\", \"title\", \"upper\"\n #   Attribute: Empty string (\"\"), \"bold\", \"italic\", \"underline\"\n #   Alignment: Empty string (\"\"), \"left\", \"center\", \"right\"\n themes:\n   # Theme name [required]\n   default:\n     # Stream Output Configuration\n     stream:\n       # Include project name prefix for each line\n       prefix: true\n\n       # Colors to alternate between for each project prefix\n       prefix_colors: [\"#d787ff\", \"#00af5f\", \"#d75f5f\", \"#5f87d7\", \"#00af87\", \"#5f00ff\"]\n\n       # Add a header before each project\n       header: true\n\n       # String value that appears before the project name in the header\n       header_prefix: \"TASK\"\n\n       # Fill remaining spaces with a character after the prefix\n       header_char: \"*\"\n\n     # Table Output Configuration\n     table:\n       # Table style\n       # Available options: ascii, light, bold, double, rounded\n       style: ascii\n\n       # Border options for table output\n       border:\n         around: false  # Border around the table\n         columns: true  # Vertical border between columns\n         header: true   # Horizontal border between headers and rows\n         rows: false    # Horizontal border between rows\n\n       header:\n         fg: \"#d787ff\"\n         attr: bold\n         format: \"\"\n\n       title_column:\n         fg: \"#5f87d7\"\n         attr: bold\n         format: \"\"\n\n     # Tree View Configuration\n     tree:\n       # Tree style\n       # Available options: ascii, light, bold, double, rounded, bullet-square, bullet-circle, bullet-star\n       style: ascii\n\n     # Block Display Configuration\n     block:\n       key:\n         fg: \"#5f87d7\"\n       separator:\n         fg: \"#5f87d7\"\n       value:\n         fg:\n       value_true:\n         fg: \"#00af5f\"\n       value_false:\n         fg: \"#d75f5f\"\n\n      # TUI Configuration\n      tui:\n        default:\n          fg:\n          bg:\n          attr:\n\n        border:\n          fg:\n        border_focus:\n          fg: \"#d787ff\"\n\n        title:\n          fg:\n          bg:\n          attr:\n          align: center\n        title_active:\n          fg: \"#000000\"\n          bg: \"#d787ff\"\n          attr:\n          align: center\n\n        button:\n          fg:\n          bg:\n          attr:\n          format:\n        button_active:\n          fg: \"#080808\"\n          bg: \"#d787ff\"\n          attr:\n          format:\n\n        table_header:\n          fg: \"#d787ff\"\n          bg:\n          attr: bold\n          align: left\n          format:\n\n        item:\n          fg:\n          bg:\n          attr:\n        item_focused:\n          fg: \"#ffffff\"\n          bg: \"#262626\"\n          attr:\n        item_selected:\n          fg: \"#5f87d7\"\n          bg:\n          attr:\n        item_dir:\n          fg: \"#d787ff\"\n          bg:\n          attr:\n        item_ref:\n          fg: \"#d787ff\"\n          bg:\n          attr:\n\n        search_label:\n          fg: \"#d7d75f\"\n          bg:\n          attr: bold\n        search_text:\n          fg:\n          bg:\n          attr:\n\n        filter_label:\n          fg: \"#d7d75f\"\n          bg:\n          attr: bold\n        filter_text:\n          fg:\n          bg:\n          attr:\n\n        shortcut_label:\n          fg: \"#00af5f\"\n          bg:\n          attr:\n        shortcut_text:\n          fg:\n          bg:\n          attr:\n.RE\n\n\n.SH EXAMPLES\n\n.TP\nInitialize mani\n.B samir@hal-9000 ~ $ mani init\n\n.nf\nInitialized mani repository in /tmp\n- Created mani.yaml\n- Created .gitignore\n\nFollowing projects were added to mani.yaml\n\n Project  | Path\n----------+------------\n test     | .\n pinto    | dev/pinto\n.fi\n\n.TP\nClone projects\n.B samir@hal-9000 ~ $ mani sync --parallel --forks 8\n.nf\npinto | Cloning into '/tmp/dev/pinto'...\n\n Project  | Synced\n----------+--------\n test     | ✓\n pinto    | ✓\n.fi\n\n.TP\nList all projects\n.B samir@hal-9000 ~ $ mani list projects\n.nf\n Project\n---------\n test\n pinto\n.fi\n\n.TP\nList all projects with output set to tree\n.nf\n.B samir@hal-9000 ~ $ mani list projects --tree\n    ── dev\n       └─ pinto\n.fi\n\n.nf\n\n.TP\nList all tags\n.B samir@hal-9000 ~ $ mani list tags\n.nf\n Tag | Project\n-----+---------\n dev | pinto\n.fi\n\n.TP\nList all tasks\n.nf\n.B samir@hal-9000 ~ $ mani list tasks\n Task             | Description\n------------------+------------------\n simple-1         | simple command 1\n simple-2         |\n advanced-command | complex task\n.fi\n\n.TP\nDescribe a task\n.nf\n.B samir@hal-9000 ~ $ mani describe tasks advanced-command\nName: advanced-command\nDescription: complex task\nTheme: default\nTarget:\n    All: true\n    Cwd: false\n    Projects: pinto\n    Paths: frontend\n    Tags: dev\n    TagsExpr: \"\"\nSpec:\n    Output: table\n    Parallel: true\n    Forks: 4\n    IgnoreErrors: false\n    IgnoreNonExisting: false\n    OmitEmptyRows: false\n    OmitEmptyColumns: false\nEnv:\n    branch: dev\n    num_lines: 2\nCmd:\n    echo advanced\n    echo command\nCommands:\n     - simple-1\n     - simple-2\n     - cmd\n.fi\n\n.TP\nRun a task for all projects with tag 'dev'\n.nf\n.B samir@hal-9000 ~ $ mani run simple-1 --tags dev\n Project | Simple-1\n---------+-------------\n pinto   | hello world\n.fi\n\n.TP\nRun a task for all projects matching tags expression 'dev && !prod'\n.nf\n.B samir@hal-9000 ~ $ mani run simple-1 --tags-expr '(dev && !prod)'\n Project | Simple-1\n---------+-------------\n pinto   | hello world\n.fi\n\n.TP\nRun ad-hoc command for all projects\n.nf\n.B samir@hal-9000 ~ $ mani exec 'echo 123' --all\n Project | Output\n---------+--------\n archive | 123\n pinto   | 123\n.fi\n\n.SH FILTERING PROJECTS\nProjects can be filtered when managing projects (sync, list, describe) or running tasks. \nFilters can be specified through CLI flags or target configurations. \nThe filtering is inclusive, meaning projects must satisfy all specified filters to be included in the results.\n\n.PP\nAvailable options:\n.RS 2\n.IP \"\\(bu\" 2\ncwd: include only the project under the current working directory, ignoring all other filters\n.IP \"\\(bu\" 2\nall: include all projects\n.IP \"\\(bu\" 2\nprojects: Filter by project names\n.IP \"\\(bu\" 2\npaths: Filter by project paths\n.IP \"\\(bu\" 2\ntags: Filter by project tags\n.IP \"\\(bu\" 2\ntags_expr: Filter using tag logic expressions\n.IP \"\\(bu\" 2\ntarget: Filter using target\n.RE\n\n.PP\n\nFor \\fBmani sync/list/describe\\fR:\n.RS 2\n.IP \"\\(bu\" 2\nNo filters: Targets all projects\n.IP \"\\(bu\" 2\nMultiple filters: Select intersection of projects/paths/tags/tags_expr/target filters\n.RE\n\nFor \\fBmani run/exec\\fR:\n.RS 2\n.IP \"1.\" 4\nRuntime flags (highest priority)\n.IP \"2.\" 4\nTarget flag configuration (\\fB--target\\fR)\n.IP \"3.\" 4\nTask's default target data (lowest priority)\n.RE\n\nThe 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`.\n\n.SH TAGS EXPRESSION\n\nTag expressions allow filtering projects using boolean operations on their tags. \nThe expression is evaluated for each project's tags to determine if the project should be included.\n\n.PP\nOperators (in precedence order):\n\n.RS 2\n.IP \"\\(bu\" 2\n(): Parentheses for grouping\n.PD 0\n.IP \"\\(bu\" 2\n!: NOT operator (logical negation)\n.PD 0\n.IP \"\\(bu\" 2\n&&: AND operator (logical conjunction)\n.PD 0\n.IP \"\\(bu\" 2\n||: OR operator (logical disjunction)\n.RE\n\n.PP\nFor example, the expression:\n\n  \\fB(main && (dev || prod)) && !test\\fR\n\n.PP\nrequires the projects to pass these conditions:\n\n.RS 2\n.IP \"\\(bu\" 2\nMust have \"main\" tag\n.PD 0\n.IP \"\\(bu\" 2\nMust have either \"dev\" OR \"prod\" tag\n.IP \"\\(bu\" 2\nMust NOT have \"test\" tag\n.PD 0\n.RE\n\n.SH FILES\n\nWhen running a command,\n.B mani\nwill check the current directory and all parent directories for the following files: mani.yaml, mani.yml, .mani.yaml, .mani.yml.\n\nAdditionally, it will import (if found) a config file from:\n\n.RS 2\n.IP \"\\(bu\" 2\nLinux: \\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.\n.IP \"\\(bu\" 2\nDarwin: \\fB$HOME/Library/Application Support/mani/config.yaml\\fR\n.IP \"\\(bu\" 2\nWindows: \\fB%AppData%\\mani\\fR\n.RE\n\nBoth the config and user config can be specified via flags or environments variables.\n\n.SH\nENVIRONMENT\n\n.TP\n.B MANI_CONFIG\nOverride config file path\n\n.TP\n.B MANI_USER_CONFIG\nOverride user config file path\n\n.TP\n.B NO_COLOR\nIf this env variable is set (regardless of value) then all colors will be disabled\n\n.SH BUGS\n\nSee GitHub Issues:\n.UR https://github.com/alajmo/mani/issues\n.ME .\n\n.SH AUTHOR\n\n.B mani\nwas written by Samir Alajmovic\n.MT alajmovic.samir@gmail.com\n.ME .\nFor updates and more information go to\n.UR https://\\:www.manicli.com\nmanicli.com\n.UE .\n"
  },
  {
    "path": "core/dao/benchmark_test.go",
    "content": "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/themes/targets\nfunc createBenchmarkConfig(numProjects, numTasks int) Config {\n\tconfig := Config{}\n\n\t// Create projects\n\tconfig.ProjectList = make([]Project, numProjects)\n\tfor i := 0; i < numProjects; i++ {\n\t\tconfig.ProjectList[i] = Project{\n\t\t\tName:    fmt.Sprintf(\"project-%d\", i),\n\t\t\tPath:    fmt.Sprintf(\"/path/to/project-%d\", i),\n\t\t\tRelPath: fmt.Sprintf(\"project-%d\", i),\n\t\t\tTags:    []string{\"tag1\", \"tag2\"},\n\t\t}\n\t}\n\n\t// Create tasks\n\tconfig.TaskList = make([]Task, numTasks)\n\tfor i := 0; i < numTasks; i++ {\n\t\tconfig.TaskList[i] = Task{\n\t\t\tName: fmt.Sprintf(\"task-%d\", i),\n\t\t\tCmd:  fmt.Sprintf(\"echo task %d\", i),\n\t\t}\n\t}\n\n\t// Create specs\n\tconfig.SpecList = []Spec{\n\t\t{Name: \"default\", Output: \"stream\", Forks: 4},\n\t\t{Name: \"parallel\", Output: \"stream\", Parallel: true, Forks: 8},\n\t}\n\n\t// Create themes\n\tconfig.ThemeList = []Theme{\n\t\t{Name: \"default\"},\n\t\t{Name: \"custom\"},\n\t}\n\n\t// Create targets\n\tconfig.TargetList = []Target{\n\t\t{Name: \"default\", All: true},\n\t\t{Name: \"frontend\", Tags: []string{\"frontend\"}},\n\t}\n\n\treturn config\n}\n\n// Lookup_GetProject: Find project by name (O(n) linear search)\nfunc BenchmarkLookup_GetProject(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\t// Look up a project in the middle\n\t\t\ttargetName := fmt.Sprintf(\"project-%d\", size/2)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProject(targetName)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Lookup_GetTask: Find task by name (O(n) linear search)\nfunc BenchmarkLookup_GetTask(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"tasks_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(10, size)\n\t\t\t// Look up a task in the middle\n\t\t\ttargetName := fmt.Sprintf(\"task-%d\", size/2)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetTask(targetName)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Lookup_GetSpec: Find spec by name\nfunc BenchmarkLookup_GetSpec(b *testing.B) {\n\tconfig := createBenchmarkConfig(10, 10)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = config.GetSpec(\"default\")\n\t}\n}\n\n// Lookup_GetTheme: Find theme by name\nfunc BenchmarkLookup_GetTheme(b *testing.B) {\n\tconfig := createBenchmarkConfig(10, 10)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = config.GetTheme(\"default\")\n\t}\n}\n\n// Lookup_GetTarget: Find target by name\nfunc BenchmarkLookup_GetTarget(b *testing.B) {\n\tconfig := createBenchmarkConfig(10, 10)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = config.GetTarget(\"default\")\n\t}\n}\n\n// Filter_ByName: Filter projects by name list\nfunc BenchmarkFilter_ByName(b *testing.B) {\n\tsizes := []int{10, 50, 100}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\t// Look up 5 projects\n\t\t\tnames := []string{\n\t\t\t\tfmt.Sprintf(\"project-%d\", size/5),\n\t\t\t\tfmt.Sprintf(\"project-%d\", size/4),\n\t\t\t\tfmt.Sprintf(\"project-%d\", size/3),\n\t\t\t\tfmt.Sprintf(\"project-%d\", size/2),\n\t\t\t\tfmt.Sprintf(\"project-%d\", size-1),\n\t\t\t}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByName(names)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Filter_ByTags: Filter projects by tags\nfunc BenchmarkFilter_ByTags(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\ttags := []string{\"tag1\"}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByTags(tags)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Filter_ByPath: Filter by path patterns (simple, *, **)\nfunc BenchmarkFilter_ByPath(b *testing.B) {\n\tsizes := []int{10, 50, 100}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d_simple\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\tpaths := []string{\"project-1\"}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByPath(paths)\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_glob\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\tpaths := []string{\"project-*\"}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByPath(paths)\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_doubleglob\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\tpaths := []string{\"**/project-*\"}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByPath(paths)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Filter_Combined: FilterProjects with multiple criteria\nfunc BenchmarkFilter_Combined(b *testing.B) {\n\tsizes := []int{10, 50, 100}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d_all\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.FilterProjects(false, true, nil, nil, nil, \"\")\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_bytags\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.FilterProjects(false, false, nil, nil, []string{\"tag1\"}, \"\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Util_ConfigLoad: Simulates config loading (ParseTask lookups)\nfunc BenchmarkUtil_ConfigLoad(b *testing.B) {\n\ttaskCounts := []int{10, 25, 50, 100}\n\n\tfor _, numTasks := range taskCounts {\n\t\tb.Run(fmt.Sprintf(\"tasks_%d\", numTasks), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(50, numTasks)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t// Simulate what happens during config load:\n\t\t\t\t// Each task calls GetTheme, GetSpec, GetTarget\n\t\t\t\tfor j := 0; j < numTasks; j++ {\n\t\t\t\t\t_, _ = config.GetTheme(\"default\")\n\t\t\t\t\t_, _ = config.GetSpec(\"default\")\n\t\t\t\t\t_, _ = config.GetTarget(\"default\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Lookup_GetCommand: Find task and convert to command\nfunc BenchmarkLookup_GetCommand(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"tasks_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(10, size)\n\t\t\ttargetName := fmt.Sprintf(\"task-%d\", size/2)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetCommand(targetName)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Filter_ByTagsExpr: Filter using tag expressions (&&, ||, !)\nfunc BenchmarkFilter_ByTagsExpr(b *testing.B) {\n\tsizes := []int{10, 50, 100}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d_simple\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByTagsExpr(\"tag1\")\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_and\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByTagsExpr(\"tag1 && tag2\")\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_or\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByTagsExpr(\"tag1 || tag2\")\n\t\t\t}\n\t\t})\n\n\t\tb.Run(fmt.Sprintf(\"projects_%d_complex\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_, _ = config.GetProjectsByTagsExpr(\"(tag1 && tag2) || !tag3\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Util_GetCwdProject: Find project matching current directory\nfunc BenchmarkUtil_GetCwdProject(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t// This will search through all projects\n\t\t\t\t// In real usage, it matches against cwd\n\t\t\t\t_, _ = config.GetCwdProject()\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Filter_Intersect: Intersection of project lists\nfunc BenchmarkFilter_Intersect(b *testing.B) {\n\tsizes := []int{10, 50, 100}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"projects_%d\", size), func(b *testing.B) {\n\t\t\tconfig := createBenchmarkConfig(size, 10)\n\t\t\t// Create two overlapping project lists\n\t\t\tlist1 := config.ProjectList[:size/2]\n\t\t\tlist2 := config.ProjectList[size/4:]\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t_ = config.GetIntersectProjects(list1, list2)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/common.go",
    "content": "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\"github.com/alajmo/mani/core\"\n)\n\n// Resource Errors\n\ntype ResourceErrors[T any] struct {\n\tResource *T\n\tErrors   []error\n}\n\ntype Resource interface {\n\tGetContext() string\n\tGetContextLine() int\n}\n\nfunc FormatErrors(re Resource, errs []error) error {\n\tvar msg = \"\"\n\tpartsRe := regexp.MustCompile(`line (\\d*): (.*)`)\n\n\tcontext := re.GetContext()\n\n\tvar errPrefix = color.FgRed.Sprintf(\"error\")\n\tvar ptrPrefix = color.FgBlue.Sprintf(\"-->\")\n\tfor _, err := range errs {\n\t\tmatch := partsRe.FindStringSubmatch(err.Error())\n\t\t// In-case matching fails, return unformatted error\n\t\tif len(match) != 3 {\n\t\t\tcontextLine := re.GetContextLine()\n\n\t\t\tif contextLine == -1 {\n\t\t\t\tmsg = fmt.Sprintf(\"%s%s: %s\\n  %s %s\\n\\n\", msg, errPrefix, err, ptrPrefix, context)\n\t\t\t} else {\n\t\t\t\tmsg = fmt.Sprintf(\"%s%s: %s\\n  %s %s:%d\\n\\n\", msg, errPrefix, err, ptrPrefix, context, contextLine)\n\t\t\t}\n\t\t} else {\n\t\t\tmsg = fmt.Sprintf(\"%s%s: %s\\n  %s %s:%s\\n\\n\", msg, errPrefix, match[2], ptrPrefix, context, match[1])\n\t\t}\n\t}\n\n\tif msg != \"\" {\n\t\treturn &core.ConfigErr{Msg: msg}\n\t}\n\n\treturn nil\n}\n\n// ENV\n\nfunc ParseNodeEnv(node yaml.Node) []string {\n\tvar envs []string\n\tcount := len(node.Content)\n\n\tfor i := 0; i < count; i += 2 {\n\t\tenv := fmt.Sprintf(\"%v=%v\", node.Content[i].Value, node.Content[i+1].Value)\n\t\tenvs = append(envs, env)\n\t}\n\n\treturn envs\n}\n\nfunc EvaluateEnv(envList []string) ([]string, error) {\n\tvar envs []string\n\n\tfor _, arg := range envList {\n\t\tkv := strings.SplitN(arg, \"=\", 2)\n\n\t\tif val, hasPrefix := strings.CutPrefix(kv[1], \"$(\"); hasPrefix {\n\t\t\tif cmdStr, hasSuffix := strings.CutSuffix(val, \")\"); hasSuffix {\n\t\t\t\tcmd := exec.Command(\"sh\", \"-c\", cmdStr)\n\t\t\t\tcmd.Env = os.Environ()\n\t\t\t\tout, err := cmd.CombinedOutput()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn envs, &core.ConfigEnvFailed{Name: kv[0], Err: string(out)}\n\t\t\t\t}\n\n\t\t\t\tenvs = append(envs, fmt.Sprintf(\"%v=%v\", kv[0], string(out)))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tenvs = append(envs, fmt.Sprintf(\"%v=%v\", kv[0], kv[1]))\n\t}\n\n\treturn envs, nil\n}\n\n// MergeEnvs Merges environment variables.\n// Priority is from highest to lowest (1st env takes precedence over the last entry).\nfunc MergeEnvs(envs ...[]string) []string {\n\tvar mergedEnvs []string\n\targs := make(map[string]bool)\n\n\tfor _, part := range envs {\n\t\tfor _, elem := range part {\n\t\t\telem = strings.TrimSuffix(elem, \"\\n\")\n\n\t\t\tkv := strings.SplitN(elem, \"=\", 2)\n\t\t\t_, ok := args[kv[0]]\n\n\t\t\tif !ok {\n\t\t\t\tmergedEnvs = append(mergedEnvs, elem)\n\t\t\t\targs[kv[0]] = true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn mergedEnvs\n}\n"
  },
  {
    "path": "core/dao/common_test.go",
    "content": "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\t\tname     string\n\t\tinput    yaml.Node\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"basic env variables\",\n\t\t\tinput: yaml.Node{\n\t\t\t\tContent: []*yaml.Node{\n\t\t\t\t\t{Value: \"KEY1\"},\n\t\t\t\t\t{Value: \"value1\"},\n\t\t\t\t\t{Value: \"KEY2\"},\n\t\t\t\t\t{Value: \"value2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"KEY1=value1\",\n\t\t\t\t\"KEY2=value2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty env\",\n\t\t\tinput: yaml.Node{\n\t\t\t\tContent: []*yaml.Node{},\n\t\t\t},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"env with special characters\",\n\t\t\tinput: yaml.Node{\n\t\t\t\tContent: []*yaml.Node{\n\t\t\t\t\t{Value: \"PATH\"},\n\t\t\t\t\t{Value: \"/usr/bin:/bin\"},\n\t\t\t\t\t{Value: \"URL\"},\n\t\t\t\t\t{Value: \"http://example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PATH=/usr/bin:/bin\",\n\t\t\t\t\"URL=http://example.com\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := ParseNodeEnv(tt.input)\n\t\t\tif !equalStringSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEnv_MergeEnvs(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinputs   [][]string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"basic merge\",\n\t\t\tinputs: [][]string{\n\t\t\t\t{\"KEY1=value1\", \"KEY2=value2\"},\n\t\t\t\t{\"KEY3=value3\"},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"KEY1=value1\",\n\t\t\t\t\"KEY2=value2\",\n\t\t\t\t\"KEY3=value3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"override priority\",\n\t\t\tinputs: [][]string{\n\t\t\t\t{\"KEY1=override\"},\n\t\t\t\t{\"KEY1=original\", \"KEY2=value2\"},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"KEY1=override\",\n\t\t\t\t\"KEY2=value2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty inputs\",\n\t\t\tinputs: [][]string{\n\t\t\t\t{},\n\t\t\t\t{},\n\t\t\t},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"with newline characters\",\n\t\t\tinputs: [][]string{\n\t\t\t\t{\"KEY1=value1\\n\", \"KEY2=value2\\n\"},\n\t\t\t\t{\"KEY3=value3\\n\"},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"KEY1=value1\",\n\t\t\t\t\"KEY2=value2\",\n\t\t\t\t\"KEY3=value3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex values\",\n\t\t\tinputs: [][]string{\n\t\t\t\t{\"PATH=/usr/bin:/bin\", \"URL=http://example.com\"},\n\t\t\t\t{\"DEBUG=true\", \"PATH=/custom/path\"},\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"PATH=/usr/bin:/bin\",\n\t\t\t\t\"URL=http://example.com\",\n\t\t\t\t\"DEBUG=true\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := MergeEnvs(tt.inputs...)\n\t\t\tif !equalStringSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/config.go",
    "content": "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/core\"\n\t\"github.com/gookit/color\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nvar (\n\tDEFAULT_SHELL         = \"bash -c\"\n\tDEFAULT_SHELL_PROGRAM = \"bash\"\n\tACCEPTABLE_FILE_NAMES = []string{\"mani.yaml\", \"mani.yml\", \".mani.yaml\", \".mani.yml\"}\n\n\tDEFAULT_THEME = Theme{\n\t\tName:   \"default\",\n\t\tStream: DefaultStream,\n\t\tTable:  DefaultTable,\n\t\tTree:   DefaultTree,\n\t\tTUI:    DefaultTUI,\n\t\tBlock:  DefaultBlock,\n\t\tColor:  core.Ptr(true),\n\t}\n\n\tDEFAULT_TARGET = Target{\n\t\tName: \"default\",\n\n\t\tAll: false,\n\t\tCwd: false,\n\n\t\tProjects: []string{},\n\t\tPaths:    []string{},\n\t\tTags:     []string{},\n\n\t\tTagsExpr: \"\",\n\t}\n\n\tDEFAULT_SPEC = Spec{\n\t\tName:   \"default\",\n\t\tOutput: \"stream\",\n\n\t\tParallel: false,\n\t\tForks:    4,\n\n\t\tIgnoreErrors:      false,\n\t\tIgnoreNonExisting: false,\n\n\t\tOmitEmptyRows:    false,\n\t\tOmitEmptyColumns: false,\n\n\t\tClearOutput: true,\n\t}\n)\n\ntype Config struct {\n\t// Internal\n\tEnvList        []string  `yaml:\"-\"`\n\tImportData     []Import  `yaml:\"-\"`\n\tThemeList      []Theme   `yaml:\"-\"`\n\tSpecList       []Spec    `yaml:\"-\"`\n\tTargetList     []Target  `yaml:\"-\"`\n\tProjectList    []Project `yaml:\"-\"`\n\tTaskList       []Task    `yaml:\"-\"`\n\tPath           string    `yaml:\"-\"`\n\tDir            string    `yaml:\"-\"`\n\tUserConfigFile *string   `yaml:\"-\"`\n\tConfigPaths    []string  `yaml:\"-\"`\n\tColor          bool      `yaml:\"-\"`\n\n\tShell                   string `yaml:\"shell\"`\n\tSyncRemotes             *bool  `yaml:\"sync_remotes\"`\n\tSyncGitignore           *bool  `yaml:\"sync_gitignore\"`\n\tRemoveOrphanedWorktrees *bool  `yaml:\"remove_orphaned_worktrees\"`\n\tReloadTUI               *bool  `yaml:\"reload_tui_on_change\"`\n\n\t// Intermediate\n\tEnv      yaml.Node `yaml:\"env\"`\n\tImport   yaml.Node `yaml:\"import\"`\n\tThemes   yaml.Node `yaml:\"themes\"`\n\tSpecs    yaml.Node `yaml:\"specs\"`\n\tTargets  yaml.Node `yaml:\"targets\"`\n\tProjects yaml.Node `yaml:\"projects\"`\n\tTasks    yaml.Node `yaml:\"tasks\"`\n}\n\nfunc (c *Config) GetContext() string {\n\treturn c.Path\n}\n\nfunc (c *Config) GetContextLine() int {\n\treturn -1\n}\n\n// Returns the config env list as a string splice in the form [key=value, key1=$(echo 123)]\nfunc (c Config) GetEnvList() []string {\n\tvar envs []string\n\tcount := len(c.Env.Content)\n\tfor i := 0; i < count; i += 2 {\n\t\tenv := fmt.Sprintf(\"%v=%v\", c.Env.Content[i].Value, c.Env.Content[i+1].Value)\n\t\tenvs = append(envs, env)\n\t}\n\n\treturn envs\n}\n\nfunc getUserConfigFile(userConfigPath string) *string {\n\t// Flag\n\tif userConfigPath != \"\" {\n\t\tif _, err := os.Stat(userConfigPath); err == nil {\n\t\t\treturn &userConfigPath\n\t\t}\n\t}\n\n\t// Env\n\tval, present := os.LookupEnv(\"MANI_USER_CONFIG\")\n\tif present {\n\t\treturn &val\n\t}\n\n\t// Default\n\tdefaultUserConfigDir, _ := os.UserConfigDir()\n\tdefaultUserConfigPath := filepath.Join(defaultUserConfigDir, \"mani\", \"config.yaml\")\n\tif _, err := os.Stat(defaultUserConfigPath); err == nil {\n\t\treturn &defaultUserConfigPath\n\t}\n\n\treturn nil\n}\n\n// Function to read Mani configs.\nfunc ReadConfig(configFilepath string, userConfigPath string, colorFlag bool) (Config, error) {\n\tcolor := CheckUserColor(colorFlag)\n\tvar configPath string\n\n\tuserConfigFile := getUserConfigFile(userConfigPath)\n\n\t// Try to find config file in current directory and all parents\n\tif configFilepath != \"\" {\n\t\tfilename, err := filepath.Abs(configFilepath)\n\t\tif err != nil {\n\t\t\treturn Config{}, err\n\t\t}\n\n\t\tconfigPath = filename\n\t} else {\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn Config{}, err\n\t\t}\n\n\t\t// Check first cwd and all parent directories, then if not found,\n\t\t// check if env variable MANI_CONFIG is set, and if not found\n\t\t// return no config found\n\t\tfilename, err := core.FindFileInParentDirs(wd, ACCEPTABLE_FILE_NAMES)\n\t\tif err != nil {\n\t\t\tval, present := os.LookupEnv(\"MANI_CONFIG\")\n\t\t\tif present {\n\t\t\t\tfilename = val\n\t\t\t} else {\n\t\t\t\treturn Config{}, err\n\t\t\t}\n\t\t}\n\n\t\tfilename, err = core.ResolveTildePath(filename)\n\t\tif err != nil {\n\t\t\treturn Config{}, err\n\t\t}\n\n\t\tfilename, err = filepath.Abs(filename)\n\t\tif err != nil {\n\t\t\treturn Config{}, err\n\t\t}\n\n\t\tconfigPath = filename\n\t}\n\n\tdat, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn Config{}, err\n\t}\n\n\t// Found config, now try to read it\n\tvar config Config\n\n\tconfig.Path = configPath\n\tconfig.Dir = filepath.Dir(configPath)\n\tconfig.UserConfigFile = userConfigFile\n\tconfig.Color = color\n\n\terr = yaml.Unmarshal(dat, &config)\n\tif err != nil {\n\t\tre := ResourceErrors[Config]{Resource: &config, Errors: []error{err}}\n\t\treturn config, FormatErrors(re.Resource, re.Errors)\n\t}\n\n\t// Set default shell command\n\tif config.Shell == \"\" {\n\t\tconfig.Shell = DEFAULT_SHELL\n\t} else {\n\t\tconfig.Shell = core.FormatShell(config.Shell)\n\t}\n\n\t// Set Sync Gitignore\n\tif config.SyncGitignore == nil {\n\t\tconfig.SyncGitignore = core.Ptr(true)\n\t}\n\n\t// Set Reload TUI\n\tif config.ReloadTUI == nil {\n\t\tconfig.ReloadTUI = core.Ptr(false)\n\t}\n\n\t// Set Sync Remote\n\tif config.SyncRemotes == nil {\n\t\tconfig.SyncRemotes = core.Ptr(false)\n\t}\n\n\t// Set Remove Orphan Worktrees\n\tif config.RemoveOrphanedWorktrees == nil {\n\t\tconfig.RemoveOrphanedWorktrees = core.Ptr(false)\n\t}\n\n\tconfigResources, err := config.importConfigs()\n\tif err != nil {\n\t\treturn config, err\n\t}\n\n\tconfig.TaskList = configResources.Tasks\n\tconfig.ProjectList = configResources.Projects\n\tconfig.ThemeList = configResources.Themes\n\tconfig.SpecList = configResources.Specs\n\tconfig.TargetList = configResources.Targets\n\tconfig.EnvList = configResources.Envs\n\n\tconfig.CheckConfigNoColor()\n\n\tfor _, configPath := range configResources.Imports {\n\t\tconfig.ConfigPaths = append(config.ConfigPaths, configPath.Path)\n\t}\n\n\t// Set default theme if it's not set already\n\t_, err = config.GetTheme(DEFAULT_THEME.Name)\n\tif err != nil {\n\t\tconfig.ThemeList = append(config.ThemeList, DEFAULT_THEME)\n\t}\n\n\t// Set default spec if it's not set already\n\t_, err = config.GetSpec(DEFAULT_SPEC.Name)\n\tif err != nil {\n\t\tconfig.SpecList = append(config.SpecList, DEFAULT_SPEC)\n\t}\n\n\t// Set default target if it's not set already\n\t_, err = config.GetTarget(DEFAULT_TARGET.Name)\n\tif err != nil {\n\t\tconfig.TargetList = append(config.TargetList, DEFAULT_TARGET)\n\t}\n\n\t// Parse all tasks\n\ttaskErrors := make([]ResourceErrors[Task], len(configResources.Tasks))\n\tfor i := range configResources.Tasks {\n\t\ttaskErrors[i].Resource = &configResources.Tasks[i]\n\t\tconfigResources.Tasks[i].ParseTask(config, &taskErrors[i])\n\t}\n\n\tvar configErr = \"\"\n\tfor _, taskError := range taskErrors {\n\t\tif len(taskError.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(taskError.Resource, taskError.Errors))\n\t\t}\n\t}\n\n\tif configErr != \"\" {\n\t\treturn config, &core.ConfigErr{Msg: configErr}\n\t}\n\n\treturn config, nil\n}\n\n// Open mani config in editor\nfunc (c Config) EditConfig() error {\n\treturn openEditor(c.Path, -1)\n}\n\nfunc openEditor(path string, lineNr int) error {\n\teditor := os.Getenv(\"EDITOR\")\n\tvar args []string\n\n\tif lineNr > 0 {\n\t\tswitch editor {\n\t\tcase \"nvim\":\n\t\t\targs = []string{fmt.Sprintf(\"+%v\", lineNr), path}\n\t\tcase \"vim\":\n\t\t\targs = []string{fmt.Sprintf(\"+%v\", lineNr), path}\n\t\tcase \"vi\":\n\t\t\targs = []string{fmt.Sprintf(\"+%v\", lineNr), path}\n\t\tcase \"emacs\":\n\t\t\targs = []string{fmt.Sprintf(\"+%v\", lineNr), path}\n\t\tcase \"nano\":\n\t\t\targs = []string{fmt.Sprintf(\"+%v\", lineNr), path}\n\t\tcase \"code\": // visual studio code\n\t\t\targs = []string{\"--goto\", fmt.Sprintf(\"%s:%v\", path, lineNr)}\n\t\tcase \"idea\": // Intellij\n\t\t\targs = []string{\"--line\", fmt.Sprintf(\"%v\", lineNr), path}\n\t\tcase \"subl\": // Sublime\n\t\t\targs = []string{fmt.Sprintf(\"%s:%v\", path, lineNr)}\n\t\tcase \"atom\":\n\t\t\targs = []string{fmt.Sprintf(\"%s:%v\", path, lineNr)}\n\t\tcase \"notepad-plus-plus\":\n\t\t\targs = []string{\"-n\", fmt.Sprintf(\"%v\", lineNr), path}\n\t\tdefault:\n\t\t\targs = []string{path}\n\t\t}\n\t} else {\n\t\targs = []string{path}\n\t}\n\n\tcmd := exec.Command(editor, args...)\n\tcmd.Env = os.Environ()\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Open mani config in editor and optionally go to line matching the task name\nfunc (c Config) EditTask(name string) error {\n\tconfigPath := c.Path\n\tif name != \"\" {\n\t\ttask, err := c.GetTask(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigPath = task.context\n\t}\n\n\tdat, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttype ConfigTmp struct {\n\t\tTasks yaml.Node\n\t}\n\n\tvar configTmp ConfigTmp\n\terr = yaml.Unmarshal([]byte(dat), &configTmp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlineNr := 0\n\tif name == \"\" {\n\t\tlineNr = configTmp.Tasks.Line - 1\n\t} else {\n\t\tfor _, task := range configTmp.Tasks.Content {\n\t\t\tif task.Value == name {\n\t\t\t\tlineNr = task.Line\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn openEditor(configPath, lineNr)\n}\n\n// Open mani config in editor and optionally go to line matching the project name\nfunc (c Config) EditProject(name string) error {\n\tconfigPath := c.Path\n\tif name != \"\" {\n\t\tproject, err := c.GetProject(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigPath = project.context\n\t}\n\n\tdat, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttype ConfigTmp struct {\n\t\tProjects yaml.Node\n\t}\n\n\tvar configTmp ConfigTmp\n\terr = yaml.Unmarshal([]byte(dat), &configTmp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlineNr := 0\n\tif name == \"\" {\n\t\tlineNr = configTmp.Projects.Line - 1\n\t} else {\n\t\tfor _, project := range configTmp.Projects.Content {\n\t\t\tif project.Value == name {\n\t\t\t\tlineNr = project.Line\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn openEditor(configPath, lineNr)\n}\n\nfunc InitMani(args []string, initFlags core.InitFlags) ([]Project, error) {\n\t// Choose to initialize mani in a different directory\n\t// 1. absolute or\n\t// 2. relative or\n\t// 3. working directory\n\tvar configDir string\n\tif len(args) > 0 && filepath.IsAbs(args[0]) {\n\t\t// absolute path\n\t\tconfigDir = args[0]\n\t} else if len(args) > 0 {\n\t\t// relative path\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tconfigDir = filepath.Join(wd, args[0])\n\t} else {\n\t\t// working directory\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tconfigDir = wd\n\t}\n\n\terr := os.MkdirAll(configDir, os.ModePerm)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\tconfigPath := filepath.Join(configDir, \"mani.yaml\")\n\tif _, err := os.Stat(configPath); err == nil {\n\t\treturn []Project{}, &core.AlreadyManiDirectory{Dir: configDir}\n\t}\n\n\t// Check if current directory is a git repository\n\tgitDir := filepath.Join(configDir, \".git\")\n\tisGitRepo := false\n\tif _, err := os.Stat(gitDir); err == nil {\n\t\tisGitRepo = true\n\t}\n\n\tvar projects []Project\n\n\t// Only add root directory as project if it IS a git repository\n\tif isGitRepo {\n\t\turl, err := core.GetWdRemoteURL(configDir)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\trootName := filepath.Base(configDir)\n\t\trootPath := \".\"\n\t\trootURL := url\n\t\trootProject := Project{Name: rootName, Path: rootPath, URL: rootURL}\n\n\t\t// Discover worktrees for root project\n\t\tif initFlags.AutoDiscovery {\n\t\t\tworktrees, _ := core.GetWorktreeList(configDir)\n\t\t\tfor wtPath, branch := range worktrees {\n\t\t\t\tif branch == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\twtRelPath, _ := filepath.Rel(configDir, wtPath)\n\t\t\t\trootProject.WorktreeList = append(rootProject.WorktreeList, Worktree{\n\t\t\t\t\tPath:   wtRelPath,\n\t\t\t\t\tBranch: branch,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tprojects = []Project{rootProject}\n\t}\n\n\tif initFlags.AutoDiscovery {\n\t\tprs, err := FindVCSystems(configDir)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tRenameDuplicates(prs)\n\n\t\tprojects = append(projects, prs...)\n\t}\n\n\tfuncMap := template.FuncMap{\n\t\t\"projectItem\": func(name string, path string, url string, worktrees []Worktree) string {\n\t\t\tvar txt = name + \":\"\n\n\t\t\tif name != path {\n\t\t\t\ttxt = txt + \"\\n    path: \" + path\n\t\t\t}\n\n\t\t\tif url != \"\" {\n\t\t\t\ttxt = txt + \"\\n    url: \" + url\n\t\t\t}\n\n\t\t\tif len(worktrees) > 0 {\n\t\t\t\ttxt = txt + \"\\n    worktrees:\"\n\t\t\t\tfor _, wt := range worktrees {\n\t\t\t\t\ttxt = txt + \"\\n      - path: \" + wt.Path\n\t\t\t\t\ttxt = txt + \"\\n        branch: \" + wt.Branch\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn txt\n\t\t},\n\t}\n\n\ttmpl, err := template.New(\"init\").Funcs(funcMap).Parse(`projects:\n  {{- range .}}\n  {{ (projectItem .Name .Path .URL .WorktreeList) }}\n  {{ end }}\ntasks:\n  hello:\n    desc: Print Hello World\n    cmd: echo \"Hello World\"\n`,\n\t)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\t// Create mani.yaml\n\tf, err := os.Create(configPath)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\terr = tmpl.Execute(f, projects)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\terr = f.Close()\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\t// Update gitignore file only if inside a git repository\n\thasURL := false\n\tfor _, project := range projects {\n\t\tif project.URL != \"\" {\n\t\t\thasURL = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif isGitRepo && hasURL && initFlags.SyncGitignore {\n\t\t// Add gitignore file\n\t\tgitignoreFilepath := filepath.Join(configDir, \".gitignore\")\n\t\tif _, err := os.Stat(gitignoreFilepath); os.IsNotExist(err) {\n\t\t\terr := os.WriteFile(gitignoreFilepath, []byte(\"\"), 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn []Project{}, err\n\t\t\t}\n\t\t}\n\n\t\tvar projectNames []string\n\t\tfor _, project := range projects {\n\t\t\tif project.URL == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif project.Path == \".\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprojectNames = append(projectNames, project.Path)\n\t\t}\n\n\t\t// Add projects to gitignore file\n\t\terr = UpdateProjectsToGitignore(projectNames, gitignoreFilepath)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t}\n\n\tfmt.Println(\"\\nInitialized mani repository in\", configDir)\n\tfmt.Println(\"- Created mani.yaml\")\n\n\tif isGitRepo && hasURL && initFlags.SyncGitignore {\n\t\tfmt.Println(\"- Created .gitignore\")\n\t}\n\n\treturn projects, nil\n}\n\nfunc RenameDuplicates(projects []Project) {\n\tprojectNamesCount := make(map[string]int)\n\t// Find duplicate names\n\tfor _, p := range projects {\n\t\tprojectNamesCount[p.Name] += 1\n\t}\n\n\t// Rename duplicate projects\n\tfor i, p := range projects {\n\t\tif projectNamesCount[p.Name] > 1 {\n\t\t\tprojects[i].Name = p.Path\n\t\t}\n\t}\n}\n\nfunc CheckUserColor(colorFlag bool) bool {\n\t_, present := os.LookupEnv(\"NO_COLOR\")\n\tif present || !colorFlag {\n\t\tcolor.Disable()\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (c *Config) CheckConfigNoColor() {\n\tfor _, env := range c.EnvList {\n\t\tname := strings.Split(env, \"=\")[0]\n\t\tif name == \"NO_COLOR\" {\n\t\t\tcolor.Disable()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/dao/config_test.go",
    "content": "package dao\n\nimport (\n\t\"testing\"\n)\n\nfunc TestConfig_DuplicateProjectName(t *testing.T) {\n\toriginalProjects := []Project{\n\t\t{Name: \"project-a\", Path: \"sub-1/project-a\"},\n\t\t{Name: \"project-a\", Path: \"sub-2/project-a\"},\n\t\t{Name: \"project-b\", Path: \"sub-3/project-b\"},\n\t}\n\n\tvar projects []Project\n\tprojects = append(projects, originalProjects...)\n\tRenameDuplicates(projects)\n\n\tif projects[0].Name != originalProjects[0].Path {\n\t\tt.Fatalf(`Wanted: %q, Found: %q`, projects[0].Path, originalProjects[0].Name)\n\t}\n\n\tif projects[1].Name != originalProjects[1].Path {\n\t\tt.Fatalf(`Wanted: %q, Found: %q`, projects[1].Path, originalProjects[1].Name)\n\t}\n\n\tif originalProjects[2].Name != projects[2].Name {\n\t\tt.Fatalf(`Wanted: %q, Found: %q`, projects[2].Name, originalProjects[2].Name)\n\t}\n}\n"
  },
  {
    "path": "core/dao/import.go",
    "content": "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.in/yaml.v3\"\n)\n\ntype Import struct {\n\tPath string\n\n\tcontext     string\n\tcontextLine int\n}\n\nfunc (i *Import) GetContext() string {\n\treturn i.context\n}\n\nfunc (i *Import) GetContextLine() int {\n\treturn i.contextLine\n}\n\n// Populates SpecList and creates a default spec if no default spec is set.\nfunc (c *Config) GetImportList() ([]Import, []ResourceErrors[Import]) {\n\tvar imports []Import\n\tcount := len(c.Import.Content)\n\n\timportErrors := []ResourceErrors[Import]{}\n\tfoundErrors := false\n\tfor i := range count {\n\t\timp := &Import{\n\t\t\tPath:        c.Import.Content[i].Value,\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Import.Content[i].Line,\n\t\t}\n\n\t\timports = append(imports, *imp)\n\t}\n\n\tif foundErrors {\n\t\treturn imports, importErrors\n\t}\n\n\treturn imports, nil\n}\n\n// Used for config imports\ntype ConfigResources struct {\n\tImports  []Import\n\tThemes   []Theme\n\tSpecs    []Spec\n\tTargets  []Target\n\tTasks    []Task\n\tProjects []Project\n\tEnvs     []string\n\n\tThemeErrors   []ResourceErrors[Theme]\n\tSpecErrors    []ResourceErrors[Spec]\n\tTargetErrors  []ResourceErrors[Target]\n\tTaskErrors    []ResourceErrors[Task]\n\tProjectErrors []ResourceErrors[Project]\n\tImportErrors  []ResourceErrors[Import]\n}\n\ntype Node struct {\n\tPath     string\n\tImports  []Import\n\tVisiting bool\n\tVisited  bool\n}\n\ntype NodeLink struct {\n\tA Node\n\tB Node\n}\n\ntype FoundCyclicDependency struct {\n\tCycles []NodeLink\n}\n\nfunc (c *FoundCyclicDependency) Error() string {\n\tvar msg string\n\n\tvar errPrefix = color.FgRed.Sprintf(\"error\")\n\tvar ptrPrefix = color.FgBlue.Sprintf(\"-->\")\n\tmsg = fmt.Sprintf(\"%s: %s\\n\", errPrefix, \"Found direct or indirect circular dependency\")\n\tfor i := range c.Cycles {\n\t\tmsg += fmt.Sprintf(\"  %s %s\\n      %s\\n\", ptrPrefix, c.Cycles[i].A.Path, c.Cycles[i].B.Path)\n\t}\n\n\treturn msg\n}\n\n// Given config imports, use a Depth-first-search algorithm to recursively\n// check for resources (tasks, projects, dirs, themes, specs, targets).\n// A struct is passed around that is populated with resources from each config.\n// In case a cyclic dependency is found (a -> b and b -> a), we return early and\n// with an error containing the cyclic dependency found.\n//\n// This is the first parsing, later on we will perform more passes where we check what commands/tasks\n// are imported.\nfunc (c Config) importConfigs() (ConfigResources, error) {\n\t// Main config\n\tci := ConfigResources{}\n\tc.loadResources(&ci)\n\n\tif c.UserConfigFile != nil {\n\t\tci.Imports = append(ci.Imports, Import{Path: *c.UserConfigFile, context: c.Path, contextLine: -1})\n\t}\n\n\t// Import other configs\n\tn := Node{\n\t\tPath:    c.Path,\n\t\tImports: ci.Imports,\n\t}\n\tm := make(map[string]*Node)\n\tm[n.Path] = &n\n\tcycles := []NodeLink{}\n\n\tdfs(&n, m, &cycles, &ci)\n\n\t// Get errors\n\tconfigErr := concatErrors(ci, &cycles)\n\n\tif configErr != nil {\n\t\treturn ci, configErr\n\t}\n\n\treturn ci, nil\n}\n\nfunc concatErrors(ci ConfigResources, cycles *[]NodeLink) error {\n\tvar configErr = \"\"\n\n\tif len(*cycles) > 0 {\n\t\terr := &FoundCyclicDependency{Cycles: *cycles}\n\t\tconfigErr = fmt.Sprintf(\"%s%s\\n\", configErr, err.Error())\n\t}\n\n\tfor _, theme := range ci.ThemeErrors {\n\t\tif len(theme.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(theme.Resource, theme.Errors))\n\t\t}\n\t}\n\n\tfor _, spec := range ci.SpecErrors {\n\t\tif len(spec.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(spec.Resource, spec.Errors))\n\t\t}\n\t}\n\n\tfor _, target := range ci.TargetErrors {\n\t\tif len(target.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(target.Resource, target.Errors))\n\t\t}\n\t}\n\n\tfor _, task := range ci.TaskErrors {\n\t\tif len(task.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(task.Resource, task.Errors))\n\t\t}\n\t}\n\n\tfor _, project := range ci.ProjectErrors {\n\t\tif len(project.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(project.Resource, project.Errors))\n\t\t}\n\t}\n\n\tfor _, imp := range ci.ImportErrors {\n\t\tif len(imp.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(imp.Resource, imp.Errors))\n\t\t}\n\t}\n\n\tif configErr != \"\" {\n\t\treturn &core.ConfigErr{Msg: configErr}\n\t}\n\n\treturn nil\n}\n\nfunc parseConfig(path string, ci *ConfigResources) ([]Import, error) {\n\tdat, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn []Import{}, err\n\t}\n\n\tabsPath, err := filepath.Abs(path)\n\tif err != nil {\n\t\treturn []Import{}, err\n\t}\n\n\t// Found config, now try to read it\n\tvar config Config\n\terr = yaml.Unmarshal(dat, &config)\n\tif err != nil {\n\t\treturn []Import{}, err\n\t}\n\n\tconfig.Path = absPath\n\tconfig.Dir = filepath.Dir(absPath)\n\timports := config.loadResources(ci)\n\n\treturn imports, nil\n}\n\nfunc (c Config) loadResources(ci *ConfigResources) []Import {\n\timports, importErrors := c.GetImportList()\n\tci.ImportErrors = append(ci.ImportErrors, importErrors...)\n\n\ttasks, taskErrors := c.GetTaskList()\n\tci.TaskErrors = append(ci.TaskErrors, taskErrors...)\n\n\tprojects, projectErrors := c.GetProjectList()\n\tci.ProjectErrors = append(ci.ProjectErrors, projectErrors...)\n\n\tthemes, themeErrors := c.ParseThemes()\n\tci.ThemeErrors = append(ci.ThemeErrors, themeErrors...)\n\n\tspecs, specErrors := c.GetSpecList()\n\tci.SpecErrors = append(ci.SpecErrors, specErrors...)\n\n\ttargets, targetErrors := c.GetTargetList()\n\tci.TargetErrors = append(ci.TargetErrors, targetErrors...)\n\n\tenvs := c.GetEnvList()\n\n\tci.Imports = append(ci.Imports, imports...)\n\tci.Tasks = append(ci.Tasks, tasks...)\n\tci.Projects = append(ci.Projects, projects...)\n\tci.Themes = append(ci.Themes, themes...)\n\tci.Specs = append(ci.Specs, specs...)\n\tci.Targets = append(ci.Targets, targets...)\n\tci.Envs = append(ci.Envs, envs...)\n\n\treturn imports\n}\n\nfunc dfs(n *Node, m map[string]*Node, cycles *[]NodeLink, ci *ConfigResources) {\n\tn.Visiting = true\n\n\tfor i := range n.Imports {\n\t\tp, err := core.GetAbsolutePath(filepath.Dir(n.Path), n.Imports[i].Path, \"\")\n\t\tif err != nil {\n\t\t\timportError := ResourceErrors[Import]{Resource: &n.Imports[i], Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tci.ImportErrors = append(ci.ImportErrors, importError)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip visited nodes\n\t\tvar nc Node\n\t\tv, exists := m[p]\n\t\tif exists {\n\t\t\tnc = *v\n\t\t} else {\n\t\t\tnc = Node{Path: p}\n\t\t\tm[nc.Path] = &nc\n\t\t}\n\n\t\tif nc.Visited {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Found cyclic dependency\n\t\tif nc.Visiting {\n\t\t\tc := NodeLink{\n\t\t\t\tA: *n,\n\t\t\t\tB: nc,\n\t\t\t}\n\n\t\t\t*cycles = append(*cycles, c)\n\t\t\tbreak\n\t\t}\n\n\t\t// Import Config\n\t\timports, err := parseConfig(nc.Path, ci)\n\t\tif err != nil {\n\t\t\timportError := ResourceErrors[Import]{Resource: &n.Imports[i], Errors: []error{err}}\n\t\t\tci.ImportErrors = append(ci.ImportErrors, importError)\n\t\t\tcontinue\n\t\t}\n\n\t\tnc.Imports = imports\n\n\t\tdfs(&nc, m, cycles, ci)\n\n\t\t// err = dfs(&nc, m, cycles, ci)\n\t\t// if err != nil {\n\t\t// \treturn err\n\t\t// }\n\t}\n\n\tn.Visiting = false\n\tn.Visited = true\n}\n"
  },
  {
    "path": "core/dao/project.go",
    "content": "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.in/yaml.v3\"\n\n\t\"github.com/alajmo/mani/core\"\n)\n\ntype Project struct {\n\tName         string   `yaml:\"name\"`\n\tPath         string   `yaml:\"path\"`\n\tDesc         string   `yaml:\"desc\"`\n\tURL          string   `yaml:\"url\"`\n\tClone        string   `yaml:\"clone\"`\n\tBranch       string   `yaml:\"branch\"`\n\tSingleBranch *bool    `yaml:\"single_branch\"`\n\tSync         *bool    `yaml:\"sync\"`\n\tTags         []string `yaml:\"tags\"`\n\tEnvList      []string `yaml:\"-\"`\n\tRemoteList   []Remote `yaml:\"-\"`\n\n\tEnv          yaml.Node  `yaml:\"env\"`\n\tRemotes      yaml.Node  `yaml:\"remotes\"`\n\tWorktrees    yaml.Node  `yaml:\"worktrees\"`\n\tWorktreeList []Worktree `yaml:\"-\"`\n\tcontext      string\n\tcontextLine  int\n\tRelPath      string\n}\n\ntype Remote struct {\n\tName string\n\tURL  string\n}\n\ntype Worktree struct {\n\tPath   string `yaml:\"path\"`\n\tBranch string `yaml:\"branch\"`\n}\n\nfunc (p *Project) GetContext() string {\n\treturn p.context\n}\n\nfunc (p *Project) GetContextLine() int {\n\treturn p.contextLine\n}\n\nfunc (p Project) IsSingleBranch() bool {\n\treturn p.SingleBranch != nil && *p.SingleBranch\n}\n\nfunc (p Project) IsSync() bool {\n\treturn p.Sync == nil || *p.Sync\n}\n\nfunc (p Project) GetValue(key string, _ int) string {\n\tswitch strings.ToLower(key) {\n\tcase \"project\":\n\t\treturn p.Name\n\tcase \"path\":\n\t\treturn p.Path\n\tcase \"relpath\":\n\t\treturn p.RelPath\n\tcase \"desc\", \"description\":\n\t\treturn p.Desc\n\tcase \"url\":\n\t\treturn p.URL\n\tcase \"tag\", \"tags\":\n\t\treturn strings.Join(p.Tags, \", \")\n\tcase \"worktree\", \"worktrees\":\n\t\tif len(p.WorktreeList) == 0 {\n\t\t\treturn \"\"\n\t\t}\n\t\tentries := make([]string, len(p.WorktreeList))\n\t\tfor i, wt := range p.WorktreeList {\n\t\t\tentries[i] = wt.Path + \":\" + wt.Branch\n\t\t}\n\t\treturn strings.Join(entries, \", \")\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *Config) GetProjectList() ([]Project, []ResourceErrors[Project]) {\n\tvar projects []Project\n\tcount := len(c.Projects.Content)\n\n\tprojectErrors := []ResourceErrors[Project]{}\n\tfoundErrors := false\n\tfor i := 0; i < count; i += 2 {\n\t\tproject := &Project{\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Projects.Content[i].Line,\n\t\t}\n\n\t\terr := c.Projects.Content[i+1].Decode(project)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tprojectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tprojectErrors = append(projectErrors, projectError)\n\t\t\tcontinue\n\t\t}\n\n\t\tproject.Name = c.Projects.Content[i].Value\n\n\t\t// Add absolute and relative path for each project\n\t\tproject.Path, err = core.GetAbsolutePath(c.Dir, project.Path, project.Name)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tprojectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tprojectErrors = append(projectErrors, projectError)\n\t\t\tcontinue\n\t\t}\n\n\t\tproject.RelPath, err = core.GetRelativePath(c.Dir, project.Path)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tprojectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tprojectErrors = append(projectErrors, projectError)\n\t\t\tcontinue\n\t\t}\n\n\t\tenvList := []string{}\n\t\tprojectEnvs, err := EvaluateEnv(ParseNodeEnv(project.Env))\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tprojectError := ResourceErrors[Project]{Resource: project, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tprojectErrors = append(projectErrors, projectError)\n\t\t\tcontinue\n\t\t}\n\t\tenvList = append(envList, projectEnvs...)\n\t\tproject.EnvList = envList\n\n\t\tprojectRemotes := ParseRemotes(project.Remotes)\n\t\tproject.RemoteList = projectRemotes\n\n\t\tprojectWorktrees, err := ParseWorktrees(project.Worktrees)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tprojectError := ResourceErrors[Project]{Resource: project, Errors: []error{err}}\n\t\t\tprojectErrors = append(projectErrors, projectError)\n\t\t\tcontinue\n\t\t}\n\t\tproject.WorktreeList = projectWorktrees\n\n\t\tprojects = append(projects, *project)\n\t}\n\n\tif foundErrors {\n\t\treturn projects, projectErrors\n\t}\n\n\treturn projects, nil\n}\n\n// GetFilteredProjects retrieves a filtered list of projects based on the provided ProjectFlags.\n// It processes various filtering criteria and returns the matching projects.\n//\n// The function follows these steps:\n// 1. If a target is specified, loads the target configuration, otherwise sets all to false\n// 2. Merges any provided flag values with the target configuration\n// 3. Applies all filtering criteria using FilterProjects\nfunc (c Config) GetFilteredProjects(flags *core.ProjectFlags) ([]Project, error) {\n\tvar err error\n\tvar projects []Project\n\n\ttarget := &Target{}\n\tif flags.Target != \"\" {\n\t\ttarget, err = c.GetTarget(flags.Target)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t}\n\n\tif len(flags.Projects) > 0 {\n\t\ttarget.Projects = flags.Projects\n\t}\n\n\tif len(flags.Paths) > 0 {\n\t\ttarget.Paths = flags.Paths\n\t}\n\n\tif len(flags.Tags) > 0 {\n\t\ttarget.Tags = flags.Tags\n\t}\n\n\tif flags.TagsExpr != \"\" {\n\t\ttarget.TagsExpr = flags.TagsExpr\n\t}\n\n\tif flags.Cwd {\n\t\ttarget.Cwd = flags.Cwd\n\t}\n\n\tif flags.All {\n\t\ttarget.All = flags.All\n\t}\n\n\tprojects, err = c.FilterProjects(\n\t\ttarget.Cwd,\n\t\ttarget.All,\n\t\ttarget.Projects,\n\t\ttarget.Paths,\n\t\ttarget.Tags,\n\t\ttarget.TagsExpr,\n\t)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\treturn projects, nil\n}\n\n// FilterProjects filters the project list based on various criteria. It supports filtering by:\n// - All projects (allProjectsFlag)\n// - Current working directory (cwdFlag)\n// - Project names (projectsFlag)\n// - Project paths (projectPathsFlag)\n// - Project tags (tagsFlag)\n// - Tag expressions (tagsExprFlag)\n//\n// Priority handling:\n//   - If cwdFlag is true, the function immediately returns only the current working directory\n//     project, ignoring all other filters.\n//   - For all other combinations of filters, the function collects projects from each filter\n//     into separate slices, then finds their intersection. If multiple\n//     filters are specified, only projects that match ALL filters will be returned.\nfunc (c Config) FilterProjects(\n\tcwdFlag bool,\n\tallProjectsFlag bool,\n\tprojectsFlag []string,\n\tprojectPathsFlag []string,\n\ttagsFlag []string,\n\ttagsExprFlag string,\n) ([]Project, error) {\n\tvar finalProjects []Project\n\n\tvar err error\n\tvar inputProjects [][]Project\n\n\tif cwdFlag {\n\t\tvar cwdProjects []Project\n\t\tcwdProject, err := c.GetCwdProject()\n\t\tcwdProjects = append(cwdProjects, cwdProject)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\n\t\treturn cwdProjects, nil\n\t}\n\n\tif allProjectsFlag {\n\t\tinputProjects = append(inputProjects, c.ProjectList)\n\t}\n\n\tif len(projectsFlag) > 0 {\n\t\tvar projects []Project\n\t\tprojects, err = c.GetProjectsByName(projectsFlag)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tinputProjects = append(inputProjects, projects)\n\t}\n\n\tif len(projectPathsFlag) > 0 {\n\t\tvar projectPaths []Project\n\t\tprojectPaths, err = c.GetProjectsByPath(projectPathsFlag)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tinputProjects = append(inputProjects, projectPaths)\n\t}\n\n\tif len(tagsFlag) > 0 {\n\t\tvar tagProjects []Project\n\t\ttagProjects, err = c.GetProjectsByTags(tagsFlag)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tinputProjects = append(inputProjects, tagProjects)\n\t}\n\n\tif tagsExprFlag != \"\" {\n\t\tvar tagExprProjects []Project\n\t\ttagExprProjects, err = c.GetProjectsByTagsExpr(tagsExprFlag)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\tinputProjects = append(inputProjects, tagExprProjects)\n\t}\n\n\tfinalProjects = c.GetIntersectProjects(inputProjects...)\n\n\treturn finalProjects, nil\n}\n\nfunc (c Config) GetProject(name string) (*Project, error) {\n\tfor _, project := range c.ProjectList {\n\t\tif name == project.Name {\n\t\t\treturn &project, nil\n\t\t}\n\t}\n\n\treturn nil, &core.ProjectNotFound{Name: []string{name}}\n}\n\nfunc (c Config) GetProjectsByName(projectNames []string) ([]Project, error) {\n\tvar matchedProjects []Project\n\n\tfoundProjectNames := make(map[string]bool)\n\tfor _, p := range projectNames {\n\t\tfoundProjectNames[p] = false\n\t}\n\n\tfor _, v := range projectNames {\n\t\tfor _, p := range c.ProjectList {\n\t\t\tif v == p.Name {\n\t\t\t\tfoundProjectNames[p.Name] = true\n\t\t\t\tmatchedProjects = append(matchedProjects, p)\n\t\t\t}\n\t\t}\n\t}\n\n\tnonExistingProjects := []string{}\n\tfor k, v := range foundProjectNames {\n\t\tif !v {\n\t\t\tnonExistingProjects = append(nonExistingProjects, k)\n\t\t}\n\t}\n\n\tif len(nonExistingProjects) > 0 {\n\t\treturn []Project{}, &core.ProjectNotFound{Name: nonExistingProjects}\n\t}\n\n\treturn matchedProjects, nil\n}\n\n// GetProjectsByPath Projects must have all dirs to match.\n// If user provides a path which does not exist, then return error containing\n// all the paths it didn't find.\n// Supports glob patterns:\n// - '*' matches any sequence of non-separator characters\n// - '**' matches any sequence of characters including separators\nfunc (c Config) GetProjectsByPath(dirs []string) ([]Project, error) {\n\tif len(dirs) == 0 {\n\t\treturn c.ProjectList, nil\n\t}\n\n\tfoundDirs := make(map[string]bool)\n\tfor _, dir := range dirs {\n\t\tfoundDirs[dir] = false\n\t}\n\n\tprojects := []Project{}\n\tfor _, project := range c.ProjectList {\n\t\t// Variable use to check that all dirs are matched\n\t\tvar numMatched = 0\n\t\tfor _, dir := range dirs {\n\n\t\t\tmatchPath := func(dir string, path string) (bool, error) {\n\t\t\t\t// Handle glob pattern\n\t\t\t\tif strings.Contains(dir, \"*\") {\n\t\t\t\t\t// Handle '**' glob pattern\n\t\t\t\t\tif strings.Contains(dir, \"**\") {\n\t\t\t\t\t\t// Convert the glob pattern to a regex pattern\n\t\t\t\t\t\tregexPattern := strings.ReplaceAll(dir, \"**/\", \"<glob>\")\n\t\t\t\t\t\tregexPattern = strings.ReplaceAll(regexPattern, \"*\", \"[^/]*\")\n\t\t\t\t\t\tregexPattern = strings.ReplaceAll(regexPattern, \"?\", \".\")\n\t\t\t\t\t\tregexPattern = strings.ReplaceAll(regexPattern, \"<glob>\", \"(.*/)*\")\n\t\t\t\t\t\tregexPattern = \"^\" + regexPattern + \"$\"\n\n\t\t\t\t\t\tmatched, err := regexp.MatchString(regexPattern, path)\n\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn false, err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif matched {\n\t\t\t\t\t\t\treturn true, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle standard glob pattern\n\t\t\t\t\tmatched, err := filepath.Match(dir, path)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn false, err\n\t\t\t\t\t}\n\n\t\t\t\t\tif matched {\n\t\t\t\t\t\treturn true, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Try matching as a partial path\n\t\t\t\tif strings.Contains(path, dir) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t}\n\n\t\t\t\treturn false, nil\n\t\t\t}\n\n\t\t\tmatched, err := matchPath(dir, project.RelPath)\n\n\t\t\tif err != nil {\n\t\t\t\treturn []Project{}, err\n\t\t\t}\n\n\t\t\tif matched {\n\t\t\t\tfoundDirs[dir] = true\n\t\t\t\tnumMatched++\n\t\t\t}\n\t\t}\n\n\t\tif numMatched == len(dirs) {\n\t\t\tprojects = append(projects, project)\n\t\t}\n\t}\n\n\tnonExistingDirs := []string{}\n\tfor k, v := range foundDirs {\n\t\tif !v {\n\t\t\tnonExistingDirs = append(nonExistingDirs, k)\n\t\t}\n\t}\n\n\tif len(nonExistingDirs) > 0 {\n\t\treturn []Project{}, &core.DirNotFound{Dirs: nonExistingDirs}\n\t}\n\n\treturn projects, nil\n}\n\n// GetProjectsByTags Projects must have all tags to match. For instance, if --tags frontend,backend\n// is passed, then a project must have both tags.\n// We only return error if the flags provided do not exist in the mani config.\nfunc (c Config) GetProjectsByTags(tags []string) ([]Project, error) {\n\tif len(tags) == 0 {\n\t\treturn c.ProjectList, nil\n\t}\n\n\tfoundTags := make(map[string]bool)\n\tfor _, tag := range tags {\n\t\tfoundTags[tag] = false\n\t}\n\n\t// Find projects matching the flag\n\tvar projects []Project\n\tfor _, project := range c.ProjectList {\n\t\t// Variable use to check that all tags are matched\n\t\tvar numMatched = 0\n\t\tfor _, tag := range tags {\n\t\t\tfor _, projectTag := range project.Tags {\n\t\t\t\tif projectTag == tag {\n\t\t\t\t\tfoundTags[tag] = true\n\t\t\t\t\tnumMatched = numMatched + 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif numMatched == len(tags) {\n\t\t\tprojects = append(projects, project)\n\t\t}\n\t}\n\n\tnonExistingTags := []string{}\n\tfor k, v := range foundTags {\n\t\tif !v {\n\t\t\tnonExistingTags = append(nonExistingTags, k)\n\t\t}\n\t}\n\n\tif len(nonExistingTags) > 0 {\n\t\treturn []Project{}, &core.TagNotFound{Tags: nonExistingTags}\n\t}\n\n\treturn projects, nil\n}\n\n// GetProjectsByTagsExpr Projects must have all tags to match. For instance, if --tags frontend,backend\n// is passed, then a project must have both tags.\n// We only return error if the tags provided do not exist.\nfunc (c Config) GetProjectsByTagsExpr(tagsExpr string) ([]Project, error) {\n\tif tagsExpr == \"\" {\n\t\treturn c.ProjectList, nil\n\t}\n\n\tvar projects []Project\n\tfor _, project := range c.ProjectList {\n\t\tmatches, err := evaluateExpression(&project, tagsExpr)\n\t\tif err != nil {\n\t\t\treturn c.ProjectList, &core.TagExprInvalid{Expression: err.Error()}\n\t\t}\n\t\tif matches {\n\t\t\tprojects = append(projects, project)\n\t\t}\n\t}\n\n\treturn projects, nil\n}\n\nfunc (c Config) GetCwdProject() (Project, error) {\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn Project{}, err\n\t}\n\n\tvar project Project\n\tparts := strings.Split(cwd, string(os.PathSeparator))\n\nout:\n\tfor i := len(parts) - 1; i >= 0; i-- {\n\t\tp := strings.Join(parts[0:i+1], string(os.PathSeparator))\n\n\t\tfor _, pro := range c.ProjectList {\n\t\t\tif p == pro.Path {\n\t\t\t\tproject = pro\n\t\t\t\tbreak out\n\t\t\t}\n\t\t}\n\t}\n\n\treturn project, nil\n}\n\n/**\n * GetProjectPaths For each project path, get all the enumerations of dirnames.\n * Example:\n * Input:\n *   - /frontend/tools/project-a\n *   - /frontend/tools/project-b\n *   - /frontend/tools/node/project-c\n *   - /backend/project-d\n * Output:\n *   - /frontend\n *   - /frontend/tools\n *   - /frontend/tools/node\n *   - /backend\n */\nfunc (c Config) GetProjectPaths() []string {\n\tdirs := []string{}\n\tfor _, project := range c.ProjectList {\n\t\t// Ignore projects outside of mani.yaml directory\n\t\tif strings.Contains(project.Path, c.Dir) {\n\t\t\tps := strings.Split(filepath.Dir(project.RelPath), string(os.PathSeparator))\n\t\t\tfor i := 1; i <= len(ps); i++ {\n\t\t\t\tp := filepath.Join(ps[0:i]...)\n\n\t\t\t\tif p != \".\" && !slices.Contains(dirs, p) {\n\t\t\t\t\tdirs = append(dirs, p)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn dirs\n}\n\nfunc (c Config) GetProjectNames() []string {\n\tnames := []string{}\n\tfor _, project := range c.ProjectList {\n\t\tnames = append(names, project.Name)\n\t}\n\n\treturn names\n}\n\nfunc (c Config) GetProjectUrls() []string {\n\turls := []string{}\n\tfor _, project := range c.ProjectList {\n\t\tif project.URL != \"\" {\n\t\t\turls = append(urls, project.URL)\n\t\t}\n\t}\n\n\treturn urls\n}\n\nfunc (c Config) GetProjectsTree(dirs []string, tags []string) ([]TreeNode, error) {\n\tdirProjects, err := c.GetProjectsByPath(dirs)\n\tif err != nil {\n\t\treturn []TreeNode{}, err\n\t}\n\n\ttagProjects, err := c.GetProjectsByTags(tags)\n\tif err != nil {\n\t\treturn []TreeNode{}, err\n\t}\n\n\tprojects := c.GetIntersectProjects(dirProjects, tagProjects)\n\n\tvar projectPaths = []TNode{}\n\tfor _, p := range projects {\n\t\tnode := TNode{Name: p.Name, Path: p.RelPath}\n\t\tprojectPaths = append(projectPaths, node)\n\t}\n\n\tvar tree []TreeNode\n\tfor i := range projectPaths {\n\t\ttree = AddToTree(tree, projectPaths[i])\n\t}\n\n\treturn tree, nil\n}\n\n// IsGitWorktree checks if the given path is a git worktree (not the main repo).\n//\n// A worktree's .git is a FILE (not directory) containing:\n// \"gitdir: /path/to/main-repo/.git/worktrees/worktree-name\"\nfunc IsGitWorktree(path string) (bool, error) {\n\tgitPath := filepath.Join(path, \".git\")\n\tinfo, err := os.Stat(gitPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// If .git is a directory, it's a regular git repo (or the main worktree)\n\tif info.IsDir() {\n\t\treturn false, nil\n\t}\n\n\t// .git is a file - read its content\n\tcontent, err := os.ReadFile(gitPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Parse \"gitdir: <path>\"\n\tcontentStr := strings.TrimSpace(string(content))\n\tgitDir, found := strings.CutPrefix(contentStr, \"gitdir: \")\n\tif !found {\n\t\treturn false, nil\n\t}\n\n\t// Make gitDir absolute if it's relative\n\tif !filepath.IsAbs(gitDir) {\n\t\tgitDir = filepath.Join(path, gitDir)\n\t\tgitDir = filepath.Clean(gitDir)\n\t}\n\n\t// Check if it matches the worktree pattern: <parent-repo>/.git/worktrees/<name>\n\tsep := string(filepath.Separator)\n\tpattern := sep + \".git\" + sep + \"worktrees\" + sep\n\tif strings.Contains(gitDir, pattern) {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc FindVCSystems(rootPath string) ([]Project, error) {\n\tprojects := []Project{}\n\n\terr := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Is file\n\t\tif !info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif path == rootPath {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Is Directory and Has a Git Dir inside, add to projects and SkipDir\n\t\tgitDir := filepath.Join(path, \".git\")\n\t\tif _, err := os.Stat(gitDir); !os.IsNotExist(err) {\n\t\t\tname := filepath.Base(path)\n\t\t\trelPath, _ := filepath.Rel(rootPath, path)\n\n\t\t\t// Check if this is a worktree (skip worktrees, they belong to parent)\n\t\t\tisWorktree, _ := IsGitWorktree(path)\n\t\t\tif isWorktree {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\t// This is a regular repository\n\t\t\tvar project Project\n\t\t\turl, rErr := core.GetRemoteURL(path)\n\t\t\tif rErr != nil {\n\t\t\t\tproject = Project{Name: name, Path: relPath}\n\t\t\t} else {\n\t\t\t\tproject = Project{Name: name, Path: relPath, URL: url}\n\t\t\t}\n\n\t\t\t// Get worktrees using git worktree list (skip detached HEAD worktrees)\n\t\t\tworktrees, _ := core.GetWorktreeList(path)\n\t\t\tfor wtPath, branch := range worktrees {\n\t\t\t\tif branch == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Convert absolute path to relative path from project\n\t\t\t\twtRelPath, _ := filepath.Rel(path, wtPath)\n\t\t\t\tproject.WorktreeList = append(project.WorktreeList, Worktree{\n\t\t\t\t\tPath:   wtRelPath,\n\t\t\t\t\tBranch: branch,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tprojects = append(projects, project)\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn projects, err\n\t}\n\n\treturn projects, nil\n}\n\nfunc UpdateProjectsToGitignore(projectNames []string, gitignoreFilename string) (err error) {\n\tl := list.New()\n\tgitignoreFile, err := os.OpenFile(gitignoreFilename, os.O_RDWR, 0644)\n\tif err != nil {\n\t\treturn &core.FailedToOpenFile{Name: gitignoreFilename}\n\t}\n\tdefer func() {\n\t\tcloseErr := gitignoreFile.Close()\n\t\tif err == nil {\n\t\t\terr = closeErr\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(gitignoreFile)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tl.PushBack(line)\n\t}\n\n\tconst maniComment = \"# mani #\"\n\tvar insideComment = false\n\tvar beginElement *list.Element\n\tvar endElement *list.Element\n\tvar next *list.Element\n\n\t// Remove all projects inside # mani #\n\tfor e := l.Front(); e != nil; e = next {\n\t\tnext = e.Next()\n\n\t\tif e.Value == maniComment && !insideComment {\n\t\t\tinsideComment = true\n\t\t\tbeginElement = e\n\t\t\tcontinue\n\t\t}\n\n\t\tif e.Value == maniComment {\n\t\t\tendElement = e\n\t\t\tbreak\n\t\t}\n\n\t\tif insideComment {\n\t\t\tl.Remove(e)\n\t\t}\n\t}\n\n\t// If missing start # mani #\n\tif beginElement == nil {\n\t\tl.PushBack(maniComment)\n\t\tbeginElement = l.Back()\n\t}\n\n\t// If missing ending # mani #\n\tif endElement == nil {\n\t\tl.PushBack(maniComment)\n\t}\n\n\t// Insert projects within # mani # section\n\tfor _, projectName := range projectNames {\n\t\tl.InsertAfter(projectName, beginElement)\n\t}\n\n\terr = gitignoreFile.Truncate(0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = gitignoreFile.Seek(0, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Write to gitignore file\n\tfor e := l.Front(); e != nil; e = e.Next() {\n\t\tstr := fmt.Sprint(e.Value)\n\t\t_, err = gitignoreFile.WriteString(str)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = gitignoreFile.WriteString(\"\\n\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ParseRemotes List of remotes (key: value)\nfunc ParseRemotes(node yaml.Node) []Remote {\n\tvar remotes []Remote\n\tcount := len(node.Content)\n\n\tfor i := 0; i < count; i += 2 {\n\t\tremote := Remote{\n\t\t\tName: node.Content[i].Value,\n\t\t\tURL:  node.Content[i+1].Value,\n\t\t}\n\n\t\tremotes = append(remotes, remote)\n\t}\n\n\treturn remotes\n}\n\n// ParseWorktrees parses worktree definitions from YAML\nfunc ParseWorktrees(node yaml.Node) ([]Worktree, error) {\n\tvar worktrees []Worktree\n\n\tfor _, content := range node.Content {\n\t\tvar wt Worktree\n\t\tif err := content.Decode(&wt); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Path is required\n\t\tif wt.Path == \"\" {\n\t\t\treturn nil, &core.WorktreePathRequired{}\n\t\t}\n\t\t// Default branch to path basename (like git does)\n\t\tif wt.Branch == \"\" {\n\t\t\twt.Branch = filepath.Base(wt.Path)\n\t\t}\n\t\tworktrees = append(worktrees, wt)\n\t}\n\n\treturn worktrees, nil\n}\n\nfunc (c Config) GetIntersectProjects(ps ...[]Project) []Project {\n\tcounts := make(map[string]int, len(c.ProjectList))\n\tfor _, projects := range ps {\n\t\tfor _, project := range projects {\n\t\t\tcounts[project.Name] += 1\n\t\t}\n\t}\n\n\tvar projects []Project\n\tfor _, p := range c.ProjectList {\n\t\tif counts[p.Name] == len(ps) && len(ps) > 0 {\n\t\t\tprojects = append(projects, p)\n\t\t}\n\t}\n\n\treturn projects\n}\n\n// TREE\n\ntype TNode struct {\n\tName string\n\tPath string\n}\n\ntype TreeNode struct {\n\tPath        string\n\tProjectName string\n\tChildren    []TreeNode\n}\n\n// AddToTree recursively builds a tree structure from path components\n// root: The current level of tree nodes\n// node: Node containing path and name information to be added\nfunc AddToTree(root []TreeNode, node TNode) []TreeNode {\n\t// Return if path is empty or starts with separator\n\titems := strings.Split(node.Path, string(os.PathSeparator))\n\tif len(items) == 0 || items[0] == \"\" {\n\t\treturn root\n\t}\n\n\tif len(items) > 0 {\n\t\tvar i int\n\t\t// Search for existing node with same path at current level\n\t\tfor i = 0; i < len(root); i++ {\n\t\t\tif root[i].Path == items[0] { // already in tree\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// If node doesn't exist at current level, create new node\n\t\tif i == len(root) {\n\t\t\troot = append(root, TreeNode{\n\t\t\t\tPath:        items[0],\n\t\t\t\tProjectName: \"\",\n\t\t\t\tChildren:    []TreeNode{},\n\t\t\t})\n\t\t}\n\n\t\t// If this is the last component in the path (leaf node/file)\n\t\tif len(items) == 1 {\n\t\t\troot[i].ProjectName = node.Name // Set name for projects only\n\t\t} else {\n\t\t\troot[i].ProjectName = \"\"\n\t\t\tstr := strings.Join(items[1:], string(os.PathSeparator))\n\t\t\tn := TNode{Name: node.Name, Path: str}\n\t\t\troot[i].Children = AddToTree(root[i].Children, n)\n\t\t}\n\t}\n\n\treturn root\n}\n"
  },
  {
    "path": "core/dao/project_test.go",
    "content": "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 *testing.T) {\n\tproject := Project{\n\t\tName:    \"test-project\",\n\t\tPath:    \"/path/to/project\",\n\t\tRelPath: \"relative/path\",\n\t\tDesc:    \"Test description\",\n\t\tURL:     \"https://example.com\",\n\t\tTags:    []string{\"frontend\", \"api\"},\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tkey      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"get project name\",\n\t\t\tkey:      \"Project\",\n\t\t\texpected: \"test-project\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get project path\",\n\t\t\tkey:      \"Path\",\n\t\t\texpected: \"/path/to/project\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get relative path\",\n\t\t\tkey:      \"RelPath\",\n\t\t\texpected: \"relative/path\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get description\",\n\t\t\tkey:      \"Desc\",\n\t\t\texpected: \"Test description\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get url\",\n\t\t\tkey:      \"Url\",\n\t\t\texpected: \"https://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get tags\",\n\t\t\tkey:      \"Tag\",\n\t\t\texpected: \"frontend, api\",\n\t\t},\n\t\t{\n\t\t\tname:     \"get invalid key\",\n\t\t\tkey:      \"InvalidKey\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := project.GetValue(tt.key, 0)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"expected %q, got %q\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_GetProjectsByName(t *testing.T) {\n\tconfig := Config{\n\t\tProjectList: []Project{\n\t\t\t{Name: \"project1\", Path: \"/path/1\"},\n\t\t\t{Name: \"project2\", Path: \"/path/2\"},\n\t\t\t{Name: \"project3\", Path: \"/path/3\"},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tprojectNames  []string\n\t\texpectError   bool\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:          \"find existing projects\",\n\t\t\tprojectNames:  []string{\"project1\", \"project2\"},\n\t\t\texpectError:   false,\n\t\t\texpectedCount: 2,\n\t\t},\n\t\t{\n\t\t\tname:          \"find non-existing project\",\n\t\t\tprojectNames:  []string{\"project1\", \"nonexistent\"},\n\t\t\texpectError:   true,\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"empty project names\",\n\t\t\tprojectNames:  []string{},\n\t\t\texpectError:   false,\n\t\t\texpectedCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprojects, err := config.GetProjectsByName(tt.projectNames)\n\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif len(projects) != tt.expectedCount {\n\t\t\t\tt.Errorf(\"expected %d projects, got %d\", tt.expectedCount, len(projects))\n\t\t\t}\n\t\t\tif err != nil && !tt.expectError {\n\t\t\t\tif _, ok := err.(*core.ProjectNotFound); !ok {\n\t\t\t\t\tt.Errorf(\"expected ProjectNotFound error, got %T\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_GetProjectsByTags(t *testing.T) {\n\tconfig := Config{\n\t\tProjectList: []Project{\n\t\t\t{Name: \"project1\", Tags: []string{\"frontend\", \"react\"}},\n\t\t\t{Name: \"project2\", Tags: []string{\"backend\", \"api\"}},\n\t\t\t{Name: \"project3\", Tags: []string{\"frontend\", \"vue\"}},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\ttags          []string\n\t\texpectError   bool\n\t\texpectedNames []string\n\t}{\n\t\t{\n\t\t\tname:          \"find projects with existing tag\",\n\t\t\ttags:          []string{\"frontend\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with multiple tags\",\n\t\t\ttags:          []string{\"frontend\", \"react\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with non-existing tag\",\n\t\t\ttags:          []string{\"nonexistent\"},\n\t\t\texpectError:   true,\n\t\t\texpectedNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty tags\",\n\t\t\ttags:          []string{},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project2\", \"project3\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprojects, err := config.GetProjectsByTags(tt.tags)\n\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tgotNames := getProjectNames(projects)\n\t\t\tif !equalStringSlices(gotNames, tt.expectedNames) {\n\t\t\t\tt.Errorf(\"expected projects %v, got %v\", tt.expectedNames, gotNames)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_GetProjectsByPath(t *testing.T) {\n\tconfig := Config{\n\t\tDir: \"/base\",\n\t\tProjectList: []Project{\n\t\t\t{Name: \"project1\", Path: \"/base/frontend/app1\", RelPath: \"frontend/app1\"},\n\t\t\t{Name: \"project2\", Path: \"/base/backend/api\", RelPath: \"backend/api\"},\n\t\t\t{Name: \"project3\", Path: \"/base/frontend/app2\", RelPath: \"frontend/app2\"},\n\t\t\t{Name: \"project4\", Path: \"/base/frontend/nested/app3\", RelPath: \"frontend/nested/app3\"},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tpaths         []string\n\t\texpectError   bool\n\t\texpectedNames []string\n\t}{\n\t\t{\n\t\t\tname:          \"find projects in frontend path\",\n\t\t\tpaths:         []string{\"frontend\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\", \"project4\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with specific path\",\n\t\t\tpaths:         []string{\"frontend/app1\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with single-level glob (1)\",\n\t\t\tpaths:         []string{\"*/app*\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with single-level glob (2)\",\n\t\t\tpaths:         []string{\"*/app?\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with double-star glob (1)\",\n\t\t\tpaths:         []string{\"frontend/**/app*\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\", \"project4\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with double-star glob (2)\",\n\t\t\tpaths:         []string{\"frontend/**/app?\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\", \"project4\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with double-star glob (3)\",\n\t\t\tpaths:         []string{\"frontend/**/**/app?\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\", \"project4\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with double-star glob (4)\",\n\t\t\tpaths:         []string{\"**/app?\"},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project3\", \"project4\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"find projects with non-existing path\",\n\t\t\tpaths:         []string{\"nonexistent\"},\n\t\t\texpectError:   true,\n\t\t\texpectedNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty paths\",\n\t\t\tpaths:         []string{},\n\t\t\texpectError:   false,\n\t\t\texpectedNames: []string{\"project1\", \"project2\", \"project3\", \"project4\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprojects, err := config.GetProjectsByPath(tt.paths)\n\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tgotNames := getProjectNames(projects)\n\t\t\tif !equalStringSlices(gotNames, tt.expectedNames) {\n\t\t\t\tt.Errorf(\"expected projects %v, got %v\", tt.expectedNames, gotNames)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_TestAddToTree(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tnodes         []TNode\n\t\texpectedPaths []string\n\t}{\n\t\t{\n\t\t\tname: \"simple tree\",\n\t\t\tnodes: []TNode{\n\t\t\t\t{Name: \"app1\", Path: \"frontend/app1\"},\n\t\t\t\t{Name: \"app2\", Path: \"frontend/app2\"},\n\t\t\t\t{Name: \"api\", Path: \"backend/api\"},\n\t\t\t},\n\t\t\texpectedPaths: []string{\"frontend\", \"backend\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nested tree\",\n\t\t\tnodes: []TNode{\n\t\t\t\t{Name: \"app1\", Path: \"frontend/web/app1\"},\n\t\t\t\t{Name: \"app2\", Path: \"frontend/mobile/app2\"},\n\t\t\t},\n\t\t\texpectedPaths: []string{\"frontend\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree []TreeNode\n\t\t\tfor _, node := range tt.nodes {\n\t\t\t\ttree = AddToTree(tree, node)\n\t\t\t}\n\n\t\t\tpaths := getTreePaths(tree)\n\t\t\tif !equalStringSlices(paths, tt.expectedPaths) {\n\t\t\t\tt.Errorf(\"expected paths %v, got %v\", tt.expectedPaths, paths)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_GetIntersectProjects(t *testing.T) {\n\tconfig := Config{\n\t\tProjectList: []Project{\n\t\t\t{Name: \"project1\", Tags: []string{\"frontend\"}},\n\t\t\t{Name: \"project2\", Tags: []string{\"backend\"}},\n\t\t\t{Name: \"project3\", Tags: []string{\"frontend\", \"api\"}},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tinputs        [][]Project\n\t\texpectedNames []string\n\t}{\n\t\t{\n\t\t\tname: \"intersect frontend and api projects\",\n\t\t\tinputs: [][]Project{\n\t\t\t\t{{Name: \"project1\"}, {Name: \"project3\"}}, // frontend projects\n\t\t\t\t{{Name: \"project3\"}},                     // api projects\n\t\t\t},\n\t\t\texpectedNames: []string{\"project3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"no intersection\",\n\t\t\tinputs: [][]Project{\n\t\t\t\t{{Name: \"project1\"}},\n\t\t\t\t{{Name: \"project2\"}},\n\t\t\t},\n\t\t\texpectedNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty input\",\n\t\t\tinputs:        [][]Project{},\n\t\t\texpectedNames: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := config.GetIntersectProjects(tt.inputs...)\n\n\t\t\tgotNames := getProjectNames(result)\n\t\t\tif !equalStringSlices(gotNames, tt.expectedNames) {\n\t\t\t\tt.Errorf(\"expected projects %v, got %v\", tt.expectedNames, gotNames)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseWorktrees(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tyaml        string\n\t\texpected    []Worktree\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"worktree with path and branch\",\n\t\t\tyaml: `\n- path: feature-branch\n  branch: feature/awesome\n`,\n\t\t\texpected: []Worktree{\n\t\t\t\t{Path: \"feature-branch\", Branch: \"feature/awesome\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple worktrees\",\n\t\t\tyaml: `\n- path: feature-branch\n  branch: feature/awesome\n- path: staging\n  branch: staging\n`,\n\t\t\texpected: []Worktree{\n\t\t\t\t{Path: \"feature-branch\", Branch: \"feature/awesome\"},\n\t\t\t\t{Path: \"staging\", Branch: \"staging\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"worktree without branch defaults to path basename\",\n\t\t\tyaml: `\n- path: hotfix\n`,\n\t\t\texpected: []Worktree{\n\t\t\t\t{Path: \"hotfix\", Branch: \"hotfix\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"worktree with nested path defaults branch to basename\",\n\t\t\tyaml: `\n- path: worktrees/feature\n`,\n\t\t\texpected: []Worktree{\n\t\t\t\t{Path: \"worktrees/feature\", Branch: \"feature\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty worktrees\",\n\t\t\tyaml:     ``,\n\t\t\texpected: []Worktree{},\n\t\t},\n\t\t{\n\t\t\tname: \"worktree without path returns error\",\n\t\t\tyaml: `\n- branch: feat\n`,\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar node yaml.Node\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yaml), &node); err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse yaml: %v\", err)\n\t\t\t}\n\n\t\t\t// Handle empty YAML case\n\t\t\tif len(node.Content) == 0 {\n\t\t\t\tresult, err := ParseWorktrees(yaml.Node{})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tif len(result) != 0 {\n\t\t\t\t\tt.Errorf(\"expected empty worktrees, got %v\", result)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresult, err := ParseWorktrees(*node.Content[0])\n\n\t\t\tif tt.expectError {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif len(result) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"expected %d worktrees, got %d\", len(tt.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i, wt := range result {\n\t\t\t\tif wt.Path != tt.expected[i].Path {\n\t\t\t\t\tt.Errorf(\"worktree[%d].Path: expected %q, got %q\", i, tt.expected[i].Path, wt.Path)\n\t\t\t\t}\n\t\t\t\tif wt.Branch != tt.expected[i].Branch {\n\t\t\t\t\tt.Errorf(\"worktree[%d].Branch: expected %q, got %q\", i, tt.expected[i].Branch, wt.Branch)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_GetValue_Worktrees(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tproject  Project\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"project with worktrees\",\n\t\t\tproject: Project{\n\t\t\t\tName: \"test-project\",\n\t\t\t\tWorktreeList: []Worktree{\n\t\t\t\t\t{Path: \"feature\", Branch: \"feature/test\"},\n\t\t\t\t\t{Path: \"staging\", Branch: \"staging\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"feature:feature/test, staging:staging\",\n\t\t},\n\t\t{\n\t\t\tname: \"project without worktrees\",\n\t\t\tproject: Project{\n\t\t\t\tName:         \"test-project\",\n\t\t\t\tWorktreeList: []Worktree{},\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.project.GetValue(\"worktrees\", 0)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"expected %q, got %q\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/spec.go",
    "content": "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              string `yaml:\"name\"`\n\tOutput            string `yaml:\"output\"`\n\tParallel          bool   `yaml:\"parallel\"`\n\tIgnoreErrors      bool   `yaml:\"ignore_errors\"`\n\tIgnoreNonExisting bool   `yaml:\"ignore_non_existing\"`\n\tOmitEmptyRows     bool   `yaml:\"omit_empty_rows\"`\n\tOmitEmptyColumns  bool   `yaml:\"omit_empty_columns\"`\n\tClearOutput       bool   `yaml:\"clear_output\"`\n\tForks             uint32 `yaml:\"forks\"`\n\n\tcontext     string\n\tcontextLine int\n}\n\nfunc (s *Spec) GetContext() string {\n\treturn s.context\n}\n\nfunc (s *Spec) GetContextLine() int {\n\treturn s.contextLine\n}\n\n// Populates SpecList and creates a default spec if no default spec is set.\nfunc (c *Config) GetSpecList() ([]Spec, []ResourceErrors[Spec]) {\n\tvar specs []Spec\n\tcount := len(c.Specs.Content)\n\n\tspecErrors := []ResourceErrors[Spec]{}\n\tfoundErrors := false\n\tfor i := 0; i < count; i += 2 {\n\t\tspec := &Spec{\n\t\t\tName:        c.Specs.Content[i].Value,\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Specs.Content[i].Line,\n\t\t}\n\n\t\terr := c.Specs.Content[i+1].Decode(spec)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tspecError := ResourceErrors[Spec]{Resource: spec, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tspecErrors = append(specErrors, specError)\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch spec.Output {\n\t\tcase \"\", \"table\", \"stream\", \"html\", \"markdown\":\n\t\tdefault:\n\t\t\tfoundErrors = true\n\t\t\tspecError := ResourceErrors[Spec]{\n\t\t\t\tResource: spec,\n\t\t\t\tErrors:   []error{&core.SpecOutputError{Name: spec.Name, Output: spec.Output}},\n\t\t\t}\n\t\t\tspecErrors = append(specErrors, specError)\n\t\t}\n\n\t\tif spec.Forks == 0 {\n\t\t\tspec.Forks = 4\n\t\t}\n\n\t\tspecs = append(specs, *spec)\n\t}\n\n\tif foundErrors {\n\t\treturn specs, specErrors\n\t}\n\n\treturn specs, nil\n}\n\nfunc (c Config) GetSpec(name string) (*Spec, error) {\n\tfor _, spec := range c.SpecList {\n\t\tif name == spec.Name {\n\t\t\treturn &spec, nil\n\t\t}\n\t}\n\n\treturn nil, &core.SpecNotFound{Name: name}\n}\n\nfunc (c Config) GetSpecNames() []string {\n\tnames := []string{}\n\tfor _, spec := range c.SpecList {\n\t\tnames = append(names, spec.Name)\n\t}\n\n\treturn names\n}\n"
  },
  {
    "path": "core/dao/spec_test.go",
    "content": "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\tspec := Spec{\n\t\tName:        \"test-spec\",\n\t\tcontext:     \"/path/to/config\",\n\t\tcontextLine: 42,\n\t}\n\n\tif spec.GetContext() != \"/path/to/config\" {\n\t\tt.Errorf(\"expected context '/path/to/config', got %q\", spec.GetContext())\n\t}\n\n\tif spec.GetContextLine() != 42 {\n\t\tt.Errorf(\"expected context line 42, got %d\", spec.GetContextLine())\n\t}\n}\n\nfunc TestSpec_GetSpecList(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfig        Config\n\t\texpectedCount int\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname: \"empty spec list\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{},\n\t\t\t},\n\t\t\texpectedCount: 0,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid specs\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"spec1\",\n\t\t\t\t\t\tOutput:   \"table\",\n\t\t\t\t\t\tParallel: true,\n\t\t\t\t\t\tForks:    4,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"spec2\",\n\t\t\t\t\t\tOutput: \"stream\",\n\t\t\t\t\t\tForks:  8,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCount: 2,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"spec with defaults\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"default-spec\",\n\t\t\t\t\t\tOutput: \"table\",\n\t\t\t\t\t\tForks:  4,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCount: 1,\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tspecs := tt.config.SpecList\n\n\t\t\tif len(specs) != tt.expectedCount {\n\t\t\t\tt.Errorf(\"expected %d specs, got %d\", tt.expectedCount, len(specs))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSpec_GetSpec(t *testing.T) {\n\tconfig := Config{\n\t\tSpecList: []Spec{\n\t\t\t{\n\t\t\t\tName:   \"spec1\",\n\t\t\t\tOutput: \"table\",\n\t\t\t\tForks:  4,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:   \"spec2\",\n\t\t\t\tOutput: \"stream\",\n\t\t\t\tForks:  8,\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tspecName      string\n\t\texpectError   bool\n\t\texpectedForks uint32\n\t}{\n\t\t{\n\t\t\tname:          \"existing spec\",\n\t\t\tspecName:      \"spec1\",\n\t\t\texpectError:   false,\n\t\t\texpectedForks: 4,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-existing spec\",\n\t\t\tspecName:      \"nonexistent\",\n\t\t\texpectError:   true,\n\t\t\texpectedForks: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tspec, err := config.GetSpec(tt.specName)\n\n\t\t\tif tt.expectError {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t}\n\t\t\t\tif _, ok := err.(*core.SpecNotFound); !ok {\n\t\t\t\t\tt.Errorf(\"expected SpecNotFound error, got %T\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif spec.Forks != tt.expectedForks {\n\t\t\t\tt.Errorf(\"expected forks %d, got %d\", tt.expectedForks, spec.Forks)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSpec_GetSpecNames(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfig        Config\n\t\texpectedNames []string\n\t}{\n\t\t{\n\t\t\tname: \"multiple specs\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{\n\t\t\t\t\t{Name: \"spec1\"},\n\t\t\t\t\t{Name: \"spec2\"},\n\t\t\t\t\t{Name: \"spec3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedNames: []string{\"spec1\", \"spec2\", \"spec3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty spec list\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{},\n\t\t\t},\n\t\t\texpectedNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"single spec\",\n\t\t\tconfig: Config{\n\t\t\t\tSpecList: []Spec{\n\t\t\t\t\t{Name: \"solo-spec\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedNames: []string{\"solo-spec\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnames := tt.config.GetSpecNames()\n\n\t\t\tif !reflect.DeepEqual(names, tt.expectedNames) {\n\t\t\t\tt.Errorf(\"expected names %v, got %v\", tt.expectedNames, names)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/tag.go",
    "content": "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) GetValue(key string, _ int) string {\n\tswitch strings.ToLower(key) {\n\tcase \"tag\":\n\t\treturn t.Name\n\tcase \"project\", \"projects\":\n\t\treturn strings.Join(t.Projects, \"\\n\")\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (c Config) GetTags() []string {\n\ttags := []string{}\n\tfor _, project := range c.ProjectList {\n\t\tfor _, tag := range project.Tags {\n\t\t\tif !slices.Contains(tags, tag) {\n\t\t\t\ttags = append(tags, tag)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn tags\n}\n\nfunc (c Config) GetTagAssocations(tags []string) ([]Tag, error) {\n\tt := []Tag{}\n\n\tfor _, tag := range tags {\n\t\tprojects, err := c.GetProjectsByTags([]string{tag})\n\t\tif err != nil {\n\t\t\treturn []Tag{}, err\n\t\t}\n\n\t\tvar projectNames []string\n\t\tfor _, p := range projects {\n\t\t\tprojectNames = append(projectNames, p.Name)\n\t\t}\n\n\t\tt = append(t, Tag{Name: tag, Projects: projectNames})\n\t}\n\n\treturn t, nil\n}\n"
  },
  {
    "path": "core/dao/tag_expr.go",
    "content": "// Package dao for evaluating boolean tag expressions against project tags.\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"unicode\"\n)\n\ntype TokenType int\n\nconst (\n\tTokenTag TokenType = iota\n\tTokenAnd\n\tTokenOr\n\tTokenNot\n\tTokenLParent\n\tTokenRParen\n\tTokenEOF\n)\n\ntype Position struct {\n\tline   int\n\tcolumn int\n}\n\ntype Token struct {\n\tType     TokenType\n\tValue    string\n\tPosition Position\n}\n\ntype Lexer struct {\n\tinput  string\n\tpos    int\n\tline   int\n\tcolumn int\n\ttokens []Token\n}\n\nfunc NewLexer(input string) *Lexer {\n\treturn &Lexer{\n\t\tinput:  input,\n\t\tpos:    0,\n\t\tline:   1,\n\t\tcolumn: 1,\n\t\ttokens: make([]Token, 0),\n\t}\n}\n\nfunc (l *Lexer) Tokenize() error {\n\tif strings.TrimSpace(l.input) == \"\" {\n\t\treturn fmt.Errorf(\"empty expression\")\n\t}\n\n\tfor l.pos < len(l.input) {\n\t\tchar := l.current()\n\t\tswitch {\n\t\tcase char == ' ' || char == '\\t':\n\t\t\tl.advance()\n\t\tcase char == '\\n':\n\t\t\tl.line++\n\t\t\tl.column = 1\n\t\t\tl.advance()\n\t\tcase char == '(':\n\t\t\tl.addToken(TokenLParent, \"(\")\n\t\t\tl.advance()\n\t\tcase char == ')':\n\t\t\tl.addToken(TokenRParen, \")\")\n\t\t\tl.advance()\n\t\tcase char == '!':\n\t\t\tl.addToken(TokenNot, \"!\")\n\t\t\tl.advance()\n\t\tcase l.matchOperator(\"&&\"):\n\t\t\tl.addToken(TokenAnd, \"&&\")\n\t\t\tl.advance()\n\t\t\tl.advance()\n\t\tcase l.matchOperator(\"||\"):\n\t\t\tl.addToken(TokenOr, \"||\")\n\t\t\tl.advance()\n\t\t\tl.advance()\n\t\tcase isValidTagStart(char):\n\t\t\tl.readTag()\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected character: %c at line %d, column %d\", char, l.line, l.column)\n\t\t}\n\t}\n\n\tl.addToken(TokenEOF, \"\")\n\treturn nil\n}\n\nfunc (l *Lexer) addToken(tokenType TokenType, value string) {\n\tl.tokens = append(l.tokens, Token{\n\t\tType:     tokenType,\n\t\tValue:    value,\n\t\tPosition: Position{line: l.line, column: l.column},\n\t})\n}\n\nfunc (l *Lexer) advance() {\n\tl.pos++\n\tl.column++\n}\n\nfunc (l *Lexer) current() rune {\n\tif l.pos >= len(l.input) {\n\t\treturn 0\n\t}\n\treturn rune(l.input[l.pos])\n}\n\nfunc (l *Lexer) matchOperator(op string) bool {\n\tif l.pos+len(op) > len(l.input) {\n\t\treturn false\n\t}\n\treturn l.input[l.pos:l.pos+len(op)] == op\n}\n\nfunc (l *Lexer) readTag() {\n\tstartPos := l.pos\n\tstartColumn := l.column\n\n\t// First character must be a letter\n\tif !isValidTagStart(l.current()) {\n\t\treturn\n\t}\n\tl.advance()\n\n\t// Subsequent characters can be letters, numbers, hyphens, or underscores\n\tfor l.pos < len(l.input) && isValidTagPart(l.current()) {\n\t\tl.advance()\n\t}\n\n\tvalue := l.input[startPos:l.pos]\n\tl.tokens = append(l.tokens, Token{\n\t\tType:     TokenTag,\n\t\tValue:    value,\n\t\tPosition: Position{line: l.line, column: startColumn},\n\t})\n}\n\nfunc isValidTagStart(r rune) bool {\n\treturn !isReservedChar(r) && !unicode.IsSpace(r)\n}\n\nfunc isValidTagPart(r rune) bool {\n\treturn !isReservedChar(r) && !unicode.IsSpace(r)\n}\n\nfunc isReservedChar(r rune) bool {\n\treturn r == '(' || r == ')' || r == '!' || r == '&' || r == '|'\n}\n\ntype Parser struct {\n\ttokens  []Token\n\tpos     int\n\tproject *Project\n}\n\nfunc NewParser(tokens []Token, project *Project) *Parser {\n\treturn &Parser{\n\t\ttokens:  tokens,\n\t\tpos:     0,\n\t\tproject: project,\n\t}\n}\n\nfunc (p *Parser) Parse() (bool, error) {\n\tif len(p.tokens) <= 1 { // Only EOF token\n\t\treturn false, fmt.Errorf(\"empty expression\")\n\t}\n\n\tresult, err := p.parseExpression()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Check if we consumed all tokens\n\tif p.current().Type != TokenEOF {\n\t\tpos := p.current().Position\n\t\treturn false, fmt.Errorf(\"unexpected token at line %d, column %d\", pos.line, pos.column)\n\t}\n\n\treturn result, nil\n}\n\nfunc (p *Parser) parseExpression() (bool, error) {\n\tleft, err := p.parseTerm()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor p.current().Type == TokenOr {\n\t\top := p.current()\n\t\tp.pos++\n\n\t\t// Check for missing right operand\n\t\tif p.current().Type == TokenEOF {\n\t\t\treturn false, fmt.Errorf(\"missing right operand for OR operator at line %d, column %d\",\n\t\t\t\top.Position.line, op.Position.column)\n\t\t}\n\n\t\tright, err := p.parseTerm()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tleft = left || right\n\t}\n\n\treturn left, nil\n}\n\nfunc (p *Parser) parseTerm() (bool, error) {\n\tleft, err := p.parseFactor()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor p.current().Type == TokenAnd {\n\t\top := p.current()\n\t\tp.pos++\n\n\t\t// Check for missing right operand\n\t\tif p.current().Type == TokenEOF {\n\t\t\treturn false, fmt.Errorf(\"missing right operand for AND operator at line %d, column %d\",\n\t\t\t\top.Position.line, op.Position.column)\n\t\t}\n\n\t\tright, err := p.parseFactor()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tleft = left && right\n\t}\n\n\treturn left, nil\n}\n\nfunc (p *Parser) parseFactor() (bool, error) {\n\ttoken := p.current()\n\n\tswitch token.Type {\n\tcase TokenNot:\n\t\tp.pos++\n\t\tif p.current().Type == TokenEOF {\n\t\t\treturn false, fmt.Errorf(\"missing operand after NOT at line %d, column %d\",\n\t\t\t\ttoken.Position.line, token.Position.column)\n\t\t}\n\t\tval, err := p.parseFactor()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\treturn !val, nil\n\n\tcase TokenLParent:\n\t\tp.pos++\n\t\t// Check for empty parentheses\n\t\tif p.current().Type == TokenRParen {\n\t\t\treturn false, fmt.Errorf(\"empty parentheses at line %d, column %d\",\n\t\t\t\ttoken.Position.line, token.Position.column)\n\t\t}\n\t\tval, err := p.parseExpression()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif p.current().Type != TokenRParen {\n\t\t\treturn false, fmt.Errorf(\"missing closing parenthesis for opening parenthesis at line %d, column %d\",\n\t\t\t\ttoken.Position.line, token.Position.column)\n\t\t}\n\t\tp.pos++\n\t\treturn val, nil\n\n\tcase TokenTag:\n\t\tp.pos++\n\t\treturn slices.Contains(p.project.Tags, token.Value), nil\n\n\tdefault:\n\t\treturn false, fmt.Errorf(\"unexpected token at line %d, column %d: %v\",\n\t\t\ttoken.Position.line, token.Position.column, token.Value)\n\t}\n}\n\nfunc (p *Parser) current() Token {\n\tif p.pos >= len(p.tokens) {\n\t\treturn Token{Type: TokenEOF}\n\t}\n\treturn p.tokens[p.pos]\n}\n\n// evaluateExpression checks if a boolean tag expression evaluates to true for a given project.\n// The function supports boolean operations on project tags with full operator precedence.\n//\n// Operators (in precedence order):\n//  1. ()  - Parentheses for grouping\n//  2. !   - NOT operator (logical negation)\n//  3. &&  - AND operator (logical conjunction)\n//  4. ||  - OR operator (logical disjunction)\n//\n// Tag Expression Example:\n//\n// Expression: (main && (dev || prod)) && !test\n//\n// Requirements:\n// 1. Must have \"main\" tag            - Mandatory\n// 2. Must have \"dev\" OR \"prod\" tag   - At least one required\n// 3. Must NOT have \"test\" tag        - Excluded if present\n//\n// Matches tags:\n//\n//\t[\"main\", \"dev\"]\n//\t[\"main\", \"prod\"]\n//\t[\"main\", \"dev\", \"prod\"]\n//\n// Does NOT match tags:\n//\n//\t[\"main\"]                   - missing dev/prod\n//\t[\"main\", \"dev\", \"test\"]    - has test tag\n//\t[\"dev\", \"prod\"]            - missing main\nfunc evaluateExpression(project *Project, expression string) (bool, error) {\n\tlexer := NewLexer(expression)\n\terr := lexer.Tokenize()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"lexer error: %v\", err)\n\t}\n\n\tparser := NewParser(lexer.tokens, project)\n\treturn parser.Parse()\n}\n\nfunc validateExpression(expression string) error {\n\tlexer := NewLexer(expression)\n\terr := lexer.Tokenize()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%v\", err)\n\t}\n\n\tproject := &Project{Tags: []string{}}\n\tparser := NewParser(lexer.tokens, project)\n\t_, err = parser.Parse()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/dao/tag_expr_test.go",
    "content": "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\tName: \"Project A\",\n\t\t\tTags: []string{\"active\", \"git\", \"frontend\"},\n\t\t},\n\t\t{\n\t\t\tName: \"Project B\",\n\t\t\tTags: []string{\"active\", \"sake\", \"backend\"},\n\t\t},\n\t}\n\n\t// Test cases for valid expressions\n\tvalidTests := []struct {\n\t\tname     string\n\t\texpr     string\n\t\tproject  string\n\t\texpected bool\n\t}{\n\t\t{\"simple AND\", \"active && git\", \"Project A\", true},\n\t\t{\"simple AND false\", \"active && git\", \"Project B\", false},\n\t\t{\"simple OR\", \"git || sake\", \"Project A\", true},\n\t\t{\"simple OR\", \"git || sake\", \"Project B\", true},\n\t\t{\"nested AND-OR\", \"((active && git) || (sake && backend))\", \"Project A\", true},\n\t\t{\"nested AND-OR\", \"((active && git) || (sake && backend))\", \"Project B\", true},\n\t\t{\"parentheses precedence\", \"(active && (git || sake))\", \"Project A\", true},\n\t\t{\"parentheses precedence\", \"(active && (git || sake))\", \"Project B\", true},\n\t\t{\"complex expression\", \"((active && git) || (active && sake)) && (frontend || backend)\", \"Project A\", true},\n\t\t{\"complex expression\", \"((active && git) || (active && sake)) && (frontend || backend)\", \"Project B\", true},\n\t\t{\"NOT operator\", \"!(active && (git || sake))\", \"Project A\", false},\n\t\t{\"NOT operator\", \"!(active && (git || sake))\", \"Project B\", false},\n\t\t{\"triple nested\", \"(((active && git) || sake) && backend)\", \"Project A\", false},\n\t\t{\"triple nested\", \"(((active && git) || sake) && backend)\", \"Project B\", true},\n\t}\n\n\tt.Run(\"valid expressions\", func(t *testing.T) {\n\t\tfor _, tt := range validTests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tvar proj Project\n\t\t\t\tfor _, p := range projects {\n\t\t\t\t\tif p.Name == tt.project {\n\t\t\t\t\t\tproj = p\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tresult, err := evaluateExpression(&proj, tt.expr)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tif result != tt.expected {\n\t\t\t\t\tt.Errorf(\"expression %q on project %q: got %v, want %v\",\n\t\t\t\t\t\ttt.expr, tt.project, result, tt.expected)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\t// Test cases for invalid expressions\n\tinvalidTests := []struct {\n\t\tname        string\n\t\texpr        string\n\t\texpectedErr string\n\t}{\n\t\t{\"empty expression\", \"\", \"empty expression\"},\n\t\t{\"operator without operands\", \"&&\", \"unexpected token\"},\n\t\t{\"missing right operand\", \"tag &&\", \"missing right operand\"},\n\t\t{\"missing left operand\", \"&& tag\", \"unexpected token\"},\n\t\t{\"empty parentheses\", \"()\", \"empty parentheses\"},\n\t\t{\"unmatched parenthesis\", \"((tag)\", \"missing closing parenthesis\"},\n\t\t{\"missing operator\", \"tag tag\", \"unexpected token\"},\n\t\t{\"double operator\", \"tag && && tag\", \"unexpected token\"},\n\t\t{\"NOT without operand\", \"!\", \"missing operand after NOT\"},\n\t}\n\n\tt.Run(\"invalid expressions\", func(t *testing.T) {\n\t\tfor _, tt := range invalidTests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\t_, err := evaluateExpression(&projects[0], tt.expr)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got nil\", tt.expectedErr)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(err.Error(), tt.expectedErr) {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got %q\", tt.expectedErr, err.Error())\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "core/dao/target.go",
    "content": "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   `yaml:\"name\"`\n\tAll      bool     `yaml:\"all\"`\n\tProjects []string `yaml:\"projects\"`\n\tPaths    []string `yaml:\"paths\"`\n\tTags     []string `yaml:\"tags\"`\n\tTagsExpr string   `yaml:\"tags_expr\"`\n\tCwd      bool     `yaml:\"cwd\"`\n\n\tcontext     string\n\tcontextLine int\n}\n\nfunc (t *Target) GetContext() string {\n\treturn t.context\n}\n\nfunc (t *Target) GetContextLine() int {\n\treturn t.contextLine\n}\n\n// Populates TargetList and creates a default target if no default target is set.\nfunc (c *Config) GetTargetList() ([]Target, []ResourceErrors[Target]) {\n\tvar targets []Target\n\tcount := len(c.Targets.Content)\n\n\ttargetErrors := []ResourceErrors[Target]{}\n\tfoundErrors := false\n\tfor i := 0; i < count; i += 2 {\n\t\ttarget := &Target{\n\t\t\tName:        c.Targets.Content[i].Value,\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Targets.Content[i].Line,\n\t\t}\n\n\t\terr := c.Targets.Content[i+1].Decode(target)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\ttargetError := ResourceErrors[Target]{Resource: target, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\ttargetErrors = append(targetErrors, targetError)\n\t\t\tcontinue\n\t\t}\n\n\t\tif target.TagsExpr != \"\" {\n\t\t\tvalid := validateExpression(target.TagsExpr)\n\t\t\tif valid != nil {\n\t\t\t\tfoundErrors = true\n\t\t\t\ttargetError := ResourceErrors[Target]{\n\t\t\t\t\tResource: target,\n\t\t\t\t\tErrors:   []error{&core.TargetTagsExprError{Name: target.Name, Err: valid}},\n\t\t\t\t}\n\t\t\t\ttargetErrors = append(targetErrors, targetError)\n\t\t\t}\n\t\t}\n\n\t\ttargets = append(targets, *target)\n\t}\n\n\tif foundErrors {\n\t\treturn targets, targetErrors\n\t}\n\n\treturn targets, nil\n}\n\nfunc (c Config) GetTarget(name string) (*Target, error) {\n\tfor _, target := range c.TargetList {\n\t\tif name == target.Name {\n\t\t\treturn &target, nil\n\t\t}\n\t}\n\n\treturn nil, &core.TargetNotFound{Name: name}\n}\n\nfunc (c Config) GetTargetNames() []string {\n\tnames := []string{}\n\tfor _, target := range c.TargetList {\n\t\tnames = append(names, target.Name)\n\t}\n\n\treturn names\n}\n"
  },
  {
    "path": "core/dao/target_test.go",
    "content": "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) {\n\ttarget := Target{\n\t\tName:        \"test-target\",\n\t\tcontext:     \"/path/to/config\",\n\t\tcontextLine: 42,\n\t}\n\n\tif target.GetContext() != \"/path/to/config\" {\n\t\tt.Errorf(\"expected context '/path/to/config', got %q\", target.GetContext())\n\t}\n\n\tif target.GetContextLine() != 42 {\n\t\tt.Errorf(\"expected context line 42, got %d\", target.GetContextLine())\n\t}\n}\n\nfunc TestTarget_GetTargetList(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfig        Config\n\t\texpectedCount int\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname: \"empty target list\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{},\n\t\t\t},\n\t\t\texpectedCount: 0,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple valid targets\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"target1\",\n\t\t\t\t\t\tProjects: []string{\"proj1\", \"proj2\"},\n\t\t\t\t\t\tTags:     []string{\"frontend\"},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"target2\",\n\t\t\t\t\t\tProjects: []string{\"proj3\"},\n\t\t\t\t\t\tTags:     []string{\"backend\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCount: 2,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"target with all flag\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"all-target\",\n\t\t\t\t\t\tAll:  true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCount: 1,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"target with paths\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:  \"path-target\",\n\t\t\t\t\t\tPaths: []string{\"path1\", \"path2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCount: 1,\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttargets := tt.config.TargetList\n\n\t\t\tif len(targets) != tt.expectedCount {\n\t\t\t\tt.Errorf(\"expected %d targets, got %d\", tt.expectedCount, len(targets))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTarget_GetTarget(t *testing.T) {\n\tconfig := Config{\n\t\tTargetList: []Target{\n\t\t\t{\n\t\t\t\tName:     \"frontend\",\n\t\t\t\tProjects: []string{\"web\", \"mobile\"},\n\t\t\t\tTags:     []string{\"frontend\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:     \"backend\",\n\t\t\t\tProjects: []string{\"api\", \"worker\"},\n\t\t\t\tTags:     []string{\"backend\"},\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\ttargetName   string\n\t\texpectError  bool\n\t\texpectedTags []string\n\t}{\n\t\t{\n\t\t\tname:         \"existing target\",\n\t\t\ttargetName:   \"frontend\",\n\t\t\texpectError:  false,\n\t\t\texpectedTags: []string{\"frontend\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"non-existing target\",\n\t\t\ttargetName:   \"nonexistent\",\n\t\t\texpectError:  true,\n\t\t\texpectedTags: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget, err := config.GetTarget(tt.targetName)\n\n\t\t\tif tt.expectError {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t}\n\t\t\t\tif _, ok := err.(*core.TargetNotFound); !ok {\n\t\t\t\t\tt.Errorf(\"expected TargetNotFound error, got %T\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(target.Tags, tt.expectedTags) {\n\t\t\t\tt.Errorf(\"expected tags %v, got %v\", tt.expectedTags, target.Tags)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTarget_GetTargetNames(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconfig        Config\n\t\texpectedNames []string\n\t}{\n\t\t{\n\t\t\tname: \"multiple targets\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{\n\t\t\t\t\t{Name: \"target1\"},\n\t\t\t\t\t{Name: \"target2\"},\n\t\t\t\t\t{Name: \"target3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedNames: []string{\"target1\", \"target2\", \"target3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty target list\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{},\n\t\t\t},\n\t\t\texpectedNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"single target\",\n\t\t\tconfig: Config{\n\t\t\t\tTargetList: []Target{\n\t\t\t\t\t{Name: \"solo-target\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedNames: []string{\"solo-target\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnames := tt.config.GetTargetNames()\n\n\t\t\tif !reflect.DeepEqual(names, tt.expectedNames) {\n\t\t\t\tt.Errorf(\"expected names %v, got %v\", tt.expectedNames, names)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/task.go",
    "content": "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/yacspin\"\n\t\"gopkg.in/yaml.v3\"\n\n\tcore \"github.com/alajmo/mani/core\"\n)\n\nvar (\n\tbuildMode = \"dev\"\n)\n\ntype Command struct {\n\tName    string    `yaml:\"name\"`\n\tDesc    string    `yaml:\"desc\"`\n\tShell   string    `yaml:\"shell\"` // should be in the format: <program> <command flag>, for instance \"sh -c\", \"node -e\"\n\tCmd     string    `yaml:\"cmd\"`   // \"echo hello world\", it should not include the program flag (-c,-e, .etc)\n\tTask    string    `yaml:\"task\"`\n\tTaskRef string    `yaml:\"-\"` // Keep a reference to the task\n\tTTY     bool      `yaml:\"tty\"`\n\tEnv     yaml.Node `yaml:\"env\"`\n\tEnvList []string  `yaml:\"-\"`\n\n\t// Internal\n\tShellProgram string   `yaml:\"-\"` // should be in the format: <program>, example: \"sh\", \"node\"\n\tCmdArg       []string `yaml:\"-\"` // is in the format [\"-c echo hello world\"] or [\"-c\", \"echo hello world\"], it includes the shell flag\n}\n\ntype Task struct {\n\tSpecData   Spec\n\tTargetData Target\n\tThemeData  Theme\n\n\tName     string    `yaml:\"name\"`\n\tDesc     string    `yaml:\"desc\"`\n\tShell    string    `yaml:\"shell\"`\n\tCmd      string    `yaml:\"cmd\"`\n\tCommands []Command `yaml:\"commands\"`\n\tEnvList  []string  `yaml:\"-\"`\n\tTTY      bool      `yaml:\"tty\"`\n\n\tEnv    yaml.Node `yaml:\"env\"`\n\tSpec   yaml.Node `yaml:\"spec\"`\n\tTarget yaml.Node `yaml:\"target\"`\n\tTheme  yaml.Node `yaml:\"theme\"`\n\n\t// Internal\n\tShellProgram string   `yaml:\"-\"` // should be in the format: <program>, example: \"sh\", \"node\"\n\tCmdArg       []string `yaml:\"-\"` // is in the format [\"-c echo hello world\"] or [\"-c\", \"echo hello world\"], it includes the shell flag\n\tcontext      string\n\tcontextLine  int\n}\n\nfunc (t *Task) GetContext() string {\n\treturn t.context\n}\n\nfunc (t *Task) GetContextLine() int {\n\treturn t.contextLine\n}\n\n// ParseTask parses tasks and builds the correct \"AST\". Depending on if the data is specified inline,\n// or if it is a reference to resource, it will handle them differently.\nfunc (t *Task) ParseTask(config Config, taskErrors *ResourceErrors[Task]) {\n\tif t.Shell == \"\" {\n\t\tt.Shell = config.Shell\n\t} else {\n\t\tt.Shell = core.FormatShell(t.Shell)\n\t}\n\n\tprogram, cmdArgs := core.FormatShellString(t.Shell, t.Cmd)\n\tt.ShellProgram = program\n\tt.CmdArg = cmdArgs\n\n\tfor j, cmd := range t.Commands {\n\t\t// Task reference\n\t\tif cmd.Task != \"\" {\n\t\t\tcmdRef, err := config.GetCommand(cmd.Task)\n\t\t\tif err != nil {\n\t\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tt.Commands[j] = *cmdRef\n\t\t\tt.Commands[j].TaskRef = cmd.Task\n\t\t}\n\n\t\tif t.Commands[j].Shell == \"\" {\n\t\t\tt.Commands[j].Shell = DEFAULT_SHELL\n\t\t}\n\n\t\tprogram, cmdArgs := core.FormatShellString(t.Commands[j].Shell, t.Commands[j].Cmd)\n\t\tt.Commands[j].ShellProgram = program\n\t\tt.Commands[j].CmdArg = cmdArgs\n\t}\n\n\tif len(t.Theme.Content) > 0 {\n\t\t// Theme value\n\t\ttheme := &Theme{}\n\t\terr := t.Theme.Decode(theme)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.ThemeData = *theme\n\t\t}\n\t} else if t.Theme.Value != \"\" {\n\t\t// Theme reference\n\t\ttheme, err := config.GetTheme(t.Theme.Value)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.ThemeData = *theme\n\t\t}\n\t} else {\n\t\t// Default theme\n\t\ttheme, err := config.GetTheme(DEFAULT_THEME.Name)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.ThemeData = *theme\n\t\t}\n\t}\n\n\tif len(t.Spec.Content) > 0 {\n\t\t// Spec value\n\t\tspec := &Spec{}\n\t\terr := t.Spec.Decode(spec)\n\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.SpecData = *spec\n\t\t}\n\t} else if t.Spec.Value != \"\" {\n\t\t// Spec reference\n\t\tspec, err := config.GetSpec(t.Spec.Value)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.SpecData = *spec\n\t\t}\n\t} else {\n\t\t// Default spec\n\t\tspec, err := config.GetSpec(DEFAULT_SPEC.Name)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.SpecData = *spec\n\t\t}\n\t}\n\n\tif len(t.Target.Content) > 0 {\n\t\t// Target value\n\t\ttarget := &Target{}\n\t\terr := t.Target.Decode(target)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.TargetData = *target\n\t\t}\n\t} else if t.Target.Value != \"\" {\n\t\t// Target reference\n\t\ttarget, err := config.GetTarget(t.Target.Value)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.TargetData = *target\n\t\t}\n\t} else {\n\t\t// Default target\n\t\ttarget, err := config.GetTarget(DEFAULT_TARGET.Name)\n\t\tif err != nil {\n\t\t\ttaskErrors.Errors = append(taskErrors.Errors, err)\n\t\t} else {\n\t\t\tt.TargetData = *target\n\t\t}\n\t}\n}\n\nfunc TaskSpinner() (yacspin.Spinner, error) {\n\tvar cfg yacspin.Config\n\n\t// NOTE: Don't print the spinner in tests since it causes\n\t// golden files to produce different results.\n\tif buildMode == \"TEST\" {\n\t\tcfg = yacspin.Config{\n\t\t\tFrequency:       100 * time.Millisecond,\n\t\t\tCharSet:         yacspin.CharSets[9],\n\t\t\tSuffixAutoColon: false,\n\t\t\tWriter:          io.Discard,\n\t\t}\n\t} else {\n\t\tcfg = yacspin.Config{\n\t\t\tFrequency:       100 * time.Millisecond,\n\t\t\tCharSet:         yacspin.CharSets[9],\n\t\t\tSuffixAutoColon: false,\n\t\t\tShowCursor:      true,\n\t\t}\n\t}\n\n\tspinner, err := yacspin.New(cfg)\n\n\treturn *spinner, err\n}\n\nfunc (t Task) GetValue(key string, _ int) string {\n\tswitch strings.ToLower(key) {\n\tcase \"name\", \"task\":\n\t\treturn t.Name\n\tcase \"desc\", \"description\":\n\t\treturn t.Desc\n\tcase \"command\":\n\t\treturn t.Cmd\n\tcase \"spec\":\n\t\treturn t.SpecData.Name\n\tcase \"target\":\n\t\treturn t.TargetData.Name\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *Config) GetTaskList() ([]Task, []ResourceErrors[Task]) {\n\tvar tasks []Task\n\tcount := len(c.Tasks.Content)\n\n\ttaskErrors := []ResourceErrors[Task]{}\n\tfoundErrors := false\n\tfor i := 0; i < count; i += 2 {\n\t\ttask := &Task{\n\t\t\tName:        c.Tasks.Content[i].Value,\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Tasks.Content[i].Line,\n\t\t}\n\n\t\t// Shorthand definition: example_task: echo 123\n\t\tif c.Tasks.Content[i+1].Kind == 8 {\n\t\t\ttask.Cmd = c.Tasks.Content[i+1].Value\n\t\t} else { // Full definition\n\t\t\terr := c.Tasks.Content[i+1].Decode(task)\n\t\t\tif err != nil {\n\t\t\t\tfoundErrors = true\n\t\t\t\ttaskError := ResourceErrors[Task]{Resource: task, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\t\ttaskErrors = append(taskErrors, taskError)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\ttasks = append(tasks, *task)\n\t}\n\n\tif foundErrors {\n\t\treturn tasks, taskErrors\n\t}\n\n\treturn tasks, nil\n}\n\nfunc ParseTaskEnv(\n\tenv yaml.Node,\n\tuserEnv []string,\n\tparentEnv []string,\n\tconfigEnv []string,\n) ([]string, error) {\n\tcmdEnv, err := EvaluateEnv(ParseNodeEnv(env))\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\tpEnv, err := EvaluateEnv(parentEnv)\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\tenvList := MergeEnvs(userEnv, cmdEnv, pEnv, configEnv)\n\n\treturn envList, nil\n}\n\nfunc ParseTasksEnv(tasks []Task) {\n\tfor i := range tasks {\n\t\tenvs, err := ParseTaskEnv(tasks[i].Env, []string{}, []string{}, []string{})\n\t\tcore.CheckIfError(err)\n\n\t\ttasks[i].EnvList = envs\n\n\t\tfor j := range tasks[i].Commands {\n\t\t\tenvs, err = ParseTaskEnv(tasks[i].Commands[j].Env, []string{}, []string{}, []string{})\n\t\t\tcore.CheckIfError(err)\n\n\t\t\ttasks[i].Commands[j].EnvList = envs\n\t\t}\n\t}\n}\n\n// GetTaskProjects retrieves a filtered list of projects for a given task, applying\n// runtime flag overrides and target configurations.\n//\n// Behavior depends on the provided runtime flags (flags, setFlags) and task target:\n//   - If runtime flags are set (Projects, Paths, Tags, etc.), they take precedence\n//     and reset the task's target configuration.\n//   - If a target is explicitly specified (flags.Target), it loads and applies that\n//     target's configuration before applying runtime flag overrides.\n//   - If no runtime flags or target are provided, the task's default target data is used.\n//\n// Filtering priority (highest to lowest):\n//  1. Runtime flags (e.g., --projects, --tags, --cwd)\n//  2. Explicit target configuration (--target)\n//  3. Task's default target data (if no overrides exist)\n//\n// Returns:\n//   - Filtered []Project based on the resolved configuration.\n//   - Non-nil error if target resolution or project filtering fails.\nfunc (c Config) GetTaskProjects(\n\ttask *Task,\n\tflags *core.RunFlags,\n\tsetFlags *core.SetRunFlags,\n) ([]Project, error) {\n\tvar err error\n\tvar projects []Project\n\n\t// Reset target if any runtime flags are used\n\tif len(flags.Projects) > 0 ||\n\t\tlen(flags.Paths) > 0 ||\n\t\tlen(flags.Tags) > 0 ||\n\t\tflags.TagsExpr != \"\" ||\n\t\tflags.Target != \"\" ||\n\t\tsetFlags.Cwd ||\n\t\tsetFlags.All {\n\t\ttask.TargetData = Target{}\n\t}\n\n\tif flags.Target != \"\" {\n\t\ttarget, err := c.GetTarget(flags.Target)\n\t\tif err != nil {\n\t\t\treturn []Project{}, err\n\t\t}\n\t\ttask.TargetData = *target\n\t}\n\n\tif len(flags.Projects) > 0 {\n\t\ttask.TargetData.Projects = flags.Projects\n\t}\n\n\tif len(flags.Paths) > 0 {\n\t\ttask.TargetData.Paths = flags.Paths\n\t}\n\n\tif len(flags.Tags) > 0 {\n\t\ttask.TargetData.Tags = flags.Tags\n\t}\n\n\tif flags.TagsExpr != \"\" {\n\t\ttask.TargetData.TagsExpr = flags.TagsExpr\n\t}\n\n\tif setFlags.Cwd {\n\t\ttask.TargetData.Cwd = flags.Cwd\n\t}\n\n\tif setFlags.All {\n\t\ttask.TargetData.All = flags.All\n\t}\n\n\tprojects, err = c.FilterProjects(\n\t\ttask.TargetData.Cwd,\n\t\ttask.TargetData.All,\n\t\ttask.TargetData.Projects,\n\t\ttask.TargetData.Paths,\n\t\ttask.TargetData.Tags,\n\t\ttask.TargetData.TagsExpr,\n\t)\n\tif err != nil {\n\t\treturn []Project{}, err\n\t}\n\n\treturn projects, nil\n}\n\nfunc (c Config) GetTasksByNames(names []string) ([]Task, error) {\n\tif len(names) == 0 {\n\t\treturn c.TaskList, nil\n\t}\n\n\tfoundTasks := make(map[string]bool)\n\tfor _, t := range names {\n\t\tfoundTasks[t] = false\n\t}\n\n\tvar filteredTasks []Task\n\tfor _, name := range names {\n\t\tif foundTasks[name] {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, task := range c.TaskList {\n\t\t\tif name == task.Name {\n\t\t\t\tfoundTasks[task.Name] = true\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\t}\n\t}\n\n\tnonExistingTasks := []string{}\n\tfor k, v := range foundTasks {\n\t\tif !v {\n\t\t\tnonExistingTasks = append(nonExistingTasks, k)\n\t\t}\n\t}\n\n\tif len(nonExistingTasks) > 0 {\n\t\treturn []Task{}, &core.TaskNotFound{Name: nonExistingTasks}\n\t}\n\n\treturn filteredTasks, nil\n}\n\nfunc (c Config) GetTaskNames() []string {\n\ttaskNames := []string{}\n\tfor _, task := range c.TaskList {\n\t\ttaskNames = append(taskNames, task.Name)\n\t}\n\n\treturn taskNames\n}\n\nfunc (c Config) GetTaskNameAndDesc() []string {\n\ttaskNames := []string{}\n\tfor _, task := range c.TaskList {\n\t\ttaskNames = append(taskNames, fmt.Sprintf(\"%s\\t%s\", task.Name, task.Desc))\n\t}\n\n\treturn taskNames\n}\n\nfunc (c Config) GetTask(name string) (*Task, error) {\n\tfor _, cmd := range c.TaskList {\n\t\tif name == cmd.Name {\n\t\t\treturn &cmd, nil\n\t\t}\n\t}\n\n\treturn nil, &core.TaskNotFound{Name: []string{name}}\n}\n\nfunc (c Config) GetCommand(taskName string) (*Command, error) {\n\tfor _, cmd := range c.TaskList {\n\t\tif taskName == cmd.Name {\n\t\t\tcmdRef := &Command{\n\t\t\t\tName:    cmd.Name,\n\t\t\t\tDesc:    cmd.Desc,\n\t\t\t\tEnvList: cmd.EnvList,\n\t\t\t\tShell:   cmd.Shell,\n\t\t\t\tCmd:     cmd.Cmd,\n\t\t\t}\n\n\t\t\treturn cmdRef, nil\n\t\t}\n\t}\n\n\treturn nil, &core.TaskNotFound{Name: []string{taskName}}\n}\n\nfunc (t Task) ConvertTaskToCommand() Command {\n\tcmd := Command{\n\t\tName:         t.Name,\n\t\tDesc:         t.Desc,\n\t\tEnvList:      t.EnvList,\n\t\tShell:        t.Shell,\n\t\tCmd:          t.Cmd,\n\t\tCmdArg:       t.CmdArg,\n\t\tShellProgram: t.ShellProgram,\n\t}\n\n\treturn cmd\n}\n\nfunc ParseCmd(\n\tcmd string,\n\trunFlags *core.RunFlags,\n\tsetFlags *core.SetRunFlags,\n\tconfig *Config,\n) ([]Task, []Project, error) {\n\ttask := Task{Name: \"output\", Cmd: cmd, TTY: runFlags.TTY}\n\ttaskErrors := make([]ResourceErrors[Task], 1)\n\ttask.ParseTask(*config, &taskErrors[0])\n\n\tvar configErr = \"\"\n\tfor _, taskError := range taskErrors {\n\t\tif len(taskError.Errors) > 0 {\n\t\t\tconfigErr = fmt.Sprintf(\"%s%s\", configErr, FormatErrors(taskError.Resource, taskError.Errors))\n\t\t}\n\t}\n\tif configErr != \"\" {\n\t\tcore.CheckIfError(errors.New(configErr))\n\t}\n\n\tprojects, err := config.GetTaskProjects(&task, runFlags, setFlags)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcore.CheckIfError(err)\n\n\tvar tasks []Task\n\tfor range projects {\n\t\tt := Task{}\n\t\terr := copier.Copy(&t, &task)\n\t\tcore.CheckIfError(err)\n\t\ttasks = append(tasks, t)\n\t}\n\n\tif len(projects) == 0 {\n\t\treturn nil, nil, &core.NoTargets{}\n\t}\n\n\treturn tasks, projects, err\n}\n\nfunc ParseSingleTask(\n\ttaskName string,\n\trunFlags *core.RunFlags,\n\tsetFlags *core.SetRunFlags,\n\tconfig *Config,\n) ([]Task, []Project, error) {\n\ttask, err := config.GetTask(taskName)\n\tcore.CheckIfError(err)\n\n\tprojects, err := config.GetTaskProjects(task, runFlags, setFlags)\n\tcore.CheckIfError(err)\n\n\tvar tasks []Task\n\tfor range projects {\n\t\tt := Task{}\n\t\terr := copier.Copy(&t, &task)\n\t\tcore.CheckIfError(err)\n\t\ttasks = append(tasks, t)\n\t}\n\n\tif len(projects) == 0 {\n\t\treturn nil, nil, &core.NoTargets{}\n\t}\n\n\treturn tasks, projects, err\n}\n\nfunc ParseManyTasks(\n\ttaskNames []string,\n\trunFlags *core.RunFlags,\n\tsetFlags *core.SetRunFlags,\n\tconfig *Config,\n) ([]Task, []Project, error) {\n\tparentTask := Task{Name: \"Tasks\", Cmd: \"\", Commands: []Command{}}\n\ttaskErrors := make([]ResourceErrors[Task], 1)\n\tparentTask.ParseTask(*config, &taskErrors[0])\n\n\tfor _, taskName := range taskNames {\n\t\ttask, err := config.GetTask(taskName)\n\t\tcore.CheckIfError(err)\n\n\t\tif task.Cmd != \"\" {\n\t\t\tcmd := task.ConvertTaskToCommand()\n\t\t\tparentTask.Commands = append(parentTask.Commands, cmd)\n\t\t} else if len(task.Commands) > 0 {\n\t\t\tparentTask.Commands = append(parentTask.Commands, task.Commands...)\n\t\t}\n\t}\n\n\tprojects, err := config.GetTaskProjects(&parentTask, runFlags, setFlags)\n\tvar tasks []Task\n\tfor range projects {\n\t\tt := Task{}\n\t\terr := copier.Copy(&t, &parentTask)\n\t\tcore.CheckIfError(err)\n\t\ttasks = append(tasks, t)\n\t}\n\n\tif len(projects) == 0 {\n\t\treturn nil, nil, &core.NoTargets{}\n\t}\n\n\treturn tasks, projects, err\n}\n"
  },
  {
    "path": "core/dao/task_test.go",
    "content": "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 *testing.T) {\n\tconfig := Config{\n\t\tShell: \"sh -c\",\n\t\tSpecList: []Spec{\n\t\t\tDEFAULT_SPEC,\n\t\t},\n\t\tTargetList: []Target{\n\t\t\tDEFAULT_TARGET,\n\t\t},\n\t\tThemeList: []Theme{\n\t\t\tDEFAULT_THEME,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\ttask          Task\n\t\texpectError   bool\n\t\texpectedShell string\n\t}{\n\t\t{\n\t\t\tname: \"basic task parsing\",\n\t\t\ttask: Task{\n\t\t\t\tName:       \"test-task\",\n\t\t\t\tCmd:        \"echo hello\",\n\t\t\t\tSpecData:   DEFAULT_SPEC,\n\t\t\t\tTargetData: DEFAULT_TARGET,\n\t\t\t\tThemeData:  DEFAULT_THEME,\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedShell: \"sh -c\",\n\t\t},\n\t\t{\n\t\t\tname: \"custom shell\",\n\t\t\ttask: Task{\n\t\t\t\tName:       \"node-task\",\n\t\t\t\tShell:      \"node -e\",\n\t\t\t\tCmd:        \"console.log('hello')\",\n\t\t\t\tSpecData:   DEFAULT_SPEC,\n\t\t\t\tTargetData: DEFAULT_TARGET,\n\t\t\t\tThemeData:  DEFAULT_THEME,\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedShell: \"node -e\",\n\t\t},\n\t\t{\n\t\t\tname: \"with commands\",\n\t\t\ttask: Task{\n\t\t\t\tName: \"multi-cmd\",\n\t\t\t\tCommands: []Command{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"cmd1\",\n\t\t\t\t\t\tCmd:  \"echo first\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"cmd2\",\n\t\t\t\t\t\tCmd:  \"echo second\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSpecData:   DEFAULT_SPEC,\n\t\t\t\tTargetData: DEFAULT_TARGET,\n\t\t\t\tThemeData:  DEFAULT_THEME,\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedShell: \"sh -c\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttaskErrors := &ResourceErrors[Task]{}\n\t\t\ttt.task.ParseTask(config, taskErrors)\n\n\t\t\tif tt.expectError && len(taskErrors.Errors) == 0 {\n\t\t\t\tt.Error(\"expected errors but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && len(taskErrors.Errors) > 0 {\n\t\t\t\tt.Errorf(\"unexpected errors: %v\", taskErrors.Errors)\n\t\t\t}\n\t\t\tif tt.task.Shell != tt.expectedShell {\n\t\t\t\tt.Errorf(\"expected shell %q, got %q\", tt.expectedShell, tt.task.Shell)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTask_GetTaskProjects(t *testing.T) {\n\tconfig := Config{\n\t\tShell: DEFAULT_SHELL,\n\t\tProjectList: []Project{\n\t\t\t{Name: \"proj1\", Tags: []string{\"frontend\"}},\n\t\t\t{Name: \"proj2\", Tags: []string{\"backend\"}},\n\t\t\t{Name: \"proj3\", Tags: []string{\"frontend\", \"api\"}},\n\t\t},\n\t\tSpecList: []Spec{\n\t\t\tDEFAULT_SPEC,\n\t\t},\n\t\tTargetList: []Target{\n\t\t\tDEFAULT_TARGET,\n\t\t},\n\t\tThemeList: []Theme{\n\t\t\tDEFAULT_THEME,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\ttask          *Task\n\t\tflags         *core.RunFlags\n\t\tsetFlags      *core.SetRunFlags\n\t\texpectedCount int\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname: \"filter by tags\",\n\t\t\ttask: &Task{\n\t\t\t\tName:  \"test-task\",\n\t\t\t\tShell: DEFAULT_SHELL,\n\t\t\t\tTargetData: Target{\n\t\t\t\t\tName: \"default\",\n\t\t\t\t\tTags: []string{\"frontend\"},\n\t\t\t\t},\n\t\t\t\tSpecData:  DEFAULT_SPEC,\n\t\t\t\tThemeData: DEFAULT_THEME,\n\t\t\t},\n\t\t\tflags:         &core.RunFlags{},\n\t\t\tsetFlags:      &core.SetRunFlags{},\n\t\t\texpectedCount: 2,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by projects\",\n\t\t\ttask: &Task{\n\t\t\t\tName: \"test-task\",\n\t\t\t\tTargetData: Target{\n\t\t\t\t\tName:     DEFAULT_TARGET.Name,\n\t\t\t\t\tProjects: []string{\"proj1\", \"proj2\"},\n\t\t\t\t},\n\t\t\t\tSpecData:  DEFAULT_SPEC,\n\t\t\t\tThemeData: DEFAULT_THEME,\n\t\t\t},\n\t\t\tflags:         &core.RunFlags{},\n\t\t\tsetFlags:      &core.SetRunFlags{},\n\t\t\texpectedCount: 2,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"override with flag projects\",\n\t\t\ttask: &Task{\n\t\t\t\tName: \"test-task\",\n\t\t\t\tTargetData: Target{\n\t\t\t\t\tName:     DEFAULT_TARGET.Name,\n\t\t\t\t\tProjects: []string{\"proj1\"},\n\t\t\t\t},\n\t\t\t\tSpecData:  DEFAULT_SPEC,\n\t\t\t\tThemeData: DEFAULT_THEME,\n\t\t\t},\n\t\t\tflags: &core.RunFlags{\n\t\t\t\tProjects: []string{\"proj2\", \"proj3\"},\n\t\t\t},\n\t\t\tsetFlags:      &core.SetRunFlags{},\n\t\t\texpectedCount: 2,\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprojects, err := config.GetTaskProjects(tt.task, tt.flags, tt.setFlags)\n\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif len(projects) != tt.expectedCount {\n\t\t\t\tt.Errorf(\"expected %d projects, got %d\", tt.expectedCount, len(projects))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTask_CmdParse(t *testing.T) {\n\tconfig := &Config{\n\t\tShell: DEFAULT_SHELL,\n\t\tProjectList: []Project{\n\t\t\t{Name: \"test-project\", Path: \"/test/path\"},\n\t\t},\n\t\tSpecList: []Spec{\n\t\t\tDEFAULT_SPEC,\n\t\t},\n\t\tTargetList: []Target{\n\t\t\tDEFAULT_TARGET,\n\t\t},\n\t\tThemeList: []Theme{\n\t\t\tDEFAULT_THEME,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname           string\n\t\tcmd            string\n\t\trunFlags       *core.RunFlags\n\t\tsetFlags       *core.SetRunFlags\n\t\texpectTasks    int\n\t\texpectProjects int\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname: \"basic command\",\n\t\t\tcmd:  \"echo hello\",\n\t\t\trunFlags: &core.RunFlags{\n\t\t\t\tTarget:   \"default\",\n\t\t\t\tProjects: []string{\"test-project\"},\n\t\t\t},\n\t\t\tsetFlags:       &core.SetRunFlags{},\n\t\t\texpectTasks:    1,\n\t\t\texpectProjects: 1,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname: \"command with no matching projects\",\n\t\t\tcmd:  \"echo hello\",\n\t\t\trunFlags: &core.RunFlags{\n\t\t\t\tProjects: []string{\"non-existent\"},\n\t\t\t\tTarget:   \"default\",\n\t\t\t},\n\t\t\tsetFlags:       &core.SetRunFlags{},\n\t\t\texpectTasks:    0,\n\t\t\texpectProjects: 0,\n\t\t\texpectError:    true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttasks, projects, err := ParseCmd(tt.cmd, tt.runFlags, tt.setFlags, config)\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif len(tasks) != tt.expectTasks {\n\t\t\t\tt.Errorf(\"expected %d tasks, got %d\", tt.expectTasks, len(tasks))\n\t\t\t}\n\t\t\tif len(projects) != tt.expectProjects {\n\t\t\t\tt.Errorf(\"expected %d projects, got %d\", tt.expectProjects, len(projects))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfig_FilterProjects(t *testing.T) {\n\t// Setup test configuration with sample projects\n\tconfig := Config{\n\t\tProjectList: []Project{\n\t\t\t{Name: \"root\", Path: \"/path\", RelPath: \".\", Tags: []string{}},\n\t\t\t{Name: \"frontend\", Path: \"/path/frontend\", RelPath: \"frontend\", Tags: []string{\"web\", \"ui\"}},\n\t\t\t{Name: \"backend\", Path: \"/path/backend\", RelPath: \"backend\", Tags: []string{\"api\", \"db\"}},\n\t\t\t{Name: \"mobile\", Path: \"/path/mobile\", RelPath: \"mobile\", Tags: []string{\"ui\", \"app\"}},\n\t\t\t{Name: \"docs\", Path: \"/path/docs\", RelPath: \"docs\", Tags: []string{\"docs\"}},\n\t\t\t{Name: \"shared\", Path: \"/path/shared\", RelPath: \"shared\", Tags: []string{\"lib\", \"shared\"}},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname             string\n\t\tcwdFlag          bool\n\t\tallProjectsFlag  bool\n\t\tprojectsFlag     []string\n\t\tprojectPathsFlag []string\n\t\ttagsFlag         []string\n\t\ttagsExprFlag     string\n\t\texpectedCount    int\n\t\texpectedNames    []string\n\t\texpectError      bool\n\t}{\n\t\t{\n\t\t\tname:            \"single project\",\n\t\t\tallProjectsFlag: true,\n\t\t\tprojectsFlag:    []string{\"frontend\"},\n\t\t\ttagsFlag:        []string{\"ui\"},\n\t\t\texpectedCount:   1,\n\t\t\texpectedNames:   []string{\"frontend\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by project names\",\n\t\t\tprojectsFlag:  []string{\"frontend\", \"backend\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedNames: []string{\"frontend\", \"backend\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:             \"partial path matching\",\n\t\t\tprojectPathsFlag: []string{\"front\"}, // Should match 'frontend'\n\t\t\texpectedCount:    1,\n\t\t\texpectedNames:    []string{\"frontend\"},\n\t\t\texpectError:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by single tag\",\n\t\t\ttagsFlag:      []string{\"ui\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedNames: []string{\"frontend\", \"mobile\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by multiple tags - intersection\",\n\t\t\ttagsFlag:      []string{\"ui\", \"web\"},\n\t\t\texpectedCount: 1,\n\t\t\texpectedNames: []string{\"frontend\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:             \"filter by project paths\",\n\t\t\tprojectPathsFlag: []string{\"frontend\"},\n\t\t\texpectedCount:    1,\n\t\t\texpectedNames:    []string{\"frontend\"},\n\t\t\texpectError:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by tags expression\",\n\t\t\ttagsExprFlag:  \"ui && !web\",\n\t\t\texpectedCount: 1,\n\t\t\texpectedNames: []string{\"mobile\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple criteria - intersection\",\n\t\t\tprojectsFlag:  []string{\"frontend\", \"mobile\", \"backend\"},\n\t\t\ttagsFlag:      []string{\"ui\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedNames: []string{\"frontend\", \"mobile\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-existent project name\",\n\t\t\tprojectsFlag:  []string{\"nonexistent\"},\n\t\t\texpectedCount: 0,\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-existent tag\",\n\t\t\ttagsFlag:      []string{\"nonexistent\"},\n\t\t\texpectedCount: 0,\n\t\t\texpectedNames: []string{},\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid tags expression\",\n\t\t\ttagsExprFlag:  \"ui && (NOT\", // Invalid syntax\n\t\t\texpectedCount: 0,\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"cwd flag with other flags\",\n\t\t\tcwdFlag:       true,\n\t\t\tprojectsFlag:  []string{\"root\"},\n\t\t\texpectedCount: 1,\n\t\t\texpectedNames: []string{\"\"},\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprojects, err := config.FilterProjects(\n\t\t\t\ttt.cwdFlag,\n\t\t\t\ttt.allProjectsFlag,\n\t\t\t\ttt.projectsFlag,\n\t\t\t\ttt.projectPathsFlag,\n\t\t\t\ttt.tagsFlag,\n\t\t\t\ttt.tagsExprFlag,\n\t\t\t)\n\n\t\t\t// Check error expectations\n\t\t\tif tt.expectError && err == nil {\n\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t}\n\t\t\tif !tt.expectError && err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\t// Skip further checks if we expected an error\n\t\t\tif tt.expectError {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Check number of projects returned\n\t\t\tif len(projects) != tt.expectedCount {\n\t\t\t\tt.Errorf(\"expected %d projects, got %d\", tt.expectedCount, len(projects))\n\t\t\t}\n\n\t\t\t// Check specific projects returned (if specified)\n\t\t\tif tt.expectedNames != nil {\n\t\t\t\tactualNames := make([]string, len(projects))\n\t\t\t\tfor i, p := range projects {\n\t\t\t\t\tactualNames[i] = p.Name\n\t\t\t\t}\n\n\t\t\t\t// Sort both slices to ensure consistent comparison\n\t\t\t\tsort.Strings(actualNames)\n\t\t\t\tsort.Strings(tt.expectedNames)\n\n\t\t\t\tif !reflect.DeepEqual(actualNames, tt.expectedNames) {\n\t\t\t\t\tt.Errorf(\"expected projects %v, got %v\", tt.expectedNames, actualNames)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/dao/theme.go",
    "content": "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.com/alajmo/mani/core\"\n\t\"github.com/gookit/color\"\n)\n\ntype ColorOptions struct {\n\tFg     *string `yaml:\"fg\"`\n\tBg     *string `yaml:\"bg\"`\n\tAlign  *string `yaml:\"align\"`\n\tAttr   *string `yaml:\"attr\"`\n\tFormat *string `yaml:\"format\"`\n}\n\ntype Theme struct {\n\tName   string `yaml:\"name\"`\n\tTable  Table  `yaml:\"table\"`\n\tTree   Tree   `yaml:\"tree\"`\n\tStream Stream `yaml:\"stream\"`\n\tBlock  Block  `yaml:\"block\"`\n\tTUI    TUI    `yaml:\"tui\"`\n\tColor  *bool  `yaml:\"color\"`\n\n\tcontext     string\n\tcontextLine int\n}\n\ntype Row struct {\n\tColumns []string\n}\n\ntype TableOutput struct {\n\tHeaders []string\n\tRows    []Row\n}\n\nfunc (t *Theme) GetContext() string {\n\treturn t.context\n}\n\nfunc (t *Theme) GetContextLine() int {\n\treturn t.contextLine\n}\n\nfunc (r Row) GetValue(_ string, i int) string {\n\tif i < len(r.Columns) {\n\t\treturn r.Columns[i]\n\t}\n\n\treturn \"\"\n}\n\n// Populates ThemeList\nfunc (c *Config) ParseThemes() ([]Theme, []ResourceErrors[Theme]) {\n\tvar themes []Theme\n\tcount := len(c.Themes.Content)\n\n\tthemeErrors := []ResourceErrors[Theme]{}\n\tfoundErrors := false\n\tfor i := 0; i < count; i += 2 {\n\t\ttheme := &Theme{\n\t\t\tName:        c.Themes.Content[i].Value,\n\t\t\tcontext:     c.Path,\n\t\t\tcontextLine: c.Themes.Content[i].Line,\n\t\t}\n\n\t\terr := c.Themes.Content[i+1].Decode(theme)\n\t\tif err != nil {\n\t\t\tfoundErrors = true\n\t\t\tthemeError := ResourceErrors[Theme]{Resource: theme, Errors: core.StringsToErrors(err.(*yaml.TypeError).Errors)}\n\t\t\tthemeErrors = append(themeErrors, themeError)\n\t\t\tcontinue\n\t\t}\n\n\t\tthemes = append(themes, *theme)\n\t}\n\n\t// Loop through themes and set default values\n\tfor i := range themes {\n\t\t// Color\n\t\tif themes[i].Color == nil {\n\t\t\tthemes[i].Color = core.Ptr(true)\n\t\t}\n\n\t\t// Stream\n\t\tLoadStreamTheme(&themes[i].Stream)\n\n\t\t// Table\n\t\tLoadTableTheme(&themes[i].Table)\n\n\t\t// Tree\n\t\tLoadTreeTheme(&themes[i].Tree)\n\n\t\t// Block\n\t\tLoadBlockTheme(&themes[i].Block)\n\n\t\t// TUI\n\t\tLoadTUITheme(&themes[i].TUI)\n\t}\n\n\tif foundErrors {\n\t\treturn themes, themeErrors\n\t}\n\n\treturn themes, nil\n}\n\nfunc (c Config) GetTheme(name string) (*Theme, error) {\n\tfor _, theme := range c.ThemeList {\n\t\tif name == theme.Name {\n\t\t\treturn &theme, nil\n\t\t}\n\t}\n\n\treturn nil, &core.ThemeNotFound{Name: name}\n}\n\nfunc (c Config) GetThemeNames() []string {\n\tnames := []string{}\n\tfor _, theme := range c.ThemeList {\n\t\tnames = append(names, theme.Name)\n\t}\n\n\treturn names\n}\n\n// Merges default with user theme.\n// Converts colors to hex, and align, attr, and format to its backend representation (single character).\nfunc MergeThemeOptions(userOption *ColorOptions, defaultOption *ColorOptions) *ColorOptions {\n\tif userOption == nil {\n\t\treturn &ColorOptions{\n\t\t\tFg:     convertToHex(defaultOption.Fg),\n\t\t\tBg:     convertToHex(defaultOption.Bg),\n\t\t\tAttr:   convertToAttr(defaultOption.Attr),\n\t\t\tAlign:  convertToAlign(defaultOption.Align),\n\t\t\tFormat: convertToFormat(defaultOption.Format),\n\t\t}\n\t}\n\tresult := &ColorOptions{}\n\n\tif userOption.Fg == nil {\n\t\tresult.Fg = convertToHex(defaultOption.Fg)\n\t} else {\n\t\tresult.Fg = convertToHex(userOption.Fg)\n\t}\n\n\tif userOption.Bg == nil {\n\t\tresult.Bg = convertToHex(defaultOption.Bg)\n\t} else {\n\t\tresult.Bg = convertToHex(userOption.Bg)\n\t}\n\n\tif userOption.Attr == nil {\n\t\tresult.Attr = convertToAttr(defaultOption.Attr)\n\t} else {\n\t\tresult.Attr = convertToAttr(userOption.Attr)\n\t}\n\n\tif userOption.Align == nil {\n\t\tresult.Align = convertToAlign(defaultOption.Align)\n\t} else {\n\t\tresult.Align = convertToAlign(userOption.Align)\n\t}\n\n\tif userOption.Format == nil {\n\t\tresult.Format = convertToFormat(defaultOption.Format)\n\t} else {\n\t\tresult.Format = convertToFormat(userOption.Format)\n\t}\n\n\treturn result\n}\n\n// Used for gookit/color printing stream\nfunc StyleFg(colr string) color.RGBColor {\n\t// User provided\n\tif colr != \"\" {\n\t\treturn color.HEX(colr)\n\t}\n\n\t// Default Fg color\n\treturn color.Normal.RGB()\n}\n\nfunc StyleFormat(text string, format string) string {\n\tswitch format {\n\tcase \"l\":\n\t\treturn strings.ToLower(text)\n\tcase \"u\":\n\t\treturn strings.ToUpper(text)\n\tcase \"t\":\n\t\tcaser := cases.Title(language.English)\n\t\treturn caser.String(text)\n\t}\n\n\treturn text\n}\n\n// Used for gookit/color printing tables/blocks\nfunc StyleString(text string, opts ColorOptions, useColors bool) string {\n\tif !useColors {\n\t\treturn text\n\t}\n\n\t// Format\n\tswitch *opts.Format {\n\tcase \"l\":\n\t\ttext = strings.ToLower(text)\n\tcase \"u\":\n\t\ttext = strings.ToUpper(text)\n\tcase \"t\":\n\t\tcaser := cases.Title(language.English)\n\t\ttext = caser.String(text)\n\t}\n\n\t// Fg\n\tvar fgStr string\n\tif *opts.Fg != \"\" {\n\t\tfgStr = color.HEX(*opts.Fg).Sprint(text)\n\t} else {\n\t\tfgStr = text\n\t}\n\n\t// Attr\n\tattr := color.OpReset\n\tswitch *opts.Attr {\n\tcase \"b\":\n\t\tattr = color.OpBold\n\tcase \"i\":\n\t\tattr = color.OpItalic\n\tcase \"u\":\n\t\tattr = color.OpUnderscore\n\t}\n\n\tstyledString := attr.Sprint(fgStr)\n\n\treturn styledString\n}\n\nfunc convertToHex(s *string) *string {\n\tif s == nil || len(*s) == 0 {\n\t\treturn core.Ptr(\"-\")\n\t}\n\n\t// Assume it's hex already\n\tif (*s)[0] == '#' {\n\t\treturn s\n\t}\n\n\t// Named color\n\thex := \"#\" + color.RGBFromString(*s).Hex()\n\treturn &hex\n}\n\nfunc convertToAttr(attr *string) *string {\n\tif attr == nil || len(*attr) == 0 {\n\t\treturn core.Ptr(\"-\")\n\t}\n\n\tattrStr := strings.ToLower(*attr)\n\tswitch attrStr {\n\tcase \"b\", \"bold\":\n\t\treturn core.Ptr(\"b\")\n\tcase \"i\", \"italic\":\n\t\treturn core.Ptr(\"i\")\n\tcase \"u\", \"underline\":\n\t\treturn core.Ptr(\"u\")\n\t}\n\n\treturn core.Ptr(\"-\")\n}\n\nfunc convertToAlign(align *string) *string {\n\tif align == nil || len(*align) == 0 {\n\t\treturn core.Ptr(\"\")\n\t}\n\n\talignStr := strings.ToLower(*align)\n\tswitch alignStr {\n\tcase \"l\", \"left\":\n\t\treturn core.Ptr(\"l\")\n\tcase \"c\", \"center\":\n\t\treturn core.Ptr(\"c\")\n\tcase \"r\", \"right\":\n\t\treturn core.Ptr(\"r\")\n\t}\n\n\treturn core.Ptr(\"\")\n}\n\nfunc convertToFormat(format *string) *string {\n\tif format == nil || len(*format) == 0 {\n\t\treturn core.Ptr(\"\")\n\t}\n\n\tformatStr := strings.ToLower(*format)\n\tswitch formatStr {\n\tcase \"t\", \"title\":\n\t\treturn core.Ptr(\"t\")\n\tcase \"l\", \"lower\":\n\t\treturn core.Ptr(\"l\")\n\tcase \"u\", \"upper\":\n\t\treturn core.Ptr(\"u\")\n\t}\n\n\treturn core.Ptr(\"\")\n}\n"
  },
  {
    "path": "core/dao/theme_block.go",
    "content": "package dao\n\nimport (\n\t\"github.com/alajmo/mani/core\"\n)\n\nvar DefaultBlock = Block{\n\tKey: &ColorOptions{\n\t\tFg:     core.Ptr(\"#5f87d7\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tSeparator: &ColorOptions{\n\t\tFg:     core.Ptr(\"#5f87d7\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tValue: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tValueTrue: &ColorOptions{\n\t\tFg:     core.Ptr(\"#00af5f\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tValueFalse: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d75f5f\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n}\n\ntype Block struct {\n\tKey        *ColorOptions `yaml:\"key\"`\n\tSeparator  *ColorOptions `yaml:\"separator\"`\n\tValue      *ColorOptions `yaml:\"value\"`\n\tValueTrue  *ColorOptions `yaml:\"value_true\"`\n\tValueFalse *ColorOptions `yaml:\"value_false\"`\n}\n\nfunc LoadBlockTheme(block *Block) {\n\tif block.Key == nil {\n\t\tblock.Key = DefaultBlock.Key\n\t} else {\n\t\tblock.Key = MergeThemeOptions(block.Key, DefaultBlock.Key)\n\t}\n\n\tif block.Value == nil {\n\t\tblock.Value = DefaultBlock.Value\n\t} else {\n\t\tblock.Value = MergeThemeOptions(block.Value, DefaultBlock.Value)\n\t}\n\n\tif block.Separator == nil {\n\t\tblock.Separator = DefaultBlock.Separator\n\t} else {\n\t\tblock.Separator = MergeThemeOptions(block.Separator, DefaultBlock.Separator)\n\t}\n\n\tif block.ValueTrue == nil {\n\t\tblock.ValueTrue = DefaultBlock.ValueTrue\n\t} else {\n\t\tblock.ValueTrue = MergeThemeOptions(block.ValueTrue, DefaultBlock.ValueTrue)\n\t}\n\n\tif block.ValueFalse == nil {\n\t\tblock.ValueFalse = DefaultBlock.ValueFalse\n\t} else {\n\t\tblock.ValueFalse = MergeThemeOptions(block.ValueFalse, DefaultBlock.ValueFalse)\n\t}\n}\n"
  },
  {
    "path": "core/dao/theme_stream.go",
    "content": "package dao\n\ntype Stream struct {\n\tPrefix       bool     `yaml:\"prefix\"`\n\tPrefixColors []string `yaml:\"prefix_colors\"`\n\tHeader       bool     `yaml:\"header\"`\n\tHeaderChar   string   `yaml:\"header_char\"`\n\tHeaderPrefix string   `yaml:\"header_prefix\"`\n}\n\nvar DefaultStream = Stream{\n\tPrefix:       true,\n\tHeader:       true,\n\tHeaderPrefix: \"TASK\",\n\tHeaderChar:   \"*\",\n\tPrefixColors: []string{\"#d787ff\", \"#00af5f\", \"#d75f5f\", \"#5f87d7\", \"#00af87\", \"#5f00ff\"},\n}\n\nfunc LoadStreamTheme(stream *Stream) {\n\tif stream.PrefixColors == nil {\n\t\tstream.PrefixColors = DefaultStream.PrefixColors\n\t} else {\n\t\tfor j := range stream.PrefixColors {\n\t\t\tstream.PrefixColors[j] = *convertToHex(&stream.PrefixColors[j])\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/dao/theme_table.go",
    "content": "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 DefaultTable = Table{\n\tBox: table.StyleDefault.Box,\n\n\tStyle: \"ascii\",\n\n\tBorder: &Border{\n\t\tAround:  core.Ptr(false),\n\t\tColumns: core.Ptr(true),\n\t\tHeader:  core.Ptr(true),\n\t\tRows:    core.Ptr(true),\n\t},\n\n\tHeader: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d787ff\"),\n\t\tAttr:   core.Ptr(\"bold\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tTitleColumn: &ColorOptions{\n\t\tFg:     core.Ptr(\"#5f87d7\"),\n\t\tAttr:   core.Ptr(\"bold\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n}\n\ntype Border struct {\n\tAround  *bool `yaml:\"around\"`\n\tColumns *bool `yaml:\"columns\"`\n\tHeader  *bool `yaml:\"header\"`\n\tRows    *bool `yaml:\"rows\"`\n}\n\ntype Table struct {\n\t// Stylable via YAML\n\tStyle       string        `yaml:\"style\"`\n\tBorder      *Border       `yaml:\"border\"`\n\tHeader      *ColorOptions `yaml:\"header\"`\n\tTitleColumn *ColorOptions `yaml:\"title_column\"`\n\n\t// Not stylable via YAML\n\tBox table.BoxStyle `yaml:\"-\"`\n}\n\nfunc LoadTableTheme(mTable *Table) {\n\t// Table\n\tstyle := strings.ToLower(mTable.Style)\n\tswitch style {\n\tcase \"light\":\n\t\tmTable.Box = table.StyleLight.Box\n\tcase \"bold\":\n\t\tmTable.Box = table.StyleBold.Box\n\tcase \"double\":\n\t\tmTable.Box = table.StyleDouble.Box\n\tcase \"rounded\":\n\t\tmTable.Box = table.StyleRounded.Box\n\tdefault: // ascii\n\t\tmTable.Box = table.StyleBoxDefault\n\t}\n\n\t// Options\n\tif mTable.Border == nil {\n\t\tmTable.Border = DefaultTable.Border\n\t} else {\n\t\tif mTable.Border.Around == nil {\n\t\t\tmTable.Border.Around = DefaultTable.Border.Around\n\t\t}\n\n\t\tif mTable.Border.Columns == nil {\n\t\t\tmTable.Border.Columns = DefaultTable.Border.Columns\n\t\t}\n\n\t\tif mTable.Border.Header == nil {\n\t\t\tmTable.Border.Header = DefaultTable.Border.Header\n\t\t}\n\n\t\tif mTable.Border.Rows == nil {\n\t\t\tmTable.Border.Rows = DefaultTable.Border.Rows\n\t\t}\n\t}\n\n\t// Header\n\tif mTable.Header == nil {\n\t\tmTable.Header = DefaultTable.Header\n\t} else {\n\t\tmTable.Header = MergeThemeOptions(mTable.Header, DefaultTable.Header)\n\t}\n\n\t// Title Column\n\tif mTable.TitleColumn == nil {\n\t\tmTable.TitleColumn = DefaultTable.TitleColumn\n\t} else {\n\t\tmTable.TitleColumn = MergeThemeOptions(mTable.TitleColumn, DefaultTable.TitleColumn)\n\t}\n}\n"
  },
  {
    "path": "core/dao/theme_tree.go",
    "content": "package dao\n\nimport (\n\t\"strings\"\n)\n\ntype Tree struct {\n\tStyle string `yaml:\"style\"`\n}\n\nvar DefaultTree = Tree{\n\tStyle: \"light\",\n}\n\nfunc LoadTreeTheme(tree *Tree) {\n\tstyle := strings.ToLower(tree.Style)\n\tswitch style {\n\tcase \"light\":\n\t\ttree.Style = \"light\"\n\tcase \"bullet-flower\":\n\t\ttree.Style = \"bullet-flower\"\n\tcase \"bullet-square\":\n\t\ttree.Style = \"bullet-square\"\n\tcase \"bullet-star\":\n\t\ttree.Style = \"bullet-star\"\n\tcase \"bullet-triangle\":\n\t\ttree.Style = \"bullet-triangle\"\n\tcase \"bold\":\n\t\ttree.Style = \"bold\"\n\tcase \"double\":\n\t\ttree.Style = \"double\"\n\tcase \"rounded\":\n\t\ttree.Style = \"rounded\"\n\tcase \"markdown\":\n\t\ttree.Style = \"markdown\"\n\tdefault:\n\t\ttree.Style = \"ascii\"\n\t}\n}\n"
  },
  {
    "path": "core/dao/theme_tui.go",
    "content": "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 add them since\n// MergeThemeOptions initializes all of the fields.\nvar DefaultTUI = TUI{\n\tDefault: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tBorder: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tBorderFocus: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d787ff\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tTitle: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tAlign:  core.Ptr(\"center\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tTitleActive: &ColorOptions{\n\t\tFg:     core.Ptr(\"#000000\"),\n\t\tBg:     core.Ptr(\"#d787ff\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tAlign:  core.Ptr(\"center\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tButton: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tAlign:  core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tButtonActive: &ColorOptions{\n\t\tFg:     core.Ptr(\"#080808\"),\n\t\tBg:     core.Ptr(\"#d787ff\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tAlign:  core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tItem: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tItemFocused: &ColorOptions{\n\t\tFg:     core.Ptr(\"#ffffff\"),\n\t\tBg:     core.Ptr(\"#262626\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tItemSelected: &ColorOptions{\n\t\tFg:     core.Ptr(\"#5f87d7\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tItemDir: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d787ff\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tItemRef: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d787ff\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tTableHeader: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d787ff\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"bold\"),\n\t\tAlign:  core.Ptr(\"left\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tSearchLabel: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d7d75f\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"bold\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tSearchText: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tFilterLabel: &ColorOptions{\n\t\tFg:     core.Ptr(\"#d7d75f\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"bold\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tFilterText: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\n\tShortcutLabel: &ColorOptions{\n\t\tFg:     core.Ptr(\"#00af5f\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n\tShortcutText: &ColorOptions{\n\t\tFg:     core.Ptr(\"\"),\n\t\tBg:     core.Ptr(\"\"),\n\t\tAttr:   core.Ptr(\"\"),\n\t\tFormat: core.Ptr(\"\"),\n\t},\n}\n\ntype TUI struct {\n\tDefault *ColorOptions `yaml:\"default\"`\n\n\tBorder      *ColorOptions `yaml:\"border\"`\n\tBorderFocus *ColorOptions `yaml:\"border_focus\"`\n\n\tTitle       *ColorOptions `yaml:\"title\"`\n\tTitleActive *ColorOptions `yaml:\"title_active\"`\n\n\tTableHeader *ColorOptions `yaml:\"table_header\"`\n\n\tItem         *ColorOptions `yaml:\"item\"`\n\tItemFocused  *ColorOptions `yaml:\"item_focused\"`\n\tItemSelected *ColorOptions `yaml:\"item_selected\"`\n\tItemDir      *ColorOptions `yaml:\"item_dir\"`\n\tItemRef      *ColorOptions `yaml:\"item_ref\"`\n\n\tButton       *ColorOptions `yaml:\"button\"`\n\tButtonActive *ColorOptions `yaml:\"button_active\"`\n\n\tSearchLabel *ColorOptions `yaml:\"search_label\"`\n\tSearchText  *ColorOptions `yaml:\"search_text\"`\n\n\tFilterLabel *ColorOptions `yaml:\"filter_label\"`\n\tFilterText  *ColorOptions `yaml:\"filter_text\"`\n\n\tShortcutLabel *ColorOptions `yaml:\"shortcut_label\"`\n\tShortcutText  *ColorOptions `yaml:\"shortcut_text\"`\n}\n\nfunc LoadTUITheme(tui *TUI) {\n\ttui.Default = MergeThemeOptions(tui.Default, DefaultTUI.Default)\n\n\ttui.Border = MergeThemeOptions(tui.Border, DefaultTUI.Border)\n\ttui.BorderFocus = MergeThemeOptions(tui.BorderFocus, DefaultTUI.BorderFocus)\n\n\ttui.Button = MergeThemeOptions(tui.Button, DefaultTUI.Button)\n\ttui.ButtonActive = MergeThemeOptions(tui.ButtonActive, DefaultTUI.ButtonActive)\n\n\ttui.Item = MergeThemeOptions(tui.Item, DefaultTUI.Item)\n\ttui.ItemFocused = MergeThemeOptions(tui.ItemFocused, DefaultTUI.ItemFocused)\n\ttui.ItemSelected = MergeThemeOptions(tui.ItemSelected, DefaultTUI.ItemSelected)\n\ttui.ItemDir = MergeThemeOptions(tui.ItemDir, DefaultTUI.ItemDir)\n\ttui.ItemRef = MergeThemeOptions(tui.ItemRef, DefaultTUI.ItemRef)\n\n\ttui.Title = MergeThemeOptions(tui.Title, DefaultTUI.Title)\n\ttui.TitleActive = MergeThemeOptions(tui.TitleActive, DefaultTUI.TitleActive)\n\n\ttui.TableHeader = MergeThemeOptions(tui.TableHeader, DefaultTUI.TableHeader)\n\n\ttui.SearchLabel = MergeThemeOptions(tui.SearchLabel, DefaultTUI.SearchLabel)\n\ttui.SearchText = MergeThemeOptions(tui.SearchText, DefaultTUI.SearchText)\n\n\ttui.FilterLabel = MergeThemeOptions(tui.FilterLabel, DefaultTUI.FilterLabel)\n\ttui.FilterText = MergeThemeOptions(tui.FilterText, DefaultTUI.FilterText)\n\n\ttui.ShortcutText = MergeThemeOptions(tui.ShortcutText, DefaultTUI.ShortcutText)\n\ttui.ShortcutLabel = MergeThemeOptions(tui.ShortcutLabel, DefaultTUI.ShortcutLabel)\n}\n"
  },
  {
    "path": "core/dao/utils_test.go",
    "content": "package dao\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n)\n\n// Helper functions\n\nfunc getProjectNames(projects []Project) []string {\n\tnames := make([]string, len(projects))\n\tfor i, p := range projects {\n\t\tnames[i] = p.Name\n\t}\n\tsort.Strings(names)\n\treturn names\n}\n\nfunc getTreePaths(nodes []TreeNode) []string {\n\tpaths := make([]string, len(nodes))\n\tfor i, node := range nodes {\n\t\tpaths[i] = node.Path\n\t}\n\tsort.Strings(paths)\n\treturn paths\n}\n\nfunc equalStringSlices(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\taCopy := make([]string, len(a))\n\tbCopy := make([]string, len(b))\n\tcopy(aCopy, a)\n\tcopy(bCopy, b)\n\n\tsort.Strings(aCopy)\n\tsort.Strings(bCopy)\n\n\treturn reflect.DeepEqual(aCopy, bCopy)\n}\n"
  },
  {
    "path": "core/errors.go",
    "content": "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\n\tErr  string\n}\n\nfunc (c *ConfigEnvFailed) Error() string {\n\treturn fmt.Sprintf(\"failed to evaluate env `%s` \\n  %s\", c.Name, c.Err)\n}\n\ntype AlreadyManiDirectory struct {\n\tDir string\n}\n\nfunc (c *AlreadyManiDirectory) Error() string {\n\treturn fmt.Sprintf(\"`%s` is already a mani directory\\n\", c.Dir)\n}\n\ntype ZeroNotAllowed struct {\n\tName string\n}\n\nfunc (c *ZeroNotAllowed) Error() string {\n\treturn fmt.Sprintf(\"invalid value for %s, cannot be 0\", c.Name)\n}\n\ntype FailedToOpenFile struct {\n\tName string\n}\n\nfunc (f *FailedToOpenFile) Error() string {\n\treturn fmt.Sprintf(\"failed to open `%s`\", f.Name)\n}\n\ntype FailedToParsePath struct {\n\tName string\n}\n\nfunc (f *FailedToParsePath) Error() string {\n\treturn fmt.Sprintf(\"failed to parse path `%s`\", f.Name)\n}\n\ntype PathDoesNotExist struct {\n\tPath string\n}\n\nfunc (p *PathDoesNotExist) Error() string {\n\treturn fmt.Sprintf(\"path `%s` does not exist\", p.Path)\n}\n\ntype TagNotFound struct {\n\tTags []string\n}\n\nfunc (c *TagNotFound) Error() string {\n\ttags := \"`\" + strings.Join(c.Tags, \"`, `\") + \"`\"\n\treturn fmt.Sprintf(\"cannot find tags %s\", tags)\n}\n\ntype DirNotFound struct {\n\tDirs []string\n}\n\nfunc (c *DirNotFound) Error() string {\n\tdirs := \"`\" + strings.Join(c.Dirs, \"`, `\") + \"`\"\n\treturn fmt.Sprintf(\"cannot find paths %s\", dirs)\n}\n\ntype NoTargets struct{}\n\nfunc (c *NoTargets) Error() string {\n\treturn \"no matching projects found\"\n}\n\ntype ProjectNotFound struct {\n\tName []string\n}\n\nfunc (c *ProjectNotFound) Error() string {\n\tprojects := \"`\" + strings.Join(c.Name, \"`, `\") + \"`\"\n\treturn fmt.Sprintf(\"cannot find projects %s\", projects)\n}\n\ntype TaskNotFound struct {\n\tName []string\n}\n\nfunc (c *TaskNotFound) Error() string {\n\ttasks := \"`\" + strings.Join(c.Name, \"`, `\") + \"`\"\n\treturn fmt.Sprintf(\"cannot find tasks %s\", tasks)\n}\n\ntype ThemeNotFound struct {\n\tName string\n}\n\nfunc (c *ThemeNotFound) Error() string {\n\treturn fmt.Sprintf(\"cannot find theme `%s`\", c.Name)\n}\n\ntype SpecNotFound struct {\n\tName string\n}\n\nfunc (c *SpecNotFound) Error() string {\n\treturn fmt.Sprintf(\"cannot find spec `%s`\", c.Name)\n}\n\ntype SpecOutputError struct {\n\tName   string\n\tOutput string\n}\n\nfunc (c *SpecOutputError) Error() string {\n\treturn fmt.Sprintf(\"invalid output for spec `%s`, found `%s`, expected one of: stream, table, html, markdown\", c.Name, c.Output)\n}\n\ntype TargetNotFound struct {\n\tName string\n}\n\nfunc (c *TargetNotFound) Error() string {\n\treturn fmt.Sprintf(\"cannot find target `%s`\", c.Name)\n}\n\ntype TargetTagsExprError struct {\n\tName string\n\tErr  error\n}\n\nfunc (c *TargetTagsExprError) Error() string {\n\treturn fmt.Sprintf(\"invalid tags_expr for target `%s`, %s\", c.Name, c.Err.Error())\n}\n\ntype TagExprInvalid struct {\n\tExpression string\n}\n\nfunc (c *TagExprInvalid) Error() string {\n\treturn fmt.Sprintf(\"invalid tags expression: %s\", c.Expression)\n}\n\ntype ConfigNotFound struct {\n\tNames []string\n}\n\nfunc (f *ConfigNotFound) Error() string {\n\treturn fmt.Sprintf(\"cannot find any configuration file %v in current directory or any of the parent directories\", f.Names)\n}\n\ntype WorktreePathRequired struct{}\n\nfunc (c *WorktreePathRequired) Error() string {\n\treturn \"worktree path is required\"\n}\n\ntype FailedToCreateWorktree struct {\n\tPath   string\n\tOutput string\n\tErr    error\n}\n\nfunc (c *FailedToCreateWorktree) Error() string {\n\treturn fmt.Sprintf(\"failed to create worktree `%s`: %s - %s\", c.Path, c.Err, c.Output)\n}\n\ntype FailedToRemoveWorktree struct {\n\tPath   string\n\tOutput string\n\tErr    error\n}\n\nfunc (c *FailedToRemoveWorktree) Error() string {\n\treturn fmt.Sprintf(\"failed to remove worktree `%s`: %s - %s\", c.Path, c.Err, c.Output)\n}\n\ntype ConfigErr struct {\n\tMsg string\n}\n\nfunc (f *ConfigErr) Error() string {\n\treturn f.Msg\n}\n\nfunc CheckIfError(err error) {\n\tif err != nil {\n\t\tswitch err.(type) {\n\t\tcase *ConfigErr:\n\t\t\t// Errors are already mapped with `error:` prefix\n\t\t\tfmt.Fprintf(os.Stderr, \"%v\\n\", err)\n\t\t\tos.Exit(1)\n\t\tdefault:\n\t\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", color.FgRed.Sprintf(\"error\"), err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n}\n\nfunc Exit(err error) {\n\tswitch err := err.(type) {\n\tcase *ConfigErr:\n\t\t// Errors are already mapped with `error:` prefix\n\t\tfmt.Fprintf(os.Stderr, \"%v\", err)\n\t\tos.Exit(1)\n\tdefault:\n\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", color.FgRed.Sprintf(\"error\"), err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "core/exec/client.go",
    "content": "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 Client struct {\n\tName string\n\tPath string\n\tEnv  []string\n\n\tcmd     *exec.Cmd\n\tstdout  io.Reader\n\tstderr  io.Reader\n\trunning bool\n}\n\nfunc (c *Client) Run(shell string, env []string, cmdStr []string) error {\n\tvar err error\n\tif c.running {\n\t\treturn fmt.Errorf(\"command already running\")\n\t}\n\n\tcmd := exec.Command(shell, cmdStr...)\n\n\tcmd.Dir = c.Path\n\tcmd.Env = append(os.Environ(), env...)\n\n\tc.cmd = cmd\n\n\tc.stdout, err = cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.stderr, err = cmd.StderrPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := c.cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\tc.running = true\n\n\treturn nil\n}\n\nfunc (c *Client) Wait() error {\n\tif !c.running {\n\t\treturn fmt.Errorf(\"trying to wait on stopped command\")\n\t}\n\n\terr := c.cmd.Wait()\n\tc.running = false\n\n\treturn err\n}\n\nfunc (c *Client) Close() error {\n\treturn nil\n}\n\nfunc (c *Client) Stderr() io.Reader {\n\treturn c.stderr\n}\n\nfunc (c *Client) Stdout() io.Reader {\n\treturn c.stdout\n}\n\nfunc (c *Client) Prefix() string {\n\treturn c.Name\n}\n"
  },
  {
    "path": "core/exec/clone.go",
    "content": "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/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n\t\"github.com/gookit/color\"\n)\n\nfunc getRemotes(project dao.Project) (map[string]string, error) {\n\tcmd := exec.Command(\"git\", \"remote\", \"-v\")\n\tcmd.Dir = project.Path\n\toutput, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutputStr := string(output)\n\tlines := strings.Split(outputStr, \"\\n\")\n\n\tremotes := make(map[string]string)\n\tfor _, line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) < 3 {\n\t\t\treturn nil, fmt.Errorf(\"unexpected line: %s\", line)\n\t\t}\n\t\tremotes[parts[0]] = parts[1]\n\t}\n\n\treturn remotes, nil\n}\n\nfunc addRemote(project dao.Project, remote dao.Remote) error {\n\tcmd := exec.Command(\"git\", \"remote\", \"add\", remote.Name, remote.URL)\n\tcmd.Dir = project.Path\n\t_, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc removeRemote(project dao.Project, name string) error {\n\tcmd := exec.Command(\"git\", \"remote\", \"remove\", name)\n\tcmd.Dir = project.Path\n\t_, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc updateRemote(project dao.Project, remote dao.Remote) error {\n\tcmd := exec.Command(\"git\", \"remote\", \"set-url\", remote.Name, remote.URL)\n\tcmd.Dir = project.Path\n\t_, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc syncRemotes(project dao.Project) error {\n\tfoundRemotes, err := getRemotes(project)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Add remotes found in RemoteList but not in .git/config\n\tfor _, remote := range project.RemoteList {\n\t\t_, found := foundRemotes[remote.Name]\n\t\tif found {\n\t\t\terr := updateRemote(project, remote)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr := addRemote(project, remote)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't remove remotes if project url is empty\n\tif project.URL == \"\" {\n\t\treturn nil\n\t}\n\n\t// Remove remotes found in .git/config but not in RemoteList\n\tfor name, foundURL := range foundRemotes {\n\t\t// Ignore origin remote (same as project url)\n\t\tif foundURL == project.URL {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if this URL exists in project.RemoteList\n\t\turlExists := false\n\t\tfor _, remote := range project.RemoteList {\n\t\t\tif foundURL == remote.URL {\n\t\t\t\turlExists = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// If URL is not in RemoteList, remove the remote\n\t\tif !urlExists {\n\t\t\terr := removeRemote(project, name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// CreateWorktree creates a git worktree at the specified path for the given branch.\n// If the branch doesn't exist, it creates a new branch.\nfunc CreateWorktree(parentPath string, worktreePath string, branch string, createBranch bool) error {\n\tvar cmd *exec.Cmd\n\tif createBranch {\n\t\tcmd = exec.Command(\"git\", \"worktree\", \"add\", \"-b\", branch, worktreePath)\n\t} else {\n\t\tcmd = exec.Command(\"git\", \"worktree\", \"add\", worktreePath, branch)\n\t}\n\tcmd.Dir = parentPath\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn &core.FailedToCreateWorktree{Path: worktreePath, Err: err, Output: string(output)}\n\t}\n\treturn nil\n}\n\n// GetWorktrees returns a map of existing worktrees (path -> branch)\nfunc GetWorktrees(parentPath string) (map[string]string, error) {\n\treturn core.GetWorktreeList(parentPath)\n}\n\n// RemoveWorktree removes a git worktree (keeps the branch)\nfunc RemoveWorktree(parentPath string, worktreePath string) error {\n\tcmd := exec.Command(\"git\", \"worktree\", \"remove\", worktreePath)\n\tcmd.Dir = parentPath\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn &core.FailedToRemoveWorktree{Path: worktreePath, Err: err, Output: string(output)}\n\t}\n\treturn nil\n}\n\n// SyncWorktrees handles worktree creation and optionally removal for a project\nfunc SyncWorktrees(config *dao.Config, project dao.Project, removeOrphans bool) error {\n\tparentPath, err := core.GetAbsolutePath(config.Path, project.Path, project.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Parent must exist first (skip if not cloned yet)\n\tif _, err := os.Stat(parentPath); os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\t// Prune stale worktree references (e.g. from manually deleted directories)\n\tpruneCmd := exec.Command(\"git\", \"worktree\", \"prune\")\n\tpruneCmd.Dir = parentPath\n\t_ = pruneCmd.Run()\n\n\t// Build map of expected worktree paths from config\n\texpectedPaths := make(map[string]bool)\n\tfor _, wt := range project.WorktreeList {\n\t\tvar wtPath string\n\t\tif filepath.IsAbs(wt.Path) {\n\t\t\twtPath = filepath.Clean(wt.Path)\n\t\t} else {\n\t\t\twtPath = filepath.Join(parentPath, wt.Path)\n\t\t}\n\t\texpectedPaths[wtPath] = true\n\n\t\t// Create worktree if it doesn't exist\n\t\tif _, err := os.Stat(wtPath); os.IsNotExist(err) {\n\t\t\t// Try checking out existing branch first (local or remote-tracking)\n\t\t\terr = CreateWorktree(parentPath, wtPath, wt.Branch, false)\n\t\t\tif err != nil {\n\t\t\t\t// Branch doesn't exist anywhere — create it\n\t\t\t\terr = CreateWorktree(parentPath, wtPath, wt.Branch, true)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove worktrees not in config (only if enabled)\n\tif removeOrphans {\n\t\texistingWorktrees, err := GetWorktrees(parentPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor wtPath := range existingWorktrees {\n\t\t\tif !expectedPaths[wtPath] {\n\t\t\t\terr := RemoveWorktree(parentPath, wtPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc CloneRepos(config *dao.Config, projects []dao.Project, syncFlags core.SyncFlags) error {\n\turls := config.GetProjectUrls()\n\tif len(urls) == 0 {\n\t\tfmt.Println(\"No projects to clone\")\n\t\treturn nil\n\t}\n\n\tvar syncProjects []dao.Project\n\tfor i := range projects {\n\t\tif !syncFlags.IgnoreSyncState && !projects[i].IsSync() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif projects[i].URL == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tprojectPath, err := core.GetAbsolutePath(config.Path, projects[i].Path, projects[i].Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Project already synced\n\t\tif _, err := os.Stat(projectPath); !os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\n\t\tsyncProjects = append(syncProjects, projects[i])\n\t}\n\n\tvar tasks []dao.Task\n\tfor i := range syncProjects {\n\t\tvar cmd string\n\t\tvar cmdArr []string\n\t\tvar shell string\n\t\tvar shellProgram string\n\n\t\tif syncProjects[i].Clone != \"\" {\n\t\t\tshell = dao.DEFAULT_SHELL\n\t\t\tshellProgram = dao.DEFAULT_SHELL_PROGRAM\n\t\t\tcmdArr = []string{\"-c\", syncProjects[i].Clone}\n\t\t\tcmd = syncProjects[i].Clone\n\t\t} else {\n\t\t\tprojectPath, err := core.GetAbsolutePath(config.Path, syncProjects[i].Path, syncProjects[i].Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tshell = \"git\"\n\t\t\tshellProgram = \"git\"\n\t\t\tif syncFlags.Parallel {\n\t\t\t\tcmdArr = []string{\"clone\", syncProjects[i].URL, projectPath}\n\t\t\t} else {\n\t\t\t\tcmdArr = []string{\"clone\", \"--progress\", syncProjects[i].URL, projectPath}\n\t\t\t}\n\n\t\t\tif syncProjects[i].Branch != \"\" {\n\t\t\t\tcmdArr = append(cmdArr, \"--branch\", syncProjects[i].Branch)\n\t\t\t}\n\n\t\t\tif syncProjects[i].IsSingleBranch() {\n\t\t\t\tcmdArr = append(cmdArr, \"--single-branch\")\n\t\t\t}\n\n\t\t\tcmd = strings.Join(cmdArr, \" \")\n\t\t}\n\n\t\tif len(syncProjects) > 0 {\n\t\t\tvar task = dao.Task{\n\t\t\t\tName: syncProjects[i].Name,\n\n\t\t\t\tShell:        shell,\n\t\t\t\tCmd:          cmd,\n\t\t\t\tShellProgram: shellProgram,\n\t\t\t\tCmdArg:       cmdArr,\n\t\t\t\tSpecData: dao.Spec{\n\t\t\t\t\tParallel:     syncFlags.Parallel,\n\t\t\t\t\tForks:        syncFlags.Forks,\n\t\t\t\t\tIgnoreErrors: false,\n\t\t\t\t},\n\n\t\t\t\tThemeData: dao.Theme{\n\t\t\t\t\tColor: core.Ptr(true),\n\t\t\t\t\tStream: dao.Stream{\n\t\t\t\t\t\tPrefix:       syncFlags.Parallel, // we only use prefix when parallel is enabled since we need to see which project returns an error\n\t\t\t\t\t\tHeader:       true,\n\t\t\t\t\t\tHeaderChar:   dao.DefaultStream.HeaderChar,\n\t\t\t\t\t\tHeaderPrefix: \"Project\",\n\t\t\t\t\t\tPrefixColors: dao.DefaultStream.PrefixColors,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ttasks = append(tasks, task)\n\t\t}\n\t}\n\n\tif len(syncProjects) > 0 {\n\t\ttarget := Exec{Projects: syncProjects, Tasks: tasks, Config: *config}\n\t\tclientCh := make(chan Client, len(syncProjects))\n\t\terr := target.SetCloneClients(clientCh)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttarget.Text(false, os.Stdout, os.Stderr)\n\t}\n\n\t// User has opt-in to Sync remotes\n\tif *config.SyncRemotes {\n\t\tfor i := range projects {\n\t\t\t// Project must have a Remote List defined\n\t\t\tif len(projects[i].RemoteList) > 0 {\n\t\t\t\terr := syncRemotes(projects[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sync worktrees: create if defined, remove orphans if enabled\n\tfor i := range projects {\n\t\tif len(projects[i].WorktreeList) > 0 || *config.RemoveOrphanedWorktrees {\n\t\t\terr := SyncWorktrees(config, projects[i], *config.RemoveOrphanedWorktrees)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc UpdateGitignoreIfExists(config *dao.Config) error {\n\t// Only add projects to gitignore if a .gitignore file exists in the mani.yaml directory\n\tgitignoreFilename := filepath.Join(filepath.Dir(config.Path), \".gitignore\")\n\tif _, err := os.Stat(gitignoreFilename); err == nil {\n\t\t// Get relative project names for gitignore file\n\t\tvar gitignoreEntries []string\n\t\tfor _, project := range config.ProjectList {\n\t\t\tif project.URL == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Project must be below mani config file to be added to gitignore\n\t\t\tvar projectPath string\n\t\t\tprojectPath, err = core.GetAbsolutePath(config.Path, project.Path, project.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Skip the root project (it is the mani directory itself)\n\t\t\tif projectPath == config.Dir {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !strings.HasPrefix(projectPath, config.Dir) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif project.Path != \"\" {\n\t\t\t\tvar relPath string\n\t\t\t\trelPath, err = filepath.Rel(config.Dir, projectPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tgitignoreEntries = append(gitignoreEntries, relPath)\n\t\t\t} else {\n\t\t\t\tgitignoreEntries = append(gitignoreEntries, project.Name)\n\t\t\t}\n\n\t\t\t// Add worktrees to gitignore as well\n\t\t\tfor _, wt := range project.WorktreeList {\n\t\t\t\tvar wtAbsPath string\n\t\t\t\tif filepath.IsAbs(wt.Path) {\n\t\t\t\t\twtAbsPath = filepath.Clean(wt.Path)\n\t\t\t\t} else {\n\t\t\t\t\twtAbsPath = filepath.Join(projectPath, wt.Path)\n\t\t\t\t}\n\n\t\t\t\t// Worktree must be below mani config file to be added to gitignore\n\t\t\t\tif !strings.HasPrefix(wtAbsPath, config.Dir) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\twtRelPath, err := filepath.Rel(config.Dir, wtAbsPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tgitignoreEntries = append(gitignoreEntries, wtRelPath)\n\t\t\t}\n\t\t}\n\n\t\terr := dao.UpdateProjectsToGitignore(gitignoreEntries, gitignoreFilename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (exec *Exec) SetCloneClients(clientCh chan Client) error {\n\tconfig := exec.Config\n\tprojects := exec.Projects\n\n\tvar clients []Client\n\tfor i, project := range projects {\n\t\tfunc(i int, project dao.Project) {\n\t\t\tclient := Client{\n\t\t\t\tPath: config.Dir,\n\t\t\t\tName: project.Name,\n\t\t\t\tEnv:  projects[i].EnvList,\n\t\t\t}\n\t\t\tclientCh <- client\n\t\t\tclients = append(clients, client)\n\t\t}(i, project)\n\t}\n\n\tclose(clientCh)\n\n\texec.Clients = clients\n\n\treturn nil\n}\n\nfunc PrintProjectStatus(config *dao.Config, projects []dao.Project) error {\n\ttheme := dao.Theme{\n\t\tColor: core.Ptr(true),\n\t\tTable: dao.DefaultTable,\n\t}\n\ttheme.Table.Border.Rows = core.Ptr(false)\n\ttheme.Table.Header.Format = core.Ptr(\"t\")\n\n\toptions := print.PrintTableOptions{\n\t\tTheme:            theme,\n\t\tOutput:           \"table\",\n\t\tColor:            *theme.Color,\n\t\tAutoWrap:         true,\n\t\tOmitEmptyRows:    false,\n\t\tOmitEmptyColumns: false,\n\t}\n\n\tdata := dao.TableOutput{\n\t\tHeaders: []string{\"project\", \"synced\"},\n\t\tRows:    []dao.Row{},\n\t}\n\n\tfor _, project := range projects {\n\t\tprojectPath, err := core.GetAbsolutePath(config.Path, project.Path, project.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif _, err := os.Stat(projectPath); !os.IsNotExist(err) {\n\t\t\t// Project synced\n\t\t\tdata.Rows = append(data.Rows, dao.Row{Columns: []string{project.Name, color.FgGreen.Sprintf(\"\\u2713\")}})\n\t\t} else {\n\t\t\t// Project not synced\n\t\t\tdata.Rows = append(data.Rows, dao.Row{Columns: []string{project.Name, color.FgRed.Sprintf(\"\\u2715\")}})\n\t\t}\n\t}\n\n\tfmt.Println()\n\tprint.PrintTable(data.Rows, options, data.Headers, []string{}, os.Stdout)\n\tfmt.Println()\n\n\treturn nil\n}\n\nfunc PrintProjectInit(projects []dao.Project) {\n\tif len(projects) == 0 {\n\t\treturn\n\t}\n\n\ttheme := dao.Theme{\n\t\tTable: dao.DefaultTable,\n\t\tColor: core.Ptr(true),\n\t}\n\ttheme.Table.Border.Rows = core.Ptr(false)\n\ttheme.Table.Header.Format = core.Ptr(\"t\")\n\n\toptions := print.PrintTableOptions{\n\t\tTheme:            theme,\n\t\tOutput:           \"table\",\n\t\tColor:            true,\n\t\tAutoWrap:         true,\n\t\tOmitEmptyRows:    true,\n\t\tOmitEmptyColumns: false,\n\t}\n\n\tdata := dao.TableOutput{\n\t\tHeaders: []string{\"project\", \"path\"},\n\t\tRows:    []dao.Row{},\n\t}\n\n\tfor _, project := range projects {\n\t\tdata.Rows = append(data.Rows, dao.Row{Columns: []string{project.Name, project.Path}})\n\t}\n\n\tfmt.Println(\"\\nFollowing projects were added to mani.yaml\")\n\tfmt.Println()\n\tprint.PrintTable(data.Rows, options, data.Headers, []string{}, os.Stdout)\n}\n"
  },
  {
    "path": "core/exec/exec.go",
    "content": "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\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/print\"\n)\n\ntype Exec struct {\n\tClients  []Client\n\tProjects []dao.Project\n\tTasks    []dao.Task\n\tConfig   dao.Config\n}\n\ntype TableCmd struct {\n\trIndex int\n\tcIndex int\n\tclient Client\n\tdryRun bool\n\n\tdesc     string\n\tname     string\n\tshell    string\n\tenv      []string\n\tcmd      string\n\tcmdArr   []string\n\tnumTasks int\n}\n\nfunc (exec *Exec) Run(\n\tuserArgs []string,\n\trunFlags *core.RunFlags,\n\tsetRunFlags *core.SetRunFlags,\n) error {\n\tprojects := exec.Projects\n\ttasks := exec.Tasks\n\n\terr := exec.ParseTask(userArgs, runFlags, setRunFlags)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclientCh := make(chan Client, len(projects))\n\terrCh := make(chan error, len(projects))\n\terr = exec.SetClients(clientCh, errCh)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Describe task\n\tif runFlags.Describe {\n\t\tout := print.PrintTaskBlock([]dao.Task{tasks[0]}, false, tasks[0].ThemeData.Block, print.GookitFormatter{})\n\t\tfmt.Print(out)\n\t}\n\n\texec.CheckTaskNoColor()\n\n\tswitch tasks[0].SpecData.Output {\n\tcase \"table\", \"html\", \"markdown\":\n\t\tfmt.Println(\"\")\n\t\tdata := exec.Table(runFlags)\n\t\toptions := print.PrintTableOptions{\n\t\t\tTheme:            tasks[0].ThemeData,\n\t\t\tOutput:           tasks[0].SpecData.Output,\n\t\t\tColor:            *tasks[0].ThemeData.Color,\n\t\t\tAutoWrap:         true,\n\t\t\tOmitEmptyRows:    tasks[0].SpecData.OmitEmptyRows,\n\t\t\tOmitEmptyColumns: tasks[0].SpecData.OmitEmptyColumns,\n\t\t}\n\t\tprint.PrintTable(data.Rows, options, data.Headers[0:1], data.Headers[1:], os.Stdout)\n\t\tfmt.Println(\"\")\n\tdefault:\n\t\texec.Text(runFlags.DryRun, os.Stdout, os.Stderr)\n\t}\n\n\treturn nil\n}\n\nfunc (exec *Exec) RunTUI(\n\tuserArgs []string,\n\trunFlags *core.RunFlags,\n\tsetRunFlags *core.SetRunFlags,\n\toutput string,\n\toutWriter io.Writer,\n\terrWriter io.Writer,\n) error {\n\tprojects := exec.Projects\n\terr := exec.ParseTask(userArgs, runFlags, setRunFlags)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttasks := exec.Tasks\n\n\tclientCh := make(chan Client, len(projects))\n\terrCh := make(chan error, len(projects))\n\terr = exec.SetClients(clientCh, errCh)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata := dao.TableOutput{}\n\tswitch output {\n\tcase \"table\":\n\t\tdata = exec.Table(runFlags)\n\t\toptions := print.PrintTableOptions{\n\t\t\tTheme:            tasks[0].ThemeData,\n\t\t\tOutput:           tasks[0].SpecData.Output,\n\t\t\tColor:            *tasks[0].ThemeData.Color,\n\t\t\tAutoWrap:         false,\n\t\t\tOmitEmptyRows:    tasks[0].SpecData.OmitEmptyRows,\n\t\t\tOmitEmptyColumns: tasks[0].SpecData.OmitEmptyColumns,\n\t\t}\n\t\tprint.PrintTable(data.Rows, options, data.Headers[0:1], data.Headers[1:], outWriter)\n\t\treturn nil\n\tdefault:\n\t\texec.Text(runFlags.DryRun, outWriter, errWriter)\n\t}\n\n\treturn err\n}\n\nfunc (exec *Exec) SetClients(\n\tclientCh chan Client,\n\terrCh chan error,\n) error {\n\tconfig := exec.Config\n\tignoreNonExisting := exec.Tasks[0].SpecData.IgnoreNonExisting\n\tprojects := exec.Projects\n\n\tvar clients []Client\n\tfor i, project := range projects {\n\t\tfunc(i int, project dao.Project) {\n\t\t\tprojectPath, err := core.GetAbsolutePath(config.Path, project.Path, project.Name)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- &core.FailedToParsePath{Name: projectPath}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, err := os.Stat(projectPath); os.IsNotExist(err) && !ignoreNonExisting {\n\t\t\t\terrCh <- &core.PathDoesNotExist{Path: projectPath}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tclient := Client{Path: projectPath, Name: project.Name, Env: project.EnvList}\n\t\t\tclientCh <- client\n\n\t\t\tclients = append(clients, client)\n\t\t}(i, project)\n\t}\n\n\tclose(clientCh)\n\tclose(errCh)\n\n\t// Return if there's any errors\n\tfor err := range errCh {\n\t\treturn err\n\t}\n\n\texec.Clients = clients\n\n\treturn nil\n}\n\n// ParseTask processes and updates task configurations based on runtime flags and user arguments.\n// It handles theme, specification, environment variables, and execution settings for each task.\n//\n// The function performs these operations for each task:\n// 1. Evaluates configuration environment variables\n// 2. Updates theme if specified\n// 3. Updates spec settings if provided\n// 4. Applies runtime execution flags\n// 5. Processes environment variables for the task and its commands\n//\n// Environment variable processing order:\n// 1. Configuration level variables\n// 2. Task level variables\n// 3. Command level variables\n// 4. User provided arguments\nfunc (exec *Exec) ParseTask(userArgs []string, runFlags *core.RunFlags, setRunFlags *core.SetRunFlags) error {\n\tconfigEnv, err := dao.EvaluateEnv(exec.Config.EnvList)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range exec.Tasks {\n\t\t// Update theme property if user flag is provided\n\t\tif runFlags.Theme != \"\" {\n\t\t\ttheme, err := exec.Config.GetTheme(runFlags.Theme)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\texec.Tasks[i].ThemeData = *theme\n\t\t}\n\n\t\tif runFlags.Spec != \"\" {\n\t\t\tspec, err := exec.Config.GetSpec(runFlags.Spec)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\texec.Tasks[i].SpecData = *spec\n\t\t}\n\n\t\t// Update output property if user flag is provided\n\t\tif runFlags.Output != \"\" {\n\t\t\texec.Tasks[i].SpecData.Output = runFlags.Output\n\t\t}\n\n\t\t// TTY\n\t\tif setRunFlags.TTY {\n\t\t\texec.Tasks[i].TTY = runFlags.TTY\n\t\t}\n\n\t\t// Omit rows which provide empty output\n\t\tif setRunFlags.OmitEmptyRows {\n\t\t\texec.Tasks[i].SpecData.OmitEmptyRows = runFlags.OmitEmptyRows\n\t\t}\n\n\t\t// Omit columns which provide empty output\n\t\tif setRunFlags.OmitEmptyColumns {\n\t\t\texec.Tasks[i].SpecData.OmitEmptyColumns = runFlags.OmitEmptyColumns\n\t\t}\n\n\t\tif setRunFlags.IgnoreErrors {\n\t\t\texec.Tasks[i].SpecData.IgnoreErrors = runFlags.IgnoreErrors\n\t\t}\n\n\t\tif setRunFlags.IgnoreNonExisting {\n\t\t\texec.Tasks[i].SpecData.IgnoreNonExisting = runFlags.IgnoreNonExisting\n\t\t}\n\n\t\t// If parallel flag is set to true, then update task specs\n\t\tif setRunFlags.Parallel {\n\t\t\texec.Tasks[i].SpecData.Parallel = runFlags.Parallel\n\t\t}\n\n\t\tif setRunFlags.Forks {\n\t\t\texec.Tasks[i].SpecData.Forks = runFlags.Forks\n\t\t}\n\n\t\t// Parse env here instead of config since we're only interested in tasks run, and not all tasks.\n\t\t// Also, userArgs is not present in the config.\n\t\tenvs, err := dao.ParseTaskEnv(exec.Tasks[i].Env, userArgs, []string{}, configEnv)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\texec.Tasks[i].EnvList = envs\n\n\t\t// Set environment variables for sub-commands\n\t\tfor j := range exec.Tasks[i].Commands {\n\t\t\tenvs, err := dao.ParseTaskEnv(exec.Tasks[i].Commands[j].Env, userArgs, exec.Tasks[i].EnvList, configEnv)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\texec.Tasks[i].Commands[j].EnvList = envs\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (exec *Exec) CheckTaskNoColor() {\n\ttask := exec.Tasks[0]\n\n\tfor _, env := range task.EnvList {\n\t\tname := strings.Split(env, \"=\")[0]\n\t\tif name == \"NO_COLOR\" {\n\t\t\tcolor.Disable()\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/exec/table.go",
    "content": "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\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfunc (exec *Exec) Table(runFlags *core.RunFlags) dao.TableOutput {\n\ttask := exec.Tasks[0]\n\tclients := exec.Clients\n\tprojects := exec.Projects\n\n\tvar spinner *yacspin.Spinner\n\tvar spinnerErr error\n\tgo func() {\n\t\tif !runFlags.Silent {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tspinner, spinnerErr = initSpinner()\n\t\t}\n\t}()\n\n\t// In-case user interrupts, make sure spinner is stopped\n\tgo func() {\n\t\tsigchan := make(chan os.Signal, 1)\n\t\tsignal.Notify(sigchan, os.Interrupt)\n\t\t<-sigchan\n\n\t\tif !runFlags.Silent && spinner != nil && spinnerErr == nil {\n\t\t\t_ = spinner.Stop()\n\t\t}\n\t\tos.Exit(0)\n\t}()\n\n\tvar data dao.TableOutput\n\tvar dataMutex = sync.RWMutex{}\n\n\t/**\n\t** Headers\n\t**/\n\tdata.Headers = append(data.Headers, \"project\")\n\n\t// Append Command names if set\n\tfor _, subTask := range task.Commands {\n\t\tif subTask.Name != \"\" {\n\t\t\tdata.Headers = append(data.Headers, subTask.Name)\n\t\t} else {\n\t\t\tdata.Headers = append(data.Headers, \"output\")\n\t\t}\n\t}\n\n\t// Append Command name if set\n\tif task.Cmd != \"\" {\n\t\tif task.Name != \"\" {\n\t\t\tdata.Headers = append(data.Headers, task.Name)\n\t\t} else {\n\t\t\tdata.Headers = append(data.Headers, \"output\")\n\t\t}\n\t}\n\n\t// Populate the rows (project name is first cell, then commands and cmd output is set to empty string)\n\tfor i, p := range projects {\n\t\tdata.Rows = append(data.Rows, dao.Row{Columns: []string{p.Name}})\n\n\t\tfor range task.Commands {\n\t\t\tdata.Rows[i].Columns = append(data.Rows[i].Columns, \"\")\n\t\t}\n\n\t\tif task.Cmd != \"\" {\n\t\t\tdata.Rows[i].Columns = append(data.Rows[i].Columns, \"\")\n\t\t}\n\t}\n\n\twg := core.NewSizedWaitGroup(task.SpecData.Forks)\n\t/**\n\t** Values\n\t**/\n\tfor i, c := range clients {\n\t\twg.Add()\n\t\tif task.SpecData.Parallel {\n\t\t\tgo func(i int, c Client, wg *core.SizedWaitGroup) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_ = exec.TableWork(i, runFlags.DryRun, data, &dataMutex)\n\t\t\t}(i, c, &wg)\n\t\t} else {\n\t\t\tfunc(i int, c Client, wg *core.SizedWaitGroup) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_ = exec.TableWork(i, runFlags.DryRun, data, &dataMutex)\n\t\t\t}(i, c, &wg)\n\t\t}\n\t}\n\twg.Wait()\n\n\tif !runFlags.Silent && spinner != nil && spinnerErr == nil {\n\t\t_ = spinner.Stop()\n\t}\n\n\treturn data\n}\n\nfunc (exec *Exec) TableWork(rIndex int, dryRun bool, data dao.TableOutput, dataMutex *sync.RWMutex) error {\n\tclient := exec.Clients[rIndex]\n\ttask := exec.Tasks[rIndex]\n\tvar wg sync.WaitGroup\n\n\tfor j, cmd := range task.Commands {\n\t\targs := TableCmd{\n\t\t\trIndex: rIndex,\n\t\t\tcIndex: j + 1,\n\t\t\tclient: client,\n\t\t\tdryRun: dryRun,\n\t\t\tshell:  cmd.ShellProgram,\n\t\t\tenv:    cmd.EnvList,\n\t\t\tcmd:    cmd.Cmd,\n\t\t\tcmdArr: cmd.CmdArg,\n\t\t}\n\n\t\tif cmd.TTY {\n\t\t\treturn ExecTTY(cmd.Cmd, cmd.EnvList)\n\t\t}\n\n\t\terr := RunTableCmd(args, data, dataMutex, &wg)\n\t\tif err != nil && !task.SpecData.IgnoreErrors {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif task.Cmd != \"\" {\n\t\targs := TableCmd{\n\t\t\trIndex: rIndex,\n\t\t\tcIndex: len(task.Commands) + 1,\n\t\t\tclient: client,\n\t\t\tdryRun: dryRun,\n\t\t\tshell:  task.ShellProgram,\n\t\t\tenv:    task.EnvList,\n\t\t\tcmd:    task.Cmd,\n\t\t\tcmdArr: task.CmdArg,\n\t\t}\n\n\t\tif task.TTY {\n\t\t\treturn ExecTTY(task.Cmd, task.EnvList)\n\t\t}\n\n\t\terr := RunTableCmd(args, data, dataMutex, &wg)\n\t\tif err != nil && !task.SpecData.IgnoreErrors {\n\t\t\treturn err\n\t\t}\n\t}\n\n\twg.Wait()\n\n\treturn nil\n}\n\nfunc RunTableCmd(t TableCmd, data dao.TableOutput, dataMutex *sync.RWMutex, wg *sync.WaitGroup) error {\n\tcombinedEnvs := dao.MergeEnvs(t.client.Env, t.env)\n\n\tif t.dryRun {\n\t\tdata.Rows[t.rIndex].Columns[t.cIndex] = t.cmd\n\t\treturn nil\n\t}\n\n\terr := t.client.Run(t.shell, combinedEnvs, t.cmdArr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Copy over commands STDOUT.\n\tvar stdoutHandler = func(client Client) {\n\t\tdefer wg.Done()\n\t\tdataMutex.Lock()\n\t\tout, err := io.ReadAll(client.Stdout())\n\t\tdata.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf(\"%s%s\", data.Rows[t.rIndex].Columns[t.cIndex], strings.TrimSuffix(string(out), \"\\n\"))\n\t\tdataMutex.Unlock()\n\n\t\tif err != nil && err != io.EOF {\n\t\t\tfmt.Fprintf(os.Stderr, \"%v\", err)\n\t\t}\n\t}\n\twg.Add(1)\n\tgo stdoutHandler(t.client)\n\n\t// Copy over tasks's STDERR.\n\tvar stderrHandler = func(client Client) {\n\t\tdefer wg.Done()\n\t\tdataMutex.Lock()\n\t\tout, err := io.ReadAll(client.Stderr())\n\t\tdata.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf(\"%s%s\", data.Rows[t.rIndex].Columns[t.cIndex], strings.TrimSuffix(string(out), \"\\n\"))\n\t\tdataMutex.Unlock()\n\t\tif err != nil && err != io.EOF {\n\t\t\tfmt.Fprintf(os.Stderr, \"%v\", err)\n\t\t}\n\t}\n\twg.Add(1)\n\tgo stderrHandler(t.client)\n\n\twg.Wait()\n\n\tif err := t.client.Wait(); err != nil {\n\t\tdata.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf(\"%s\\n%s\", data.Rows[t.rIndex].Columns[t.cIndex], err.Error())\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc initSpinner() (*yacspin.Spinner, error) {\n\tspinner, err := dao.TaskSpinner()\n\tif err != nil {\n\t\treturn &spinner, err\n\t}\n\n\terr = spinner.Start()\n\tif err != nil {\n\t\treturn &spinner, err\n\t}\n\n\tspinner.Message(\" Running\")\n\n\treturn &spinner, nil\n}\n"
  },
  {
    "path": "core/exec/text.go",
    "content": "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\"github.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfunc (exec *Exec) Text(\n\tdryRun bool,\n\tstdout io.Writer,\n\tstderr io.Writer,\n) {\n\ttask := exec.Tasks[0]\n\tclients := exec.Clients\n\n\tprefixMaxLen := calcMaxPrefixLength(clients)\n\n\twg := core.NewSizedWaitGroup(task.SpecData.Forks)\n\tfor i, c := range clients {\n\t\ttask := exec.Tasks[i]\n\t\twg.Add()\n\n\t\tif task.SpecData.Parallel {\n\t\t\tgo func(i int, c Client, wg *core.SizedWaitGroup) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_ = exec.TextWork(i, prefixMaxLen, dryRun, stdout, stderr)\n\t\t\t}(i, c, &wg)\n\t\t} else {\n\t\t\tfunc(i int, c Client, wg *core.SizedWaitGroup) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t_ = exec.TextWork(i, prefixMaxLen, dryRun, stdout, stderr)\n\t\t\t}(i, c, &wg)\n\t\t}\n\t}\n\n\twg.Wait()\n\n\tfmt.Fprintf(stdout, \"\\n\")\n}\n\nfunc (exec *Exec) TextWork(\n\trIndex int,\n\tprefixMaxLen int,\n\tdryRun bool,\n\tstdout io.Writer,\n\tstderr io.Writer,\n) error {\n\tclient := exec.Clients[rIndex]\n\ttask := exec.Tasks[rIndex]\n\tprefix := getPrefixer(client, rIndex, prefixMaxLen, task.ThemeData.Stream, task.SpecData.Parallel)\n\n\tvar numTasks int\n\tif task.Cmd != \"\" {\n\t\tnumTasks = len(task.Commands) + 1\n\t} else {\n\t\tnumTasks = len(task.Commands)\n\t}\n\n\tvar wg sync.WaitGroup\n\tfor j, cmd := range task.Commands {\n\t\targs := TableCmd{\n\t\t\trIndex:   rIndex,\n\t\t\tcIndex:   j,\n\t\t\tclient:   client,\n\t\t\tdryRun:   dryRun,\n\t\t\tshell:    cmd.ShellProgram,\n\t\t\tenv:      cmd.EnvList,\n\t\t\tcmd:      cmd.Cmd,\n\t\t\tcmdArr:   cmd.CmdArg,\n\t\t\tdesc:     cmd.Desc,\n\t\t\tname:     cmd.Name,\n\t\t\tnumTasks: numTasks,\n\t\t}\n\n\t\tif cmd.TTY {\n\t\t\treturn ExecTTY(cmd.Cmd, cmd.EnvList)\n\t\t}\n\n\t\terr := RunTextCmd(args, task.ThemeData.Stream, prefix, task.SpecData.Parallel, &wg, stdout, stderr)\n\t\tif err != nil && !task.SpecData.IgnoreErrors {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif task.Cmd != \"\" {\n\t\targs := TableCmd{\n\t\t\trIndex:   rIndex,\n\t\t\tcIndex:   len(task.Commands),\n\t\t\tclient:   client,\n\t\t\tdryRun:   dryRun,\n\t\t\tshell:    task.ShellProgram,\n\t\t\tenv:      task.EnvList,\n\t\t\tcmd:      task.Cmd,\n\t\t\tcmdArr:   task.CmdArg,\n\t\t\tdesc:     task.Desc,\n\t\t\tname:     task.Name,\n\t\t\tnumTasks: numTasks,\n\t\t}\n\n\t\tif task.TTY {\n\t\t\treturn ExecTTY(task.Cmd, task.EnvList)\n\t\t}\n\n\t\terr := RunTextCmd(args, task.ThemeData.Stream, prefix, task.SpecData.Parallel, &wg, stdout, stderr)\n\t\tif err != nil && !task.SpecData.IgnoreErrors {\n\t\t\treturn err\n\t\t}\n\t}\n\n\twg.Wait()\n\n\treturn nil\n}\n\nfunc RunTextCmd(\n\tt TableCmd,\n\ttextStyle dao.Stream,\n\tprefix string,\n\tparallel bool,\n\twg *sync.WaitGroup,\n\tstdout io.Writer,\n\tstderr io.Writer,\n) error {\n\tcombinedEnvs := dao.MergeEnvs(t.client.Env, t.env)\n\n\tif textStyle.Header && !parallel {\n\t\tprintHeader(stdout, t.cIndex, t.numTasks, t.name, t.desc, textStyle)\n\t}\n\n\tif t.dryRun {\n\t\tprintCmd(prefix, t.cmd)\n\t\treturn nil\n\t}\n\n\terr := t.client.Run(t.shell, combinedEnvs, t.cmdArr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Copy over commands STDOUT.\n\tgo func(client Client) {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tif prefix != \"\" {\n\t\t\t_, err = io.Copy(stdout, core.NewPrefixer(client.Stdout(), prefix))\n\t\t} else {\n\t\t\t_, err = io.Copy(stdout, client.Stdout())\n\t\t}\n\n\t\tif err != nil && err != io.EOF {\n\t\t\tfmt.Fprintf(stderr, \"%s\", err)\n\t\t}\n\t}(t.client)\n\twg.Add(1)\n\n\t// Copy over tasks's STDERR.\n\tgo func(client Client) {\n\t\tdefer wg.Done()\n\t\tvar err error\n\t\tif prefix != \"\" {\n\t\t\t_, err = io.Copy(stderr, core.NewPrefixer(client.Stderr(), prefix))\n\t\t} else {\n\t\t\t_, err = io.Copy(stderr, client.Stderr())\n\t\t}\n\n\t\tif err != nil && err != io.EOF {\n\t\t\tfmt.Fprintf(stderr, \"%s\", err)\n\t\t}\n\t}(t.client)\n\twg.Add(1)\n\n\twg.Wait()\n\n\tif err := t.client.Wait(); err != nil {\n\t\tif prefix != \"\" {\n\t\t\tfmt.Fprintf(stderr, \"%s%s\\n\", prefix, err)\n\t\t} else {\n\t\t\tfmt.Fprintf(stderr, \"%s\\n\", err)\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// TASK [pwd] -------------\nfunc printHeader(stdout io.Writer, i int, numTasks int, name string, desc string, ts dao.Stream) {\n\tvar header string\n\n\tprefixName := \"\"\n\tif name == \"\" {\n\t\tprefixName = color.Bold.Sprint(\"Command\")\n\t} else {\n\t\tprefixName = color.Bold.Sprint(name)\n\t}\n\n\tvar prefixPart1 string\n\tif numTasks > 1 {\n\t\tprefixPart1 = fmt.Sprintf(\"%s (%d/%d)\", color.Bold.Sprint(ts.HeaderPrefix), i+1, numTasks)\n\t} else {\n\t\tprefixPart1 = color.Bold.Sprint(ts.HeaderPrefix)\n\t}\n\n\tvar prefixPart2 string\n\tif desc != \"\" {\n\t\tprefixPart2 = fmt.Sprintf(\"[%s: %s]\", prefixName, desc)\n\t} else {\n\t\tprefixPart2 = fmt.Sprintf(\"[%s]\", prefixName)\n\t}\n\n\twidth, _, _ := term.GetSize(0)\n\n\tif prefixPart1 != \"\" {\n\t\theader = fmt.Sprintf(\"%s %s\", prefixPart1, prefixPart2)\n\t} else {\n\t\theader = prefixPart2\n\t}\n\theaderLength := len(core.Strip(header))\n\n\tif width > 0 && ts.HeaderChar != \"\" {\n\t\trepeatCount := max(0, width-headerLength-1)\n\t\theader = fmt.Sprintf(\"\\n%s %s\\n\\n\", header, strings.Repeat(ts.HeaderChar, repeatCount))\n\t} else {\n\t\theader = fmt.Sprintf(\"\\n%s\\n\\n\", header)\n\t}\n\tfmt.Fprint(stdout, header)\n}\n\n// mani | /projects/mani\nfunc getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Stream, parallel bool) string {\n\tif !textStyle.Prefix {\n\t\treturn \"\"\n\t}\n\n\t// Project name color\n\tvar prefixColor color.RGBColor\n\tif len(textStyle.PrefixColors) < 1 {\n\t\tprefixColor = dao.StyleFg(\"\")\n\t} else {\n\t\tfg := textStyle.PrefixColors[i%len(textStyle.PrefixColors)]\n\t\tprefixColor = dao.StyleFg(fg)\n\t}\n\n\tprefix := client.Prefix()\n\tprefixLen := len(prefix)\n\t// If we don't have a task header or the execution is parallel, then left pad the prefix.\n\tif (!textStyle.Header || parallel) && len(prefix) < prefixMaxLen { // Left padding.\n\t\tprefixString := prefix + strings.Repeat(\" \", prefixMaxLen-prefixLen) + \" | \"\n\t\tprefix = prefixColor.Sprint(prefixString)\n\t} else {\n\t\tprefixString := prefix + \" | \"\n\t\tprefix = prefixColor.Sprint(prefixString)\n\t}\n\n\treturn prefix\n}\n\nfunc calcMaxPrefixLength(clients []Client) int {\n\tvar prefixMaxLen = 0\n\tfor _, c := range clients {\n\t\tprefix := c.Prefix()\n\t\tif len(prefix) > prefixMaxLen {\n\t\t\tprefixMaxLen = len(prefix)\n\t\t}\n\t}\n\n\treturn prefixMaxLen\n}\n\nfunc printCmd(prefix string, cmd string) {\n\tscanner := bufio.NewScanner(strings.NewReader(cmd))\n\tfor scanner.Scan() {\n\t\tfmt.Printf(\"%s%s\\n\", prefix, scanner.Text())\n\t}\n}\n"
  },
  {
    "path": "core/exec/unix.go",
    "content": "//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 ExecTTY(cmd string, envs []string) error {\n\tshell := \"bash\"\n\tfoundShell, found := os.LookupEnv(\"SHELL\")\n\tif found {\n\t\tshell = foundShell\n\t}\n\n\texecBin, err := exec.LookPath(shell)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserEnv := append(os.Environ(), envs...)\n\terr = unix.Exec(execBin, []string{shell, \"-c\", cmd}, userEnv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/exec/windows.go",
    "content": "//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",
    "content": "package core\n\n// CMD Flags\n\ntype TUIFlags struct {\n\tTheme  string\n\tReload bool\n}\n\ntype ListFlags struct {\n\tOutput string\n\tTheme  string\n\tTree   bool\n}\n\ntype DescribeFlags struct {\n\tTheme string\n}\n\ntype SetProjectFlags struct {\n\tAll    bool\n\tCwd    bool\n\tTarget bool\n}\n\ntype ProjectFlags struct {\n\tAll      bool\n\tCwd      bool\n\tTags     []string\n\tTagsExpr string\n\tPaths    []string\n\tProjects []string\n\tTarget   string\n\tHeaders  []string\n\tEdit     bool\n}\n\ntype TagFlags struct {\n\tHeaders []string\n}\n\ntype TaskFlags struct {\n\tHeaders []string\n\tEdit    bool\n}\n\ntype RunFlags struct {\n\tEdit     bool\n\tParallel bool\n\tDryRun   bool\n\tSilent   bool\n\tDescribe bool\n\tCwd      bool\n\tTTY      bool\n\tTheme    string\n\tTarget   string\n\tSpec     string\n\n\tAll      bool\n\tProjects []string\n\tPaths    []string\n\tTags     []string\n\tTagsExpr string\n\n\tIgnoreErrors      bool\n\tIgnoreNonExisting bool\n\tOmitEmptyRows     bool\n\tOmitEmptyColumns  bool\n\tOutput            string\n\tForks             uint32\n}\n\ntype SetRunFlags struct {\n\tTTY bool\n\n\tAll bool\n\tCwd bool\n\n\tParallel          bool\n\tOmitEmptyColumns  bool\n\tOmitEmptyRows     bool\n\tIgnoreErrors      bool\n\tIgnoreNonExisting bool\n\tForks             bool\n}\n\ntype SyncFlags struct {\n\tIgnoreSyncState         bool\n\tParallel                bool\n\tSyncGitignore           bool\n\tStatus                  bool\n\tSyncRemotes             bool\n\tRemoveOrphanedWorktrees bool\n\tForks                   uint32\n}\n\ntype SetSyncFlags struct {\n\tParallel                bool\n\tSyncGitignore           bool\n\tSyncRemotes             bool\n\tRemoveOrphanedWorktrees bool\n\tForks                   bool\n}\n\ntype InitFlags struct {\n\tAutoDiscovery bool\n\tSyncGitignore bool\n}\n"
  },
  {
    "path": "core/man.go",
    "content": "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 GenManPages(dir string) error {\n\tmanPath := filepath.Join(dir, \"mani.1\")\n\terr := os.WriteFile(manPath, ConfigMan, 0644)\n\tCheckIfError(err)\n\n\tfmt.Printf(\"Created %s\\n\", manPath)\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/man_gen.go",
    "content": "// This source will generate\n//   - core/mani.1\n//   - docs/commands.md\n//\n// and is not included in the final build.\n\npackage core\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/cobra/doc\"\n\t\"github.com/spf13/pflag\"\n)\n\n//go:embed config.man\nvar ConfigMd []byte\n\ntype genManHeaders struct {\n\tTitle   string\n\tSection string\n\tDate    string\n\tSource  string\n\tManual  string\n\tVersion string\n\tDesc    string\n}\n\nfunc CreateManPage(desc string, version string, date string, rootCmd *cobra.Command, cmds ...*cobra.Command) error {\n\theader := &genManHeaders{\n\t\tTitle:   \"MANI\",\n\t\tSection: \"1\",\n\t\tSource:  \"Mani Manual\",\n\t\tManual:  \"mani\",\n\t\tVersion: version,\n\t\tDate:    date,\n\t\tDesc:    desc,\n\t}\n\n\tres := genMan(header, rootCmd, cmds...)\n\tres = append(res, ConfigMd...)\n\tmanPath := filepath.Join(\"./core/\", \"mani.1\")\n\terr := os.WriteFile(manPath, res, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Created %s\\n\", manPath)\n\n\tmd, err := genDoc(rootCmd, cmds...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmdPath := filepath.Join(\"./docs/\", \"commands.md\")\n\terr = os.WriteFile(mdPath, md, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"Created %s\\n\", mdPath)\n\n\treturn nil\n}\n\nfunc manPreamble(buf io.StringWriter, header *genManHeaders, cmd *cobra.Command) {\n\tpreamble := `.TH \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"`\n\tcobra.WriteStringAndCheck(buf, fmt.Sprintf(preamble, header.Title, header.Section, header.Date, header.Version, header.Source, header.Manual))\n\n\tcobra.WriteStringAndCheck(buf, \"\\n\")\n\n\tcobra.WriteStringAndCheck(buf, \".SH NAME\\n\")\n\tcobra.WriteStringAndCheck(buf, fmt.Sprintf(\"%s - %s\\n\", header.Manual, cmd.Short))\n\n\tcobra.WriteStringAndCheck(buf, \"\\n\")\n\n\tcobra.WriteStringAndCheck(buf, \".SH SYNOPSIS\\n\")\n\tcobra.WriteStringAndCheck(buf, \".B mani [command] [flags]\\n\")\n\n\tcobra.WriteStringAndCheck(buf, \"\\n\")\n\n\tcobra.WriteStringAndCheck(buf, \".SH DESCRIPTION\\n\")\n\n\tcobra.WriteStringAndCheck(buf, header.Desc+\"\\n\\n\")\n}\n\nfunc manCommand(buf io.StringWriter, cmd *cobra.Command) {\n\tcobra.WriteStringAndCheck(buf, \".TP\\n\")\n\tcobra.WriteStringAndCheck(buf, fmt.Sprintf(`.B %s`, cmd.UseLine()))\n\tcobra.WriteStringAndCheck(buf, \"\\n\")\n\tcobra.WriteStringAndCheck(buf, fmt.Sprintf(\"%s\\n\\n\", cmd.Long))\n\n\tnonInheritedFlags := cmd.NonInheritedFlags()\n\tinheritedFlags := cmd.InheritedFlags()\n\tif !nonInheritedFlags.HasAvailableFlags() && !inheritedFlags.HasAvailableFlags() {\n\t\treturn\n\t}\n\n\tcobra.WriteStringAndCheck(buf, \"\\n.B Available Options:\\n\")\n\tcobra.WriteStringAndCheck(buf, \".RS\\n\")\n\tcobra.WriteStringAndCheck(buf, \".RS\\n\")\n\tif nonInheritedFlags.HasAvailableFlags() {\n\t\tmanPrintFlags(buf, nonInheritedFlags)\n\t}\n\n\tif inheritedFlags.HasAvailableFlags() && cmd.Name() != \"gen\" {\n\t\tmanPrintFlags(buf, inheritedFlags)\n\t\tcobra.WriteStringAndCheck(buf, \"\\n\")\n\t}\n\n\tcobra.WriteStringAndCheck(buf, \".RE\\n\")\n\tcobra.WriteStringAndCheck(buf, \".RE\\n\")\n}\n\nfunc manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) {\n\tflags.VisitAll(func(flag *pflag.Flag) {\n\t\tif len(flag.Deprecated) > 0 || flag.Hidden {\n\t\t\treturn\n\t\t}\n\t\tformat := \"\"\n\n\t\tif len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {\n\t\t\tformat = fmt.Sprintf(\"-%s, --%s\", flag.Shorthand, flag.Name)\n\t\t} else {\n\t\t\tformat = fmt.Sprintf(\"--%s\", flag.Name)\n\t\t}\n\t\tif len(flag.NoOptDefVal) > 0 {\n\t\t\tformat += \"[\"\n\t\t}\n\t\tif flag.Value.Type() == \"string\" {\n\t\t\t// put quotes on the value\n\t\t\tformat += \"=%q\"\n\t\t} else {\n\t\t\tformat += \"=%s\"\n\t\t}\n\t\tif len(flag.NoOptDefVal) > 0 {\n\t\t\tformat += \"]\"\n\t\t}\n\n\t\tformat = fmt.Sprintf(`\\fB%s\\fR`, format)\n\t\tformat = fmt.Sprintf(format, flag.DefValue)\n\t\tformat = fmt.Sprintf(\".TP\\n%s\\n%s\\n\", format, flag.Usage)\n\t\tcobra.WriteStringAndCheck(buf, format)\n\t})\n}\n\nfunc genMan(header *genManHeaders, cmd *cobra.Command, cmds ...*cobra.Command) []byte {\n\tcmd.InitDefaultHelpCmd()\n\tcmd.InitDefaultHelpFlag()\n\n\tbuf := new(bytes.Buffer)\n\n\t// PREAMBLE\n\tmanPreamble(buf, header, cmd)\n\tflags := cmd.NonInheritedFlags()\n\n\t// OPTIONS\n\tcobra.WriteStringAndCheck(buf, \".SH OPTIONS\\n\")\n\n\t// FLAGS\n\tmanPrintFlags(buf, flags)\n\n\tbuf.WriteString(\".SH\\nCOMMANDS\\n\")\n\n\t// COMMANDS\n\tfor _, c := range cmds {\n\t\tcbuf := new(bytes.Buffer)\n\n\t\tif !slices.Contains([]string{\"list\", \"describe\"}, c.Name()) {\n\t\t\tmanCommand(cbuf, c)\n\t\t}\n\n\t\tif len(c.Commands()) > 0 {\n\t\t\tfor _, cc := range c.Commands() {\n\t\t\t\t// Don't include help command\n\t\t\t\tif cc.Name() != \"help\" {\n\t\t\t\t\tmanCommand(cbuf, cc)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tbuf.Write(cbuf.Bytes())\n\t}\n\n\treturn buf.Bytes()\n}\n\nfunc genDoc(cmd *cobra.Command, cmds ...*cobra.Command) ([]byte, error) {\n\tcmd.InitDefaultHelpCmd()\n\tcmd.InitDefaultHelpFlag()\n\n\tout := new(bytes.Buffer)\n\terr := doc.GenMarkdown(cmd, out)\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\n\tmd := out.String()\n\tmd = strings.Split(md, \"### SEE ALSO\")[0]\n\tmd = fmt.Sprintf(\"%s\\n\\n%s\", \"# Commands\", md)\n\n\tfor _, c := range cmds {\n\t\tif !slices.Contains([]string{\"list\", \"describe\"}, c.Name()) {\n\t\t\tcOut := new(bytes.Buffer)\n\t\t\terr := doc.GenMarkdown(c, cOut)\n\t\t\tif err != nil {\n\t\t\t\treturn []byte{}, err\n\t\t\t}\n\n\t\t\tcMd := cOut.String()\n\t\t\tcMd = strings.Split(cMd, \"### SEE ALSO\")[0]\n\t\t\tmd += cMd\n\t\t}\n\n\t\tif len(c.Commands()) > 0 {\n\t\t\tfor _, cc := range c.Commands() {\n\t\t\t\t// Don't include help command\n\t\t\t\tif cc.Name() != \"help\" {\n\t\t\t\t\tccOut := new(bytes.Buffer)\n\t\t\t\t\terr := doc.GenMarkdown(cc, ccOut)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn []byte{}, err\n\t\t\t\t\t}\n\t\t\t\t\tccMd := ccOut.String()\n\t\t\t\t\tccMd = strings.Split(ccMd, \"### SEE ALSO\")[0]\n\t\t\t\t\tmd += ccMd\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn []byte(md), nil\n}\n"
  },
  {
    "path": "core/mani.1",
    "content": ".TH \"MANI\" \"1\" \"2025 December 05\" \"v0.31.2\" \"Mani Manual\" \"mani\"\n.SH NAME\nmani - repositories manager and task runner\n\n.SH SYNOPSIS\n.B mani [command] [flags]\n\n.SH DESCRIPTION\nmani is a CLI tool that helps you manage multiple repositories.\n\nIt's useful when you are working with microservices, multi-project systems, multiple libraries, or just a collection \nof repositories and want a central place for pulling all repositories and running commands across them.\n\n.SH OPTIONS\n.TP\n\\fB--color[=true]\\fR\nenable color\n.TP\n\\fB-c, --config=\"\"\\fR\nspecify config\n.TP\n\\fB-h, --help[=false]\\fR\nhelp for mani\n.TP\n\\fB-u, --user-config=\"\"\\fR\nspecify user config\n.SH\nCOMMANDS\n.TP\n.B run <task>\nRun tasks.\n\nThe tasks are specified in a mani.yaml file along with the projects you can target.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-a, --all[=false]\\fR\nselect all projects\n.TP\n\\fB-k, --cwd[=false]\\fR\nselect current working directory\n.TP\n\\fB--describe[=false]\\fR\ndisplay task information\n.TP\n\\fB--dry-run[=false]\\fR\ndisplay the task without execution\n.TP\n\\fB-e, --edit[=false]\\fR\nedit task\n.TP\n\\fB-f, --forks=4\\fR\nmaximum number of concurrent processes\n.TP\n\\fB--ignore-errors[=false]\\fR\ncontinue execution despite errors\n.TP\n\\fB--ignore-non-existing[=false]\\fR\nskip non-existing projects\n.TP\n\\fB--omit-empty-columns[=false]\\fR\nhide empty columns in table output\n.TP\n\\fB--omit-empty-rows[=false]\\fR\nhide empty rows in table output\n.TP\n\\fB-o, --output=\"\"\\fR\nset output format [stream|table|markdown|html]\n.TP\n\\fB--parallel[=false]\\fR\nexecute tasks in parallel across projects\n.TP\n\\fB-d, --paths=[]\\fR\nselect projects by path\n.TP\n\\fB-p, --projects=[]\\fR\nselect projects by name\n.TP\n\\fB-s, --silent[=false]\\fR\nhide progress output during task execution\n.TP\n\\fB-J, --spec=\"\"\\fR\nset spec\n.TP\n\\fB-t, --tags=[]\\fR\nselect projects by tag\n.TP\n\\fB-E, --tags-expr=\"\"\\fR\nselect projects by tags expression\n.TP\n\\fB-T, --target=\"\"\\fR\nselect projects by target name\n.TP\n\\fB--theme=\"\"\\fR\nset theme\n.TP\n\\fB--tty[=false]\\fR\nreplace current process\n.RE\n.RE\n.TP\n.B exec <command> [flags]\nExecute arbitrary commands.\nUse single quotes around your command to prevent file globbing and \nenvironment variable expansion from occurring before the command is \nexecuted in each directory.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-a, --all[=false]\\fR\ntarget all projects\n.TP\n\\fB-k, --cwd[=false]\\fR\nuse current working directory\n.TP\n\\fB--dry-run[=false]\\fR\nprint commands without executing them\n.TP\n\\fB-f, --forks=4\\fR\nmaximum number of concurrent processes\n.TP\n\\fB--ignore-errors[=false]\\fR\nignore errors\n.TP\n\\fB--ignore-non-existing[=false]\\fR\nignore non-existing projects\n.TP\n\\fB--omit-empty-columns[=false]\\fR\nomit empty columns in table output\n.TP\n\\fB--omit-empty-rows[=false]\\fR\nomit empty rows in table output\n.TP\n\\fB-o, --output=\"\"\\fR\nset output format [stream|table|markdown|html]\n.TP\n\\fB--parallel[=false]\\fR\nrun tasks in parallel across projects\n.TP\n\\fB-d, --paths=[]\\fR\nselect projects by path\n.TP\n\\fB-p, --projects=[]\\fR\nselect projects by name\n.TP\n\\fB-s, --silent[=false]\\fR\nhide progress when running tasks\n.TP\n\\fB-J, --spec=\"\"\\fR\nset spec\n.TP\n\\fB-t, --tags=[]\\fR\nselect projects by tag\n.TP\n\\fB-E, --tags-expr=\"\"\\fR\nselect projects by tags expression\n.TP\n\\fB-T, --target=\"\"\\fR\ntarget projects by target name\n.TP\n\\fB--theme=\"\"\\fR\nset theme\n.TP\n\\fB--tty[=false]\\fR\nreplace current process\n.RE\n.RE\n.TP\n.B init [flags]\nInitialize a mani repository.\n\nCreates a new mani repository by generating a mani.yaml configuration file \nand a .gitignore file in the current directory.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB--auto-discovery[=true]\\fR\nautomatically discover and add Git repositories to mani.yaml\n.TP\n\\fB-g, --sync-gitignore[=true]\\fR\nsynchronize .gitignore file\n.RE\n.RE\n.TP\n.B sync [flags]\nClone repositories and update .gitignore file.\nFor repositories requiring authentication, disable parallel cloning to enter\ncredentials for each repository individually.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-f, --forks=4\\fR\nmaximum number of concurrent processes\n.TP\n\\fB--ignore-sync-state[=false]\\fR\nsync project even if the project's sync field is set to false\n.TP\n\\fB-p, --parallel[=false]\\fR\nclone projects in parallel\n.TP\n\\fB-d, --paths=[]\\fR\nclone projects by path\n.TP\n\\fB-s, --status[=false]\\fR\ndisplay status only\n.TP\n\\fB-g, --sync-gitignore[=true]\\fR\nsync gitignore\n.TP\n\\fB-r, --sync-remotes[=false]\\fR\nupdate git remote state\n.TP\n\\fB-t, --tags=[]\\fR\nclone projects by tags\n.TP\n\\fB-E, --tags-expr=\"\"\\fR\nclone projects by tag expression\n.RE\n.RE\n.TP\n.B edit\nOpen up mani config file in $EDITOR.\n\n.TP\n.B edit project [project]\nEdit mani project in $EDITOR.\n\n.TP\n.B edit task [task]\nEdit mani task in $EDITOR.\n\n.TP\n.B list projects [projects] [flags]\nList projects.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-a, --all[=true]\\fR\nselect all projects\n.TP\n\\fB-k, --cwd[=false]\\fR\nselect current working directory\n.TP\n\\fB--headers=[project,tag,description]\\fR\nspecify columns to display [project, path, relpath, description, url, tag]\n.TP\n\\fB-d, --paths=[]\\fR\nselect projects by paths\n.TP\n\\fB-t, --tags=[]\\fR\nselect projects by tags\n.TP\n\\fB-E, --tags-expr=\"\"\\fR\nselect projects by tags expression\n.TP\n\\fB-T, --target=\"\"\\fR\nselect projects by target name\n.TP\n\\fB--tree[=false]\\fR\ndisplay output in tree format\n.TP\n\\fB-o, --output=\"table\"\\fR\nset output format [table|markdown|html]\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n\n.RE\n.RE\n.TP\n.B list tags [tags] [flags]\nList tags.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB--headers=[tag,project]\\fR\nspecify columns to display [project, tag]\n.TP\n\\fB-o, --output=\"table\"\\fR\nset output format [table|markdown|html]\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n\n.RE\n.RE\n.TP\n.B list tasks [tasks] [flags]\nList tasks.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB--headers=[task,description]\\fR\nspecify columns to display [task, description, target, spec]\n.TP\n\\fB-o, --output=\"table\"\\fR\nset output format [table|markdown|html]\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n\n.RE\n.RE\n.TP\n.B describe projects [projects] [flags]\nDescribe projects.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-a, --all[=true]\\fR\nselect all projects\n.TP\n\\fB-k, --cwd[=false]\\fR\nselect current working directory\n.TP\n\\fB-e, --edit[=false]\\fR\nedit project\n.TP\n\\fB-d, --paths=[]\\fR\nfilter projects by paths\n.TP\n\\fB-t, --tags=[]\\fR\nfilter projects by tags\n.TP\n\\fB-E, --tags-expr=\"\"\\fR\ntarget projects by tags expression\n.TP\n\\fB-T, --target=\"\"\\fR\ntarget projects by target name\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n\n.RE\n.RE\n.TP\n.B describe tasks [tasks] [flags]\nDescribe tasks.\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-e, --edit[=false]\\fR\nedit task\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n\n.RE\n.RE\n.TP\n.B tui [flags]\nRun TUI\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-r, --reload-on-change[=false]\\fR\nreload mani on config change\n.TP\n\\fB--theme=\"default\"\\fR\nset theme\n.RE\n.RE\n.TP\n.B check\nValidate config.\n\n.TP\n.B gen\n\n\n\n.B Available Options:\n.RS\n.RS\n.TP\n\\fB-d, --dir=\"./\"\\fR\ndirectory to save manpage to\n.RE\n.RE\n.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, which may be git repositories, in which case they have an URL attribute\n.PD 0\n.IP \"\\(bu\" 2\n\\fBtasks\\fR are shell commands that you write and then run for selected \\fBprojects\\fR\n.IP \"\\(bu\" 2\n\\fBspecs\\fR are configs that alter \\fBtask\\fR execution and output\n.PD 0\n.IP \"\\(bu\" 2\n\\fBtargets\\fR are configs that provide shorthand filtering of \\fBprojects\\fR when executing tasks\n.PD 0\n.IP \"\\(bu\" 2\n\\fBenv\\fR are environment variables that can be defined globally, per project and per task\n.PD 0\n.RE\n\n\\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.\n\nCheck the files and environment section to see how the config file is loaded.\n\nBelow is a config file detailing all of the available options and their defaults.\n\n.RS 4\n # Import projects/tasks/env/specs/themes/targets from other configs\n import:\n   - ./some-dir/mani.yaml\n\n # Shell used for commands\n # If you use any other program than bash, zsh, sh, node, and python\n # then you have to provide the command flag if you want the command-line string evaluted\n # For instance: bash -c\n shell: bash\n\n # If set to true, mani will override the URL of any existing remote\n # and remove remotes not found in the config\n sync_remotes: false\n\n # Determines whether the .gitignore should be updated when syncing projects\n sync_gitignore: true\n\n # When running the TUI, specifies whether it should reload when the mani config is changed\n reload_tui_on_change: false\n\n # List of Projects\n projects:\n   # Project name [required]\n   pinto:\n     # Determines if the project should be synchronized during 'mani sync'\n     sync: true\n\n     # Project path relative to the config file\n     # Defaults to project name if not specified\n     path: frontend/pinto\n\n     # Repository URL\n     url: git@github.com:alajmo/pinto\n\n     # Project description\n     desc: A vim theme editor\n\n     # Custom clone command\n     # Defaults to \"git clone URL PATH\"\n     clone: git clone git@github.com:alajmo/pinto --branch main\n\n     # Branch to use as primary HEAD when cloning\n     # Defaults to repository's primary HEAD\n     branch:\n\n     # When true, clones only the specified branch or primary HEAD\n     single_branch: false\n\n     # Project tags\n     tags: [dev]\n\n     # Remote repositories\n     # Key is the remote name, value is the URL\n     remotes:\n       foo: https://github.com/bar\n\n     # Project-specific environment variables\n     env:\n       # Simple string value\n       branch: main\n\n       # Shell command substitution\n       date: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n # List of Specs\n specs:\n   default:\n     # Output format for task results\n     # Options: stream, table, html, markdown\n     output: stream\n\n     # Enable parallel task execution\n     parallel: false\n\n     # Maximum number of concurrent tasks when running in parallel\n     forks: 4\n\n     # When true, continues execution if a command fails in a multi-command task\n     ignore_errors: false\n\n     # When true, skips project entries in the config that don't exist\n     # on the filesystem without throwing an error\n     ignore_non_existing: false\n\n     # Hide projects with no command output\n     omit_empty_rows: false\n\n     # Hide columns with no data\n     omit_empty_columns: false\n\n     # Clear screen before task execution (TUI only)\n     clear_output: true\n\n # List of targets\n targets:\n   default:\n     # Select all projects\n     all: false\n\n     # Select project in current working directory\n     cwd: false\n\n     # Select projects by name\n     projects: []\n\n     # Select projects by path\n     paths: []\n\n     # Select projects by tag\n     tags: []\n\n     # Select projects by tag expression\n     tags_expr: \"\"\n\n # Environment variables available to all tasks\n env:\n   # Simple string value\n   AUTHOR: \"alajmo\"\n\n   # Shell command substitution\n   DATE: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n # List of tasks\n tasks:\n   # Command name [required]\n   simple-2: echo \"hello world\"\n\n   # Command name [required]\n   simple-1:\n     cmd: |\n       echo \"hello world\"\n     desc: simple command 1\n\n   # Command name [required]\n   advanced-command:\n     # Task description\n     desc: complex task\n\n     # Task theme\n     theme: default\n\n     # Shell interpreter\n     shell: bash\n\n     # Task-specific environment variables\n     env:\n       # Static value\n       branch: main\n\n       # Dynamic shell command output\n       num_lines: $(ls -1 | wc -l)\n\n     # Can reference predefined spec:\n     # spec: custom_spec\n     # or define inline:\n     spec:\n       output: table\n       parallel: true\n       forks: 4\n       ignore_errors: false\n       ignore_non_existing: true\n       omit_empty_rows: true\n       omit_empty_columns: true\n\n     # Can reference predefined target:\n     # target: custom_target\n     # or define inline:\n     target:\n       all: true\n       cwd: false\n       projects: [pinto]\n       paths: [frontend]\n       tags: [dev]\n       tags_expr: (prod || dev) && !test\n\n     # Single multi-line command\n     cmd: |\n       echo complex\n       echo command\n\n     # Multiple commands\n     commands:\n       # Node.js command example\n       - name: node-example\n \t       shell: node\n         cmd: console.log(\"hello world from node.js\");\n\n       # Reference to another task\n       - task: simple-1\n\n # List of themes\n # Styling Options:\n #   Fg (foreground color): Empty string (\"\"), hex color, or named color from W3C standard\n #   Bg (background color): Empty string (\"\"), hex color, or named color from W3C standard\n #   Format: Empty string (\"\"), \"lower\", \"title\", \"upper\"\n #   Attribute: Empty string (\"\"), \"bold\", \"italic\", \"underline\"\n #   Alignment: Empty string (\"\"), \"left\", \"center\", \"right\"\n themes:\n   # Theme name [required]\n   default:\n     # Stream Output Configuration\n     stream:\n       # Include project name prefix for each line\n       prefix: true\n\n       # Colors to alternate between for each project prefix\n       prefix_colors: [\"#d787ff\", \"#00af5f\", \"#d75f5f\", \"#5f87d7\", \"#00af87\", \"#5f00ff\"]\n\n       # Add a header before each project\n       header: true\n\n       # String value that appears before the project name in the header\n       header_prefix: \"TASK\"\n\n       # Fill remaining spaces with a character after the prefix\n       header_char: \"*\"\n\n     # Table Output Configuration\n     table:\n       # Table style\n       # Available options: ascii, light, bold, double, rounded\n       style: ascii\n\n       # Border options for table output\n       border:\n         around: false  # Border around the table\n         columns: true  # Vertical border between columns\n         header: true   # Horizontal border between headers and rows\n         rows: false    # Horizontal border between rows\n\n       header:\n         fg: \"#d787ff\"\n         attr: bold\n         format: \"\"\n\n       title_column:\n         fg: \"#5f87d7\"\n         attr: bold\n         format: \"\"\n\n     # Tree View Configuration\n     tree:\n       # Tree style\n       # Available options: ascii, light, bold, double, rounded, bullet-square, bullet-circle, bullet-star\n       style: ascii\n\n     # Block Display Configuration\n     block:\n       key:\n         fg: \"#5f87d7\"\n       separator:\n         fg: \"#5f87d7\"\n       value:\n         fg:\n       value_true:\n         fg: \"#00af5f\"\n       value_false:\n         fg: \"#d75f5f\"\n\n      # TUI Configuration\n      tui:\n        default:\n          fg:\n          bg:\n          attr:\n\n        border:\n          fg:\n        border_focus:\n          fg: \"#d787ff\"\n\n        title:\n          fg:\n          bg:\n          attr:\n          align: center\n        title_active:\n          fg: \"#000000\"\n          bg: \"#d787ff\"\n          attr:\n          align: center\n\n        button:\n          fg:\n          bg:\n          attr:\n          format:\n        button_active:\n          fg: \"#080808\"\n          bg: \"#d787ff\"\n          attr:\n          format:\n\n        table_header:\n          fg: \"#d787ff\"\n          bg:\n          attr: bold\n          align: left\n          format:\n\n        item:\n          fg:\n          bg:\n          attr:\n        item_focused:\n          fg: \"#ffffff\"\n          bg: \"#262626\"\n          attr:\n        item_selected:\n          fg: \"#5f87d7\"\n          bg:\n          attr:\n        item_dir:\n          fg: \"#d787ff\"\n          bg:\n          attr:\n        item_ref:\n          fg: \"#d787ff\"\n          bg:\n          attr:\n\n        search_label:\n          fg: \"#d7d75f\"\n          bg:\n          attr: bold\n        search_text:\n          fg:\n          bg:\n          attr:\n\n        filter_label:\n          fg: \"#d7d75f\"\n          bg:\n          attr: bold\n        filter_text:\n          fg:\n          bg:\n          attr:\n\n        shortcut_label:\n          fg: \"#00af5f\"\n          bg:\n          attr:\n        shortcut_text:\n          fg:\n          bg:\n          attr:\n.RE\n\n\n.SH EXAMPLES\n\n.TP\nInitialize mani\n.B samir@hal-9000 ~ $ mani init\n\n.nf\nInitialized mani repository in /tmp\n- Created mani.yaml\n- Created .gitignore\n\nFollowing projects were added to mani.yaml\n\n Project  | Path\n----------+------------\n test     | .\n pinto    | dev/pinto\n.fi\n\n.TP\nClone projects\n.B samir@hal-9000 ~ $ mani sync --parallel --forks 8\n.nf\npinto | Cloning into '/tmp/dev/pinto'...\n\n Project  | Synced\n----------+--------\n test     | ✓\n pinto    | ✓\n.fi\n\n.TP\nList all projects\n.B samir@hal-9000 ~ $ mani list projects\n.nf\n Project\n---------\n test\n pinto\n.fi\n\n.TP\nList all projects with output set to tree\n.nf\n.B samir@hal-9000 ~ $ mani list projects --tree\n    ── dev\n       └─ pinto\n.fi\n\n.nf\n\n.TP\nList all tags\n.B samir@hal-9000 ~ $ mani list tags\n.nf\n Tag | Project\n-----+---------\n dev | pinto\n.fi\n\n.TP\nList all tasks\n.nf\n.B samir@hal-9000 ~ $ mani list tasks\n Task             | Description\n------------------+------------------\n simple-1         | simple command 1\n simple-2         |\n advanced-command | complex task\n.fi\n\n.TP\nDescribe a task\n.nf\n.B samir@hal-9000 ~ $ mani describe tasks advanced-command\nName: advanced-command\nDescription: complex task\nTheme: default\nTarget:\n    All: true\n    Cwd: false\n    Projects: pinto\n    Paths: frontend\n    Tags: dev\n    TagsExpr: \"\"\nSpec:\n    Output: table\n    Parallel: true\n    Forks: 4\n    IgnoreErrors: false\n    IgnoreNonExisting: false\n    OmitEmptyRows: false\n    OmitEmptyColumns: false\nEnv:\n    branch: dev\n    num_lines: 2\nCmd:\n    echo advanced\n    echo command\nCommands:\n     - simple-1\n     - simple-2\n     - cmd\n.fi\n\n.TP\nRun a task for all projects with tag 'dev'\n.nf\n.B samir@hal-9000 ~ $ mani run simple-1 --tags dev\n Project | Simple-1\n---------+-------------\n pinto   | hello world\n.fi\n\n.TP\nRun a task for all projects matching tags expression 'dev && !prod'\n.nf\n.B samir@hal-9000 ~ $ mani run simple-1 --tags-expr '(dev && !prod)'\n Project | Simple-1\n---------+-------------\n pinto   | hello world\n.fi\n\n.TP\nRun ad-hoc command for all projects\n.nf\n.B samir@hal-9000 ~ $ mani exec 'echo 123' --all\n Project | Output\n---------+--------\n archive | 123\n pinto   | 123\n.fi\n\n.SH FILTERING PROJECTS\nProjects can be filtered when managing projects (sync, list, describe) or running tasks. \nFilters can be specified through CLI flags or target configurations. \nThe filtering is inclusive, meaning projects must satisfy all specified filters to be included in the results.\n\n.PP\nAvailable options:\n.RS 2\n.IP \"\\(bu\" 2\ncwd: include only the project under the current working directory, ignoring all other filters\n.IP \"\\(bu\" 2\nall: include all projects\n.IP \"\\(bu\" 2\nprojects: Filter by project names\n.IP \"\\(bu\" 2\npaths: Filter by project paths\n.IP \"\\(bu\" 2\ntags: Filter by project tags\n.IP \"\\(bu\" 2\ntags_expr: Filter using tag logic expressions\n.IP \"\\(bu\" 2\ntarget: Filter using target\n.RE\n\n.PP\n\nFor \\fBmani sync/list/describe\\fR:\n.RS 2\n.IP \"\\(bu\" 2\nNo filters: Targets all projects\n.IP \"\\(bu\" 2\nMultiple filters: Select intersection of projects/paths/tags/tags_expr/target filters\n.RE\n\nFor \\fBmani run/exec\\fR:\n.RS 2\n.IP \"1.\" 4\nRuntime flags (highest priority)\n.IP \"2.\" 4\nTarget flag configuration (\\fB--target\\fR)\n.IP \"3.\" 4\nTask's default target data (lowest priority)\n.RE\n\nThe 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`.\n\n.SH TAGS EXPRESSION\n\nTag expressions allow filtering projects using boolean operations on their tags. \nThe expression is evaluated for each project's tags to determine if the project should be included.\n\n.PP\nOperators (in precedence order):\n\n.RS 2\n.IP \"\\(bu\" 2\n(): Parentheses for grouping\n.PD 0\n.IP \"\\(bu\" 2\n!: NOT operator (logical negation)\n.PD 0\n.IP \"\\(bu\" 2\n&&: AND operator (logical conjunction)\n.PD 0\n.IP \"\\(bu\" 2\n||: OR operator (logical disjunction)\n.RE\n\n.PP\nFor example, the expression:\n\n  \\fB(main && (dev || prod)) && !test\\fR\n\n.PP\nrequires the projects to pass these conditions:\n\n.RS 2\n.IP \"\\(bu\" 2\nMust have \"main\" tag\n.PD 0\n.IP \"\\(bu\" 2\nMust have either \"dev\" OR \"prod\" tag\n.IP \"\\(bu\" 2\nMust NOT have \"test\" tag\n.PD 0\n.RE\n\n.SH FILES\n\nWhen running a command,\n.B mani\nwill check the current directory and all parent directories for the following files: mani.yaml, mani.yml, .mani.yaml, .mani.yml.\n\nAdditionally, it will import (if found) a config file from:\n\n.RS 2\n.IP \"\\(bu\" 2\nLinux: \\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.\n.IP \"\\(bu\" 2\nDarwin: \\fB$HOME/Library/Application Support/mani/config.yaml\\fR\n.IP \"\\(bu\" 2\nWindows: \\fB%AppData%\\mani\\fR\n.RE\n\nBoth the config and user config can be specified via flags or environments variables.\n\n.SH\nENVIRONMENT\n\n.TP\n.B MANI_CONFIG\nOverride config file path\n\n.TP\n.B MANI_USER_CONFIG\nOverride user config file path\n\n.TP\n.B NO_COLOR\nIf this env variable is set (regardless of value) then all colors will be disabled\n\n.SH BUGS\n\nSee GitHub Issues:\n.UR https://github.com/alajmo/mani/issues\n.ME .\n\n.SH AUTHOR\n\n.B mani\nwas written by Samir Alajmovic\n.MT alajmovic.samir@gmail.com\n.ME .\nFor updates and more information go to\n.UR https://\\:www.manicli.com\nmanicli.com\n.UE .\n"
  },
  {
    "path": "core/prefixer.go",
    "content": "// Source: https://github.com/goware/prefixer\n// Author: goware\npackage core\n\nimport (\n\t\"bufio\"\n\t\"io\"\n)\n\n// Prefixer implements io.Reader and io.WriterTo. It reads\n// data from the underlying reader and prepends every line\n// with a given string.\ntype Prefixer struct {\n\treader *bufio.Reader\n\tprefix []byte\n\tunread []byte\n\teof    bool\n}\n\n// New creates a new instance of Prefixer.\nfunc NewPrefixer(r io.Reader, prefix string) *Prefixer {\n\treturn &Prefixer{\n\t\treader: bufio.NewReader(r),\n\t\tprefix: []byte(prefix),\n\t}\n}\n\n// Read implements io.Reader. It reads data into p from the\n// underlying reader and prepends every line with a prefix.\n// It does not block if no data is available yet.\n// It returns the number of bytes read into p.\nfunc (r *Prefixer) Read(p []byte) (n int, err error) {\n\tfor {\n\t\t// Write unread data from previous read.\n\t\tif len(r.unread) > 0 {\n\t\t\tm := copy(p[n:], r.unread)\n\t\t\tn += m\n\t\t\tr.unread = r.unread[m:]\n\t\t\tif len(r.unread) > 0 {\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t}\n\n\t\t// The underlying Reader already returned EOF, do not read again.\n\t\tif r.eof {\n\t\t\treturn n, io.EOF\n\t\t}\n\n\t\t// Read new line, including delim.\n\t\tr.unread, err = r.reader.ReadBytes('\\n')\n\n\t\tif err == io.EOF {\n\t\t\tr.eof = true\n\t\t}\n\n\t\t// No new data, do not block.\n\t\tif len(r.unread) == 0 {\n\t\t\treturn n, err\n\t\t}\n\n\t\t// Some new data, prepend prefix.\n\t\t// TODO: We could write the prefix to r.unread buffer just once\n\t\t//       and re-use it instead of prepending every time.\n\t\tr.unread = append(r.prefix, r.unread...)\n\n\t\tif err != nil {\n\t\t\tif err == io.EOF && len(r.unread) > 0 {\n\t\t\t\t// The underlying Reader already returned EOF, but we still\n\t\t\t\t// have some unread data to send, thus clear the error.\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t\treturn n, err\n\t\t}\n\t}\n}\n\nfunc (r *Prefixer) WriteTo(w io.Writer) (n int64, err error) {\n\tfor {\n\t\t// Write unread data from previous read.\n\t\tif len(r.unread) > 0 {\n\t\t\tm, err := w.Write(r.unread)\n\t\t\tn += int64(m)\n\t\t\tif err != nil {\n\t\t\t\treturn n, err\n\t\t\t}\n\n\t\t\tr.unread = r.unread[m:]\n\t\t\tif len(r.unread) > 0 {\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t}\n\n\t\t// The underlying Reader already returned EOF, do not read again.\n\t\tif r.eof {\n\t\t\treturn n, io.EOF\n\t\t}\n\n\t\t// Read new line, including delim.\n\t\tr.unread, err = r.reader.ReadBytes('\\n')\n\n\t\tif err == io.EOF {\n\t\t\tr.eof = true\n\t\t}\n\n\t\t// No new data, do not block.\n\t\tif len(r.unread) == 0 {\n\t\t\treturn n, err\n\t\t}\n\n\t\t// Some new data, prepend prefix.\n\t\t// TODO: We could write the prefix to r.unread buffer just once\n\t\t//       and re-use it instead of prepending every time.\n\t\tr.unread = append(r.prefix, r.unread...)\n\n\t\tif err != nil {\n\t\t\tif err == io.EOF && len(r.unread) > 0 {\n\t\t\t\t// The underlying Reader already returned EOF, but we still\n\t\t\t\t// have some unread data to send, thus clear the error.\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t\treturn n, err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/prefixer_benchmark_test.go",
    "content": "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 and sizes\nfunc BenchmarkPrefixer_Read(b *testing.B) {\n\tlineCounts := []int{10, 100, 1000}\n\tlineSizes := []int{50, 200, 500}\n\n\tfor _, lineCount := range lineCounts {\n\t\tfor _, lineSize := range lineSizes {\n\t\t\tname := fmt.Sprintf(\"lines_%d_size_%d\", lineCount, lineSize)\n\t\t\tb.Run(name, func(b *testing.B) {\n\t\t\t\t// Create input with specified number of lines\n\t\t\t\tvar input strings.Builder\n\t\t\t\tline := strings.Repeat(\"x\", lineSize) + \"\\n\"\n\t\t\t\tfor i := 0; i < lineCount; i++ {\n\t\t\t\t\tinput.WriteString(line)\n\t\t\t\t}\n\t\t\t\tinputStr := input.String()\n\n\t\t\t\tb.ResetTimer()\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\treader := strings.NewReader(inputStr)\n\t\t\t\t\tprefixer := NewPrefixer(reader, \"[project-name] \")\n\n\t\t\t\t\tbuf := make([]byte, 4096)\n\t\t\t\t\tfor {\n\t\t\t\t\t\t_, err := prefixer.Read(buf)\n\t\t\t\t\t\tif err == io.EOF {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n}\n\n// Prefixer_WriteTo: WriteTo() with varying line counts\nfunc BenchmarkPrefixer_WriteTo(b *testing.B) {\n\tlineCounts := []int{10, 100, 1000}\n\n\tfor _, lineCount := range lineCounts {\n\t\tname := fmt.Sprintf(\"lines_%d\", lineCount)\n\t\tb.Run(name, func(b *testing.B) {\n\t\t\t// Create input with specified number of lines\n\t\t\tvar input strings.Builder\n\t\t\tline := strings.Repeat(\"x\", 80) + \"\\n\"\n\t\t\tfor i := 0; i < lineCount; i++ {\n\t\t\t\tinput.WriteString(line)\n\t\t\t}\n\t\t\tinputStr := input.String()\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\treader := strings.NewReader(inputStr)\n\t\t\t\tprefixer := NewPrefixer(reader, \"[project-name] \")\n\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\t_, _ = prefixer.WriteTo(&buf)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Prefixer_PrefixLen: Impact of prefix length on performance\nfunc BenchmarkPrefixer_PrefixLen(b *testing.B) {\n\tprefixLengths := []int{10, 50, 100}\n\n\tfor _, prefixLen := range prefixLengths {\n\t\tname := fmt.Sprintf(\"prefix_%d\", prefixLen)\n\t\tb.Run(name, func(b *testing.B) {\n\t\t\t// Create input with 100 lines\n\t\t\tvar input strings.Builder\n\t\t\tline := strings.Repeat(\"x\", 80) + \"\\n\"\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tinput.WriteString(line)\n\t\t\t}\n\t\t\tinputStr := input.String()\n\t\t\tprefix := strings.Repeat(\"P\", prefixLen) + \" \"\n\n\t\t\tb.ResetTimer()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\treader := strings.NewReader(inputStr)\n\t\t\t\tprefixer := NewPrefixer(reader, prefix)\n\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\t_, _ = prefixer.WriteTo(&buf)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Prefixer_Allocs: Memory allocation count (optimization target)\nfunc BenchmarkPrefixer_Allocs(b *testing.B) {\n\t// Create input with 100 lines\n\tvar input strings.Builder\n\tline := strings.Repeat(\"x\", 80) + \"\\n\"\n\tfor i := 0; i < 100; i++ {\n\t\tinput.WriteString(line)\n\t}\n\tinputStr := input.String()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\treader := strings.NewReader(inputStr)\n\t\tprefixer := NewPrefixer(reader, \"[project-name] \")\n\n\t\tvar buf bytes.Buffer\n\t\t_, _ = prefixer.WriteTo(&buf)\n\t}\n}\n"
  },
  {
    "path": "core/print/lib.go",
    "content": "package print\n\nimport (\n\t\"bufio\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nfunc GetMaxTextWidth(text string) int {\n\tscanner := bufio.NewScanner(strings.NewReader(text))\n\tmaxWidth := 0\n\n\tfor scanner.Scan() {\n\t\tlineWidth := utf8.RuneCountInString(scanner.Text())\n\t\tif lineWidth > maxWidth {\n\t\t\tmaxWidth = lineWidth\n\t\t}\n\t}\n\n\treturn maxWidth\n}\n\nfunc GetTextDimensions(text string) (int, int) {\n\t// TODO: Seems it also counts color codes, so need to skip that\n\tscanner := bufio.NewScanner(strings.NewReader(text))\n\tmaxWidth := 0\n\theight := 0\n\n\tfor scanner.Scan() {\n\t\theight++\n\t\tlineWidth := utf8.RuneCountInString(scanner.Text())\n\t\tif lineWidth > maxWidth {\n\t\t\tmaxWidth = lineWidth\n\t\t}\n\t}\n\n\treturn maxWidth, height\n}\n"
  },
  {
    "path": "core/print/print_block.go",
    "content": "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 Formatter\nvar COLORIZE bool\nvar BLOCK dao.Block\n\nfunc PrintProjectBlocks(projects []dao.Project, colorize bool, block dao.Block, f Formatter) string {\n\tif len(projects) == 0 {\n\t\treturn \"\"\n\t}\n\n\tFORMATTER = f\n\tCOLORIZE = colorize\n\tBLOCK = block\n\n\toutput := \"\"\n\toutput += fmt.Sprintln()\n\n\tfor i, project := range projects {\n\t\toutput += printKeyValue(false, \"\", \"name\", \":\", project.Name, *block.Key, *block.Value)\n\t\toutput += printKeyValue(false, \"\", \"sync\", \":\", strconv.FormatBool(project.IsSync()), *block.Key, trueOrFalse(project.IsSync()))\n\t\tif project.Desc != \"\" {\n\t\t\toutput += printKeyValue(false, \"\", \"description\", \":\", project.Desc, *block.Key, *block.Value)\n\t\t}\n\t\tif project.RelPath != project.Name {\n\t\t\toutput += printKeyValue(false, \"\", \"path\", \":\", project.RelPath, *block.Key, *block.Value)\n\t\t}\n\n\t\toutput += printKeyValue(false, \"\", \"url\", \":\", project.URL, *block.Key, *block.Value)\n\n\t\tif len(project.RemoteList) > 0 {\n\t\t\toutput += printKeyValue(false, \"\", \"remotes\", \":\", \"\", *block.Key, *block.Value)\n\t\t\tfor _, remote := range project.RemoteList {\n\t\t\t\toutput += printKeyValue(true, \"\", remote.Name, \":\", remote.URL, *block.Key, *block.Value)\n\t\t\t}\n\t\t}\n\n\t\tif len(project.WorktreeList) > 0 {\n\t\t\toutput += printKeyValue(false, \"\", \"worktrees\", \":\", \"\", *block.Key, *block.Value)\n\t\t\tfor _, wt := range project.WorktreeList {\n\t\t\t\toutput += printKeyValue(true, \"- \", \"path\", \":\", wt.Path, *block.Key, *block.Value)\n\t\t\t\toutput += printKeyValue(true, \"  \", \"branch\", \":\", wt.Branch, *block.Key, *block.Value)\n\t\t\t}\n\t\t}\n\n\t\tif project.Branch != \"\" {\n\t\t\toutput += printKeyValue(false, \"\", \"branch\", \":\", project.Branch, *block.Key, *block.Value)\n\t\t}\n\n\t\toutput += printKeyValue(false, \"\", \"single_branch\", \":\", strconv.FormatBool(project.IsSingleBranch()), *block.Key, trueOrFalse(project.IsSingleBranch()))\n\n\t\tif len(project.Tags) > 0 {\n\t\t\toutput += printKeyValue(false, \"\", \"tags\", \":\", project.GetValue(\"tag\", 0), *block.Key, *block.Value)\n\t\t}\n\n\t\tif len(project.EnvList) > 0 {\n\t\t\toutput += printEnv(project.EnvList, block)\n\t\t}\n\n\t\tif i < len(projects)-1 {\n\t\t\toutput += \"\\n--\\n\\n\"\n\t\t}\n\t}\n\n\toutput += fmt.Sprintln()\n\n\treturn output\n}\n\nfunc PrintTaskBlock(tasks []dao.Task, colorize bool, block dao.Block, f Formatter) string {\n\tif len(tasks) == 0 {\n\t\treturn \"\"\n\t}\n\tFORMATTER = f\n\tCOLORIZE = colorize\n\tBLOCK = block\n\n\toutput := \"\"\n\toutput += fmt.Sprintln()\n\n\tfor i, task := range tasks {\n\t\toutput += printKeyValue(false, \"\", \"name\", \":\", task.Name, *block.Key, *block.Value)\n\t\toutput += printKeyValue(false, \"\", \"description\", \":\", task.Desc, *block.Key, *block.Value)\n\t\toutput += printKeyValue(false, \"\", \"theme\", \":\", task.ThemeData.Name, *block.Key, *block.Value)\n\t\toutput += printKeyValue(false, \"\", \"target\", \":\", \"\", *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"all\", \":\", strconv.FormatBool(task.TargetData.All), *block.Key, trueOrFalse(task.TargetData.All))\n\t\toutput += printKeyValue(true, \"\", \"cwd\", \":\", strconv.FormatBool(task.TargetData.Cwd), *block.Key, trueOrFalse(task.TargetData.Cwd))\n\t\toutput += printKeyValue(true, \"\", \"projects\", \":\", strings.Join(task.TargetData.Projects, \", \"), *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"paths\", \":\", strings.Join(task.TargetData.Paths, \", \"), *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"tags\", \":\", strings.Join(task.TargetData.Tags, \", \"), *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"tags_expr\", \":\", task.TargetData.TagsExpr, *block.Key, *block.Value)\n\n\t\toutput += printKeyValue(false, \"\", \"spec\", \":\", \"\", *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"output\", \":\", task.SpecData.Output, *block.Key, *block.Value)\n\t\toutput += printKeyValue(true, \"\", \"parallel\", \":\", strconv.FormatBool(task.SpecData.Parallel), *block.Key, trueOrFalse(task.SpecData.Parallel))\n\t\toutput += printKeyValue(true, \"\", \"ignore_errors\", \":\", strconv.FormatBool(task.SpecData.IgnoreErrors), *block.Key, trueOrFalse(task.SpecData.IgnoreErrors))\n\t\toutput += printKeyValue(true, \"\", \"omit_empty_rows\", \":\", strconv.FormatBool(task.SpecData.OmitEmptyRows), *block.Key, trueOrFalse(task.SpecData.OmitEmptyRows))\n\t\toutput += printKeyValue(true, \"\", \"omit_empty_columns\", \":\", strconv.FormatBool(task.SpecData.OmitEmptyColumns), *block.Key, trueOrFalse(task.SpecData.OmitEmptyColumns))\n\n\t\tif len(task.EnvList) > 0 {\n\t\t\toutput += printEnv(task.EnvList, block)\n\t\t}\n\n\t\tif task.Cmd != \"\" {\n\t\t\toutput += printKeyValue(false, \"\", \"cmd\", \":\", \"\", *block.Key, *block.Value)\n\t\t\toutput += printCmd(task.Cmd)\n\t\t}\n\n\t\tif len(task.Commands) > 0 {\n\t\t\toutput += printKeyValue(false, \"\", \"commands\", \":\", \"\", *block.Key, *block.Value)\n\t\t\tfor _, subCommand := range task.Commands {\n\t\t\t\tif subCommand.Name != \"\" {\n\t\t\t\t\tif subCommand.Desc != \"\" {\n\t\t\t\t\t\toutput += printKeyValue(true, \"- \", subCommand.Name, \":\", subCommand.Desc, *block.Key, *block.Value)\n\t\t\t\t\t} else {\n\t\t\t\t\t\toutput += printKeyValue(true, \"- \", subCommand.Name, \"\", \"\", *block.Key, *block.Value)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\toutput += printKeyValue(true, \"- \", \"cmd\", \"\", \"\", *block.Value, *block.Value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif i < len(tasks)-1 {\n\t\t\toutput += \"\\n--\\n\\n\"\n\t\t}\n\t}\n\toutput += fmt.Sprintln()\n\n\treturn output\n}\n\ntype Formatter interface {\n\tFormat(prefix string, key string, value string, separator string, keyColor *dao.ColorOptions, valueColor *dao.ColorOptions) string\n}\n\nfunc printKeyValue(\n\tpadding bool,\n\tprefix string,\n\tkey string,\n\tseparator string,\n\tvalue string,\n\tkeyStyle dao.ColorOptions,\n\tvalueStyle dao.ColorOptions,\n) string {\n\tif !COLORIZE {\n\t\tstr := fmt.Sprintf(\"%s%s%s %s\\n\", prefix, key, separator, value)\n\t\tif padding {\n\t\t\treturn fmt.Sprintf(\"%4s%s\", \" \", str)\n\t\t}\n\t\treturn str\n\t}\n\n\tstr := FORMATTER.Format(prefix, key, value, separator, &keyStyle, &valueStyle)\n\tstr += \"\\n\"\n\n\tif padding {\n\t\tstr = fmt.Sprintf(\"%4s%s\", \" \", str)\n\t}\n\n\treturn str\n}\n\nfunc printCmd(cmd string) string {\n\toutput := \"\"\n\tscanner := bufio.NewScanner(strings.NewReader(cmd))\n\tfor scanner.Scan() {\n\t\toutput += fmt.Sprintf(\"%4s%s\\n\", \" \", scanner.Text())\n\t}\n\n\treturn output\n}\n\nfunc printEnv(env []string, block dao.Block) string {\n\toutput := \"\"\n\n\toutput += printKeyValue(false, \"\", \"env\", \":\", \"\", *block.Key, *block.Value)\n\n\tfor _, env := range env {\n\t\tparts := strings.SplitN(strings.TrimSuffix(env, \"\\n\"), \"=\", 2)\n\t\toutput += printKeyValue(true, \"\", parts[0], \":\", parts[1], *block.Key, *block.Value)\n\t}\n\n\treturn output\n}\n\nfunc trueOrFalse(value bool) dao.ColorOptions {\n\tif value {\n\t\treturn *BLOCK.ValueTrue\n\t}\n\treturn *BLOCK.ValueFalse\n}\n\ntype TviewFormatter struct{}\ntype GookitFormatter struct{}\n\nfunc (t TviewFormatter) Format(\n\tprefix string,\n\tkey string,\n\tvalue string,\n\tseparator string,\n\tkeyColor *dao.ColorOptions,\n\tvalueColor *dao.ColorOptions,\n) string {\n\tsepStr := fmt.Sprintf(\"[%s:-:%s]%s\", *BLOCK.Separator.Fg, *BLOCK.Separator.Attr, separator)\n\treturn fmt.Sprintf(\n\t\t\"[%s:-:%s]%s%s[-::-]%s[-:-:-] [%s:-:%s]%s\",\n\t\t*keyColor.Fg, *keyColor.Attr, prefix, key, sepStr, *valueColor.Fg, *valueColor.Attr, value,\n\t)\n}\n\nfunc (g GookitFormatter) Format(\n\tprefix string,\n\tkey string,\n\tvalue string,\n\tseparator string,\n\tkeyColor *dao.ColorOptions,\n\tvalueColor *dao.ColorOptions,\n) string {\n\tprefixStr := dao.StyleString(prefix, *keyColor, true)\n\tkeyStr := dao.StyleString(key, *keyColor, true)\n\tsepStr := dao.StyleString(separator, *BLOCK.Separator, true)\n\tvalueStr := dao.StyleString(value, *valueColor, true)\n\n\treturn fmt.Sprintf(\"%s%s%s %s\", prefixStr, keyStr, sepStr, valueStr)\n}\n"
  },
  {
    "path": "core/print/print_table.go",
    "content": "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 interface {\n\tGetValue(string, int) string\n}\n\ntype PrintTableOptions struct {\n\tOutput           string\n\tTheme            dao.Theme\n\tTree             bool\n\tColor            bool\n\tAutoWrap         bool\n\tOmitEmptyRows    bool\n\tOmitEmptyColumns bool\n}\n\nfunc PrintTable[T Items](\n\tdata []T,\n\toptions PrintTableOptions,\n\tdefaultHeaders []string,\n\ttaskHeaders []string,\n\twriter io.Writer,\n) {\n\t// Colors not supported for markdown and html\n\tswitch options.Output {\n\tcase \"markdown\":\n\t\toptions.Color = false\n\tcase \"html\":\n\t\toptions.Color = false\n\t}\n\n\tt := CreateTable(options, defaultHeaders, taskHeaders, data, writer)\n\ttheme := options.Theme\n\n\t// Headers\n\tvar headers table.Row\n\tfor _, h := range defaultHeaders {\n\t\theaders = append(headers, dao.StyleString(h, *theme.Table.Header, options.Color))\n\t}\n\tfor _, h := range taskHeaders {\n\t\theaders = append(headers, dao.StyleString(h, *theme.Table.Header, options.Color))\n\t}\n\tt.AppendHeader(headers)\n\n\t// Rows\n\theaderNames := append(defaultHeaders, taskHeaders...)\n\tfor _, item := range data {\n\t\trow := table.Row{}\n\t\tfor i, h := range headerNames {\n\t\t\tvalue := item.GetValue(h, i)\n\t\t\tif i == 0 {\n\t\t\t\tvalue = dao.StyleString(value, *theme.Table.TitleColumn, options.Color)\n\t\t\t}\n\t\t\trow = append(row, value)\n\t\t}\n\n\t\tif options.OmitEmptyRows {\n\t\t\tempty := true\n\t\t\tfor _, v := range row[1:] {\n\t\t\t\tif v != \"\" {\n\t\t\t\t\tempty = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif empty {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tt.AppendRow(row)\n\t}\n\n\tRenderTable(t, options.Output)\n}\n"
  },
  {
    "path": "core/print/print_tree.go",
    "content": "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\"github.com/alajmo/mani/core/dao\"\n)\n\nfunc PrintTree(config *dao.Config, theme dao.Theme, listFlags *core.ListFlags, tree []dao.TreeNode) {\n\t// Style\n\tvar treeStyle list.Style\n\tswitch theme.Tree.Style {\n\tcase \"light\":\n\t\ttreeStyle = list.StyleConnectedLight\n\tcase \"bullet-flower\":\n\t\ttreeStyle = list.StyleBulletFlower\n\tcase \"bullet-square\":\n\t\ttreeStyle = list.StyleBulletSquare\n\tcase \"bullet-star\":\n\t\ttreeStyle = list.StyleBulletStar\n\tcase \"bullet-triangle\":\n\t\ttreeStyle = list.StyleBulletTriangle\n\tcase \"bold\":\n\t\ttreeStyle = list.StyleConnectedBold\n\tcase \"double\":\n\t\ttreeStyle = list.StyleConnectedDouble\n\tcase \"rounded\":\n\t\ttreeStyle = list.StyleConnectedRounded\n\tcase \"markdown\":\n\t\ttreeStyle = list.StyleMarkdown\n\tdefault:\n\t\ttreeStyle = list.StyleDefault\n\t}\n\n\t// Print\n\tl := list.NewWriter()\n\tl.SetStyle(treeStyle)\n\tprintTreeNodes(l, tree, 0)\n\tswitch listFlags.Output {\n\tcase \"markdown\":\n\t\tprintTree(l.RenderMarkdown())\n\tcase \"html\":\n\t\tprintTree(l.RenderHTML())\n\tdefault:\n\t\tprintTree(l.Render())\n\t}\n}\n\nfunc printTreeNodes(l list.Writer, tree []dao.TreeNode, depth int) {\n\tfor _, n := range tree {\n\t\tfor range depth {\n\t\t\tl.Indent()\n\t\t}\n\n\t\tl.AppendItem(n.Path)\n\t\tprintTreeNodes(l, n.Children, depth+1)\n\n\t\tfor range depth {\n\t\t\tl.UnIndent()\n\t\t}\n\t}\n}\n\nfunc printTree(content string) {\n\tfor line := range strings.SplitSeq(content, \"\\n\") {\n\t\tfmt.Printf(\"%s\\n\", line)\n\t}\n\tfmt.Println()\n}\n"
  },
  {
    "path": "core/print/table.go",
    "content": "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.org/x/term\"\n\n\t\"github.com/alajmo/mani/core/dao\"\n)\n\nfunc CreateTable[T Items](\n\toptions PrintTableOptions,\n\tdefaultHeaders []string,\n\ttaskHeaders []string,\n\tdata []T,\n\twriter io.Writer,\n) table.Writer {\n\tt := table.NewWriter()\n\n\ttheme := options.Theme\n\n\tt.SetOutputMirror(writer)\n\tt.SetStyle(FormatTable(theme))\n\n\tif options.OmitEmptyColumns {\n\t\tt.SuppressEmptyColumns()\n\t}\n\n\tcanWrap, maxColumnWidths := calcColumnWidths(defaultHeaders, taskHeaders, data)\n\n\theaders := []table.ColumnConfig{}\n\tfor i := range defaultHeaders {\n\t\theaderStyle := table.ColumnConfig{\n\t\t\tNumber: i + 1,\n\t\t}\n\t\tif options.AutoWrap && canWrap {\n\t\t\theaderStyle.WidthMaxEnforcer = text.WrapText\n\t\t\theaderStyle.WidthMax = maxColumnWidths[i]\n\t\t}\n\t\theaders = append(headers, headerStyle)\n\t}\n\n\tfor i := range taskHeaders {\n\t\toffset := len(defaultHeaders) + i\n\t\theaderStyle := table.ColumnConfig{\n\t\t\tNumber: len(defaultHeaders) + 1 + i,\n\t\t}\n\n\t\tif options.AutoWrap && canWrap {\n\t\t\theaderStyle.WidthMaxEnforcer = text.WrapText\n\t\t\theaderStyle.WidthMax = maxColumnWidths[offset]\n\t\t}\n\t\theaders = append(headers, headerStyle)\n\t}\n\n\tt.SetColumnConfigs(headers)\n\n\treturn t\n}\n\nfunc FormatTable(theme dao.Theme) table.Style {\n\treturn table.Style{\n\t\tName: theme.Name,\n\t\tBox:  theme.Table.Box,\n\n\t\tOptions: table.Options{\n\t\t\tDrawBorder:      *theme.Table.Border.Around,\n\t\t\tSeparateColumns: *theme.Table.Border.Columns,\n\t\t\tSeparateHeader:  *theme.Table.Border.Header,\n\t\t\tSeparateRows:    *theme.Table.Border.Rows,\n\t\t},\n\t}\n}\n\nfunc RenderTable(t table.Writer, output string) {\n\tswitch output {\n\tcase \"markdown\":\n\t\tt.RenderMarkdown()\n\tcase \"html\":\n\t\tt.RenderHTML()\n\tdefault:\n\t\tt.Render()\n\t}\n}\n\nfunc calcColumnWidths[T Items](\n\tdefaultHeaders []string,\n\ttaskHeaders []string,\n\tdata []T,\n) (bool, []int) {\n\theaders := append(defaultHeaders, taskHeaders...)\n\tcolumnWidths := make([]int, len(headers))\n\theaderPaddingsSum := 3*len(headers) + 1\n\n\t// Initialize column widths based on headers\n\tfor i, header := range headers {\n\t\tcolumnWidths[i] = GetMaxTextWidth(header)\n\t}\n\n\t// Update column widths based on rows\n\tfor _, row := range data {\n\t\tfor j, column := range headers {\n\t\t\tvalue := row.GetValue(column, j)\n\t\t\tcolumnWidth := GetMaxTextWidth(value)\n\t\t\tif columnWidths[j] < columnWidth {\n\t\t\t\tcolumnWidths[j] = columnWidth\n\t\t\t}\n\t\t}\n\t}\n\n\t// Calculate total width and check against terminal width\n\tcolumnSum := headerPaddingsSum\n\tfor _, width := range columnWidths {\n\t\tcolumnSum += width\n\t}\n\n\tterminalWidth, _, _ := term.GetSize(0)\n\tif columnSum < terminalWidth {\n\t\treturn false, columnWidths\n\t}\n\n\tmaxColumnWidth := (terminalWidth - headerPaddingsSum) / (len(columnWidths))\n\tvar affectedColumns []int\n\tfor i := range columnWidths {\n\t\tif columnWidths[i] > maxColumnWidth {\n\t\t\tcolumnWidths[i] = maxColumnWidth\n\t\t\taffectedColumns = append(affectedColumns, i)\n\t\t}\n\t}\n\n\tcolumnSum = headerPaddingsSum\n\tfor _, width := range columnWidths {\n\t\tcolumnSum += width\n\t}\n\n\taddToEach := (terminalWidth - columnSum) / len(affectedColumns)\n\tfor _, col := range affectedColumns {\n\t\tcolumnWidths[col] += addToEach\n\t}\n\n\treturn true, columnWidths\n}\n"
  },
  {
    "path": "core/sizedwaitgroup.go",
    "content": "// Source: https://github.com/remeh/sizedwaitgroup/blob/master/sizedwaitgroup.go\n// Author: Rémy Mathieu\n\npackage core\n\nimport (\n\t\"context\"\n\t\"math\"\n\t\"sync\"\n)\n\n// SizedWaitGroup has the same role and close to the\n// same API as the Golang sync.WaitGroup but adds a limit of\n// the amount of goroutines started concurrently.\ntype SizedWaitGroup struct {\n\tSize uint32\n\n\tcurrent chan struct{}\n\twg      sync.WaitGroup\n}\n\n// New creates a SizedWaitGroup.\n// The limit parameter is the maximum amount of\n// goroutines which can be started concurrently.\nfunc NewSizedWaitGroup(limit uint32) SizedWaitGroup {\n\tvar size uint32\n\tsize = math.MaxUint32 // 2^31 - 1\n\tif limit > 0 {\n\t\tsize = limit\n\t}\n\treturn SizedWaitGroup{\n\t\tSize: size,\n\n\t\tcurrent: make(chan struct{}, size),\n\t\twg:      sync.WaitGroup{},\n\t}\n}\n\n// Add increments the internal WaitGroup counter.\n// It can be blocking if the limit of spawned goroutines\n// has been reached. It will stop blocking when Done is\n// been called.\n//\n// See sync.WaitGroup documentation for more information.\nfunc (s *SizedWaitGroup) Add() {\n\t_ = s.AddWithContext(context.Background())\n}\n\n// AddWithContext increments the internal WaitGroup counter.\n// It can be blocking if the limit of spawned goroutines\n// has been reached. It will stop blocking when Done is\n// been called, or when the context is canceled. Returns nil on\n// success or an error if the context is canceled before the lock\n// is acquired.\n//\n// See sync.WaitGroup documentation for more information.\nfunc (s *SizedWaitGroup) AddWithContext(ctx context.Context) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase s.current <- struct{}{}:\n\t\tbreak\n\t}\n\ts.wg.Add(1)\n\treturn nil\n}\n\n// Done decrements the SizedWaitGroup counter.\n// See sync.WaitGroup documentation for more information.\nfunc (s *SizedWaitGroup) Done() {\n\t<-s.current\n\ts.wg.Done()\n}\n\n// Wait blocks until the SizedWaitGroup counter is zero.\n// See sync.WaitGroup documentation for more information.\nfunc (s *SizedWaitGroup) Wait() {\n\ts.wg.Wait()\n}\n"
  },
  {
    "path": "core/tui/components/tui_button.go",
    "content": "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/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc CreateButton(label string) *tview.Button {\n\tlabel = dao.StyleFormat(label, misc.STYLE_BUTTON.FormatStr)\n\tbutton := tview.NewButton(label)\n\tSetInactiveButtonStyle(button)\n\treturn button\n}\n\nfunc SetActiveButtonStyle(button *tview.Button) {\n\tlabel := button.GetLabel()\n\tbutton.SetLabel(dao.StyleFormat(label, misc.STYLE_BUTTON_ACTIVE.FormatStr))\n\tbutton.\n\t\tSetStyle(tcell.StyleDefault.\n\t\t\tForeground(misc.STYLE_BUTTON_ACTIVE.Fg).\n\t\t\tBackground(misc.STYLE_BUTTON_ACTIVE.Bg).\n\t\t\tAttributes(misc.STYLE_BUTTON_ACTIVE.Attr)).\n\t\tSetActivatedStyle(tcell.StyleDefault.\n\t\t\tForeground(misc.STYLE_BUTTON_ACTIVE.Fg).\n\t\t\tBackground(misc.STYLE_BUTTON_ACTIVE.Bg).\n\t\t\tAttributes(misc.STYLE_BUTTON_ACTIVE.Attr))\n}\n\nfunc SetInactiveButtonStyle(button *tview.Button) {\n\tlabel := button.GetLabel()\n\tbutton.SetLabel(dao.StyleFormat(label, misc.STYLE_BUTTON.FormatStr))\n\tbutton.\n\t\tSetStyle(tcell.StyleDefault.\n\t\t\tForeground(misc.STYLE_BUTTON.Fg).\n\t\t\tBackground(misc.STYLE_BUTTON.Bg).\n\t\t\tAttributes(misc.STYLE_BUTTON.Attr)).\n\t\tSetActivatedStyle(tcell.StyleDefault.\n\t\t\tForeground(misc.STYLE_BUTTON.Fg).\n\t\t\tBackground(misc.STYLE_BUTTON.Bg).\n\t\t\tAttributes(misc.STYLE_BUTTON.Attr))\n}\n"
  },
  {
    "path": "core/tui/components/tui_checkbox.go",
    "content": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc Checkbox(label string, checked *bool, onFocus func(), onBlur func()) *tview.Checkbox {\n\tcheckbox := tview.NewCheckbox().SetLabel(\" \" + label + \" \")\n\tcheckbox.SetChecked(*checked)\n\tcheckbox.SetCheckedStyle(misc.STYLE_ITEM_SELECTED.Style)\n\tcheckbox.SetUncheckedStyle(misc.STYLE_ITEM.Style)\n\n\tcheckbox.SetFieldTextColor(misc.STYLE_ITEM_FOCUSED.Bg)\n\tcheckbox.SetFieldBackgroundColor(misc.STYLE_ITEM.Bg)\n\tcheckbox.SetCheckedString(\"\")\n\n\tif *checked {\n\t\tcheckbox.SetLabelStyle(misc.STYLE_ITEM_SELECTED.Style)\n\t} else {\n\t\tcheckbox.SetLabelStyle(misc.STYLE_ITEM.Style)\n\t}\n\n\t// Callbacks\n\tcheckbox.SetFocusFunc(func() {\n\t\tif *checked {\n\t\t\tcheckbox.SetLabelColor(misc.STYLE_ITEM_SELECTED.Fg)\n\t\t} else {\n\t\t\tcheckbox.SetLabelColor(misc.STYLE_ITEM_FOCUSED.Fg)\n\t\t}\n\n\t\tcheckbox.SetBackgroundColor(misc.STYLE_ITEM_FOCUSED.Bg)\n\t\tonFocus()\n\t})\n\tcheckbox.SetBlurFunc(func() {\n\t\tif *checked {\n\t\t\tcheckbox.SetLabelColor(misc.STYLE_ITEM_SELECTED.Fg)\n\t\t} else {\n\t\t\tcheckbox.SetLabelColor(misc.STYLE_ITEM.Fg)\n\t\t}\n\n\t\tcheckbox.SetBackgroundColor(misc.STYLE_ITEM.Bg)\n\t\tonBlur()\n\t})\n\tcheckbox.SetChangedFunc(func(isChecked bool) {\n\t\tif isChecked {\n\t\t\tcheckbox.SetLabelStyle(misc.STYLE_ITEM_SELECTED.Style)\n\t\t} else {\n\t\t\tcheckbox.SetLabelStyle(misc.STYLE_ITEM.Style)\n\t\t}\n\t\t*checked = !*checked\n\t})\n\n\treturn checkbox\n}\n"
  },
  {
    "path": "core/tui/components/tui_filter.go",
    "content": "package components\n\nimport (\n\t\"github.com/rivo/tview\"\n\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\nfunc CreateFilter() *tview.InputField {\n\tfilter := tview.NewInputField().\n\t\tSetLabel(\"\").\n\t\tSetLabelStyle(misc.STYLE_FILTER_LABEL.Style).\n\t\tSetFieldStyle(misc.STYLE_FILTER_TEXT.Style)\n\n\treturn filter\n}\n\nfunc ShowFilter(filter *tview.InputField, text string) {\n\tfilter.SetLabel(misc.Colorize(\"Filter:\", *misc.TUITheme.FilterLabel))\n\tfilter.SetText(text)\n\tmisc.App.SetFocus(filter)\n}\n\nfunc CloseFilter(filter *tview.InputField) {\n\tfilter.SetLabel(\"\")\n\tfilter.SetText(\"\")\n}\n\nfunc InitFilter(filter *tview.InputField, text string) {\n\tif text != \"\" {\n\t\tfilter.SetLabel(\" Filter: \")\n\t\tfilter.SetText(text)\n\t} else {\n\t\tfilter.SetLabel(\"\")\n\t\tfilter.SetText(\"\")\n\t}\n}\n"
  },
  {
    "path": "core/tui/components/tui_list.go",
    "content": "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/misc\"\n)\n\ntype TList struct {\n\tRoot   *tview.Flex\n\tList   *tview.List\n\tFilter *tview.InputField\n\n\tTitle       string\n\tFilterValue *string\n\n\tIsItemSelected   func(item string) bool\n\tToggleSelectItem func(i int, itemName string)\n\tSelectAll        func()\n\tUnselectAll      func()\n\tFilterItems      func()\n}\n\nfunc (l *TList) Create() {\n\t// Init\n\tlist := tview.NewList().\n\t\tShowSecondaryText(false).\n\t\tSetHighlightFullLine(true).\n\t\tSetSelectedStyle(misc.STYLE_ITEM_FOCUSED.Style).\n\t\tSetMainTextColor(misc.STYLE_ITEM.Fg)\n\tfilter := CreateFilter()\n\n\troot := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(list, 0, 1, true).\n\t\tAddItem(filter, 1, 0, false)\n\troot.SetTitleColor(misc.STYLE_TITLE.Fg)\n\troot.SetTitleAlign(misc.STYLE_TITLE.Align).\n\t\tSetBorder(true).\n\t\tSetBorderPadding(1, 0, 1, 1)\n\n\tl.Filter = filter\n\tl.Root = root\n\tl.List = list\n\n\tif l.Title != \"\" {\n\t\tmisc.SetActive(l.Root.Box, l.Title, false)\n\t}\n\n\tl.IsItemSelected = func(item string) bool { return false }\n\tl.ToggleSelectItem = func(i int, itemName string) {}\n\tl.SelectAll = func() {}\n\tl.UnselectAll = func() {}\n\tl.FilterItems = func() {}\n\n\t// Filter\n\tl.Filter.SetChangedFunc(func(_ string) {\n\t\tl.applyFilter()\n\t\tl.FilterItems()\n\t})\n\n\tl.Filter.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tcurrentFocus := misc.App.GetFocus()\n\t\tif currentFocus == filter {\n\t\t\tswitch event.Key() {\n\t\t\tcase tcell.KeyEscape:\n\t\t\t\tl.ClearFilter()\n\t\t\t\tmisc.App.SetFocus(list)\n\t\t\t\treturn nil\n\t\t\tcase tcell.KeyEnter:\n\t\t\t\tl.applyFilter()\n\t\t\t\tmisc.App.SetFocus(list)\n\t\t\t}\n\t\t\treturn event\n\t\t}\n\t\treturn event\n\t})\n\n\t// Input\n\tl.List.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\t// Need to check filter in-case list is empty\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'f': // Filter\n\t\t\t\tShowFilter(filter, *l.FilterValue)\n\t\t\t\treturn nil\n\t\t\tcase 'F': // Remove filter\n\t\t\t\tCloseFilter(filter)\n\t\t\t\t*l.FilterValue = \"\"\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tnumItems := l.List.GetItemCount()\n\t\tif numItems == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tcurrentItemIndex := l.List.GetCurrentItem()\n\t\t_, secondaryText := l.List.GetItemText(currentItemIndex)\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEnter:\n\t\t\tl.ToggleSelectItem(currentItemIndex, secondaryText)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlD:\n\t\t\tcurrent := list.GetCurrentItem()\n\t\t\t_, _, _, height := list.GetInnerRect()\n\t\t\tnewPos := min(current+height/2, list.GetItemCount()-1)\n\t\t\tlist.SetCurrentItem(newPos)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlU:\n\t\t\tcurrent := list.GetCurrentItem()\n\t\t\t_, _, _, height := list.GetInnerRect()\n\t\t\tnewPos := max(current-height/2, 0)\n\t\t\tlist.SetCurrentItem(newPos)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlF:\n\t\t\tcurrent := list.GetCurrentItem()\n\t\t\t_, _, _, height := list.GetInnerRect()\n\t\t\tnewPos := min(current+height, list.GetItemCount()-1)\n\t\t\tlist.SetCurrentItem(newPos)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlB:\n\t\t\tcurrent := list.GetCurrentItem()\n\t\t\t_, _, _, height := list.GetInnerRect()\n\t\t\tnewPos := max(current-height, 0)\n\t\t\tlist.SetCurrentItem(newPos)\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'g': // top\n\t\t\t\tl.List.SetCurrentItem(0)\n\t\t\t\treturn nil\n\t\t\tcase 'G': // bottom\n\t\t\t\tl.List.SetCurrentItem(numItems - 1)\n\t\t\t\treturn nil\n\t\t\tcase 'j': // down\n\t\t\t\tnextItem := currentItemIndex + 1\n\t\t\t\tif nextItem < numItems {\n\t\t\t\t\tl.List.SetCurrentItem(nextItem)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'k': // up\n\t\t\t\tnextItem := currentItemIndex - 1\n\t\t\t\tif nextItem >= 0 {\n\t\t\t\t\tl.List.SetCurrentItem(nextItem)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'a': // Select all\n\t\t\t\tl.SelectAll()\n\t\t\t\treturn nil\n\t\t\tcase 'c': // Unselect all\n\t\t\t\tl.UnselectAll()\n\t\t\t\treturn nil\n\t\t\tcase ' ': // Select (Space)\n\t\t\t\tl.ToggleSelectItem(currentItemIndex, secondaryText)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\t// Events\n\tl.List.SetFocusFunc(func() {\n\t\tmisc.PreviousPane = l.List\n\t\tmisc.SetActive(l.Root.Box, l.Title, true)\n\t})\n\tl.List.SetBlurFunc(func() {\n\t\tmisc.PreviousPane = l.List\n\t\tmisc.SetActive(l.Root.Box, l.Title, false)\n\t})\n}\n\nfunc (l *TList) Update(items []string) {\n\tl.List.Clear()\n\tfor _, name := range items {\n\t\tl.List.AddItem(l.getItemText(name), name, 0, nil)\n\t}\n}\n\nfunc (l *TList) SetItemSelect(i int, item string) {\n\tif l.IsItemSelected(item) {\n\t\tvalue := misc.Colorize(item, *misc.TUITheme.ItemSelected)\n\t\tl.List.SetItemText(i, value, item)\n\t} else {\n\t\tvalue := misc.Colorize(item, *misc.TUITheme.Item)\n\t\tl.List.SetItemText(i, value, item)\n\t}\n}\n\nfunc (l *TList) ClearFilter() {\n\tCloseFilter(l.Filter)\n\t*l.FilterValue = \"\"\n}\n\nfunc (l *TList) applyFilter() {\n\t*l.FilterValue = l.Filter.GetText()\n}\n\nfunc (l *TList) getItemText(item string) string {\n\tif l.IsItemSelected(item) {\n\t\tvalue := misc.Colorize(item, *misc.TUITheme.ItemSelected)\n\t\treturn value\n\t}\n\treturn misc.PadString(item)\n}\n"
  },
  {
    "path": "core/tui/components/tui_modal.go",
    "content": "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\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\n// OpenModal Used for when a custom tview Flex is passed to a modal.\nfunc OpenModal(pageTitle string, title string, contentPane *tview.Flex, width int, height int) {\n\ttermWidth, termHeight, _ := term.GetSize(0)\n\tif width > termWidth {\n\t\twidth = termWidth - 5\n\t}\n\tif height > termHeight {\n\t\theight = termHeight - 5\n\t}\n\n\tformattedTitle := misc.ColorizeTitle(dao.StyleFormat(title, misc.STYLE_TITLE_ACTIVE.FormatStr), *misc.TUITheme.TitleActive)\n\tcontentPane.SetTitle(formattedTitle)\n\n\tbackground := tview.NewBox()\n\tcontainerFlex := tview.NewFlex().\n\t\tAddItem(contentPane, 0, 1, true)\n\tcontainerFlex.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {\n\t\tbackground.SetRect(x, y, width, height)\n\t\tbackground.Draw(screen)\n\t\tcontentPane.SetRect(x, y, width, height)\n\t\tcontentPane.Draw(screen)\n\t\treturn x, y, width, height\n\t})\n\n\tmodal := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(nil, 0, 1, false).\n\t\tAddItem(\n\t\t\ttview.NewFlex().SetDirection(tview.FlexColumn).\n\t\t\t\tAddItem(nil, 0, 1, false).\n\t\t\t\tAddItem(containerFlex, width, 1, true).\n\t\t\t\tAddItem(nil, 0, 1, false),\n\t\t\theight, 1, true,\n\t\t).\n\t\tAddItem(nil, 0, 1, false)\n\n\tmodal.SetFullScreen(true)\n\n\tEmptySearch()\n\n\tmisc.Pages.AddPage(pageTitle, modal, false, true)\n\tmisc.App.SetFocus(containerFlex)\n}\n\n// OpenTextModal Used for when text is passed to a modal.\nfunc OpenTextModal(pageTitle string, textColor string, textNoColor string, title string) {\n\twidth, height := misc.GetTexztModalSize(textNoColor)\n\ttextColor = strings.TrimSpace(textColor)\n\n\t// Text\n\tcontentPane := tview.NewTextView().\n\t\tSetText(textColor).\n\t\tSetTextAlign(tview.AlignLeft).\n\t\tSetDynamicColors(true)\n\n\t\t// Border\n\n\tformattedTitle := misc.ColorizeTitle(dao.StyleFormat(title, misc.STYLE_TITLE_ACTIVE.FormatStr), *misc.TUITheme.TitleActive)\n\tcontentPane.SetBorder(true).\n\t\tSetTitle(formattedTitle).\n\t\tSetTitleAlign(misc.STYLE_TITLE.Align).\n\t\tSetBorderColor(misc.STYLE_BORDER_FOCUS.Fg).\n\t\tSetBorderPadding(1, 1, 2, 2)\n\n\t\t// Colors\n\tcontentPane.SetBackgroundColor(misc.STYLE_DEFAULT.Bg)\n\tcontentPane.SetTextColor(misc.STYLE_DEFAULT.Fg)\n\n\t// Container\n\tmodal := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(nil, 0, 1, false).\n\t\tAddItem(\n\t\t\ttview.NewFlex().SetDirection(tview.FlexColumn).\n\t\t\t\tAddItem(nil, 0, 1, false).\n\t\t\t\tAddItem(contentPane, width, 1, true).\n\t\t\t\tAddItem(nil, 0, 1, false),\n\t\t\theight, 1, true,\n\t\t).\n\t\tAddItem(nil, 0, 1, false)\n\n\tmodal.SetFullScreen(true).SetBackgroundColor(misc.STYLE_DEFAULT.Fg)\n\n\tEmptySearch()\n\n\tmisc.Pages.AddPage(pageTitle, modal, false, true)\n\tmisc.App.SetFocus(contentPane)\n}\n\nfunc CloseModal() {\n\t// Need to store before removing, because otherwise\n\t// the first pane gets focused and so misc.PreviousPage\n\t// doesn't work as intended.\n\tpreviousPane := misc.PreviousPane\n\tfrontPageName, _ := misc.Pages.GetFrontPage()\n\tmisc.Pages.RemovePage(frontPageName)\n\tmisc.App.SetFocus(previousPane)\n}\n\nfunc IsModalOpen() bool {\n\tfrontPageName, _ := misc.Pages.GetFrontPage()\n\treturn strings.Contains(frontPageName, \"-modal\")\n}\n"
  },
  {
    "path": "core/tui/components/tui_output.go",
    "content": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc CreateOutputView(title string) (*tview.TextView, *misc.ThreadSafeWriter) {\n\tstreamView := CreateText(title)\n\tansiWriter := misc.NewThreadSafeWriter(streamView)\n\n\treturn streamView, ansiWriter\n}\n"
  },
  {
    "path": "core/tui/components/tui_search.go",
    "content": "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 CreateSearch() *tview.InputField {\n\tsearch := tview.NewInputField().\n\t\tSetLabel(\"\").\n\t\tSetLabelStyle(misc.STYLE_SEARCH_LABEL.Style).\n\t\tSetFieldStyle(misc.STYLE_SEARCH_TEXT.Style)\n\treturn search\n}\n\nfunc ShowSearch() {\n\tmisc.Search.SetLabel(misc.Colorize(\"Search:\", *misc.TUITheme.SearchLabel))\n\tmisc.Search.SetText(\"\")\n\tmisc.App.SetFocus(misc.Search)\n}\n\nfunc EmptySearch() {\n\tmisc.Search.SetLabel(\"\")\n\tmisc.Search.SetText(\"\")\n}\n\nfunc SearchInTable(table *tview.Table, query string, lastFoundRow, lastFoundCol *int, direction int) {\n\tquery = strings.ToLower(query)\n\trowCount := table.GetRowCount()\n\tcolCount := table.GetColumnCount()\n\tstartRow := *lastFoundRow\n\n\tif startRow == -1 {\n\t\tstartRow = 0\n\t} else {\n\t\tstartRow += direction\n\t}\n\n\tsearchRow := startRow\n\tfor range rowCount {\n\t\tif searchRow < 0 {\n\t\t\tsearchRow = rowCount - 1\n\t\t} else if searchRow >= rowCount {\n\t\t\tsearchRow = 0\n\t\t}\n\n\t\tfor col := range colCount {\n\t\t\tif cell := table.GetCell(searchRow, col); cell != nil {\n\t\t\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(cell.Text)), query) {\n\t\t\t\t\ttable.Select(searchRow, col)\n\t\t\t\t\t*lastFoundRow, *lastFoundCol = searchRow, col\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tsearchRow += direction\n\t}\n\n\t*lastFoundRow, *lastFoundCol = -1, -1\n}\n\nfunc SearchInTree(tree *TTree, query string, lastFoundIndex *int, direction int) {\n\tquery = strings.ToLower(query)\n\titemCount := len(tree.List)\n\tstartIndex := *lastFoundIndex\n\n\tif startIndex == -1 {\n\t\tstartIndex = 0\n\t} else {\n\t\tstartIndex += direction\n\t}\n\n\tsearchIndex := startIndex\n\tfor range itemCount {\n\t\tif searchIndex < 0 {\n\t\t\tsearchIndex = itemCount - 1\n\t\t} else if searchIndex >= itemCount {\n\t\t\tsearchIndex = 0\n\t\t}\n\n\t\tname := strings.ToLower(tree.List[searchIndex].DisplayName)\n\t\tif strings.Contains(name, query) {\n\t\t\ttree.Tree.SetCurrentNode(tree.List[searchIndex].TreeNode)\n\t\t\t*lastFoundIndex = searchIndex\n\t\t\treturn\n\t\t}\n\n\t\tsearchIndex += direction\n\t}\n\n\t*lastFoundIndex = -1\n\n}\n\nfunc SearchInList(list *tview.List, query string, lastFoundIndex *int, direction int) {\n\tquery = strings.ToLower(query)\n\titemCount := list.GetItemCount()\n\tstartIndex := *lastFoundIndex\n\n\tif startIndex == -1 {\n\t\tstartIndex = 0\n\t} else {\n\t\tstartIndex += direction\n\t}\n\n\tsearchIndex := startIndex\n\tfor range itemCount {\n\t\tif searchIndex < 0 {\n\t\t\tsearchIndex = itemCount - 1\n\t\t} else if searchIndex >= itemCount {\n\t\t\tsearchIndex = 0\n\t\t}\n\n\t\tmainText, secondaryText := list.GetItemText(searchIndex)\n\t\tif strings.Contains(strings.ToLower(mainText), query) ||\n\t\t\tstrings.Contains(strings.ToLower(secondaryText), query) {\n\t\t\tlist.SetCurrentItem(searchIndex)\n\t\t\t*lastFoundIndex = searchIndex\n\t\t\treturn\n\t\t}\n\n\t\tsearchIndex += direction\n\t}\n\n\t*lastFoundIndex = -1\n}\n"
  },
  {
    "path": "core/tui/components/tui_table.go",
    "content": "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/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\ntype TTable struct {\n\tRoot   *tview.Flex\n\tTable  *tview.Table\n\tFilter *tview.InputField\n\n\tTitle         string\n\tFilterValue   *string\n\tShowHeaders   bool\n\tToggleEnabled bool\n\n\tIsRowSelected   func(name string) bool\n\tToggleSelectRow func(name string)\n\tSelectAll       func()\n\tUnselectAll     func()\n\tFilterRows      func()\n\tDescribeRow     func(name string)\n\tEditRow         func(name string)\n}\n\nfunc (t *TTable) Create() {\n\t// Init\n\ttable := tview.NewTable()\n\ttable.SetFixed(1, 1)             // Fixed header + name column\n\ttable.Select(1, 0)               // Select first row\n\ttable.SetEvaluateAllRows(true)   // Avoid resizing of headers when scrolling\n\ttable.SetSelectable(true, false) // Only rows can be selected\n\ttable.SetBackgroundColor(misc.STYLE_ITEM.Bg)\n\tfilter := CreateFilter()\n\n\troot := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(table, 0, 1, true).\n\t\tAddItem(filter, 1, 0, false)\n\n\troot.SetTitleColor(misc.STYLE_TITLE.Fg)\n\troot.SetTitleAlign(misc.STYLE_TITLE.Align).\n\t\tSetBorder(true).\n\t\tSetBorderPadding(1, 0, 1, 1)\n\n\tt.Table = table\n\tt.Filter = filter\n\tt.Root = root\n\n\tif t.Title != \"\" {\n\t\tmisc.SetActive(t.Root.Box, t.Title, false)\n\t}\n\n\t// Methods\n\n\tt.IsRowSelected = func(name string) bool { return false }\n\tt.ToggleSelectRow = func(name string) {}\n\tt.SelectAll = func() {}\n\tt.UnselectAll = func() {}\n\tt.FilterRows = func() {}\n\tt.DescribeRow = func(_ string) {}\n\tt.EditRow = func(projectName string) {}\n\n\t// Filter\n\tt.Filter.SetChangedFunc(func(_ string) {\n\t\tt.applyFilter()\n\t\tt.FilterRows()\n\t})\n\n\tt.Filter.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tcurrentFocus := misc.App.GetFocus()\n\t\tif currentFocus == filter {\n\t\t\tswitch event.Key() {\n\t\t\tcase tcell.KeyEscape:\n\t\t\t\tt.ClearFilter()\n\t\t\t\tt.FilterRows()\n\t\t\t\tmisc.App.SetFocus(table)\n\t\t\t\treturn nil\n\t\t\tcase tcell.KeyEnter:\n\t\t\t\tt.applyFilter()\n\t\t\t\tt.FilterRows()\n\t\t\t\tmisc.App.SetFocus(table)\n\t\t\t}\n\t\t\treturn event\n\t\t}\n\t\treturn event\n\t})\n\n\t// Input\n\tt.Table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEnter:\n\t\t\tif t.ToggleEnabled {\n\t\t\t\trow, _ := table.GetSelection()\n\t\t\t\tname := strings.TrimSpace(table.GetCell(row, 0).Text)\n\t\t\t\tt.ToggleSelectRow(name)\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlD:\n\t\t\trow, _ := table.GetSelection()\n\t\t\t_, _, _, height := table.GetInnerRect()\n\t\t\tnewRow := min(row+height/2, table.GetRowCount()-1)\n\t\t\ttable.Select(newRow, 0)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlU:\n\t\t\trow, _ := table.GetSelection()\n\t\t\t_, _, _, height := table.GetInnerRect()\n\t\t\tnewRow := max(row-height/2, 1)\n\t\t\ttable.Select(newRow, 0)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlF:\n\t\t\trow, _ := table.GetSelection()\n\t\t\t_, _, _, height := table.GetInnerRect()\n\t\t\tnewRow := min(row+height, table.GetRowCount()-1)\n\t\t\tif newRow == 0 {\n\t\t\t\tnewRow = 1 // Skip header\n\t\t\t}\n\t\t\ttable.Select(newRow, 0)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlB:\n\t\t\trow, _ := table.GetSelection()\n\t\t\t_, _, _, height := table.GetInnerRect()\n\t\t\tnewRow := max(row-height, 1)\n\t\t\ttable.Select(newRow, 0)\n\t\t\treturn nil\n\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase ' ': // Toggle item (space)\n\t\t\t\tif t.ToggleEnabled {\n\t\t\t\t\trow, _ := table.GetSelection()\n\t\t\t\t\tname := strings.TrimSpace(table.GetCell(row, 0).Text)\n\t\t\t\t\tt.ToggleSelectRow(name)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'a': // Select all\n\t\t\t\tif t.ToggleEnabled {\n\t\t\t\t\tt.SelectAll()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'c': // Unselect all\n\t\t\t\tif t.ToggleEnabled {\n\t\t\t\t\tt.UnselectAll()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'f': // Filter rows\n\t\t\t\tShowFilter(filter, *t.FilterValue)\n\t\t\t\treturn nil\n\t\t\tcase 'F': // Remove filter\n\t\t\t\tCloseFilter(filter)\n\t\t\t\t*t.FilterValue = \"\"\n\t\t\t\treturn nil\n\t\t\tcase 'o': // Edit in editor\n\t\t\t\trow, _ := t.Table.GetSelection()\n\t\t\t\tname := strings.TrimSpace(t.Table.GetCell(row, 0).Text)\n\t\t\t\tt.EditRow(name)\n\t\t\t\treturn nil\n\t\t\tcase 'd': // Open description modal\n\t\t\t\trow, _ := t.Table.GetSelection()\n\t\t\t\tname := strings.TrimSpace(t.Table.GetCell(row, 0).Text)\n\t\t\t\tt.DescribeRow(name)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn event\n\t})\n\n\t// Events\n\tt.Table.SetSelectionChangedFunc(func(row, column int) {\n\t\tt.UpdateRowStyle()\n\t})\n\n\tt.Table.SetFocusFunc(func() {\n\t\tInitFilter(t.Filter, *t.FilterValue)\n\n\t\tmisc.PreviousPane = t.Table\n\t\tmisc.SetActive(t.Root.Box, t.Title, true)\n\t})\n\n\tt.Table.SetBlurFunc(func() {\n\t\tmisc.PreviousPane = t.Table\n\t\tmisc.SetActive(t.Root.Box, t.Title, false)\n\t})\n}\n\nfunc (t *TTable) CreateTableHeader(header string) *tview.TableCell {\n\t// TODO: format\n\treturn tview.NewTableCell(dao.StyleFormat(header, misc.STYLE_TABLE_HEADER.FormatStr)).\n\t\tSetTextColor(misc.STYLE_TABLE_HEADER.Fg).\n\t\tSetAttributes(misc.STYLE_TABLE_HEADER.Attr).\n\t\tSetAlign(misc.STYLE_TABLE_HEADER.Align).\n\t\tSetSelectable(false)\n}\n\nfunc (t *TTable) Update(headers []string, rows [][]string) {\n\tt.Table.Clear()\n\n\t// Add headers and updates style\n\tfor col, header := range headers {\n\t\tif t.ShowHeaders {\n\t\t\tt.Table.SetCell(0, col, t.CreateTableHeader(misc.PadString(header)))\n\t\t} else {\n\t\t\tt.Table.SetCell(0, col, t.CreateTableHeader(\"\"))\n\t\t}\n\t}\n\n\t// Add rows and updates style\n\tfor i := range rows {\n\t\tfor j := range rows[i] {\n\t\t\tname := misc.PadString(rows[i][j])\n\t\t\tcell := tview.NewTableCell(name)\n\t\t\tt.Table.SetCell(i+1, j, cell)\n\t\t\tt.SetRowSelect(i + 1)\n\t\t}\n\t}\n}\n\nfunc (t *TTable) UpdateRowStyle() {\n\tfor row := 1; row < t.Table.GetRowCount(); row++ {\n\t\tt.SetRowSelect(row)\n\t}\n}\n\nfunc (t *TTable) ToggleSelectCurrentRow(name string) {\n\tindex := -1\n\tfor row := 1; row < t.Table.GetRowCount(); row++ {\n\t\tcell := strings.TrimSpace(t.Table.GetCell(row, 0).Text)\n\t\tif cell == name {\n\t\t\tindex = row\n\t\t\tbreak\n\t\t}\n\t}\n\tt.SetRowSelect(index)\n}\n\nfunc (t *TTable) SetRowSelect(row int) {\n\t// Ignore header row\n\tfocusedRow, _ := t.Table.GetSelection()\n\tif focusedRow == 0 {\n\t\treturn\n\t}\n\n\tname := strings.TrimSpace(t.Table.GetCell(row, 0).Text)\n\tisSelected := t.IsRowSelected(name)\n\tisFocused := row == focusedRow\n\n\tstyle := tcell.StyleDefault\n\tif isFocused && isSelected {\n\t\tstyle = style.\n\t\t\tForeground(misc.STYLE_ITEM_SELECTED.Fg).\n\t\t\tBackground(misc.STYLE_ITEM_FOCUSED.Bg).\n\t\t\tAttributes(misc.STYLE_ITEM_SELECTED.Attr)\n\t} else if isFocused {\n\t\tstyle = style.\n\t\t\tForeground(misc.STYLE_ITEM_FOCUSED.Fg).\n\t\t\tBackground(misc.STYLE_ITEM_FOCUSED.Bg).\n\t\t\tAttributes(misc.STYLE_ITEM_FOCUSED.Attr)\n\t} else if isSelected {\n\t\tstyle = style.\n\t\t\tForeground(misc.STYLE_ITEM_SELECTED.Fg).\n\t\t\tBackground(misc.STYLE_ITEM_SELECTED.Bg).\n\t\t\tAttributes(misc.STYLE_ITEM_SELECTED.Attr)\n\t} else {\n\t\tstyle = style.\n\t\t\tForeground(misc.STYLE_ITEM.Fg).\n\t\t\tBackground(misc.STYLE_ITEM.Bg).\n\t\t\tAttributes(misc.STYLE_ITEM.Attr)\n\t}\n\n\t// Apply styles to all cells in the row\n\tfor col := range t.Table.GetColumnCount() {\n\t\tcell := t.Table.GetCell(row, col)\n\t\tcell.SetStyle(style)\n\t\tcell.SetSelectedStyle(style)\n\t}\n}\n\nfunc (t *TTable) ClearFilter() {\n\tCloseFilter(t.Filter)\n\t*t.FilterValue = \"\"\n}\n\nfunc (t *TTable) applyFilter() {\n\t*t.FilterValue = t.Filter.GetText()\n}\n"
  },
  {
    "path": "core/tui/components/tui_text.go",
    "content": "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/tview\"\n)\n\nfunc CreateText(title string) *tview.TextView {\n\ttextview := tview.NewTextView()\n\ttextview.SetBorder(true)\n\ttextview.SetBorderPadding(0, 0, 2, 1)\n\ttextview.SetDynamicColors(true)\n\ttextview.SetWrap(false)\n\ttextTitle := title\n\n\tif textTitle != \"\" {\n\t\ttextTitle = misc.Colorize(title, *misc.TUITheme.Title)\n\t\ttextview.SetTitle(textTitle)\n\t}\n\n\ttextview.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\t_, _, _, height := textview.GetInnerRect()\n\t\trow, _ := textview.GetScrollOffset()\n\t\tswitch {\n\t\tcase event.Key() == tcell.KeyCtrlD || event.Rune() == 'd':\n\t\t\ttextview.ScrollTo(row+height/2, 0)\n\t\t\treturn nil\n\t\tcase event.Key() == tcell.KeyCtrlU || event.Rune() == 'u':\n\t\t\ttextview.ScrollTo(row-height/2, 0)\n\t\t\treturn nil\n\t\tcase event.Key() == tcell.KeyCtrlF || event.Rune() == 'f':\n\t\t\ttextview.ScrollTo(row+height, 0)\n\t\t\treturn nil\n\t\tcase event.Key() == tcell.KeyCtrlB || event.Rune() == 'b':\n\t\t\ttextview.ScrollTo(row-height, 0)\n\t\t\treturn nil\n\t\t}\n\t\treturn event\n\t})\n\n\t// Callbacks\n\ttextview.SetFocusFunc(func() {\n\t\tmisc.PreviousPane = textview\n\t\tmisc.SetActive(textview.Box, title, true)\n\t})\n\ttextview.SetBlurFunc(func() {\n\t\tmisc.PreviousPane = textview\n\t\tmisc.SetActive(textview.Box, title, false)\n\t})\n\n\treturn textview\n}\n"
  },
  {
    "path": "core/tui/components/tui_textarea.go",
    "content": "package components\n\nimport (\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc CreateTextArea(title string) *tview.TextArea {\n\ttextarea := tview.NewTextArea()\n\ttextarea.SetBorder(true)\n\ttextarea.SetWrap(true)\n\ttextarea.SetTitle(title)\n\ttextarea.SetTitleAlign(misc.STYLE_TITLE.Align)\n\ttextarea.SetTitleColor(misc.STYLE_DEFAULT.Fg)\n\ttextarea.SetBackgroundColor(misc.STYLE_DEFAULT.Bg)\n\ttextarea.SetBorderPadding(0, 0, 1, 1)\n\n\t// Callbacks\n\ttextarea.SetFocusFunc(func() {\n\t\tmisc.PreviousPane = textarea\n\t\tmisc.SetActive(textarea.Box, title, true)\n\t})\n\ttextarea.SetBlurFunc(func() {\n\t\tmisc.PreviousPane = textarea\n\t\tmisc.SetActive(textarea.Box, title, false)\n\t})\n\n\treturn textarea\n}\n"
  },
  {
    "path": "core/tui/components/tui_toggle_text.go",
    "content": "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/tview\"\n)\n\ntype TToggleText struct {\n\tValue    *string\n\tOption1  string\n\tOption2  string\n\tLabel1   string\n\tLabel2   string\n\tData1    string\n\tData2    string\n\tTextView *tview.TextView\n}\n\nfunc (t *TToggleText) Create() {\n\ttextview := tview.NewTextView()\n\ttextview.SetTitle(\"\")\n\tif *t.Value == t.Option1 {\n\t\ttextview.SetText(t.Label1)\n\t} else {\n\t\ttextview.SetText(t.Label2)\n\t}\n\ttextview.SetSize(1, 18)\n\ttextview.SetBorder(false)\n\ttextview.SetBorderPadding(0, 0, 0, 0)\n\ttextview.SetBackgroundColor(misc.STYLE_ITEM.Bg)\n\n\ttoggleOutput := func() {\n\n\t\tif *t.Value == t.Option1 {\n\t\t\t*t.Value = t.Option2\n\t\t\ttextview.SetText(t.Label2)\n\t\t} else {\n\t\t\t*t.Value = t.Option1\n\t\t\ttextview.SetText(t.Label1)\n\t\t}\n\t}\n\n\ttextview.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEnter:\n\t\t\ttoggleOutput()\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase ' ': // space\n\t\t\t\ttoggleOutput()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\ttextview.SetFocusFunc(func() {\n\t\ttextview.SetTextColor(misc.STYLE_ITEM_FOCUSED.Fg)\n\t\ttextview.SetBackgroundColor(misc.STYLE_ITEM_FOCUSED.Bg)\n\t})\n\n\ttextview.SetBlurFunc(func() {\n\t\ttextview.SetTextColor(misc.STYLE_ITEM.Fg)\n\t\ttextview.SetBackgroundColor(misc.STYLE_ITEM.Bg)\n\t})\n\n\tt.TextView = textview\n}\n"
  },
  {
    "path": "core/tui/components/tui_tree.go",
    "content": "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/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype TTree struct {\n\tTree     *tview.TreeView\n\tRoot     *tview.Flex\n\tRootNode *tview.TreeNode\n\tFilter   *tview.InputField\n\n\tList          []*TNode\n\tTitle         string\n\tRootTitle     string\n\tFilterValue   *string\n\tSelectEnabled bool\n\n\tIsNodeSelected   func(name string) bool\n\tToggleSelectNode func(name string)\n\tSelectAll        func()\n\tUnselectAll      func()\n\tFilterNodes      func()\n\tDescribeNode     func(name string)\n\tEditNode         func(name string)\n}\n\ntype TNode struct {\n\tID          string // The reference\n\tDisplayName string // What is shown\n\tType        string\n\n\tTreeNode *tview.TreeNode\n\tChildren *[]TNode\n}\n\nfunc (t *TTree) Create() {\n\ttitle := misc.Colorize(t.RootTitle, *misc.TUITheme.Item)\n\trootNode := tview.NewTreeNode(title)\n\trootNode.SetColor(misc.STYLE_DEFAULT.Fg)\n\trootNode.SetSelectable(false)\n\n\tt.IsNodeSelected = func(name string) bool { return false }\n\tt.ToggleSelectNode = func(name string) {}\n\tt.SelectAll = func() {}\n\tt.UnselectAll = func() {}\n\tt.FilterNodes = func() {}\n\tt.DescribeNode = func(name string) {}\n\tt.EditNode = func(name string) {}\n\n\ttree := tview.NewTreeView().\n\t\tSetRoot(rootNode).\n\t\tSetCurrentNode(rootNode)\n\ttree.SetGraphics(true)\n\n\tfilter := CreateFilter()\n\n\troot := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(tree, 0, 1, true).\n\t\tAddItem(filter, 1, 0, false)\n\troot.SetTitleAlign(misc.STYLE_TITLE.Align).\n\t\tSetBorder(true).\n\t\tSetBorderPadding(0, 0, 1, 1)\n\n\tt.Root = root\n\tt.Filter = filter\n\tt.RootNode = rootNode\n\tt.Tree = tree\n\n\tif t.Title != \"\" {\n\t\ttitle := misc.Colorize(t.Title, *misc.TUITheme.Title)\n\t\tt.Root.SetTitle(title)\n\t}\n\n\t// Methods\n\tt.IsNodeSelected = func(name string) bool { return false }\n\n\t// Filter\n\tt.Filter.SetChangedFunc(func(_ string) {\n\t\tt.applyFilter()\n\t\tt.FilterNodes()\n\t})\n\n\tt.Filter.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tcurrentFocus := misc.App.GetFocus()\n\t\tif currentFocus == filter {\n\t\t\tswitch event.Key() {\n\t\t\tcase tcell.KeyEscape:\n\t\t\t\tt.ClearFilter()\n\t\t\t\tt.FilterNodes()\n\t\t\t\tmisc.App.SetFocus(tree)\n\t\t\t\treturn nil\n\t\t\tcase tcell.KeyEnter:\n\t\t\t\tt.applyFilter()\n\t\t\t\tt.FilterNodes()\n\t\t\t\tmisc.App.SetFocus(tree)\n\t\t\t}\n\t\t\treturn event\n\t\t}\n\t\treturn event\n\t})\n\n\t// Input\n\tt.Tree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEnter:\n\t\t\tif t.SelectEnabled {\n\t\t\t\tnode := t.Tree.GetCurrentNode()\n\t\t\t\tname := node.GetReference().(string)\n\t\t\t\tt.ToggleSelectNode(name)\n\t\t\t}\n\t\tcase tcell.KeyCtrlD:\n\t\t\tcurrent := t.Tree.GetCurrentNode()\n\t\t\t_, _, _, height := t.Tree.GetInnerRect()\n\t\t\tvisibleNodes := t.getVisibleNodes()\n\t\t\tcurrentIndex := t.findNodeIndex(visibleNodes, current)\n\t\t\tnewIndex := min(currentIndex+height/2, len(visibleNodes)-1)\n\t\t\tif newIndex > 0 && newIndex < len(visibleNodes) {\n\t\t\t\tt.Tree.SetCurrentNode(visibleNodes[newIndex])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlU:\n\t\t\tcurrent := t.Tree.GetCurrentNode()\n\t\t\t_, _, _, height := t.Tree.GetInnerRect()\n\t\t\tvisibleNodes := t.getVisibleNodes()\n\t\t\tcurrentIndex := t.findNodeIndex(visibleNodes, current)\n\t\t\tnewIndex := max(currentIndex-height/2, 0)\n\t\t\tif newIndex >= 0 && newIndex < len(visibleNodes) {\n\t\t\t\tt.Tree.SetCurrentNode(visibleNodes[newIndex])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlF:\n\t\t\tcurrent := t.Tree.GetCurrentNode()\n\t\t\t_, _, _, height := t.Tree.GetInnerRect()\n\t\t\tvisibleNodes := t.getVisibleNodes()\n\t\t\tcurrentIndex := t.findNodeIndex(visibleNodes, current)\n\t\t\tnewIndex := min(currentIndex+height, len(visibleNodes)-1)\n\t\t\tif newIndex > 0 && newIndex < len(visibleNodes) {\n\t\t\t\tt.Tree.SetCurrentNode(visibleNodes[newIndex])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlB:\n\t\t\tcurrent := t.Tree.GetCurrentNode()\n\t\t\t_, _, _, height := t.Tree.GetInnerRect()\n\t\t\tvisibleNodes := t.getVisibleNodes()\n\t\t\tcurrentIndex := t.findNodeIndex(visibleNodes, current)\n\t\t\tnewIndex := max(currentIndex-height, 0)\n\t\t\tif newIndex >= 0 && newIndex < len(visibleNodes) {\n\t\t\t\tt.Tree.SetCurrentNode(visibleNodes[newIndex])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase ' ': // Toggle item (space)\n\t\t\t\tif t.SelectEnabled {\n\t\t\t\t\tnode := t.Tree.GetCurrentNode()\n\t\t\t\t\tname := node.GetReference().(string)\n\t\t\t\t\tt.ToggleSelectNode(name)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'a': // Select all\n\t\t\t\tif t.SelectEnabled {\n\t\t\t\t\tt.SelectAll()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'c': // Unselect all all\n\t\t\t\tif t.SelectEnabled {\n\t\t\t\t\tt.UnselectAll()\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'f': // Filter rows\n\t\t\t\tShowFilter(filter, *t.FilterValue)\n\t\t\t\treturn nil\n\t\t\tcase 'F': // Remove filter\n\t\t\t\tCloseFilter(filter)\n\t\t\t\t*t.FilterValue = \"\"\n\t\t\t\treturn nil\n\t\t\tcase 'o': // Edit in editor\n\t\t\t\titem := tree.GetCurrentNode()\n\t\t\t\tname := item.GetReference().(string)\n\t\t\t\tt.EditNode(name)\n\t\t\t\treturn nil\n\t\t\tcase 'd': // Open description modal\n\t\t\t\titem := tree.GetCurrentNode()\n\t\t\t\tname := item.GetReference().(string)\n\t\t\t\tt.DescribeNode(name)\n\t\t\t\treturn nil\n\t\t\tcase 'g': // Top\n\t\t\t\ttree.SetCurrentNode(rootNode)\n\t\t\t\tmisc.App.QueueEvent(tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone))\n\t\t\t\treturn nil\n\t\t\tcase 'G': // Bottom\n\t\t\t\tchildren := rootNode.GetChildren()\n\t\t\t\tlast := children[len(children)-1]\n\t\t\t\tname := last.GetReference().(string)\n\n\t\t\t\tif name == \"\" {\n\t\t\t\t\tchildren = last.GetChildren()\n\t\t\t\t\tlast = children[len(children)-1]\n\t\t\t\t}\n\n\t\t\t\ttree.SetCurrentNode(last)\n\t\t\t\tmisc.App.QueueEvent(tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn event\n\t})\n\n\t// Events\n\tvar previousNode *tview.TreeNode\n\tvar previousColor tcell.Color\n\ttree.SetChangedFunc(func(node *tview.TreeNode) {\n\t\tif previousNode != nil {\n\t\t\tpreviousNode.SetColor(previousColor)\n\t\t}\n\t\tif node != nil {\n\t\t\tpreviousColor = node.GetColor()\n\t\t\tpreviousNode = node\n\t\t\tnode.SetColor(misc.STYLE_ITEM_FOCUSED.Bg)\n\t\t}\n\t})\n\tt.Tree.SetFocusFunc(func() {\n\t\tInitFilter(t.Filter, *t.FilterValue)\n\n\t\tmisc.PreviousPane = t.Tree\n\t\tmisc.PreviousModel = t\n\t\tmisc.SetActive(t.Root.Box, t.Title, true)\n\t})\n\tt.Tree.SetBlurFunc(func() {\n\t\tmisc.PreviousPane = t.Tree\n\t\tmisc.PreviousModel = t\n\t\tmisc.SetActive(t.Root.Box, t.Title, false)\n\t})\n}\n\nfunc (t *TTree) UpdateProjects(paths []dao.TNode) {\n\tt.RootNode.ClearChildren()\n\n\tvar itree []dao.TreeNode\n\tfor i := range paths {\n\t\titree = dao.AddToTree(itree, paths[i])\n\t}\n\n\tt.List = []*TNode{}\n\tfor i := range itree {\n\t\tt.BuildProjectTree(t.RootNode, itree[i])\n\t}\n}\n\nfunc (t *TTree) UpdateProjectsStyle() {\n\tfor _, node := range t.List {\n\t\tt.setNodeSelect(node)\n\t}\n}\n\nfunc (t *TTree) BuildProjectTree(node *tview.TreeNode, tnode dao.TreeNode) {\n\t// Project\n\tif len(tnode.Children) == 0 {\n\t\tpathName := misc.Colorize(tnode.Path, *misc.TUITheme.Item)\n\t\tchildTreeNode := tview.NewTreeNode(pathName).\n\t\t\tSetReference(tnode.ProjectName).\n\t\t\tSetSelectable(true)\n\n\t\tnode.AddChild(childTreeNode)\n\t\tchildListNode := &TNode{\n\t\t\tID:          tnode.ProjectName,\n\t\t\tDisplayName: tnode.Path,\n\t\t\tType:        \"project\",\n\t\t\tTreeNode:    childTreeNode,\n\t\t\tChildren:    &[]TNode{},\n\t\t}\n\t\tt.List = append(t.List, childListNode)\n\t\treturn\n\t}\n\n\t// Directory\n\tpathName := misc.Colorize(tnode.Path, *misc.TUITheme.ItemDir)\n\tparentTreeNode := tview.NewTreeNode(pathName).\n\t\tSetReference(\"\").\n\t\tSetSelectable(false)\n\tnode.AddChild(parentTreeNode)\n\n\tparentListNode := &TNode{\n\t\tID:          tnode.ProjectName,\n\t\tDisplayName: tnode.Path,\n\t\tTreeNode:    parentTreeNode,\n\t\tType:        \"directory\",\n\t}\n\tt.List = append(t.List, parentListNode)\n\tfor i := range tnode.Children {\n\t\tt.BuildProjectTree(parentTreeNode, tnode.Children[i])\n\t}\n}\n\nfunc (t *TTree) UpdateTasks(nodes []TNode) {\n\tt.RootNode.ClearChildren()\n\tt.List = []*TNode{}\n\n\tfor _, parentNode := range nodes {\n\t\t// Parent\n\t\tdisplayName := misc.Colorize(parentNode.DisplayName, *misc.TUITheme.Item)\n\t\tparentTreeNode := tview.NewTreeNode(displayName).\n\t\t\tSetReference(parentNode.ID).\n\t\t\tSetSelectable(true)\n\t\tt.RootNode.AddChild(parentTreeNode)\n\n\t\tparentListNode := &TNode{\n\t\t\tDisplayName: parentNode.DisplayName,\n\t\t\tID:          parentNode.DisplayName,\n\t\t\tType:        parentNode.Type,\n\t\t\tTreeNode:    parentTreeNode,\n\t\t\tChildren:    &[]TNode{},\n\t\t}\n\t\tt.List = append(t.List, parentListNode)\n\n\t\t// Children\n\t\tfor _, childNode := range *parentNode.Children {\n\t\t\tdisplayName := misc.Colorize(parentNode.DisplayName, *misc.TUITheme.Item)\n\t\t\tchildTreeNode := tview.\n\t\t\t\tNewTreeNode(displayName).\n\t\t\t\tSetSelectable(false)\n\t\t\tparentTreeNode.AddChild(childTreeNode)\n\n\t\t\tlistChildNode := &TNode{\n\t\t\t\tDisplayName: childNode.DisplayName,\n\t\t\t\tType:        childNode.Type,\n\t\t\t\tTreeNode:    childTreeNode,\n\t\t\t\tChildren:    &[]TNode{},\n\t\t\t}\n\t\t\t*parentListNode.Children = append(*parentListNode.Children, *listChildNode)\n\t\t}\n\t}\n}\n\nfunc (t *TTree) UpdateTasksStyle() {\n\tfor _, node := range t.List {\n\t\tif t.IsNodeSelected(node.DisplayName) {\n\t\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.ItemSelected)\n\t\t\tnode.TreeNode.SetText(displayName)\n\t\t\tfor _, child := range *node.Children {\n\t\t\t\tdisplayName := misc.Colorize(child.DisplayName, *misc.TUITheme.ItemSelected)\n\t\t\t\tchild.TreeNode.SetText(displayName)\n\t\t\t}\n\t\t} else {\n\t\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.Item)\n\t\t\tnode.TreeNode.SetText(displayName)\n\t\t\tfor _, child := range *node.Children {\n\t\t\t\tif child.Type == \"task-ref\" {\n\t\t\t\t\tdisplayName := misc.Colorize(child.DisplayName, *misc.TUITheme.ItemRef)\n\t\t\t\t\tchild.TreeNode.SetText(displayName)\n\t\t\t\t} else {\n\t\t\t\t\tdisplayName := misc.Colorize(child.DisplayName, *misc.TUITheme.Item)\n\t\t\t\t\tchild.TreeNode.SetText(displayName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *TTree) ToggleSelectCurrentNode(id string) {\n\tfor i := range len(t.List) {\n\t\tnode := t.List[i]\n\t\tif node.ID == id {\n\t\t\tt.setNodeSelect(node)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (t *TTree) setNodeSelect(node *TNode) {\n\tif t.IsNodeSelected(node.ID) {\n\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.ItemSelected)\n\t\tnode.TreeNode.SetText(displayName)\n\t\tfor _, childNode := range *node.Children {\n\t\t\tdisplayName := misc.Colorize(childNode.DisplayName, *misc.TUITheme.ItemSelected)\n\t\t\tchildNode.TreeNode.SetText(displayName)\n\t\t}\n\t\treturn\n\t}\n\n\tswitch node.Type {\n\tcase \"directory\":\n\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.ItemDir)\n\t\tnode.TreeNode.SetText(displayName)\n\tcase \"task\":\n\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.Item)\n\t\tnode.TreeNode.SetText(displayName)\n\t\tfor _, childNode := range *node.Children {\n\t\t\tif childNode.Type == \"task-ref\" {\n\t\t\t\tdisplayName := misc.Colorize(childNode.DisplayName, *misc.TUITheme.ItemRef)\n\t\t\t\tchildNode.TreeNode.SetText(displayName)\n\t\t\t} else {\n\t\t\t\tdisplayName := misc.Colorize(childNode.DisplayName, *misc.TUITheme.Item)\n\t\t\t\tchildNode.TreeNode.SetText(displayName)\n\t\t\t}\n\t\t}\n\tcase \"project\":\n\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.Item)\n\t\tnode.TreeNode.SetText(displayName)\n\tdefault:\n\t\tdisplayName := misc.Colorize(node.DisplayName, *misc.TUITheme.Item)\n\t\tnode.TreeNode.SetText(displayName)\n\t}\n}\n\nfunc (t *TTree) FocusFirst() {\n\tt.Tree.SetCurrentNode(t.RootNode)\n}\n\nfunc (t *TTree) FocusLast() {\n\tchildren := t.RootNode.GetChildren()\n\tlast := children[len(children)-1]\n\tname := last.GetReference().(string)\n\n\tif name == \"\" {\n\t\tchildren = last.GetChildren()\n\t\tlast = children[len(children)-1]\n\t}\n\n\tt.Tree.SetCurrentNode(last)\n}\n\nfunc (t *TTree) ClearFilter() {\n\tCloseFilter(t.Filter)\n\t*t.FilterValue = \"\"\n}\n\nfunc (t *TTree) applyFilter() {\n\t*t.FilterValue = t.Filter.GetText()\n}\n\nfunc (t *TTree) getVisibleNodes() []*tview.TreeNode {\n\tvar nodes []*tview.TreeNode\n\tvar walk func(*tview.TreeNode)\n\twalk = func(node *tview.TreeNode) {\n\t\tif node == nil {\n\t\t\treturn\n\t\t}\n\t\tref := node.GetReference()\n\t\tif ref != nil && ref.(string) != \"\" {\n\t\t\tnodes = append(nodes, node)\n\t\t}\n\t\tif node.IsExpanded() {\n\t\t\tfor _, child := range node.GetChildren() {\n\t\t\t\twalk(child)\n\t\t\t}\n\t\t}\n\t}\n\twalk(t.RootNode)\n\treturn nodes\n}\n\nfunc (t *TTree) findNodeIndex(nodes []*tview.TreeNode, target *tview.TreeNode) int {\n\tfor i, node := range nodes {\n\t\tif node == target {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "core/tui/misc/tui_event.go",
    "content": "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\ntype EventEmitter struct {\n\tlisteners map[string][]EventListener\n\tmu        sync.RWMutex\n}\n\nfunc NewEventEmitter() *EventEmitter {\n\treturn &EventEmitter{\n\t\tlisteners: make(map[string][]EventListener),\n\t}\n}\n\nfunc (ee *EventEmitter) Subscribe(eventName string, listener EventListener) {\n\tee.mu.Lock()\n\tdefer ee.mu.Unlock()\n\tee.listeners[eventName] = append(ee.listeners[eventName], listener)\n}\n\nfunc (ee *EventEmitter) Publish(event Event) {\n\tee.mu.RLock()\n\tdefer ee.mu.RUnlock()\n\tif listeners, ok := ee.listeners[event.Name]; ok {\n\t\tfor _, listener := range listeners {\n\t\t\tgo listener(event)\n\t\t}\n\t}\n}\n\nfunc (ee *EventEmitter) PublishAndWait(event Event) {\n\tee.mu.RLock()\n\tlisteners := ee.listeners[event.Name]\n\tee.mu.RUnlock()\n\n\tvar wg sync.WaitGroup\n\tfor _, listener := range listeners {\n\t\twg.Add(1)\n\t\tgo func(l EventListener) {\n\t\t\tdefer wg.Done()\n\t\t\tl(event)\n\t\t}(listener)\n\t}\n\twg.Wait()\n}\n"
  },
  {
    "path": "core/tui/misc/tui_focus.go",
    "content": "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.Primitive\n\tBox       *tview.Box\n}\n\nfunc FocusNext(elements []*TItem) *tview.Primitive {\n\tif len(elements) == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentFocus := App.GetFocus()\n\tnextIndex := -1\n\tvar nextFocusItem TItem\n\tfor i, element := range elements {\n\t\tif element.Primitive == currentFocus {\n\t\t\tnextIndex = (i + 1) % len(elements)\n\t\t\tnextFocusItem = *elements[nextIndex]\n\t\t}\n\t\telement.Box.SetBorderColor(STYLE_BORDER.Fg)\n\t}\n\n\t// In-case no nextIndex is found, use the previous page as base to find nextFocusItem\n\tif nextIndex < 0 {\n\t\tfor i, element := range elements {\n\t\t\tif element.Primitive == PreviousPane {\n\t\t\t\tnextIndex = (i + 1) % len(elements)\n\t\t\t\tnextFocusItem = *elements[nextIndex]\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback to first element if still not found\n\tif nextIndex < 0 {\n\t\tnextFocusItem = *elements[0]\n\t}\n\n\t// Set border and focus\n\tnextFocusItem.Box.SetBorderColor(STYLE_BORDER_FOCUS.Fg)\n\tApp.SetFocus(nextFocusItem.Primitive)\n\n\treturn &nextFocusItem.Primitive\n}\n\nfunc FocusPrevious(elements []*TItem) *tview.Primitive {\n\tif len(elements) == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentFocus := App.GetFocus()\n\tprevIndex := -1\n\tvar nextFocusItem TItem\n\tfor i, element := range elements {\n\t\tif element.Primitive == currentFocus {\n\t\t\tprevIndex = (i - 1 + len(elements)) % len(elements)\n\t\t\tnextFocusItem = *elements[prevIndex]\n\t\t}\n\t\telement.Box.SetBorderColor(STYLE_BORDER.Fg)\n\t}\n\n\t// In-case no prevIndex is found, use the previous page as base to find nextFocusItem\n\tif prevIndex < 0 {\n\t\tfor i, element := range elements {\n\t\t\tif element.Primitive == PreviousPane {\n\t\t\t\tprevIndex = (i - 1 + len(elements)) % len(elements)\n\t\t\t\tnextFocusItem = *elements[prevIndex]\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback to first element if still not found\n\tif prevIndex < 0 {\n\t\tnextFocusItem = *elements[0]\n\t}\n\n\t// Set border and focus\n\tnextFocusItem.Box.SetBorderColor(STYLE_BORDER_FOCUS.Fg)\n\tApp.SetFocus(nextFocusItem.Primitive)\n\n\treturn &nextFocusItem.Primitive\n}\n\nfunc FocusPage(event *tcell.EventKey, focusable []*TItem) {\n\ti := int(event.Rune()-'0') - 1\n\tif i < len(focusable) {\n\t\tApp.SetFocus(focusable[i].Box)\n\t}\n}\n\nfunc FocusPreviousPage() {\n\tApp.SetFocus(PreviousPane)\n}\n\nfunc GetTUIItem(primitive tview.Primitive, box *tview.Box) *TItem {\n\treturn &TItem{\n\t\tPrimitive: primitive,\n\t\tBox:       box,\n\t}\n}\n"
  },
  {
    "path": "core/tui/misc/tui_global.go",
    "content": "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 ThemeName *string\nvar TUITheme *dao.TUI\nvar BlockTheme *dao.Block\n\nvar App *tview.Application\nvar Pages *tview.Pages\nvar MainPage *tview.Pages\nvar PreviousPane tview.Primitive\n\nvar PreviousModel interface{}\n\n// Nav\nvar ProjectBtn *tview.Button\nvar TaskBtn *tview.Button\nvar RunBtn *tview.Button\nvar ExecBtn *tview.Button\nvar HelpBtn *tview.Button\n\nvar ProjectsLastFocus *tview.Primitive\nvar TasksLastFocus *tview.Primitive\nvar RunLastFocus *tview.Primitive\nvar ExecLastFocus *tview.Primitive\n\n// Misc\nvar HelpModal *tview.Modal\nvar Search *tview.InputField\n"
  },
  {
    "path": "core/tui/misc/tui_theme.go",
    "content": "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/rivo/tview\"\n)\n\n// Default\nvar STYLE_DEFAULT StyleOption\n\n// Border\nvar STYLE_BORDER StyleOption\nvar STYLE_BORDER_FOCUS StyleOption\n\n// Title\nvar STYLE_TITLE StyleOption\nvar STYLE_TITLE_ACTIVE StyleOption\n\n// Table Header\nvar STYLE_TABLE_HEADER StyleOption\n\n// Item\nvar STYLE_ITEM StyleOption\nvar STYLE_ITEM_FOCUSED StyleOption\nvar STYLE_ITEM_SELECTED StyleOption\n\n// Button\nvar STYLE_BUTTON StyleOption\nvar STYLE_BUTTON_ACTIVE StyleOption\n\n// Search\nvar STYLE_SEARCH_LABEL StyleOption\nvar STYLE_SEARCH_TEXT StyleOption\n\n// Filter\nvar STYLE_FILTER_LABEL StyleOption\nvar STYLE_FILTER_TEXT StyleOption\n\n// Shortcut\nvar STYLE_SHORTCUT_LABEL StyleOption\nvar STYLE_SHORTCUT_TEXT StyleOption\n\ntype StyleOption struct {\n\tFg    tcell.Color\n\tBg    tcell.Color\n\tAttr  tcell.AttrMask\n\tAlign int\n\n\tFgStr     string\n\tBgStr     string\n\tAttrStr   string\n\tAlignStr  string\n\tFormatStr string\n\n\tStyle tcell.Style\n}\n\nfunc LoadStyles(tui *dao.TUI) {\n\t// Default\n\tSTYLE_DEFAULT = initStyle(tui.Default)\n\n\t// Border\n\tSTYLE_BORDER = initStyle(tui.Border)\n\tSTYLE_BORDER_FOCUS = initStyle(tui.BorderFocus)\n\n\t// Title\n\tSTYLE_TITLE = initStyle(tui.Title)\n\tSTYLE_TITLE_ACTIVE = initStyle(tui.TitleActive)\n\n\t// Table Header\n\tSTYLE_TABLE_HEADER = initStyle(tui.TableHeader)\n\n\t// Item\n\tSTYLE_ITEM = initStyle(tui.Item)\n\tSTYLE_ITEM_FOCUSED = initStyle(tui.ItemFocused)\n\tSTYLE_ITEM_SELECTED = initStyle(tui.ItemSelected)\n\n\t// Button\n\tSTYLE_BUTTON = initStyle(tui.Button)\n\tSTYLE_BUTTON_ACTIVE = initStyle(tui.ButtonActive)\n\n\t// Search\n\tSTYLE_SEARCH_LABEL = initStyle(tui.SearchLabel)\n\tSTYLE_SEARCH_TEXT = initStyle(tui.SearchText)\n\n\t// Filter\n\tSTYLE_FILTER_LABEL = initStyle(tui.FilterLabel)\n\tSTYLE_FILTER_TEXT = initStyle(tui.FilterText)\n\n\t// Shortcut\n\tSTYLE_SHORTCUT_LABEL = initStyle(tui.ShortcutLabel)\n\tSTYLE_SHORTCUT_TEXT = initStyle(tui.ShortcutText)\n}\n\nfunc initStyle(opts *dao.ColorOptions) StyleOption {\n\tfg := tcell.GetColor(*opts.Fg)\n\tbg := tcell.GetColor(*opts.Bg)\n\tattr := getAttr(*opts.Attr)\n\n\tstyle := StyleOption{\n\t\tFg:    fg,\n\t\tBg:    bg,\n\t\tAttr:  attr,\n\t\tAlign: getAlign(opts.Align),\n\n\t\tFgStr:     *opts.Fg,\n\t\tBgStr:     *opts.Bg,\n\t\tAttrStr:   *opts.Attr,\n\t\tFormatStr: *opts.Format,\n\n\t\tStyle: tcell.StyleDefault.Foreground(fg).Background(bg).Attributes(attr),\n\t}\n\n\treturn style\n}\n\nfunc Colorize(value string, opts dao.ColorOptions) string {\n\treturn \" [-:-:-]\" + fmt.Sprintf(\"[%s:%s:%s]%s\", *opts.Fg, *opts.Bg, *opts.Attr, value) + \"[-:-:-] \"\n}\n\nfunc ColorizeTitle(value string, opts dao.ColorOptions) string {\n\treturn \" [-:-:-]\" + fmt.Sprintf(\"[%s:%s:%s] %s \", *opts.Fg, *opts.Bg, *opts.Attr, value) + \"[-:-:-] \"\n}\n\nfunc getAttr(attrStr string) tcell.AttrMask {\n\tvar attr tcell.AttrMask\n\tswitch attrStr {\n\tcase \"b\", \"bold\":\n\t\tattr = tcell.AttrBold\n\tcase \"d\", \"dim\":\n\t\tattr = tcell.AttrDim\n\tcase \"i\", \"italic\":\n\t\tattr = tcell.AttrItalic\n\tcase \"u\", \"underline\":\n\t\tattr = tcell.AttrUnderline\n\tdefault:\n\t\tattr = tcell.AttrNone\n\t}\n\n\treturn attr\n}\n\nfunc getAlign(alignStr *string) int {\n\tif alignStr == nil {\n\t\treturn tview.AlignLeft\n\t}\n\n\tlowerAlign := strings.ToLower(*alignStr)\n\tswitch lowerAlign {\n\tcase \"l\", \"left\":\n\t\treturn tview.AlignLeft\n\tcase \"r\", \"right\":\n\t\treturn tview.AlignRight\n\tcase \"b\", \"bottom\":\n\t\treturn tview.AlignBottom\n\tcase \"t\", \"top\":\n\t\treturn tview.AlignTop\n\tcase \"c\", \"center\":\n\t\treturn tview.AlignCenter\n\t}\n\n\treturn tview.AlignLeft\n}\n\nfunc PadString(name string) string {\n\treturn \" \" + strings.TrimSpace(name) + \" \"\n}\n"
  },
  {
    "path": "core/tui/misc/tui_utils.go",
    "content": "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\t\"golang.org/x/term\"\n)\n\nfunc SetActive(box *tview.Box, title string, active bool) {\n\tif active {\n\t\tbox.SetBorderColor(STYLE_BORDER_FOCUS.Fg)\n\t\tbox.SetTitleAlign(STYLE_TITLE_ACTIVE.Align)\n\t\ttitle = dao.StyleFormat(title, STYLE_TITLE_ACTIVE.FormatStr)\n\t\tif title != \"\" {\n\t\t\ttitle = ColorizeTitle(title, *TUITheme.TitleActive)\n\t\t\tbox.SetTitle(title)\n\t\t}\n\t} else {\n\t\tbox.SetBorderColor(STYLE_BORDER.Fg)\n\t\tbox.SetTitleAlign(STYLE_TITLE.Align)\n\t\ttitle = dao.StyleFormat(title, STYLE_TITLE.FormatStr)\n\t\tif title != \"\" {\n\t\t\ttitle = ColorizeTitle(title, *TUITheme.Title)\n\t\t\tbox.SetTitle(title)\n\t\t}\n\t}\n}\n\nfunc GetTexztModalSize(text string) (int, int) {\n\ttermWidth, termHeight, _ := term.GetSize(0)\n\ttextWidth, textHeight := print.GetTextDimensions(text)\n\n\twidth := textWidth\n\theight := textHeight\n\n\t// Min Width - sane minimum default width\n\tif width < 45 {\n\t\twidth = 45\n\t}\n\n\t// Max Width - can't be wider than terminal width\n\tif width > termWidth {\n\t\twidth = termWidth - 20 // Add some margin left/right\n\t\theight = height + 4    // Since text wraps, add some margin to height\n\t}\n\n\t// Max Height - can't be taller than terminal width\n\tif height > termHeight {\n\t\theight = termHeight - 5 // Add some margin top/bottom\n\t}\n\n\twidth += 8  // Add some padding\n\theight += 2 // Add some padding\n\n\treturn width, height\n}\n"
  },
  {
    "path": "core/tui/misc/tui_writer.go",
    "content": "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 it thread-safe\ntype ThreadSafeWriter struct {\n\twriter io.Writer\n\tmutex  sync.Mutex\n}\n\n// NewThreadSafeWriter creates a new thread-safe writer for tview\nfunc NewThreadSafeWriter(view *tview.TextView) *ThreadSafeWriter {\n\treturn &ThreadSafeWriter{\n\t\twriter: tview.ANSIWriter(view),\n\t}\n}\n\n// Write implements io.Writer interface in a thread-safe manner\nfunc (w *ThreadSafeWriter) Write(p []byte) (n int, err error) {\n\tw.mutex.Lock()\n\tdefer w.mutex.Unlock()\n\treturn w.writer.Write(p)\n}\n"
  },
  {
    "path": "core/tui/pages/tui_exec.go",
    "content": "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.com/alajmo/mani/core\"\n\t\"github.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/exec\"\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/mani/core/tui/views\"\n)\n\ntype TExecPage struct {\n\tfocusable []*misc.TItem\n}\n\nfunc CreateExecPage(\n\tprojects []dao.Project,\n\tprojectTags []string,\n\tprojectPaths []string,\n) *tview.Flex {\n\te := &TExecPage{}\n\tprojectData := views.CreateProjectsData(\n\t\tprojects,\n\t\tprojectTags,\n\t\tprojectPaths,\n\t\t[]string{\"Project\", \"Description\", \"Tag\"},\n\t\t2,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t)\n\n\t// Views\n\tstreamView, ansiWriter := components.CreateOutputView(\"[2] Output\")\n\tprojectInfo := views.CreateRunInfoVIew()\n\tcmdInfo := views.CreateExecInfoView()\n\tcmdView := components.CreateTextArea(\"[1] Command\")\n\tspec := views.CreateSpecView()\n\n\t// Pages\n\texecPage := e.createSelectPage(projectData, projectInfo, cmdView)\n\toutputPage := e.createOutputPage(cmdInfo, cmdView, streamView)\n\tpages := tview.NewPages().\n\t\tAddPage(\"exec-projects\", execPage, true, true).\n\t\tAddPage(\"exec-run\", outputPage, true, false)\n\n\t// Main page\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(pages, 0, 1, true).\n\t\tAddItem(misc.Search, 1, 0, false)\n\n\t// Focus\n\te.focusable = e.updateSelectFocusable(*projectData, cmdView)\n\tmisc.ExecLastFocus = &e.focusable[0].Primitive\n\n\t// Shortcuts\n\tpage.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlS:\n\t\t\te.focusable = e.switchView(pages, projectData, cmdView, streamView)\n\t\t\tmisc.App.SetFocus(e.focusable[0].Primitive)\n\t\t\tmisc.ExecLastFocus = &e.focusable[0].Primitive\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlR:\n\t\t\te.focusable = e.switchBeforeRun(pages, e.focusable, cmdView, streamView)\n\t\t\tmisc.App.SetFocus(e.focusable[0].Primitive)\n\t\t\tmisc.ExecLastFocus = &e.focusable[0].Primitive\n\t\t\tcmd := cmdView.GetText()\n\t\t\te.runCmd(streamView, cmd, projectData.Projects, projectData.ProjectsSelected, spec, ansiWriter)\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyTab:\n\t\t\tnextPrimitive := misc.FocusNext(e.focusable)\n\t\t\tmisc.ExecLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyBacktab:\n\t\t\tnextPrimitive := misc.FocusPrevious(e.focusable)\n\t\t\tmisc.ExecLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlO:\n\t\t\tcomponents.OpenModal(\"spec-modal\", \"Options\", spec.View, 30, 11)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlX:\n\t\t\tstreamView.Clear()\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tif _, ok := misc.App.GetFocus().(*tview.TextArea); ok {\n\t\t\t\treturn event\n\t\t\t}\n\n\t\t\tname, _ := pages.GetFrontPage()\n\t\t\tif name == \"exec-projects\" {\n\t\t\t\tswitch event.Rune() {\n\t\t\t\tcase 'C': // Clear filters\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_filter\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_selections\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_filter\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_selections\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.Publish(misc.Event{Name: \"filter_projects\", Data: \"\"})\n\t\t\t\t\treturn nil\n\t\t\t\tcase '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\t\t\t\tmisc.FocusPage(event, e.focusable)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif name == \"exec-run\" {\n\t\t\t\tswitch event.Rune() {\n\t\t\t\tcase '1':\n\t\t\t\t\tmisc.App.SetFocus(cmdView)\n\t\t\t\t\treturn nil\n\t\t\t\tcase '2':\n\t\t\t\t\tmisc.App.SetFocus(streamView)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\treturn page\n}\n\nfunc (e *TExecPage) createSelectPage(\n\tprojectData *views.TProject,\n\tinfoPane *tview.TextView,\n\texecInput *tview.TextArea,\n) *tview.Flex {\n\tisProjectTable := projectData.ProjectStyle == \"project-table\"\n\tprojectPages := tview.NewPages().\n\t\tAddPage(\"project-table\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTableView.Root, 0, 1, true), true, isProjectTable).\n\t\tAddPage(\"project-tree\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTreeView.Root, 0, 8, false), true, !isProjectTable)\n\tprojectPages.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlE:\n\t\t\tif projectData.ProjectStyle == \"project-table\" {\n\t\t\t\tprojectData.ProjectStyle = \"project-tree\"\n\t\t\t} else {\n\t\t\t\tprojectData.ProjectStyle = \"project-table\"\n\t\t\t}\n\t\t\tprojectPages.SwitchToPage(projectData.ProjectStyle)\n\t\t\te.focusable = e.updateSelectFocusable(*projectData, execInput)\n\t\t\tmisc.App.SetFocus(e.focusable[1].Primitive)\n\t\t\tmisc.RunLastFocus = &e.focusable[1].Primitive\n\t\t\treturn nil\n\t\t}\n\t\treturn event\n\t})\n\n\t// Always show both panes, even when empty\n\tprojectData.ContextView = tview.NewFlex().SetDirection(tview.FlexRow)\n\tprojectData.ContextView.AddItem(projectData.TagView.Root, 0, 1, false)\n\tprojectData.ContextView.AddItem(projectData.PathView.Root, 0, 1, false)\n\n\tbottom := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(projectPages, 0, 1, false).\n\t\tAddItem(projectData.ContextView, 30, 1, false)\n\n\t// Container\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(execInput, 8, 0, true).\n\t\tAddItem(bottom, 0, 1, false).\n\t\tAddItem(infoPane, 1, 0, false)\n\n\treturn page\n}\n\nfunc (e *TExecPage) createOutputPage(\n\tinfoPane *tview.TextView,\n\texecInput *tview.TextArea,\n\tstreamView *tview.TextView,\n) *tview.Flex {\n\toutputView := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(execInput, 8, 0, true).\n\t\tAddItem(streamView, 0, 1, false).\n\t\tAddItem(infoPane, 1, 0, false)\n\n\treturn outputView\n}\n\nfunc (e *TExecPage) updateSelectFocusable(\n\tprojectData views.TProject,\n\texecInput *tview.TextArea,\n) []*misc.TItem {\n\tfocusable := []*misc.TItem{\n\t\tmisc.GetTUIItem(\n\t\t\texecInput,\n\t\t\texecInput.Box,\n\t\t),\n\t}\n\n\t// Project\n\tif projectData.ProjectStyle == \"project-table\" {\n\t\tfocusable = append(\n\t\t\tfocusable, misc.GetTUIItem(\n\t\t\t\tprojectData.ProjectTableView.Table,\n\t\t\t\tprojectData.ProjectTableView.Table.Box,\n\t\t\t))\n\t} else {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\tprojectData.ProjectTreeView.Tree,\n\t\t\t\tprojectData.ProjectTreeView.Tree.Box,\n\t\t\t))\n\t}\n\n\t// Always include Tags and Paths panes (even when empty)\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tprojectData.TagView.List,\n\t\t\tprojectData.TagView.List.Box,\n\t\t))\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tprojectData.PathView.List,\n\t\t\tprojectData.PathView.List.Box,\n\t\t))\n\n\treturn focusable\n}\n\nfunc (e *TExecPage) updateStreamFocusable(\n\texecInput *tview.TextArea,\n\tstreamView *tview.TextView,\n) []*misc.TItem {\n\tfocusable := []*misc.TItem{\n\t\tmisc.GetTUIItem(execInput, execInput.Box),\n\t\tmisc.GetTUIItem(streamView, streamView.Box),\n\t}\n\treturn focusable\n}\n\nfunc (e *TExecPage) switchView(\n\tpages *tview.Pages,\n\tdata *views.TProject,\n\tcmdView *tview.TextArea,\n\tstreamView *tview.TextView,\n) []*misc.TItem {\n\tname, _ := pages.GetFrontPage()\n\tvar focusable []*misc.TItem\n\tif name == \"exec-run\" {\n\t\tpages.SwitchToPage(\"exec-projects\")\n\t\tfocusable = e.updateSelectFocusable(*data, cmdView)\n\t} else {\n\t\tpages.SwitchToPage(\"exec-run\")\n\t\tfocusable = e.updateStreamFocusable(cmdView, streamView)\n\t}\n\n\treturn focusable\n}\n\nfunc (e *TExecPage) switchBeforeRun(\n\tpages *tview.Pages,\n\tfocusable []*misc.TItem,\n\tcmdView *tview.TextArea,\n\tstreamView *tview.TextView,\n) []*misc.TItem {\n\tname, _ := pages.GetFrontPage()\n\tif name == \"exec-projects\" {\n\t\tpages.SwitchToPage(\"exec-run\")\n\t\tfocusable = e.updateStreamFocusable(cmdView, streamView)\n\t}\n\n\treturn focusable\n}\n\nfunc (e *TExecPage) runCmd(\n\tstreamView *tview.TextView,\n\tcmd string,\n\tprojects []dao.Project,\n\tprojectsSelectMap map[string]bool,\n\tspec *views.TSpec,\n\tansiWriter *misc.ThreadSafeWriter,\n) {\n\t// Check if any projects selected\n\tselectedProjects := []dao.Project{}\n\tfor _, project := range projects {\n\t\tif projectsSelectMap[project.Name] {\n\t\t\tselectedProjects = append(selectedProjects, project)\n\t\t}\n\t}\n\tif len(selectedProjects) < 1 {\n\t\treturn\n\t}\n\n\t// Task\n\ttask := dao.Task{Name: \"\", Cmd: cmd}\n\ttaskErrors := make([]dao.ResourceErrors[dao.Task], 1)\n\ttask.ParseTask(*misc.Config, &taskErrors[0])\n\ttask.SpecData.Output = spec.Output\n\ttask.SpecData.Parallel = spec.Parallel\n\ttask.SpecData.IgnoreErrors = spec.IgnoreErrors\n\ttask.SpecData.IgnoreNonExisting = spec.IgnoreNonExisting\n\ttask.SpecData.OmitEmptyRows = spec.OmitEmptyRows\n\ttask.SpecData.OmitEmptyColumns = spec.OmitEmptyColumns\n\n\t// Flags\n\trunFlags := core.RunFlags{\n\t\tSilent: true,\n\n\t\t// Target\n\t\tCwd:      false,\n\t\tAll:      false,\n\t\tTagsExpr: \"\",\n\n\t\tTarget: \"default\",\n\t\tSpec:   \"default\",\n\n\t\tOutput:            spec.Output,\n\t\tParallel:          spec.Parallel,\n\t\tIgnoreErrors:      spec.IgnoreErrors,\n\t\tIgnoreNonExisting: spec.IgnoreNonExisting,\n\t\tOmitEmptyRows:     spec.OmitEmptyRows,\n\t\tOmitEmptyColumns:  spec.OmitEmptyColumns,\n\t}\n\tsetRunFlags := core.SetRunFlags{\n\t\tParallel:          spec.Parallel,\n\t\tAll:               true,\n\t\tCwd:               true,\n\t\tIgnoreErrors:      true,\n\t\tIgnoreNonExisting: true,\n\t\tOmitEmptyRows:     true,\n\t\tOmitEmptyColumns:  true,\n\t}\n\n\t// Preprocess\n\tvar tasks []dao.Task\n\tfor range selectedProjects {\n\t\tt := dao.Task{}\n\t\terr := copier.Copy(&t, &task)\n\t\tcore.CheckIfError(err)\n\t\ttasks = append(tasks, t)\n\t}\n\n\t// Run\n\ttarget := exec.Exec{Projects: selectedProjects, Tasks: tasks, Config: *misc.Config}\n\n\tif spec.ClearBeforeRun {\n\t\tstreamView.Clear()\n\t}\n\n\tif spec.Output == \"table\" {\n\t\ttext := streamView.GetText(false)\n\t\tstreamView.SetText(text + \"\\n\")\n\t} else {\n\t\ttext := streamView.GetText(false)\n\t\tstreamView.SetText(text + \"\\n\")\n\t}\n\n\terr := target.RunTUI([]string{}, &runFlags, &setRunFlags, spec.Output, ansiWriter, ansiWriter)\n\tcore.CheckIfError(err)\n\n\tstreamView.ScrollToEnd()\n}\n"
  },
  {
    "path": "core/tui/pages/tui_project.go",
    "content": "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/mani/core/tui/views\"\n\n\t\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype TProjectPage struct {\n\tfocusable []*misc.TItem\n}\n\nfunc CreateProjectsPage(\n\tprojects []dao.Project,\n\tprojectTags []string,\n\tprojectPaths []string,\n) *tview.Flex {\n\tp := &TProjectPage{}\n\n\t// Data\n\tprojectData := views.CreateProjectsData(\n\t\tprojects,\n\t\tprojectTags,\n\t\tprojectPaths,\n\t\t[]string{\"Project\", \"Description\", \"Tag\", \"Url\", \"Path\"},\n\t\t1,\n\t\ttrue,\n\t\ttrue,\n\t\tfalse,\n\t\ttrue,\n\t\ttrue,\n\t)\n\n\t// Views\n\tprojectInfo := views.CreateProjectInfoView()\n\tprojectTablePage := p.createProjectPage(projectData)\n\n\t// Context page (always show both panes, even when empty)\n\tprojectData.ContextView = tview.NewFlex().SetDirection(tview.FlexRow)\n\tprojectData.ContextView.AddItem(projectData.TagView.Root, 0, 1, true)\n\tprojectData.ContextView.AddItem(projectData.PathView.Root, 0, 1, true)\n\n\t// Page\n\tprojectData.Page = tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(\n\t\t\ttview.NewFlex().SetDirection(tview.FlexColumn).\n\t\t\t\tAddItem(projectTablePage, 0, 1, true).\n\t\t\t\tAddItem(projectData.ContextView, 30, 1, false),\n\t\t\t0, 1, true).\n\t\tAddItem(projectInfo, 1, 0, false).\n\t\tAddItem(misc.Search, 1, 0, false)\n\n\t// Focusable\n\tp.focusable = p.updateProjectFocusable(projectData)\n\tmisc.ProjectsLastFocus = &p.focusable[0].Primitive\n\n\t// Shortcuts\n\tprojectData.Page.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tif misc.App.GetFocus() == misc.Search {\n\t\t\treturn event\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyTab:\n\t\t\tnextPrimitive := misc.FocusNext(p.focusable)\n\t\t\tmisc.ProjectsLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyBacktab:\n\t\t\tnextPrimitive := misc.FocusPrevious(p.focusable)\n\t\t\tmisc.ProjectsLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'C': // Clear filters\n\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_filter\", Data: \"\"})\n\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_selections\", Data: \"\"})\n\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_filter\", Data: \"\"})\n\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_selections\", Data: \"\"})\n\t\t\t\tprojectData.Emitter.Publish(misc.Event{Name: \"filter_projects\", Data: \"\"})\n\t\t\t\treturn nil\n\t\t\tcase '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\t\t\tmisc.FocusPage(event, p.focusable)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn event\n\t})\n\n\treturn projectData.Page\n}\n\nfunc (p *TProjectPage) createProjectPage(projectData *views.TProject) *tview.Flex {\n\tisTable := projectData.ProjectStyle == \"project-table\"\n\n\tpages := tview.NewPages().\n\t\tAddPage(\"project-table\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTableView.Root, 0, 1, true), true, isTable).\n\t\tAddPage(\"project-tree\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTreeView.Root, 0, 8, false), true, !isTable)\n\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(pages, 0, 1, true)\n\n\tpage.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tif misc.App.GetFocus() == misc.Search {\n\t\t\treturn event\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlE:\n\t\t\tif projectData.ProjectStyle == \"project-table\" {\n\t\t\t\tprojectData.ProjectStyle = \"project-tree\"\n\t\t\t} else {\n\t\t\t\tprojectData.ProjectStyle = \"project-table\"\n\t\t\t}\n\t\t\tpages.SwitchToPage(projectData.ProjectStyle)\n\t\t\tp.focusable = p.updateProjectFocusable(projectData)\n\t\t\tmisc.App.SetFocus(p.focusable[0].Primitive)\n\t\t\tmisc.ProjectsLastFocus = &p.focusable[0].Primitive\n\t\t\treturn nil\n\n\t\t}\n\t\treturn event\n\t})\n\n\treturn page\n}\n\nfunc (p *TProjectPage) updateProjectFocusable(\n\tdata *views.TProject,\n) []*misc.TItem {\n\tfocusable := []*misc.TItem{}\n\n\tif data.ProjectStyle == \"project-table\" {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\tdata.ProjectTableView.Table,\n\t\t\t\tdata.ProjectTableView.Table.Box,\n\t\t\t))\n\t} else {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\tdata.ProjectTreeView.Tree,\n\t\t\t\tdata.ProjectTreeView.Tree.Box,\n\t\t\t))\n\t}\n\n\t// Always include Tags and Paths panes (even when empty)\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tdata.TagView.List,\n\t\t\tdata.TagView.List.Box))\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tdata.PathView.List,\n\t\t\tdata.PathView.List.Box))\n\n\treturn focusable\n}\n"
  },
  {
    "path": "core/tui/pages/tui_run.go",
    "content": "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.com/alajmo/mani/core/dao\"\n\t\"github.com/alajmo/mani/core/exec\"\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/mani/core/tui/views\"\n)\n\ntype TRunPage struct {\n\tfocusable []*misc.TItem\n}\n\nfunc CreateRunPage(\n\ttasks []dao.Task,\n\tprojects []dao.Project,\n\tprojectTags []string,\n\tprojectPaths []string,\n) *tview.Flex {\n\tr := &TRunPage{}\n\n\t// Data\n\ttaskData := views.CreateTasksData(\n\t\ttasks,\n\t\t[]string{\"Name\", \"Description\"},\n\t\t1,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t)\n\tprojectData := views.CreateProjectsData(\n\t\tprojects,\n\t\tprojectTags,\n\t\tprojectPaths,\n\t\t[]string{\"Project\", \"Description\", \"Tag\"},\n\t\t2,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t\ttrue,\n\t)\n\n\t// Views\n\tstreamView, ansiWriter := components.CreateOutputView(\"[1] Output\")\n\trunInfoView := views.CreateRunInfoVIew()\n\texecInfoView := views.CreateExecInfoView()\n\tspec := views.CreateSpecView()\n\n\t// Pages\n\trunPage := r.createSelectPage(taskData, projectData, runInfoView)\n\toutputPage := r.createOutputPage(execInfoView, streamView)\n\tpages := tview.NewPages().\n\t\tAddPage(\"exec-projects\", runPage, true, true).\n\t\tAddPage(\"exec-run\", outputPage, true, false)\n\n\t// Main page\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(pages, 0, 1, true).\n\t\tAddItem(misc.Search, 1, 0, false)\n\n\t// Focus\n\tr.focusable = r.updateRunFocusable(*taskData, *projectData)\n\tmisc.RunLastFocus = &r.focusable[0].Primitive\n\n\t// Shortcuts\n\tpage.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlS:\n\t\t\tr.focusable = r.switchView(pages, taskData, projectData, streamView)\n\t\t\tmisc.App.SetFocus(r.focusable[0].Primitive)\n\t\t\tmisc.RunLastFocus = &r.focusable[0].Primitive\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlR:\n\t\t\tr.focusable = r.switchBeforeRun(pages, r.focusable, streamView)\n\t\t\tmisc.App.SetFocus(r.focusable[0].Primitive)\n\t\t\tmisc.RunLastFocus = &r.focusable[0].Primitive\n\t\t\tr.runTasks(streamView, *taskData, *projectData, spec, ansiWriter)\n\t\t\treturn nil\n\t\t}\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyTab:\n\t\t\tnextPrimitive := misc.FocusNext(r.focusable)\n\t\t\tmisc.RunLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyBacktab:\n\t\t\tnextPrimitive := misc.FocusPrevious(r.focusable)\n\t\t\tmisc.RunLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlO:\n\t\t\tcomponents.OpenModal(\"spec-modal\", \"Options\", spec.View, 30, 11)\n\t\t\treturn nil\n\t\tcase tcell.KeyCtrlX:\n\t\t\tstreamView.Clear()\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tif _, ok := misc.App.GetFocus().(*tview.InputField); ok {\n\t\t\t\treturn event\n\t\t\t}\n\t\t\tname, _ := pages.GetFrontPage()\n\t\t\tif name == \"exec-projects\" {\n\t\t\t\tswitch event.Rune() {\n\t\t\t\tcase 'C': // Clear filters\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_filter\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_tag_path_selections\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_filter\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.PublishAndWait(misc.Event{Name: \"remove_project_selections\", Data: \"\"})\n\t\t\t\t\tprojectData.Emitter.Publish(misc.Event{Name: \"filter_projects\", Data: \"\"})\n\n\t\t\t\t\ttaskData.Emitter.PublishAndWait(misc.Event{Name: \"remove_task_filter\", Data: \"\"})\n\t\t\t\t\ttaskData.Emitter.PublishAndWait(misc.Event{Name: \"remove_task_selections\", Data: \"\"})\n\t\t\t\t\ttaskData.Emitter.Publish(misc.Event{Name: \"filter_tasks\", Data: \"\"})\n\t\t\t\t\treturn nil\n\t\t\t\tcase '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\t\t\t\tmisc.FocusPage(event, r.focusable)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\treturn page\n}\n\nfunc (r *TRunPage) createSelectPage(\n\ttaskData *views.TTask,\n\tprojectData *views.TProject,\n\tinfo *tview.TextView,\n) *tview.Flex {\n\t// Tasks\n\tisTaskTable := taskData.TaskStyle == \"task-table\"\n\ttaskPages := tview.NewPages().\n\t\tAddPage(\n\t\t\t\"task-table\",\n\t\t\ttview.NewFlex().SetDirection(tview.FlexRow).\n\t\t\t\tAddItem(taskData.TaskTableView.Root, 0, 1, true),\n\t\t\ttrue, isTaskTable,\n\t\t).\n\t\tAddPage(\n\t\t\t\"task-tree\",\n\t\t\ttview.NewFlex().SetDirection(tview.FlexRow).\n\t\t\t\tAddItem(taskData.TaskTreeView.Root, 0, 8, false),\n\t\t\ttrue, !isTaskTable,\n\t\t)\n\ttaskPages.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlE:\n\t\t\tif taskData.TaskStyle == \"task-table\" {\n\t\t\t\ttaskData.TaskStyle = \"task-tree\"\n\t\t\t} else {\n\t\t\t\ttaskData.TaskStyle = \"task-table\"\n\t\t\t}\n\t\t\ttaskPages.SwitchToPage(taskData.TaskStyle)\n\t\t\tr.focusable = r.updateRunFocusable(*taskData, *projectData)\n\t\t\tmisc.App.SetFocus(r.focusable[0].Primitive)\n\t\t\tmisc.RunLastFocus = &r.focusable[0].Primitive\n\t\t\treturn nil\n\t\t}\n\t\treturn event\n\t})\n\n\t// Projects\n\tisProjectTable := projectData.ProjectStyle == \"project-table\"\n\tprojectPages := tview.NewPages().\n\t\tAddPage(\"project-table\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTableView.Root, 0, 1, true), true, isProjectTable).\n\t\tAddPage(\"project-tree\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(projectData.ProjectTreeView.Root, 0, 8, false), true, !isProjectTable)\n\tprojectPages.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlE:\n\t\t\tif projectData.ProjectStyle == \"project-table\" {\n\t\t\t\tprojectData.ProjectStyle = \"project-tree\"\n\t\t\t} else {\n\t\t\t\tprojectData.ProjectStyle = \"project-table\"\n\t\t\t}\n\t\t\tprojectPages.SwitchToPage(projectData.ProjectStyle)\n\t\t\tr.focusable = r.updateRunFocusable(*taskData, *projectData)\n\t\t\tmisc.App.SetFocus(r.focusable[1].Primitive)\n\t\t\tmisc.RunLastFocus = &r.focusable[1].Primitive\n\t\t\treturn nil\n\t\t}\n\t\treturn event\n\t})\n\n\t// Always show both panes, even when empty\n\tprojectData.ContextView = tview.NewFlex().SetDirection(tview.FlexRow)\n\tprojectData.ContextView.AddItem(projectData.TagView.Root, 0, 1, true)\n\tprojectData.ContextView.AddItem(projectData.PathView.Root, 0, 1, true)\n\ttaskProjects := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(projectPages, 0, 1, true).\n\t\tAddItem(projectData.ContextView, 30, 1, false)\n\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(taskPages, 0, 1, true).\n\t\tAddItem(taskProjects, 0, 1, false)\n\n\treturn tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(page, 0, 1, true).\n\t\tAddItem(info, 1, 0, false)\n}\n\nfunc (r *TRunPage) createOutputPage(\n\tinfo *tview.TextView,\n\tstreamView *tview.TextView,\n) *tview.Flex {\n\toutputView := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(streamView, 0, 1, false).\n\t\tAddItem(info, 1, 0, true)\n\n\treturn outputView\n}\n\nfunc (r *TRunPage) updateRunFocusable(\n\ttaskData views.TTask,\n\tprojectData views.TProject,\n) []*misc.TItem {\n\tfocusable := []*misc.TItem{}\n\n\t// Task\n\tif taskData.TaskStyle == \"task-table\" {\n\t\tfocusable = append(\n\t\t\tfocusable, misc.GetTUIItem(\n\t\t\t\ttaskData.TaskTableView.Table,\n\t\t\t\ttaskData.TaskTableView.Table.Box,\n\t\t\t))\n\t} else {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\ttaskData.TaskTreeView.Tree,\n\t\t\t\ttaskData.TaskTreeView.Tree.Box,\n\t\t\t))\n\t}\n\n\t// Project\n\tif projectData.ProjectStyle == \"project-table\" {\n\t\tfocusable = append(\n\t\t\tfocusable, misc.GetTUIItem(\n\t\t\t\tprojectData.ProjectTableView.Table,\n\t\t\t\tprojectData.ProjectTableView.Table.Box,\n\t\t\t))\n\t} else {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\tprojectData.ProjectTreeView.Tree,\n\t\t\t\tprojectData.ProjectTreeView.Tree.Box,\n\t\t\t))\n\t}\n\n\t// Project Context (always include Tags and Paths panes, even when empty)\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tprojectData.TagView.List,\n\t\t\tprojectData.TagView.List.Box),\n\t)\n\tfocusable = append(\n\t\tfocusable,\n\t\tmisc.GetTUIItem(\n\t\t\tprojectData.PathView.List,\n\t\t\tprojectData.PathView.List.Box),\n\t)\n\n\treturn focusable\n}\n\nfunc (r *TRunPage) updateStreamFocusable(streamView *tview.TextView) []*misc.TItem {\n\tfocusable := []*misc.TItem{\n\t\tmisc.GetTUIItem(streamView, streamView.Box),\n\t}\n\treturn focusable\n}\n\nfunc (r *TRunPage) switchView(\n\tpages *tview.Pages,\n\ttaskData *views.TTask,\n\tprojectData *views.TProject,\n\tstreamView *tview.TextView,\n) []*misc.TItem {\n\tname, _ := pages.GetFrontPage()\n\tvar focusable []*misc.TItem\n\tif name == \"exec-run\" {\n\t\tpages.SwitchToPage(\"exec-projects\")\n\t\tfocusable = r.updateRunFocusable(*taskData, *projectData)\n\t} else {\n\t\tpages.SwitchToPage(\"exec-run\")\n\t\tfocusable = r.updateStreamFocusable(streamView)\n\t}\n\n\treturn focusable\n}\n\nfunc (r *TRunPage) switchBeforeRun(\n\tpages *tview.Pages,\n\tfocusable []*misc.TItem,\n\tstreamView *tview.TextView,\n) []*misc.TItem {\n\tname, _ := pages.GetFrontPage()\n\tif name == \"exec-projects\" {\n\t\tpages.SwitchToPage(\"exec-run\")\n\t\tfocusable = r.updateStreamFocusable(streamView)\n\t}\n\n\treturn focusable\n}\n\nfunc (r *TRunPage) runTasks(\n\tstreamView *tview.TextView,\n\ttaskData views.TTask,\n\tprojectData views.TProject,\n\tspec *views.TSpec,\n\tansiWriter *misc.ThreadSafeWriter,\n) {\n\t// Check if any projects selected\n\tselectedProjects := []dao.Project{}\n\tfor _, project := range projectData.Projects {\n\t\tif projectData.ProjectsSelected[project.Name] {\n\t\t\tselectedProjects = append(selectedProjects, project)\n\t\t}\n\t}\n\tif len(selectedProjects) < 1 {\n\t\treturn\n\t}\n\n\t// Task\n\tvar taskNames []string\n\tfor _, task := range taskData.Tasks {\n\t\tif taskData.TasksSelected[task.Name] {\n\t\t\ttaskNames = append(taskNames, task.Name)\n\t\t}\n\t}\n\tvar projectNames []string\n\tfor _, project := range selectedProjects {\n\t\tprojectNames = append(projectNames, project.Name)\n\t}\n\n\t// Flags\n\trunFlags := core.RunFlags{\n\t\tSilent: true,\n\n\t\t// Filter\n\t\tCwd:      false,\n\t\tAll:      false,\n\t\tTagsExpr: \"\",\n\n\t\tTarget: \"default\",\n\t\tSpec:   \"default\",\n\n\t\tProjects:          projectNames,\n\t\tOutput:            spec.Output,\n\t\tParallel:          spec.Parallel,\n\t\tIgnoreErrors:      spec.IgnoreErrors,\n\t\tIgnoreNonExisting: spec.IgnoreNonExisting,\n\t\tOmitEmptyRows:     spec.OmitEmptyRows,\n\t\tOmitEmptyColumns:  spec.OmitEmptyColumns,\n\t}\n\tsetRunFlags := core.SetRunFlags{\n\t\tParallel:          spec.Parallel,\n\t\tAll:               true,\n\t\tCwd:               true,\n\t\tIgnoreErrors:      true,\n\t\tIgnoreNonExisting: true,\n\t\tOmitEmptyRows:     true,\n\t\tOmitEmptyColumns:  true,\n\t}\n\n\t// Parse Task\n\tvar err error\n\tvar tasks []dao.Task\n\tvar projects []dao.Project\n\tif len(taskNames) == 1 {\n\t\ttasks, projects, err = dao.ParseSingleTask(taskNames[0], &runFlags, &setRunFlags, misc.Config)\n\t} else {\n\t\ttasks, projects, err = dao.ParseManyTasks(taskNames, &runFlags, &setRunFlags, misc.Config)\n\t}\n\tif err != nil {\n\t\tmisc.App.Stop()\n\t}\n\n\t// Run task\n\ttarget := exec.Exec{Projects: projects, Tasks: tasks, Config: *misc.Config}\n\n\tif spec.ClearBeforeRun {\n\t\tstreamView.Clear()\n\t}\n\n\tif spec.Output == \"table\" {\n\t\ttext := streamView.GetText(false)\n\t\tstreamView.SetText(text + \"\\n\")\n\t} else {\n\t\ttext := streamView.GetText(false)\n\t\tstreamView.SetText(text + \"\\n\")\n\t}\n\n\terr = target.RunTUI([]string{}, &runFlags, &setRunFlags, spec.Output, ansiWriter, ansiWriter)\n\tif err != nil {\n\t\tmisc.App.Stop()\n\t}\n\n\tstreamView.ScrollToEnd()\n}\n"
  },
  {
    "path": "core/tui/pages/tui_task.go",
    "content": "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\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/mani/core/tui/views\"\n)\n\ntype TTaskPage struct {\n\tfocusable []*misc.TItem\n}\n\nfunc CreateTasksPage(tasks []dao.Task) *tview.Flex {\n\tt := &TTaskPage{}\n\n\t// Data\n\ttaskData := views.CreateTasksData(\n\t\ttasks,\n\t\t[]string{\"Task\", \"Description\", \"Target\", \"Spec\"},\n\t\t1,\n\t\ttrue,\n\t\ttrue,\n\t\tfalse,\n\t)\n\n\t// Views\n\ttaskInfo := views.CreateTaskInfoView()\n\n\t// Pages\n\ttaskTablePage := t.createTaskPage(taskData)\n\n\ttaskData.Page = tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(taskTablePage, 0, 1, true).\n\t\tAddItem(taskInfo, 1, 0, false).\n\t\tAddItem(misc.Search, 1, 0, false)\n\n\tt.focusable = t.updateTaskFocusable(taskData)\n\tmisc.TasksLastFocus = &t.focusable[0].Primitive\n\n\t// Shortcuts\n\ttaskData.Page.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tif misc.App.GetFocus() == misc.Search {\n\t\t\treturn event\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyTab:\n\t\t\tnextPrimitive := misc.FocusNext(t.focusable)\n\t\t\tmisc.TasksLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyBacktab:\n\t\t\tnextPrimitive := misc.FocusPrevious(t.focusable)\n\t\t\tmisc.TasksLastFocus = nextPrimitive\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tif _, ok := misc.App.GetFocus().(*tview.InputField); ok {\n\t\t\t\treturn event\n\t\t\t}\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'C': // Clear filters\n\t\t\t\ttaskData.Emitter.PublishAndWait(misc.Event{Name: \"remove_task_filter\", Data: \"\"})\n\t\t\t\ttaskData.Emitter.PublishAndWait(misc.Event{Name: \"remove_task_selections\", Data: \"\"})\n\t\t\t\ttaskData.Emitter.Publish(misc.Event{Name: \"filter_tasks\", Data: \"\"})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn event\n\t})\n\n\treturn taskData.Page\n}\n\nfunc (taskPage *TTaskPage) createTaskPage(taskData *views.TTask) *tview.Flex {\n\tisTable := taskData.TaskStyle == \"task-table\"\n\n\tpages := tview.NewPages().\n\t\tAddPage(\"task-table\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(taskData.TaskTableView.Root, 0, 1, true), true, isTable).\n\t\tAddPage(\"task-tree\", tview.NewFlex().SetDirection(tview.FlexRow).AddItem(taskData.TaskTreeView.Root, 0, 8, false), true, !isTable)\n\n\tpage := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(pages, 0, 1, true)\n\n\tpage.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tif misc.App.GetFocus() == misc.Search {\n\t\t\treturn event\n\t\t}\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyCtrlE:\n\t\t\tif taskData.TaskStyle == \"task-table\" {\n\t\t\t\ttaskData.TaskStyle = \"task-tree\"\n\t\t\t} else {\n\t\t\t\ttaskData.TaskStyle = \"task-table\"\n\t\t\t}\n\t\t\tpages.SwitchToPage(taskData.TaskStyle)\n\t\t\ttaskPage.focusable = taskPage.updateTaskFocusable(taskData)\n\t\t\tmisc.App.SetFocus(taskPage.focusable[0].Primitive)\n\t\t\tmisc.TasksLastFocus = &taskPage.focusable[0].Primitive\n\t\t\treturn nil\n\t\t}\n\t\treturn event\n\t})\n\n\treturn page\n}\n\nfunc (taskPage *TTaskPage) updateTaskFocusable(\n\tdata *views.TTask,\n) []*misc.TItem {\n\tfocusable := []*misc.TItem{}\n\n\tif data.TaskStyle == \"task-table\" {\n\t\tfocusable = append(\n\t\t\tfocusable, misc.GetTUIItem(\n\t\t\t\tdata.TaskTableView.Table,\n\t\t\t\tdata.TaskTableView.Table.Box,\n\t\t\t))\n\t} else {\n\t\tfocusable = append(\n\t\t\tfocusable,\n\t\t\tmisc.GetTUIItem(\n\t\t\t\tdata.TaskTreeView.Tree,\n\t\t\t\tdata.TaskTreeView.Tree.Box,\n\t\t\t))\n\t}\n\n\treturn focusable\n}\n"
  },
  {
    "path": "core/tui/pages.go",
    "content": "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/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/mani/core/tui/pages\"\n\t\"github.com/alajmo/mani/core/tui/views\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc createPages(\n\tprojects []dao.Project,\n\tprojectTags []string,\n\tprojectPaths []string,\n\ttasks []dao.Task,\n) *tview.Pages {\n\tappPages := tview.NewPages()\n\tnavPane := createNav()\n\tsearch := components.CreateSearch()\n\tmisc.Search = search\n\n\tprojectsPage := pages.CreateProjectsPage(projects, projectTags, projectPaths)\n\ttasksPage := pages.CreateTasksPage(tasks)\n\trunPage := pages.CreateRunPage(tasks, projects, projectTags, projectPaths)\n\texecPage := pages.CreateExecPage(projects, projectTags, projectPaths)\n\n\tmisc.MainPage = tview.NewPages().\n\t\tAddPage(\"run\", runPage, true, true).\n\t\tAddPage(\"exec\", execPage, true, false).\n\t\tAddPage(\"projects\", projectsPage, true, false).\n\t\tAddPage(\"tasks\", tasksPage, true, false)\n\n\tmainLayout := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(navPane, 2, 1, false).\n\t\tAddItem(misc.MainPage, 0, 1, true)\n\tappPages.AddPage(\"main\", mainLayout, true, true)\n\n\tSwitchToPage(\"run\")\n\n\treturn appPages\n}\n\nfunc createNav() *tview.Flex {\n\t// Buttons\n\tmisc.ProjectBtn = components.CreateButton(\"Projects\")\n\tmisc.ProjectBtn.SetSelectedFunc(func() {\n\t\tSwitchToPage(\"projects\")\n\t\tmisc.App.SetFocus(*misc.ProjectsLastFocus)\n\t})\n\n\tmisc.TaskBtn = components.CreateButton(\"Tasks\")\n\tmisc.TaskBtn.SetSelectedFunc(func() {\n\t\tSwitchToPage(\"tasks\")\n\t\tmisc.App.SetFocus(*misc.TasksLastFocus)\n\t})\n\n\tmisc.RunBtn = components.CreateButton(\"Run\")\n\tmisc.RunBtn.SetSelectedFunc(func() {\n\t\tSwitchToPage(\"run\")\n\t\tmisc.App.SetFocus(*misc.RunLastFocus)\n\t})\n\n\tmisc.ExecBtn = components.CreateButton(\"Exec\")\n\tmisc.ExecBtn.SetSelectedFunc(func() {\n\t\tSwitchToPage(\"exec\")\n\t\tmisc.App.SetFocus(*misc.ExecLastFocus)\n\t})\n\n\tmisc.HelpBtn = components.CreateButton(\"Help\")\n\tmisc.HelpBtn.SetSelectedFunc(func() {\n\t\tviews.ShowHelpModal()\n\t})\n\n\t// Left\n\tleft := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(misc.RunBtn, 7, 0, false).      // 3 size + 2 padding\n\t\tAddItem(misc.ExecBtn, 8, 0, false).     // 4 size + 2 padding\n\t\tAddItem(misc.ProjectBtn, 12, 0, false). // 8 size + 2 padding\n\t\tAddItem(misc.TaskBtn, 9, 0, false)      // 5 size + 2 padding\n\n\t// Right\n\tright := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(misc.HelpBtn, 5, 0, false)\n\n\t\t// Nav\n\tnavPane := tview.NewFlex().\n\t\tSetDirection(tview.FlexColumn).\n\t\tAddItem(left, 0, 1, false).\n\t\tAddItem(nil, 0, 1, false).\n\t\tAddItem(right, 4, 0, false)\n\tnavPane.SetBorderPadding(0, 1, 1, 1)\n\n\treturn navPane\n}\n\nfunc SwitchToPage(pageName string) {\n\tmisc.MainPage.SwitchToPage(pageName)\n\n\tswitch pageName {\n\tcase \"projects\":\n\t\tcomponents.SetActiveButtonStyle(misc.ProjectBtn)\n\n\t\tcomponents.SetInactiveButtonStyle(misc.HelpBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.RunBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.TaskBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ExecBtn)\n\tcase \"tasks\":\n\t\tcomponents.SetActiveButtonStyle(misc.TaskBtn)\n\n\t\tcomponents.SetInactiveButtonStyle(misc.HelpBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ProjectBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.RunBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ExecBtn)\n\tcase \"run\":\n\t\tcomponents.SetActiveButtonStyle(misc.RunBtn)\n\n\t\tcomponents.SetInactiveButtonStyle(misc.HelpBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ProjectBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.TaskBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ExecBtn)\n\tcase \"exec\":\n\t\tcomponents.SetActiveButtonStyle(misc.ExecBtn)\n\n\t\tcomponents.SetInactiveButtonStyle(misc.HelpBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.ProjectBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.TaskBtn)\n\t\tcomponents.SetInactiveButtonStyle(misc.RunBtn)\n\t}\n\n\t_, page := misc.MainPage.GetFrontPage()\n\tmisc.App.SetFocus(page)\n}\n\nfunc setupStyles() {\n\t// Foreground / Background\n\ttview.Styles.PrimaryTextColor = misc.STYLE_DEFAULT.Fg\n\ttview.Styles.PrimitiveBackgroundColor = misc.STYLE_DEFAULT.Bg\n\n\t// Borders Colors\n\ttview.Styles.BorderColor = misc.STYLE_BORDER.Fg\n\n\t// Border style\n\ttview.Borders.HorizontalFocus = tview.BoxDrawingsLightHorizontal\n\ttview.Borders.VerticalFocus = tview.BoxDrawingsLightVertical\n\ttview.Borders.TopLeftFocus = tview.BoxDrawingsLightDownAndRight\n\ttview.Borders.TopRightFocus = tview.BoxDrawingsLightDownAndLeft\n\ttview.Borders.BottomLeftFocus = tview.BoxDrawingsLightUpAndRight\n\ttview.Borders.BottomRightFocus = tview.BoxDrawingsLightUpAndLeft\n}\n"
  },
  {
    "path": "core/tui/tui.go",
    "content": "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/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\nfunc RunTui(config *dao.Config, themeName string, reload bool) {\n\tapp := NewApp(config, themeName)\n\n\tif reload {\n\t\tWatchFiles(app, append([]string{config.Path}, config.ConfigPaths...)...)\n\t}\n\n\tif err := app.Run(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\ntype App struct {\n\tApp *tview.Application\n}\n\nfunc NewApp(config *dao.Config, themeName string) *App {\n\tapp := &App{\n\t\tApp: tview.NewApplication(),\n\t}\n\tapp.setupApp(config, themeName)\n\n\treturn app\n}\n\nfunc (app *App) Run() error {\n\treturn app.App.SetRoot(misc.Pages, true).EnableMouse(true).Run()\n}\n\nfunc (app *App) Reload() {\n\tconfig, configErr := dao.ReadConfig(misc.Config.Path, \"\", true)\n\tif configErr != nil {\n\t\tapp.App.Stop()\n\t}\n\n\tapp.setupApp(&config, *misc.ThemeName)\n\tapp.App.SetRoot(misc.Pages, true)\n\tapp.App.Draw()\n}\n\nfunc (app *App) setupApp(config *dao.Config, themeName string) {\n\tmisc.Config = config\n\tmisc.ThemeName = &themeName\n\ttheme, err := misc.Config.GetTheme(themeName)\n\tcore.CheckIfError(err)\n\n\tmisc.LoadStyles(&theme.TUI)\n\tmisc.TUITheme = &theme.TUI\n\tmisc.BlockTheme = &theme.Block\n\n\t// Data\n\tprojects := config.ProjectList\n\ttasks := config.TaskList\n\tdao.ParseTasksEnv(tasks)\n\tprojectTags := config.GetTags()\n\tprojectPaths := config.GetProjectPaths()\n\n\t// Styles\n\tsetupStyles()\n\n\t// Create pages\n\tmisc.App = app.App\n\tmisc.Pages = createPages(projects, projectTags, projectPaths, tasks)\n\n\t// Global input handling\n\tHandleInput(app)\n}\n"
  },
  {
    "path": "core/tui/tui_input.go",
    "content": "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/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/alajmo/mani/core/tui/views\"\n)\n\nfunc HandleInput(app *App) {\n\tvar lastSearchQuery string\n\tvar lastFoundRow, lastFoundCol int\n\tsearchDirection := 1\n\n\tmisc.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tcurrentFocus := misc.App.GetFocus()\n\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyF1:\n\t\t\tSwitchToPage(\"run\")\n\t\t\tmisc.App.SetFocus(*misc.RunLastFocus)\n\t\t\treturn nil\n\t\tcase tcell.KeyF2:\n\t\t\tSwitchToPage(\"exec\")\n\t\t\tmisc.App.SetFocus(*misc.ExecLastFocus)\n\t\t\treturn nil\n\t\tcase tcell.KeyF3:\n\t\t\tSwitchToPage(\"projects\")\n\t\t\tmisc.App.SetFocus(*misc.ProjectsLastFocus)\n\t\t\treturn nil\n\t\tcase tcell.KeyF4:\n\t\t\tSwitchToPage(\"tasks\")\n\t\t\tmisc.App.SetFocus(*misc.TasksLastFocus)\n\t\t\treturn nil\n\t\tcase tcell.KeyF5:\n\t\t\tgo app.Reload()\n\t\t\treturn nil\n\t\tcase tcell.KeyF6:\n\t\t\tmisc.App.Sync()\n\t\t\treturn nil\n\t\t}\n\n\t\t// Modal\n\t\tif components.IsModalOpen() {\n\t\t\tswitch event.Key() {\n\t\t\tcase tcell.KeyEscape:\n\t\t\t\tcomponents.CloseModal()\n\t\t\t\treturn nil\n\t\t\tcase tcell.KeyRune:\n\t\t\t\tswitch event.Rune() {\n\t\t\t\tcase 'q':\n\t\t\t\t\tmisc.App.Stop()\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn event\n\t\t}\n\n\t\t// Search\n\t\tif currentFocus == misc.Search {\n\t\t\tlastFoundRow, lastFoundCol = -1, -1\n\t\t\tswitch event.Key() {\n\t\t\tcase tcell.KeyEscape:\n\t\t\t\tcomponents.EmptySearch()\n\t\t\t\tmisc.FocusPreviousPage()\n\t\t\t\treturn nil\n\t\t\tcase tcell.KeyEnter:\n\t\t\t\treturn handleSearchInput(event, searchDirection, &lastFoundRow, &lastFoundCol)\n\t\t\t}\n\t\t\treturn event\n\t\t}\n\n\t\t// Input\n\t\tif _, ok := currentFocus.(*tview.InputField); ok {\n\t\t\treturn event\n\t\t}\n\t\t// TextArea\n\t\tif _, ok := currentFocus.(*tview.TextArea); ok {\n\t\t\treturn event\n\t\t}\n\n\t\t// Main\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyEscape:\n\t\t\tcomponents.EmptySearch()\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'q':\n\t\t\t\tmisc.App.Stop()\n\t\t\t\treturn nil\n\t\t\tcase 'R':\n\t\t\t\tmisc.App.Sync()\n\t\t\t\treturn nil\n\t\t\tcase 'p':\n\t\t\t\tSwitchToPage(\"projects\")\n\t\t\t\tmisc.App.SetFocus(*misc.ProjectsLastFocus)\n\t\t\t\treturn nil\n\t\t\tcase 't':\n\t\t\t\tSwitchToPage(\"tasks\")\n\t\t\t\tmisc.App.SetFocus(*misc.TasksLastFocus)\n\t\t\t\treturn nil\n\t\t\tcase 'r':\n\t\t\t\tSwitchToPage(\"run\")\n\t\t\t\tmisc.App.SetFocus(*misc.RunLastFocus)\n\t\t\t\treturn nil\n\t\t\tcase 'e':\n\t\t\t\tSwitchToPage(\"exec\")\n\t\t\t\tmisc.App.SetFocus(*misc.ExecLastFocus)\n\t\t\t\treturn nil\n\t\t\tcase '?':\n\t\t\t\tviews.ShowHelpModal()\n\t\t\t\treturn nil\n\t\t\tcase '/':\n\t\t\t\tcomponents.ShowSearch()\n\t\t\t\treturn nil\n\t\t\tcase 'n':\n\t\t\t\tsearchDirection = 1\n\t\t\t\treturn handleSearchInput(event, searchDirection, &lastFoundRow, &lastFoundCol)\n\t\t\tcase 'N':\n\t\t\t\tsearchDirection = -1\n\t\t\t\treturn handleSearchInput(event, searchDirection, &lastFoundRow, &lastFoundCol)\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\tmisc.Search.SetChangedFunc(func(query string) {\n\t\tif query != lastSearchQuery {\n\t\t\tlastSearchQuery = query\n\t\t\tlastFoundRow, lastFoundCol = -1, -1\n\t\t\tsearchDirection = 1\n\n\t\t\tswitch prevPage := misc.PreviousPane.(type) {\n\t\t\tcase *tview.Table:\n\t\t\t\tcomponents.SearchInTable(prevPage, query, &lastFoundRow, &lastFoundCol, searchDirection)\n\t\t\tcase *tview.TreeView:\n\t\t\t\tif tree, ok := misc.PreviousModel.(*components.TTree); ok {\n\t\t\t\t\tcomponents.SearchInTree(tree, query, &lastFoundRow, searchDirection)\n\t\t\t\t}\n\t\t\tcase *tview.List:\n\t\t\t\tcomponents.SearchInList(prevPage, query, &lastFoundRow, searchDirection)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc handleSearchInput(_ *tcell.EventKey, searchDirection int, lastFoundRow *int, lastFoundCol *int) *tcell.EventKey {\n\tquery := misc.Search.GetText()\n\tif query == \"\" {\n\t\treturn nil\n\t}\n\n\tswitch prevPage := misc.PreviousPane.(type) {\n\tcase *tview.Table:\n\t\tmisc.App.SetFocus(prevPage)\n\t\tcomponents.SearchInTable(prevPage, query, lastFoundRow, lastFoundCol, searchDirection)\n\tcase *tview.TreeView:\n\t\tmisc.App.SetFocus(prevPage)\n\t\tif tree, ok := misc.PreviousModel.(*components.TTree); ok {\n\t\t\tcomponents.SearchInTree(tree, query, lastFoundRow, searchDirection)\n\t\t}\n\tcase *tview.List:\n\t\tmisc.App.SetFocus(prevPage)\n\t\tcomponents.SearchInList(prevPage, query, lastFoundRow, searchDirection)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/tui/views/tui_help.go",
    "content": "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\"github.com/rivo/tview\"\n)\n\nvar Version = \"v0.31.2\"\n\nfunc ShowHelpModal() {\n\tt, table := createShortcutsTable()\n\tcomponents.OpenModal(\"help-modal\", \"Help\", t, 65, 37)\n\tmisc.App.SetFocus(table)\n}\n\nfunc shortcutRow(shortcut string, description string) (*tview.TableCell, *tview.TableCell) {\n\tshortcut = fmt.Sprintf(\"[%s:%s:%s]%s[-:-:-]\",\n\t\tmisc.STYLE_SHORTCUT_LABEL.Fg, misc.STYLE_SHORTCUT_LABEL.Bg, misc.STYLE_SHORTCUT_LABEL.AttrStr, shortcut,\n\t)\n\n\tdescription = fmt.Sprintf(\"[%s:%s:%s]%s[-:-:-]\",\n\t\tmisc.STYLE_SHORTCUT_TEXT.Fg, misc.STYLE_SHORTCUT_TEXT.Bg, misc.STYLE_SHORTCUT_TEXT.AttrStr, description,\n\t)\n\n\tr1 := tview.NewTableCell(shortcut + \"  \").\n\t\tSetTextColor(misc.STYLE_SHORTCUT_TEXT.Fg).\n\t\tSetAlign(tview.AlignRight).\n\t\tSetSelectable(false)\n\n\tr2 := tview.NewTableCell(description).\n\t\tSetAlign(tview.AlignLeft).\n\t\tSetSelectable(false)\n\n\treturn r1, r2\n}\n\nfunc titleRow(title string) (*tview.TableCell, *tview.TableCell) {\n\tr1 := tview.NewTableCell(\"\").\n\t\tSetTextColor(misc.STYLE_SHORTCUT_TEXT.Fg).\n\t\tSetAlign(tview.AlignRight).\n\t\tSetSelectable(false)\n\n\tr2 := tview.NewTableCell(title).\n\t\tSetTextColor(misc.STYLE_TABLE_HEADER.Fg).\n\t\tSetAttributes(misc.STYLE_TABLE_HEADER.Attr).\n\t\tSetAlign(tview.AlignLeft).\n\t\tSetSelectable(false)\n\n\treturn r1, r2\n}\n\nfunc createShortcutsTable() (*tview.Flex, *tview.Table) {\n\ttable := tview.NewTable()\n\ttable.SetEvaluateAllRows(true)\n\ttable.SetBackgroundColor(misc.STYLE_DEFAULT.Bg)\n\n\tsections := []struct {\n\t\ttitle     string\n\t\tshortcuts [][2]string\n\t}{\n\t\t{\n\t\t\ttitle: \"--- Global ---\",\n\t\t\tshortcuts: [][2]string{\n\t\t\t\t{\"?\", \"Show this help\"},\n\t\t\t\t{\"q, Ctrl + c\", \"Quits program\"},\n\t\t\t\t{\"F5\", \"Reload app\"},\n\t\t\t\t{\"F6\", \"Re-sync screen buffer\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"--- Navigation ---\",\n\t\t\tshortcuts: [][2]string{\n\t\t\t\t{\"r, F1\", \"Switch to run page\"},\n\t\t\t\t{\"e, F2\", \"Switch to exec page\"},\n\t\t\t\t{\"p, F3\", \"Switch to projects page\"},\n\t\t\t\t{\"t, F4\", \"Switch to tasks page\"},\n\t\t\t\t{\"1-9\", \"Focus specific pane\"},\n\t\t\t\t{\"Tab\", \"Focus next pane\"},\n\t\t\t\t{\"Shift + Tab\", \"Focus previous pane\"},\n\t\t\t\t{\"g\", \"Go to first item in the current pane\"},\n\t\t\t\t{\"G\", \"Go to last item in the current pane\"},\n\t\t\t\t{\"Ctrl + o\", \"Show task options\"},\n\t\t\t\t{\"Ctrl + s\", \"Toggle between selection and output view\"},\n\t\t\t\t{\"Ctrl + e\", \"Toggle between Table and Tree view\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"--- Actions ---\",\n\t\t\tshortcuts: [][2]string{\n\t\t\t\t{\"Escape\", \"Close\"},\n\t\t\t\t{\"/\", \"Free text search\"},\n\t\t\t\t{\"f\", \"Filter items for the current pane\"},\n\t\t\t\t{\"F\", \"Clear filter for the current selected pane\"},\n\t\t\t\t{\"a\", \"Select all items in the current pane\"},\n\t\t\t\t{\"c\", \"Clear all selections in the current pane\"},\n\t\t\t\t{\"C\", \"Clear all filters and selections\"},\n\t\t\t\t{\"d\", \"Describe the selected item\"},\n\t\t\t\t{\"o\", \"Open the current selected item in $EDITOR\"},\n\t\t\t\t{\"Space, Enter\", \"Toggle selection\"},\n\t\t\t\t{\"Ctrl + r\", \"Run tasks\"},\n\t\t\t\t{\"Ctrl + x\", \"Clear\"},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Populate table with sections\n\tcurrentRow := 0\n\tfor i, section := range sections {\n\t\t// Add spacing between sections except for the first one\n\t\tif i > 0 {\n\t\t\tr1, r2 := titleRow(\"\")\n\t\t\ttable.SetCell(currentRow, 0, r1)\n\t\t\ttable.SetCell(currentRow, 1, r2)\n\t\t\tcurrentRow++\n\t\t}\n\n\t\t// Add section title\n\t\tr1, r2 := titleRow(section.title)\n\t\ttable.SetCell(currentRow, 0, r1)\n\t\ttable.SetCell(currentRow, 1, r2)\n\t\tcurrentRow++\n\n\t\t// Add shortcuts for this section\n\t\tfor _, shortcut := range section.shortcuts {\n\t\t\tr1, r2 := shortcutRow(shortcut[0], shortcut[1])\n\t\t\ttable.SetCell(currentRow, 0, r1)\n\t\t\ttable.SetCell(currentRow, 1, r2)\n\t\t\tcurrentRow++\n\t\t}\n\t}\n\n\tversionString := fmt.Sprintf(\"[-:-:b]Mani %s\", Version)\n\ttext := tview.NewTextView()\n\ttext.SetDynamicColors(true)\n\ttext.SetText(versionString).SetTextAlign(tview.AlignRight)\n\n\troot := tview.NewFlex().\n\t\tSetDirection(tview.FlexRow).\n\t\tAddItem(text, 1, 0, true).\n\t\tAddItem(table, 0, 1, true)\n\n\troot.SetBorder(true)\n\troot.SetBorderPadding(0, 0, 2, 1)\n\troot.SetBorderColor(misc.STYLE_BORDER_FOCUS.Fg)\n\n\treturn root, table\n}\n"
  },
  {
    "path": "core/tui/views/tui_project_view.go",
    "content": "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/alajmo/mani/core/print\"\n\t\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n)\n\ntype TProject struct {\n\t// UI\n\tPage             *tview.Flex\n\tContextView      *tview.Flex\n\tProjectTableView *components.TTable\n\tProjectTreeView  *components.TTree\n\tTagView          *components.TList\n\tPathView         *components.TList\n\n\t// Project\n\tProjects           []dao.Project\n\tProjectsFiltered   []dao.Project\n\tProjectsSelected   map[string]bool\n\tprojectFilterValue *string\n\tHeaders            []string\n\tShowHeaders        bool\n\tProjectStyle       string\n\n\t// Tags\n\tProjectTags           []string\n\tProjectTagsFiltered   []string\n\tProjectTagsSelected   map[string]bool\n\tprojectTagFilterValue *string\n\n\t// Paths\n\tProjectPaths           []string\n\tProjectPathsFiltered   []string\n\tProjectPathsSelected   map[string]bool\n\tprojectPathFilterValue *string\n\n\t// Misc\n\tEmitter *misc.EventEmitter\n}\n\nfunc CreateProjectsData(\n\tprojects []dao.Project,\n\tprojectTags []string,\n\tprojectPaths []string,\n\theaders []string,\n\tprefixNumber int,\n\tshowTitle bool,\n\tshowHeaders bool,\n\tselectEnabled bool,\n\tshowTags bool,\n\tshowPaths bool,\n) *TProject {\n\tp := &TProject{\n\t\tProjects:           projects,\n\t\tProjectsFiltered:   projects,\n\t\tProjectsSelected:   make(map[string]bool),\n\t\tprojectFilterValue: new(string),\n\n\t\tProjectTags:           projectTags,\n\t\tProjectTagsFiltered:   projectTags,\n\t\tProjectTagsSelected:   make(map[string]bool),\n\t\tprojectTagFilterValue: new(string),\n\n\t\tProjectPaths:           projectPaths,\n\t\tProjectPathsFiltered:   projectPaths,\n\t\tProjectPathsSelected:   make(map[string]bool),\n\t\tprojectPathFilterValue: new(string),\n\n\t\tProjectStyle: \"project-table\",\n\t\tShowHeaders:  showHeaders,\n\t\tHeaders:      headers,\n\n\t\tEmitter: misc.NewEventEmitter(),\n\t}\n\n\tfor _, project := range p.Projects {\n\t\tp.ProjectsSelected[project.Name] = false\n\t}\n\tfor _, tag := range p.ProjectTags {\n\t\tp.ProjectTagsSelected[tag] = false\n\t}\n\tfor _, projectPath := range p.ProjectPaths {\n\t\tp.ProjectPathsSelected[projectPath] = false\n\t}\n\n\ttitle := \"\"\n\tif showTitle && prefixNumber > 0 {\n\t\ttitle = fmt.Sprintf(\"[%d] Projects (%d)\", prefixNumber, len(projects))\n\t\tprefixNumber += 1\n\t} else if showTitle {\n\t\ttitle = fmt.Sprintf(\"Projects (%d)\", len(projects))\n\t}\n\n\trows := p.getTableRows()\n\tprojectTable := p.CreateProjectsTable(selectEnabled, title, headers, rows)\n\tp.ProjectTableView = projectTable\n\n\tpaths := p.getTreeHierarchy()\n\tprojectTree := p.CreateProjectsTree(selectEnabled, title, paths)\n\tp.ProjectTreeView = projectTree\n\n\tif showTags {\n\t\ttagTitle := \"\"\n\t\tif showTitle && prefixNumber > 0 {\n\t\t\ttagTitle = fmt.Sprintf(\"[%d] Tags (%d)\", prefixNumber, len(projectTags))\n\t\t\tprefixNumber += 1\n\t\t} else {\n\t\t\ttagTitle = fmt.Sprintf(\"Tags (%d)\", len(projectTags))\n\t\t}\n\n\t\ttagsList := p.CreateProjectsTagsList(tagTitle)\n\t\tp.TagView = tagsList\n\t}\n\n\tif showPaths {\n\t\tpathTitle := \"\"\n\t\tif showTitle && prefixNumber > 0 {\n\t\t\tpathTitle = fmt.Sprintf(\"[%d] Paths (%d)\", prefixNumber, len(projectPaths))\n\t\t} else {\n\t\t\tpathTitle = fmt.Sprintf(\"Paths (%d)\", len(projectPaths))\n\t\t}\n\n\t\tpathsList := p.CreateProjectsPathsList(pathTitle)\n\t\tp.PathView = pathsList\n\t}\n\n\t// Events\n\tp.Emitter.Subscribe(\"remove_tag_path_filter\", func(e misc.Event) {\n\t\tp.TagView.ClearFilter()\n\t\tp.PathView.ClearFilter()\n\t})\n\tp.Emitter.Subscribe(\"remove_tag_path_selections\", func(e misc.Event) {\n\t\tp.unselectAllTags()\n\t\tp.unselectAllPaths()\n\t})\n\tp.Emitter.Subscribe(\"remove_project_filter\", func(e misc.Event) {\n\t\tp.ProjectTableView.ClearFilter()\n\t\tp.ProjectTreeView.ClearFilter()\n\t})\n\tp.Emitter.Subscribe(\"remove_project_selections\", func(event misc.Event) {\n\t\tp.unselectAllProjects()\n\t})\n\tp.Emitter.Subscribe(\"filter_projects\", func(e misc.Event) {\n\t\tp.filterProjects()\n\t})\n\n\treturn p\n}\n\nfunc (p *TProject) CreateProjectsTable(\n\tselectEnabled bool,\n\ttitle string,\n\theaders []string,\n\trows [][]string,\n) *components.TTable {\n\ttable := &components.TTable{\n\t\tTitle:         title,\n\t\tToggleEnabled: selectEnabled,\n\t\tShowHeaders:   p.ShowHeaders,\n\t\tFilterValue:   p.projectFilterValue,\n\t}\n\ttable.Create()\n\ttable.Update(headers, rows)\n\n\t// Methods\n\ttable.IsRowSelected = func(name string) bool {\n\t\treturn p.ProjectsSelected[name]\n\t}\n\ttable.ToggleSelectRow = func(name string) {\n\t\tp.toggleSelectProject(name)\n\t}\n\ttable.SelectAll = func() {\n\t\tp.selectAllProjects()\n\t}\n\ttable.UnselectAll = func() {\n\t\tp.unselectAllProjects()\n\t}\n\ttable.FilterRows = func() {\n\t\tp.filterProjects()\n\t}\n\ttable.DescribeRow = func(projectName string) {\n\t\tif projectName != \"\" {\n\t\t\tp.showProjectDescModal(projectName)\n\t\t}\n\t}\n\ttable.EditRow = func(projectName string) {\n\t\tif projectName != \"\" {\n\t\t\tp.editProject(projectName)\n\t\t}\n\t}\n\treturn table\n}\n\nfunc (p *TProject) CreateProjectsTree(\n\tselectEnabled bool,\n\ttitle string,\n\tpaths []dao.TNode,\n) *components.TTree {\n\ttree := &components.TTree{\n\t\tTitle:         title,\n\t\tRootTitle:     \"\",\n\t\tSelectEnabled: selectEnabled,\n\t\tFilterValue:   p.projectFilterValue,\n\t}\n\ttree.Create()\n\ttree.UpdateProjects(paths)\n\n\ttree.IsNodeSelected = func(name string) bool {\n\t\treturn p.ProjectsSelected[name]\n\t}\n\ttree.ToggleSelectNode = func(name string) {\n\t\tp.toggleSelectProject(name)\n\t}\n\ttree.SelectAll = func() {\n\t\tp.selectAllProjects()\n\t}\n\ttree.UnselectAll = func() {\n\t\tp.unselectAllProjects()\n\t}\n\ttree.FilterNodes = func() {\n\t\tp.filterProjects()\n\t}\n\ttree.DescribeNode = func(projectName string) {\n\t\tif projectName != \"\" {\n\t\t\tp.showProjectDescModal(projectName)\n\t\t}\n\t}\n\ttree.EditNode = func(projectName string) {\n\t\tif projectName != \"\" {\n\t\t\tp.editProject(projectName)\n\t\t}\n\t}\n\n\treturn tree\n}\n\nfunc (p *TProject) CreateProjectsTagsList(title string) *components.TList {\n\tlist := &components.TList{\n\t\tTitle:       title,\n\t\tFilterValue: p.projectTagFilterValue,\n\t}\n\tlist.Create()\n\tlist.Update(p.ProjectTags)\n\n\t// Methods\n\tlist.IsItemSelected = func(name string) bool {\n\t\treturn p.ProjectTagsSelected[name]\n\t}\n\tlist.ToggleSelectItem = func(i int, tag string) {\n\t\tp.ProjectTagsSelected[tag] = !p.ProjectTagsSelected[tag]\n\t\tlist.SetItemSelect(i, tag)\n\t\tp.filterProjects()\n\t}\n\tlist.SelectAll = func() {\n\t\tp.selectAllTags()\n\t\tp.filterProjects()\n\t}\n\tlist.UnselectAll = func() {\n\t\tp.unselectAllTags()\n\t\tp.filterProjects()\n\t}\n\tlist.FilterItems = func() {\n\t\tp.filterTags()\n\t}\n\n\treturn list\n}\n\nfunc (p *TProject) CreateProjectsPathsList(title string) *components.TList {\n\tlist := &components.TList{\n\t\tTitle:       title,\n\t\tFilterValue: p.projectPathFilterValue,\n\t}\n\tlist.Create()\n\tlist.Update(p.ProjectPaths)\n\n\t// Methods\n\tlist.IsItemSelected = func(name string) bool {\n\t\treturn p.ProjectPathsSelected[name]\n\t}\n\tlist.ToggleSelectItem = func(i int, tag string) {\n\t\tp.ProjectPathsSelected[tag] = !p.ProjectPathsSelected[tag]\n\t\tlist.SetItemSelect(i, tag)\n\t\tp.filterProjects()\n\t}\n\tlist.SelectAll = func() {\n\t\tp.selectAllPaths()\n\t\tp.filterProjects()\n\t}\n\tlist.UnselectAll = func() {\n\t\tp.unselectAllPaths()\n\t\tp.filterProjects()\n\t}\n\tlist.FilterItems = func() {\n\t\tp.filterPaths()\n\t}\n\n\treturn list\n}\n\nfunc (p *TProject) getTableRows() [][]string {\n\tvar rows = make([][]string, len(p.ProjectsFiltered))\n\tfor i, project := range p.ProjectsFiltered {\n\t\trows[i] = make([]string, len(p.Headers))\n\t\tfor j, header := range p.Headers {\n\t\t\trows[i][j] = project.GetValue(header, 0)\n\t\t}\n\t}\n\treturn rows\n}\n\nfunc (p *TProject) getTreeHierarchy() []dao.TNode {\n\tvar paths = []dao.TNode{}\n\tfor _, p := range p.ProjectsFiltered {\n\t\tnode := dao.TNode{Name: p.Name, Path: p.RelPath}\n\t\tpaths = append(paths, node)\n\t}\n\n\treturn paths\n}\n\nfunc (p *TProject) toggleSelectProject(name string) {\n\tp.ProjectsSelected[name] = !p.ProjectsSelected[name]\n\tp.ProjectTableView.ToggleSelectCurrentRow(name)\n\tp.ProjectTreeView.ToggleSelectCurrentNode(name)\n}\n\nfunc (p *TProject) filterProjects() {\n\tprojectTags := []string{}\n\tfor key, filtered := range p.ProjectTagsSelected {\n\t\tif filtered {\n\t\t\tprojectTags = append(projectTags, key)\n\t\t}\n\t}\n\n\tprojectPaths := []string{}\n\tfor key, filtered := range p.ProjectPathsSelected {\n\t\tif filtered {\n\t\t\tprojectPaths = append(projectPaths, key)\n\t\t}\n\t}\n\n\tif len(projectTags) > 0 || len(projectPaths) > 0 {\n\t\tprojects, _ := misc.Config.FilterProjects(false, false, []string{}, projectPaths, projectTags, \"\")\n\t\tp.ProjectsFiltered = projects\n\t} else {\n\t\tp.ProjectsFiltered = p.Projects\n\t}\n\n\tvar finalProjects []dao.Project\n\tfor _, project := range p.ProjectsFiltered {\n\t\tif strings.Contains(project.Name, *p.projectFilterValue) {\n\t\t\tfinalProjects = append(finalProjects, project)\n\t\t}\n\t}\n\tp.ProjectsFiltered = finalProjects\n\n\t// Table\n\trows := p.getTableRows()\n\tp.ProjectTableView.Update(p.Headers, rows)\n\tp.ProjectTableView.Table.ScrollToBeginning()\n\tp.ProjectTableView.Table.Select(1, 0)\n\n\t// Tree\n\tpaths := p.getTreeHierarchy()\n\tp.ProjectTreeView.UpdateProjects(paths)\n\tp.ProjectTreeView.UpdateProjectsStyle()\n\tp.ProjectTreeView.FocusFirst()\n}\n\nfunc (p *TProject) filterTags() {\n\tvar finalTags []string\n\tfor _, tag := range p.ProjectTags {\n\t\tif strings.Contains(tag, *p.projectTagFilterValue) {\n\t\t\tfinalTags = append(finalTags, tag)\n\t\t}\n\t}\n\tp.ProjectTagsFiltered = finalTags\n\tp.TagView.Update(p.ProjectTagsFiltered)\n}\n\nfunc (p *TProject) filterPaths() {\n\tvar finalPaths []string\n\tfor _, path := range p.ProjectPaths {\n\t\tif strings.Contains(path, *p.projectPathFilterValue) {\n\t\t\tfinalPaths = append(finalPaths, path)\n\t\t}\n\t}\n\tp.ProjectPathsFiltered = finalPaths\n\tp.PathView.Update(p.ProjectPathsFiltered)\n}\n\nfunc (p *TProject) selectAllProjects() {\n\tfor _, project := range p.ProjectsFiltered {\n\t\tp.ProjectsSelected[project.Name] = true\n\t}\n\tp.ProjectTableView.UpdateRowStyle()\n\tp.ProjectTreeView.UpdateProjectsStyle()\n}\n\nfunc (p *TProject) selectAllTags() {\n\tfor _, tag := range p.ProjectTagsFiltered {\n\t\tp.ProjectTagsSelected[tag] = true\n\t}\n\tp.TagView.Update(p.ProjectTagsFiltered)\n}\n\nfunc (p *TProject) selectAllPaths() {\n\tfor _, path := range p.ProjectPathsFiltered {\n\t\tp.ProjectPathsSelected[path] = true\n\t}\n\tp.PathView.Update(p.ProjectPathsFiltered)\n}\n\nfunc (p *TProject) unselectAllProjects() {\n\tfor _, project := range p.ProjectsFiltered {\n\t\tp.ProjectsSelected[project.Name] = false\n\t}\n\tp.ProjectTableView.UpdateRowStyle()\n\tp.ProjectTreeView.UpdateProjectsStyle()\n}\n\nfunc (p *TProject) unselectAllTags() {\n\tfor _, tag := range p.ProjectTagsFiltered {\n\t\tp.ProjectTagsSelected[tag] = false\n\t}\n\tp.TagView.Update(p.ProjectTagsFiltered)\n}\n\nfunc (p *TProject) unselectAllPaths() {\n\tfor _, path := range p.ProjectPathsFiltered {\n\t\tp.ProjectPathsSelected[path] = false\n\t}\n\tp.PathView.Update(p.ProjectPathsFiltered)\n}\n\nfunc (p *TProject) showProjectDescModal(name string) {\n\tproject, err := misc.Config.GetProject(name)\n\tif err != nil {\n\t\treturn\n\t}\n\tdescription := print.PrintProjectBlocks([]dao.Project{*project}, true, *misc.BlockTheme, print.TviewFormatter{})\n\tdescriptionNoColor := print.PrintProjectBlocks([]dao.Project{*project}, false, *misc.BlockTheme, print.TviewFormatter{})\n\tcomponents.OpenTextModal(\"project-description-modal\", description, descriptionNoColor, project.Name)\n}\n\nfunc (p *TProject) editProject(projectName string) {\n\tmisc.App.Suspend(func() {\n\t\terr := misc.Config.EditProject(projectName)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "core/tui/views/tui_shortcut_info.go",
    "content": "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 Shortcut struct {\n\tshortcut string\n\tlabel    string\n}\n\nfunc getShortcutInfo(shortcuts []Shortcut) string {\n\tvar formattedShortcuts []string\n\tfor _, s := range shortcuts {\n\t\tvalue := fmt.Sprintf(\"[%s:%s:%s]%s[-:-:-] [%s:%s:%s]%s[-:-:-]\",\n\t\t\tmisc.STYLE_SHORTCUT_LABEL.Fg, misc.STYLE_SHORTCUT_LABEL.Bg, misc.STYLE_SHORTCUT_LABEL.AttrStr, s.label,\n\t\t\tmisc.STYLE_SHORTCUT_TEXT.Fg, misc.STYLE_SHORTCUT_TEXT.Bg, misc.STYLE_SHORTCUT_TEXT.AttrStr, s.shortcut,\n\t\t)\n\t\tformattedShortcuts = append(formattedShortcuts, value)\n\t}\n\treturn strings.Join(formattedShortcuts, \"  \")\n}\n\nfunc CreateRunInfoVIew() *tview.TextView {\n\tshortcuts := []Shortcut{\n\t\t{\"Ctrl-r\", \"Run\"},\n\t\t{\"Ctrl-s\", \"Toggle View\"},\n\t\t{\"Ctrl-e\", \"Toggle Table/Tree\"},\n\t\t{\"Ctrl-o\", \"Options\"},\n\t}\n\ttext := getShortcutInfo(shortcuts)\n\n\thelpInfo := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetText(text)\n\thelpInfo.SetTextAlign(tview.AlignRight)\n\thelpInfo.SetBorderPadding(0, 0, 0, 1)\n\treturn helpInfo\n}\n\nfunc CreateExecInfoView() *tview.TextView {\n\tshortcuts := []Shortcut{\n\t\t{\"Ctrl-r\", \"Run\"},\n\t\t{\"Ctrl-x\", \"Clear\"},\n\t\t{\"Ctrl-s\", \"Toggle View\"},\n\t\t{\"Ctrl-o\", \"Options\"},\n\t}\n\ttext := getShortcutInfo(shortcuts)\n\n\thelpInfo := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetText(text)\n\thelpInfo.SetTextAlign(tview.AlignRight)\n\thelpInfo.SetBorderPadding(0, 0, 0, 1)\n\treturn helpInfo\n}\n\nfunc CreateProjectInfoView() *tview.TextView {\n\tshortcuts := []Shortcut{\n\t\t{\"Ctrl-e\", \"Toggle Table/Tree\"},\n\t}\n\ttext := getShortcutInfo(shortcuts)\n\n\thelpInfo := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetText(text)\n\thelpInfo.SetTextAlign(tview.AlignRight)\n\thelpInfo.SetBorderPadding(0, 0, 0, 1)\n\treturn helpInfo\n}\n\nfunc CreateTaskInfoView() *tview.TextView {\n\tshortcuts := []Shortcut{\n\t\t{\"Ctrl-e\", \"Toggle Table/Tree\"},\n\t}\n\ttext := getShortcutInfo(shortcuts)\n\thelpInfo := tview.NewTextView().\n\t\tSetDynamicColors(true).\n\t\tSetText(text)\n\thelpInfo.SetTextAlign(tview.AlignRight)\n\thelpInfo.SetBorderPadding(0, 0, 0, 1)\n\treturn helpInfo\n}\n"
  },
  {
    "path": "core/tui/views/tui_spec_view.go",
    "content": "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\"github.com/gdamore/tcell/v2\"\n\t\"github.com/rivo/tview\"\n)\n\ntype TSpec struct {\n\tView  *tview.Flex\n\titems []*tview.Box\n\n\t// Spec\n\tOutput            string\n\tClearBeforeRun    bool\n\tParallel          bool\n\tIgnoreErrors      bool\n\tIgnoreNonExisting bool\n\tOmitEmptyRows     bool\n\tOmitEmptyColumns  bool\n}\n\nfunc CreateSpecView() *TSpec {\n\tdefSpec, err := misc.Config.GetSpec(\"default\")\n\tif err != nil {\n\t\tos.Exit(0)\n\t}\n\n\tspec := &TSpec{\n\t\tOutput:            defSpec.Output,\n\t\tClearBeforeRun:    defSpec.ClearOutput,\n\t\tParallel:          defSpec.Parallel,\n\t\tIgnoreErrors:      defSpec.IgnoreErrors,\n\t\tIgnoreNonExisting: defSpec.IgnoreNonExisting,\n\t\tOmitEmptyRows:     defSpec.OmitEmptyRows,\n\t\tOmitEmptyColumns:  defSpec.OmitEmptyColumns,\n\t}\n\n\tview := tview.NewFlex().SetDirection(tview.FlexRow)\n\tview.SetBorder(true).SetBorderPadding(1, 1, 1, 1).\n\t\tSetBorderColor(misc.STYLE_BORDER_FOCUS.Fg).\n\t\tSetBorderPadding(1, 1, 2, 2)\n\tspec.View = view\n\n\t// Output type\n\toutputType := &components.TToggleText{\n\t\tValue:   &spec.Output,\n\t\tOption1: \"stream\",\n\t\tOption2: \"table\",\n\t\tLabel1:  \" Output stream \",\n\t\tLabel2:  \" Output table \",\n\t\tData1:   \"exec-stream\",\n\t\tData2:   \"exec-table\",\n\t}\n\toutputType.Create()\n\n\tclearBeforeRun := spec.AddCheckbox(\"Clear Before Run\", &spec.ClearBeforeRun)\n\tparallel := spec.AddCheckbox(\"Parallel\", &spec.Parallel)\n\tignoreErrors := spec.AddCheckbox(\"Ignore Errors\", &spec.IgnoreErrors)\n\tignoreNonExisting := spec.AddCheckbox(\"Ignore Non Existing\", &spec.IgnoreNonExisting)\n\tomitEmptyRows := spec.AddCheckbox(\"Omit Empty Rows\", &spec.OmitEmptyRows)\n\tomitEmptyColumns := spec.AddCheckbox(\"Omit Empty Columns\", &spec.OmitEmptyColumns)\n\n\t// Add checkboxes\n\tview.AddItem(outputType.TextView, 1, 0, false)\n\tview.AddItem(clearBeforeRun, 1, 0, false)\n\tview.AddItem(parallel, 1, 0, false)\n\tview.AddItem(ignoreErrors, 1, 0, false)\n\tview.AddItem(ignoreNonExisting, 1, 0, false)\n\tview.AddItem(omitEmptyRows, 1, 0, false)\n\tview.AddItem(omitEmptyColumns, 1, 0, false)\n\n\tcheckboxes := []*tview.Box{\n\t\toutputType.TextView.Box,\n\t\tclearBeforeRun.Box,\n\t\tparallel.Box,\n\t\tignoreErrors.Box,\n\t\tignoreNonExisting.Box,\n\t\tomitEmptyRows.Box,\n\t\tomitEmptyColumns.Box,\n\t}\n\n\t// Input\n\tcurrentFocus := 0\n\tview.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {\n\t\tswitch event.Key() {\n\t\tcase tcell.KeyDown:\n\t\t\tif currentFocus < (len(checkboxes) - 1) {\n\t\t\t\tcurrentFocus += 1\n\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyUp:\n\t\t\tif currentFocus > 0 {\n\t\t\t\tcurrentFocus -= 1\n\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t}\n\t\t\treturn nil\n\t\tcase tcell.KeyRune:\n\t\t\tswitch event.Rune() {\n\t\t\tcase 'g': // top\n\t\t\t\tcurrentFocus = 0\n\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t\treturn nil\n\t\t\tcase 'G': // bottom\n\t\t\t\tcurrentFocus = len(checkboxes) - 1\n\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t\treturn nil\n\t\t\tcase 'j': // down\n\t\t\t\tif currentFocus < (len(checkboxes) - 1) {\n\t\t\t\t\tcurrentFocus += 1\n\t\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\tcase 'k': // up\n\t\t\t\tif currentFocus > 0 {\n\t\t\t\t\tcurrentFocus -= 1\n\t\t\t\t\tmisc.App.SetFocus(checkboxes[currentFocus])\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn event\n\t})\n\n\tview.SetFocusFunc(func() {\n\t\tcurrentFocus = 0\n\t\tmisc.App.SetFocus(outputType.TextView)\n\t})\n\n\treturn spec\n}\n\nfunc (spec *TSpec) AddCheckbox(title string, checked *bool) *tview.Checkbox {\n\tonFocus := func() {}\n\tonBlur := func() {}\n\n\tcheckbox := components.Checkbox(title, checked, onFocus, onBlur)\n\tspec.items = append(spec.items, checkbox.Box)\n\treturn checkbox\n}\n"
  },
  {
    "path": "core/tui/views/tui_task_view.go",
    "content": "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\"github.com/alajmo/mani/core/tui/components\"\n\t\"github.com/alajmo/mani/core/tui/misc\"\n\t\"github.com/rivo/tview\"\n)\n\ntype TTask struct {\n\t// UI\n\tPage          *tview.Flex\n\tTaskTableView *components.TTable\n\tTaskTreeView  *components.TTree\n\tContextView   *tview.Flex\n\n\t// Data\n\tTasks           []dao.Task\n\tTasksFiltered   []dao.Task\n\tTasksSelected   map[string]bool\n\tHeaders         []string\n\tShowHeaders     bool\n\tTaskStyle       string\n\ttaskFilterValue *string\n\n\t// Misc\n\tEmitter *misc.EventEmitter\n}\n\nfunc CreateTasksData(\n\ttasks []dao.Task,\n\theaders []string,\n\tprefixNumber int,\n\tshowTitle bool,\n\tshowHeaders bool,\n\tselectEnabled bool,\n) *TTask {\n\tt := &TTask{\n\t\tTasks:           tasks,\n\t\tTasksFiltered:   tasks,\n\t\tTasksSelected:   make(map[string]bool),\n\t\tHeaders:         headers,\n\t\tShowHeaders:     showHeaders,\n\t\tTaskStyle:       \"task-table\",\n\t\ttaskFilterValue: new(string),\n\n\t\tEmitter: misc.NewEventEmitter(),\n\t}\n\n\tfor _, task := range t.Tasks {\n\t\tt.TasksSelected[task.Name] = false\n\t}\n\n\ttitle := \"\"\n\tif showTitle && prefixNumber > 0 {\n\t\ttitle = fmt.Sprintf(\"[%d] Tasks (%d)\", prefixNumber, len(tasks))\n\t} else if showTitle {\n\t\ttitle = fmt.Sprintf(\"Tasks (%d)\", len(tasks))\n\t}\n\n\trows := t.getTableRows()\n\ttaskTable := t.CreateTasksTable(selectEnabled, title, headers, rows)\n\tt.TaskTableView = taskTable\n\n\tnodes := t.getTreeHierarchy()\n\ttaskTree := t.CreateTasksTree(selectEnabled, title, nodes)\n\tt.TaskTreeView = taskTree\n\n\t// Events\n\tt.Emitter.Subscribe(\"remove_task_filter\", func(e misc.Event) {\n\t\tt.TaskTableView.ClearFilter()\n\t\tt.TaskTreeView.ClearFilter()\n\t})\n\tt.Emitter.Subscribe(\"remove_task_selections\", func(event misc.Event) {\n\t\tt.unselectAllTasks()\n\t})\n\tt.Emitter.Subscribe(\"filter_tasks\", func(e misc.Event) {\n\t\tt.filterTasks()\n\t})\n\n\treturn t\n}\n\nfunc (t *TTask) CreateTasksTable(\n\tselectEnabled bool,\n\ttitle string,\n\theaders []string,\n\trows [][]string,\n) *components.TTable {\n\ttable := &components.TTable{\n\t\tTitle:         title,\n\t\tToggleEnabled: selectEnabled,\n\t\tShowHeaders:   t.ShowHeaders,\n\t\tFilterValue:   t.taskFilterValue,\n\t}\n\ttable.Create()\n\ttable.Update(headers, rows)\n\n\t// Methods\n\ttable.IsRowSelected = func(name string) bool {\n\t\treturn t.TasksSelected[name]\n\t}\n\ttable.ToggleSelectRow = func(name string) {\n\t\tt.toggleSelectTask(name)\n\t}\n\ttable.SelectAll = func() {\n\t\tt.selectAllTasks()\n\t}\n\ttable.UnselectAll = func() {\n\t\tt.unselectAllTasks()\n\t}\n\ttable.FilterRows = func() {\n\t\tt.filterTasks()\n\t}\n\ttable.DescribeRow = func(taskName string) {\n\t\tif taskName != \"\" {\n\t\t\tt.showTaskDescModal(taskName)\n\t\t}\n\t}\n\ttable.EditRow = func(taskName string) {\n\t\tif taskName != \"\" {\n\t\t\tt.editTask(taskName)\n\t\t}\n\t}\n\n\treturn table\n}\n\nfunc (t *TTask) CreateTasksTree(\n\tselectEnabled bool,\n\ttitle string,\n\tnodes []components.TNode,\n) *components.TTree {\n\ttree := &components.TTree{\n\t\tTitle:         title,\n\t\tRootTitle:     \"\",\n\t\tSelectEnabled: selectEnabled,\n\t\tFilterValue:   t.taskFilterValue,\n\t}\n\ttree.Create()\n\ttree.UpdateTasks(nodes)\n\ttree.UpdateTasksStyle()\n\n\ttree.IsNodeSelected = func(name string) bool {\n\t\treturn t.TasksSelected[name]\n\t}\n\ttree.ToggleSelectNode = func(name string) {\n\t\tt.toggleSelectTask(name)\n\t}\n\ttree.SelectAll = func() {\n\t\tt.selectAllTasks()\n\t}\n\ttree.UnselectAll = func() {\n\t\tt.unselectAllTasks()\n\t}\n\ttree.FilterNodes = func() {\n\t\tt.filterTasks()\n\t}\n\ttree.DescribeNode = func(taskName string) {\n\t\tif taskName != \"\" {\n\t\t\tt.showTaskDescModal(taskName)\n\t\t}\n\t}\n\ttree.EditNode = func(taskName string) {\n\t\tif taskName != \"\" {\n\t\t\tt.editTask(taskName)\n\t\t}\n\t}\n\n\treturn tree\n}\n\nfunc (t *TTask) getTableRows() [][]string {\n\tvar rows = make([][]string, len(t.TasksFiltered))\n\tfor i, task := range t.TasksFiltered {\n\t\trows[i] = make([]string, len(t.Headers))\n\t\tfor j, header := range t.Headers {\n\t\t\trows[i][j] = task.GetValue(header, 0)\n\t\t}\n\t}\n\treturn rows\n}\n\nfunc (t *TTask) getTreeHierarchy() []components.TNode {\n\tvar nodes = []components.TNode{}\n\tfor _, task := range t.TasksFiltered {\n\t\tparentNode := &components.TNode{\n\t\t\tDisplayName: task.Name,\n\t\t\tID:          task.Name,\n\t\t\tType:        \"task\",\n\t\t\tChildren:    &[]components.TNode{},\n\t\t}\n\n\t\t// Sub-commands\n\t\tnodes = append(nodes, *parentNode)\n\t\tfor _, subTask := range task.Commands {\n\t\t\tvar node *components.TNode\n\t\t\tif subTask.TaskRef != \"\" {\n\t\t\t\tnode = &components.TNode{\n\t\t\t\t\tDisplayName: subTask.Name,\n\t\t\t\t\tID:          task.Name,\n\t\t\t\t\tType:        \"task-ref\",\n\t\t\t\t\tChildren:    &[]components.TNode{},\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif subTask.Name == \"\" {\n\t\t\t\t\tsubTask.Name = \"cmd\"\n\t\t\t\t}\n\t\t\t\tnode = &components.TNode{\n\t\t\t\t\tDisplayName: subTask.Name,\n\t\t\t\t\tID:          task.Name,\n\t\t\t\t\tType:        \"command\",\n\t\t\t\t\tChildren:    &[]components.TNode{},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t*parentNode.Children = append(*parentNode.Children, *node)\n\t\t}\n\t}\n\n\treturn nodes\n}\n\nfunc (t *TTask) toggleSelectTask(name string) {\n\tt.TasksSelected[name] = !t.TasksSelected[name]\n\tt.TaskTableView.ToggleSelectCurrentRow(name)\n\tt.TaskTreeView.ToggleSelectCurrentNode(name)\n}\n\nfunc (t *TTask) filterTasks() {\n\tvar finalTasks []dao.Task\n\tfor _, task := range t.Tasks {\n\t\tif strings.Contains(task.Name, *t.taskFilterValue) {\n\t\t\tfinalTasks = append(finalTasks, task)\n\t\t}\n\t}\n\tt.TasksFiltered = finalTasks\n\n\t// Table\n\trows := t.getTableRows()\n\tt.TaskTableView.Update(t.Headers, rows)\n\tt.TaskTableView.Table.ScrollToBeginning()\n\tt.TaskTableView.Table.Select(1, 0)\n\n\t// Tree\n\ttaskTree := t.getTreeHierarchy()\n\tt.TaskTreeView.UpdateTasks(taskTree)\n\tt.TaskTreeView.UpdateTasksStyle()\n\tt.TaskTreeView.FocusFirst()\n}\n\nfunc (t *TTask) selectAllTasks() {\n\tfor _, task := range t.TasksFiltered {\n\t\tt.TasksSelected[task.Name] = true\n\t}\n\tt.TaskTableView.UpdateRowStyle()\n\tt.TaskTreeView.UpdateTasksStyle()\n}\n\nfunc (t *TTask) unselectAllTasks() {\n\tfor _, task := range t.TasksFiltered {\n\t\tt.TasksSelected[task.Name] = false\n\t}\n\tt.TaskTableView.UpdateRowStyle()\n\tt.TaskTreeView.UpdateTasksStyle()\n}\n\nfunc (t *TTask) showTaskDescModal(name string) {\n\ttask, err := misc.Config.GetTask(name)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tdescription := print.PrintTaskBlock([]dao.Task{*task}, true, *misc.BlockTheme, print.TviewFormatter{})\n\tdescriptionNoColor := print.PrintTaskBlock([]dao.Task{*task}, false, *misc.BlockTheme, print.TviewFormatter{})\n\tcomponents.OpenTextModal(\"task-description-modal\", description, descriptionNoColor, task.Name)\n}\n\nfunc (t *TTask) editTask(taskName string) {\n\tmisc.App.Suspend(func() {\n\t\terr := misc.Config.EditTask(taskName)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "core/tui/watcher.go",
    "content": "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 ...string) {\n\tif len(files) < 1 {\n\t\treturn\n\t}\n\n\tw, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\t_ = w.Close()\n\t\t}()\n\n\t\tfor _, p := range files {\n\t\t\tst, err := os.Lstat(p)\n\t\t\tif err != nil {\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif st.IsDir() {\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\terr = w.Add(filepath.Dir(p))\n\t\t\tif err != nil {\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\n\t\tlastMod := make(map[string]time.Time)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase _, ok := <-w.Errors:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tos.Exit(1)\n\t\t\tcase e, ok := <-w.Events:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor _, f := range files {\n\t\t\t\t\tif f == e.Name {\n\t\t\t\t\t\tstat, err := os.Stat(f)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcurrentMod := stat.ModTime()\n\t\t\t\t\t\tif lastMod[f] != currentMod {\n\t\t\t\t\t\t\t// TODO: For some reason, the reload is not working correctly, must be due to it being called in a goroutine\n\t\t\t\t\t\t\t// Sleeping resolves it somehow.\n\t\t\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\t\t\t\tapp.Reload()\n\t\t\t\t\t\t\tlastMod[f] = currentMod\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "core/utils.go",
    "content": "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\"\n\t\"strings\"\n)\n\nconst ANSI = \"[\\u001B\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[a-zA-Z\\\\d]*)*)?\\u0007)|(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PRZcf-ntqry=><~]))\"\n\nvar RE = regexp.MustCompile(ANSI)\n\nfunc Strip(str string) string {\n\treturn RE.ReplaceAllString(str, \"\")\n}\n\nfunc Intersection(a []string, b []string) []string {\n\tvar i []string\n\tfor _, s := range a {\n\t\tif slices.Contains(b, s) {\n\t\t\ti = append(i, s)\n\t\t}\n\t}\n\n\treturn i\n}\n\nfunc GetWdRemoteURL(path string) (string, error) {\n\tgitDir := filepath.Join(path, \".git\")\n\tif _, err := os.Stat(gitDir); !os.IsNotExist(err) {\n\t\treturn GetRemoteURL(path)\n\t}\n\n\treturn \"\", nil\n}\n\nfunc GetRemoteURL(path string) (string, error) {\n\tcmd := exec.Command(\"git\", \"config\", \"--get\", \"remote.origin.url\")\n\tcmd.Dir = path\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", nil\n\t}\n\n\treturn strings.TrimSuffix(string(output), \"\\n\"), nil\n}\n\n// GetWorktreeList returns a map of worktrees (absolute path -> branch) for a git repo\n// Excludes the main worktree (the repo itself)\nfunc GetWorktreeList(repoPath string) (map[string]string, error) {\n\tcmd := exec.Command(\"git\", \"worktree\", \"list\", \"--porcelain\")\n\tcmd.Dir = repoPath\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tworktrees := make(map[string]string)\n\tcleanRepoPath := filepath.Clean(repoPath)\n\tvar currentPath string\n\n\tfor line := range strings.SplitSeq(string(output), \"\\n\") {\n\t\tif path, found := strings.CutPrefix(line, \"worktree \"); found {\n\t\t\tcurrentPath = filepath.Clean(path)\n\t\t} else if branch, found := strings.CutPrefix(line, \"branch refs/heads/\"); found {\n\t\t\t// Skip the main worktree (same as repoPath)\n\t\t\tif currentPath != cleanRepoPath {\n\t\t\t\tworktrees[currentPath] = branch\n\t\t\t}\n\t\t}\n\t\t// Detached HEAD worktrees (line == \"detached\") are intentionally\n\t\t// ignored — they have no branch to track.\n\t}\n\n\treturn worktrees, nil\n}\n\nfunc FindFileInParentDirs(path string, files []string) (string, error) {\n\tfor _, file := range files {\n\t\tpathToFile := filepath.Join(path, file)\n\t\tif _, err := os.Stat(pathToFile); err == nil {\n\t\t\treturn pathToFile, nil\n\t\t}\n\t}\n\n\tparentDir := filepath.Dir(path)\n\tif parentDir == path {\n\t\treturn \"\", &ConfigNotFound{files}\n\t}\n\n\treturn FindFileInParentDirs(parentDir, files)\n}\n\nfunc GetRelativePath(configDir string, path string) (string, error) {\n\trelPath, err := filepath.Rel(configDir, path)\n\treturn relPath, err\n}\n\n// Get the absolute path\n// Need to support following path types:\n//\n//\tlala/land\n//\t./lala/land\n//\t../lala/land\n//\t/lala/land\n//\t$HOME/lala/land\n//\t~/lala/land\n//\t~root/lala/land\nfunc GetAbsolutePath(configDir string, path string, name string) (string, error) {\n\tpath = os.ExpandEnv(path)\n\n\tusr, err := user.Current()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thomeDir := usr.HomeDir\n\n\t// TODO: Remove any .., make path absolute and then cut of configDir\n\tif path == \"~\" {\n\t\tpath = homeDir\n\t} else if strings.HasPrefix(path, \"~/\") {\n\t\tpath = filepath.Join(homeDir, path[2:])\n\t} else if len(path) > 0 && filepath.IsAbs(path) { // TODO: Rewrite this\n\t} else if len(path) > 0 {\n\t\tpath = filepath.Join(configDir, path)\n\t} else {\n\t\tpath = filepath.Join(configDir, name)\n\t}\n\n\treturn path, nil\n}\n\n// Get the absolute path\n// Need to support following path types:\n//\n//\tlala/land\n//\t./lala/land\n//\t../lala/land\n//\t/lala/land\n//\t$HOME/lala/land\n//\t~/lala/land\n//\t~root/lala/land\nfunc ResolveTildePath(path string) (string, error) {\n\tpath = os.ExpandEnv(path)\n\n\tusr, err := user.Current()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thomeDir := usr.HomeDir\n\n\tvar p string\n\tif path == \"~\" {\n\t\tp = homeDir\n\t} else if strings.HasPrefix(path, \"~/\") {\n\t\tp = filepath.Join(homeDir, path[2:])\n\t} else {\n\t\tp = path\n\t}\n\n\treturn p, nil\n}\n\n// FormatShell returns the shell program and associated command flag\nfunc FormatShell(shell string) string {\n\ts := strings.Split(shell, \" \")\n\n\tif len(s) > 1 { // User provides correct flag, bash -c, /bin/bash -c, /bin/sh -c\n\t\treturn shell\n\t} else if strings.Contains(shell, \"bash\") { // bash, /bin/bash\n\t\treturn shell + \" -c\"\n\t} else if strings.Contains(shell, \"zsh\") { // zsh, /bin/zsh\n\t\treturn shell + \" -c\"\n\t} else if strings.Contains(shell, \"sh\") { // sh, /bin/sh\n\t\treturn shell + \" -c\"\n\t} else if strings.Contains(shell, \"node\") { // node, /bin/node\n\t\treturn shell + \" -e\"\n\t} else if strings.Contains(shell, \"python\") { // python, /bin/python\n\t\treturn shell + \" -c\"\n\t}\n\t// TODO: Add fish and other shells\n\n\treturn shell\n}\n\n// FormatShellString returns the shell program (bash,sh,.etc) along with the\n// command flag and subsequent commands\n// Example:\n// \"bash\", \"-c echo hello world\"\nfunc FormatShellString(shell string, command string) (string, []string) {\n\tshellProgram := FormatShell(shell)\n\targs := strings.SplitN(shellProgram, \" \", 2)\n\treturn args[0], append(args[1:], command)\n}\n\n// Used when creating pointers to literal. Useful when you want set/unset attributes.\nfunc Ptr[T any](t T) *T {\n\treturn &t\n}\n\nfunc StringsToErrors(str []string) []error {\n\terrs := []error{}\n\tfor _, s := range str {\n\t\terrs = append(errs, errors.New(s))\n\t}\n\n\treturn errs\n}\n\nfunc DebugPrint(data any) {\n\ts, _ := json.MarshalIndent(data, \"\", \"\\t\")\n\tfmt.Println()\n\tfmt.Print(string(s))\n\tfmt.Println()\n}\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "# Changelog\n\n## Unreleased\n\n### Features\n\n- Added Git worktree support for projects [#119](https://github.com/alajmo/mani/issues/119)\n  - Define worktrees in project config with `path` (required) and `branch` (optional, defaults to path basename)\n  - `mani init` auto-discovers existing worktrees using `git worktree list`\n  - `mani sync` creates worktrees defined in config\n  - Worktrees can be inside or outside the parent project directory\n- Added `remove_orphaned_worktrees` config option to remove worktrees not in config\n- Added `--remove-orphaned-worktrees` / `-w` flag to `mani sync`\n\n### Fixes\n\n- Fixed TUI to always show Tags/Paths panes even when empty\n- Fixed TUI search/filter label showing raw color tags when using default theme\n- Fixed `mani init` to only add root directory as project when inside a git repo\n\n## 0.31.2\n\n### Fixes\n\n- Fixed `--tags-expr` flag to allow special characters in tag names (matching config file behavior) [#116](https://github.com/alajmo/mani/issues/116)\n- Fixed infinite recursion on Windows when finding mani config [#113](https://github.com/alajmo/mani/issues/113) [contributor: @aabiskar1]\n\n## 0.31.1\n\n### Fixes\n\n- Fix panic when running task for a repository with a long name [#111](https://github.com/alajmo/mani/issues/111)\n\n## 0.31.0\n\n### Features\n\n- Support fuzzy path selection #101 [contributor: @lucas-bremond]\n\n## 0.30.1\n\n### Fixes\n\n- Reset task target when providing runtime flags [#92](https://github.com/alajmo/mani/issues/92)\n\n## 0.30.0\n\n### Features\n\n- Added a sub-command to launch a TUI\n- Added `--forks` flag to limit parallel task execution [#74](https://github.com/alajmo/mani/issues/74)\n- Added `--target` specification from flags [#82](https://github.com/alajmo/mani/issues/82)\n- Added `--spec` specification from flags\n- Added `--ignore-sync-state` flag to `mani sync` to ignore `sync` status set projects [#83](https://github.com/alajmo/mani/issues/83)\n- Added `--tags-expr` flag for complex tag filtering expressions (e.g., (active || git) targets projects with either active or git tag) [#85](https://github.com/alajmo/mani/issues/85)\n- Added `--sync-gitignore` flag to opt out of `.gitignore` file modifications [#87](https://github.com/alajmo/mani/issues/87)\n- Added `tty` attribute to tasks which will replace the command and allow attaching to docker containers\n\n### Fixes\n\n- Fixed `mani init` behavior when root directory contains `.git` [#78](https://github.com/alajmo/mani/issues/78)\n- Fixed `mani sync` execution when running `mani init` with remotes [#84](https://github.com/alajmo/mani/issues/84)\n- Fixed table column truncation when output exceeds terminal width\n\n### Misc\n\n- Changed filtering tags/paths behavior to use intersection instead of union\n- Changed default shell from `sh` to `bash`\n- Improved multiple task execution by treating them as sub-commands for cleaner output\n- Renamed `--no-color` flag to `--color`\n- Changed output `text` to `stream` for all outputs (`flags`, `themes`, and `spec`)\n- Updated theme configuration system\n- Enhanced remote management: `mani` now removes git remotes if specified via global field `sync_remotes` config or flag `--sync-remotes`\n\n## 0.25.0\n\n### Features\n\n- Add more box styles to table and tree output\n\n### Misc\n\n- Update golang to 1.20.0\n\n## 0.24.0\n\n### Features\n\n- Add ability to create/sync remotes\n\n## 0.23.0\n\n### Features\n\n- Add option `--ignore-non-existings` to ignore projects that don't exist\n- Add flag `--ignore-errors` to ignore errors\n\n## 0.22.0\n\n### Features\n\n- Add filter options to sub-command sync [#52](https://github.com/alajmo/mani/pull/52)\n- Add check sub-command to validate mani config\n- Add option to disable spinner when running tasks [#54](https://github.com/alajmo/mani/pull/54)\n\n### Fixes\n\n- Fix wrongly formatted YAML for init command\n\n## 0.21.0\n\n### Features\n\n- Add path and url env to project clone command\n\n## 0.20.1\n\n### Fixes\n\n- Fix evaluate env for MANI_CONFIG and MANI_USER_CONFIG\n- Fix parallel sync, limit to 20 projects at a time\n\n### Changes\n\n- Use `mani --version` flag instead of `mani version`\n\n## 0.20.0\n\nA lot of refactoring and some new features added. There's also some breaking changes, notably how themes work.\n\n### Features\n\n- Add option to skip sync on projects by setting `sync` property to `false`\n- Add flag to disable colors and respect NO_COLOR env variable when set\n- Add env variables MANI_CONFIG and MANI_USER_CONFIG that checks main config and user config\n- Add desc of tasks when auto-completing\n- Add man page generation\n- [BREAKING CHANGE]: Major theme overhaul, allow granular theme modification\n\n### Fix\n\n- Don't automatically create the `$XDG_CONFIG_HOME/mani/config.yaml` file\n- Fix overriding spec data (parallel and omit-empty-columns) with flags\n- Fix when initializing mani with multiple repos having the same name [#30](https://github.com/alajmo/mani/issues/30), thanks to https://github.com/stessaris for finding the bug\n- Omit empty now checks all command outputs, and omits iff all of them are empty\n- Start spinner after 500 ms to avoid flickering when running commands which take less than 500 ms to execute\n\n### Changes\n\n- [BREAKING CHANGE]: Remove no-headers flag\n- [BREAKING CHANGE]: Remove no-borders flag and enable it to be configurable via theme\n- [BREAKING CHANGE]: Removed default env variables that was set previously (MANI_PROJECT_PATH, .etc)\n- Remove some acceptable mani config filenames (notably, those that do not end in .yaml/.yml)\n- Update task and project describe\n- Improve error messages\n\n### Internal\n\n- A lot of refactoring\n  - Rework exec.Cmd\n  - Remove aurora color library dependency and use the one provided by go-pretty\n\n## v0.12.2\n\n### Fixes\n\n- Allow placing mani config inside one of directories of a mani project when syncing\n\n## v0.12.0\n\n### Features\n\n- Add option to omit empty results\n- Add --vcs flag to mani init to choose vcs\n- Add default import from user config directory\n- [BREAKING CHANGE]: Add spec property to allow reusing common properties\n- Add target property to allow reusing common properties\n\n### Fixes\n\n- Fix header bug in run print when task has both commands and cmd\n- Fix `mani edit` to run even if config file is malformed (wrong YAML syntax)\n\n### Misc\n\n- [BREAKING CHANGE]: Move tree feature to list projects as a flag instead of it being a special sub-command\n- [BREAKING CHANGE]: Rename flag --all-projects to all\n- Remove legacy code related to Dirs entity\n- Change default value of --parallel flag to false when syncing\n- Allow omitting the -c when specifying shell for bash, zsh, sh, node and python\n\n## v0.11.1\n\n### Fixes\n\n- Use syncmap to allow safe concurrent writes when running `mani sync` in parallel, previously there was a race condition that occurred when cloning many repos\n\n### Features\n\n- Add `env` property to projects to enable project specific variables\n\n## v0.10.0\n\n### Features\n\n- Add ability to import projects, tasks and themes\n- Possible to run tasks in parallel now per each project\n- Add sub-commands project/task to edit command to open editor at line corresponding to project/task\n- Add edit flag to describe/run sub-commands to open up editor\n- Sync projects in parallel by default and add flag serial to opt out\n- Add support for referencing commands in Commands property\n- Run commands in serial, if one fails, dont run other tasks\n- Add directory entity, similar to project, just without a url/clone property\n\n### Misc\n\n- Add new acceptable filenames Manifile, Manifile.yaml, Manifile.yml\n- Don't create .gitignore if no projects with url exists on mani init/sync\n- List tags now shows associated dirs/projects\n- If user uses a cwd/tag/project/dir flag, then disable task targets\n- [BREAKING CHANGE:] A lot of syntax changes, use object notation instead of array list for projects, themes and tasks\n\n## v0.6.1\n\n### Features\n\n- Add dirs filtering property to commands struct\n\n### Fixes\n\n- Correct project path in gitignore file when running mani init\n\n### Misc\n\n- Update help text for dirs flag\n\n## v0.6.0\n\n### Features\n\n- New tree command that list contents of projects in a tree-like format\n- Add filtering on directory for tree/list/describe/run/exec cmd\n- Add global environment variables\n- Add describe flag to run cmd to suppress command information\n- Add sub-commands\n- Add possibility to run multiple commands from cli\n- Add default tags/projects/output to tasks\n- Add new table style that can be configured only from mani config\n- Add progress spinner for run/exec cmd\n\n### Misc\n\n- [BREAKING CHANGE]: Renamed args in command block to env\n- [BREAKING CHANGE]: Renamed commands in root block to tasks\n- Environment variables now support shell execution\n- Rename flag format to output when listing\n\n## v0.5.1\n\n### Fixes\n\n- Fix auto-complete for flag format in list command\n\n## v0.5.0\n\n### Features\n\n- Add MANI environment variable that is cwd of the current context mani.yaml file\n- Add mani edit command which opens mani.yaml in preferred editor\n- Add describe cmd, display commands and projects in detail\n- Append default shell to commands\n- Add output formats table, markdown and html\n- Add no-borders, no-headers flags to print\n- Allow users to specify headers to be printed in list command\n- Sync creates gitignore file if not found\n- Use CLI spinner when syncing projects\n- Update info cmd to print git version\n\n### Fixes\n\n- Output args at top for run commands instead of for each run\n- Output error message when running commands in non-mani directory that require mani config\n\n### Misc\n\n- Refactor and make code more DRY\n- Refactor list and describe cmd to use sub-commands\n- With no projects to sync, output helpful message: \"No projects to sync\"\n- With all projects synced, output helpful message: \"All projects synced\"\n\n## v0.4.0\n\n### Features\n\n- Allow users to set global and command level shell commands\n\n## v0.3.0\n\n### Features\n\n- Add support for running from nested sub-directories\n- Add info sub-command that shows which configuration file is being used\n- Add flag to point to config file\n- Accept different config names (.mani, .mani.yaml, .mani.yml, mani.yaml, mani.yml)\n- Add new command exec to run arbitrary command\n- Add config flag\n- Add first argument to init should be path, if empty, current dir\n- Add completion for all commands bash\n- Update auto-discovery to equal true by default\n- Add option to filter list command on tags and projects\n- Add Nicer output on failed git sync\n- Add cwd flag to target current directory\n- Add comment section in .gitignore so users can modify the gitignore without mani overwriting all parts\n- Improved listing for projects/tags\n\n### Fixes\n\n- Fix crashing on not found config file\n- Check possible, non-handled nil/err values\n- Don't add project to gitignore if doesn't have a url\n- Remove path if path is same as name\n- Fix gitignore sync, removing old entries\n- Fix broken init command\n- Fix so path accepts environment variables\n- Fix auto-complete when not in mani directory\n\n### Misc\n\n- Update golang version and dependencies\n- Add integration tests\n\n\n\n"
  },
  {
    "path": "docs/commands.md",
    "content": "# Commands\n\n## mani\n\nrepositories manager and task runner\n\n### Synopsis\n\nmani is a CLI tool that helps you manage multiple repositories.\n\nIt's useful when you are working with microservices, multi-project systems, multiple libraries, or just a collection \nof repositories and want a central place for pulling all repositories and running commands across them.\n\n### Options\n\n```\n      --color                enable color (default true)\n  -c, --config string        specify config\n  -h, --help                 help for mani\n  -u, --user-config string   specify user config\n```\n\n## run\n\nRun tasks\n\n### Synopsis\n\nRun tasks.\n\nThe tasks are specified in a mani.yaml file along with the projects you can target.\n\n```\nrun <task>\n```\n\n### Examples\n\n```\n  # Execute task for all projects\n  mani run <task> --all\n\n  # Execute a task in parallel with a maximum of 8 concurrent processes\n  mani run <task> --projects <project> --parallel --forks 8\n\n  # Execute task for a specific projects\n  mani run <task> --projects <project>\n\n  # Execute a task for projects with specific tags\n  mani run <task> --tags <tag>\n\n  # Execute a task for projects matching specific paths\n  mani run <task> --paths <path>\n\n  # Execute a task for all projects matching a tag expression\n  mani run <task> --tags-expr 'active || git' <tag>\n\n  # Execute a task with environment variables from shell\n  mani run <task> key=value\n```\n\n### Options\n\n```\n  -a, --all                   select all projects\n  -k, --cwd                   select current working directory\n      --describe              display task information\n      --dry-run               display the task without execution\n  -e, --edit                  edit task\n  -f, --forks uint32          maximum number of concurrent processes (default 4)\n  -h, --help                  help for run\n      --ignore-errors         continue execution despite errors\n      --ignore-non-existing   skip non-existing projects\n      --omit-empty-columns    hide empty columns in table output\n      --omit-empty-rows       hide empty rows in table output\n  -o, --output string         set output format [stream|table|markdown|html]\n      --parallel              execute tasks in parallel across projects\n  -d, --paths strings         select projects by path\n  -p, --projects strings      select projects by name\n  -s, --silent                hide progress output during task execution\n  -J, --spec string           set spec\n  -t, --tags strings          select projects by tag\n  -E, --tags-expr string      select projects by tags expression\n  -T, --target string         select projects by target name\n      --theme string          set theme\n      --tty                   replace current process\n```\n\n## exec\n\nExecute arbitrary commands\n\n### Synopsis\n\nExecute arbitrary commands.\nUse single quotes around your command to prevent file globbing and \nenvironment variable expansion from occurring before the command is \nexecuted in each directory.\n\n```\nexec <command> [flags]\n```\n\n### Examples\n\n```\n  # List files in all projects\n  mani exec --all ls\n\n  # List git files with markdown suffix in all projects\n  mani exec --all 'git ls-files | grep -e \".md\"'\n```\n\n### Options\n\n```\n  -a, --all                   target all projects\n  -k, --cwd                   use current working directory\n      --dry-run               print commands without executing them\n  -f, --forks uint32          maximum number of concurrent processes (default 4)\n  -h, --help                  help for exec\n      --ignore-errors         ignore errors\n      --ignore-non-existing   ignore non-existing projects\n      --omit-empty-columns    omit empty columns in table output\n      --omit-empty-rows       omit empty rows in table output\n  -o, --output string         set output format [stream|table|markdown|html]\n      --parallel              run tasks in parallel across projects\n  -d, --paths strings         select projects by path\n  -p, --projects strings      select projects by name\n  -s, --silent                hide progress when running tasks\n  -J, --spec string           set spec\n  -t, --tags strings          select projects by tag\n  -E, --tags-expr string      select projects by tags expression\n  -T, --target string         target projects by target name\n      --theme string          set theme\n      --tty                   replace current process\n```\n\n## init\n\nInitialize a mani repository\n\n### Synopsis\n\nInitialize a mani repository.\n\nCreates a mani.yaml configuration file in the current directory. When inside a git\nrepository, it also creates/updates .gitignore. When auto-discovery is enabled,\nit finds Git repositories and their worktrees.\n\n```\ninit [flags]\n```\n\n### Examples\n\n```\n  # Initialize with default settings (discovers repos and worktrees)\n  mani init\n\n  # Initialize without auto-discovering projects\n  mani init --auto-discovery=false\n\n  # Initialize without updating .gitignore\n  mani init --sync-gitignore=false\n```\n\n### Options\n\n```\n      --auto-discovery   automatically discover and add Git repositories and worktrees to mani.yaml (default true)\n  -h, --help             help for init\n  -g, --sync-gitignore   synchronize .gitignore file (default true)\n```\n\n## sync\n\nClone repositories and update .gitignore\n\n### Synopsis\n\nClone repositories and update .gitignore file.\nFor repositories requiring authentication, disable parallel cloning to enter\ncredentials for each repository individually.\n\n```\nsync [flags]\n```\n\n### Examples\n\n```\n  # Clone repositories one at a time\n  mani sync\n\n  # Clone repositories in parallel\n  mani sync --parallel\n\n  # Disable updating .gitignore file\n  mani sync --sync-gitignore=false\n\n  # Sync project remotes. This will modify the projects .git state\n  mani sync --sync-remotes\n\n  # Create worktrees defined in config (default behavior)\n  mani sync\n\n  # Remove worktrees not defined in config\n  mani sync --remove-orphaned-worktrees\n\n  # Clone repositories even if project sync field is set to false\n  mani sync --ignore-sync-state\n\n  # Display sync status\n  mani sync --status\n```\n\n### Options\n\n```\n  -f, --forks uint32                maximum number of concurrent processes (default 4)\n  -h, --help                        help for sync\n      --ignore-sync-state           sync project even if the project's sync field is set to false\n  -p, --parallel                    clone projects in parallel\n  -d, --paths strings               clone projects by path\n  -w, --remove-orphaned-worktrees   remove git worktrees not in config\n  -s, --status                      display status only\n  -g, --sync-gitignore              sync gitignore (default true)\n  -r, --sync-remotes                update git remote state\n  -t, --tags strings                clone projects by tags\n  -E, --tags-expr string            clone projects by tag expression\n```\n\n## edit\n\nOpen up mani config file\n\n### Synopsis\n\nOpen up mani config file in $EDITOR.\n\n```\nedit [flags]\n```\n\n### Examples\n\n```\n  # Edit current context\n  mani edit\n```\n\n### Options\n\n```\n  -h, --help   help for edit\n```\n\n## edit project\n\nEdit mani project\n\n### Synopsis\n\nEdit mani project in $EDITOR.\n\n```\nedit project [project] [flags]\n```\n\n### Examples\n\n```\n  # Edit projects\n  mani edit project\n\n  # Edit project <project>\n  mani edit project <project>\n```\n\n### Options\n\n```\n  -h, --help   help for project\n```\n\n## edit task\n\nEdit mani task\n\n### Synopsis\n\nEdit mani task in $EDITOR.\n\n```\nedit task [task] [flags]\n```\n\n### Examples\n\n```\n  # Edit tasks\n  mani edit task\n\n  # Edit task <task>\n  mani edit task <task>\n```\n\n### Options\n\n```\n  -h, --help   help for task\n```\n\n## list projects\n\nList projects\n\n### Synopsis\n\nList projects.\n\n```\nlist projects [projects] [flags]\n```\n\n### Examples\n\n```\n  # List all projects\n  mani list projects\n\n  # List projects by name\n  mani list projects <project>\n\n  # List projects by tags\n  mani list projects --tags <tag>\n\n  # List projects by paths\n  mani list projects --paths <path>\n\n\t# List projects matching a tag expression\n\tmani run <task> --tags-expr '<tag-1> || <tag-2>'\n```\n\n### Options\n\n```\n  -a, --all                select all projects (default true)\n  -k, --cwd                select current working directory\n      --headers strings    specify columns to display [project, path, relpath, description, url, tag, worktree] (default [project,tag,description])\n  -h, --help               help for projects\n  -d, --paths strings      select projects by paths\n  -t, --tags strings       select projects by tags\n  -E, --tags-expr string   select projects by tags expression\n  -T, --target string      select projects by target name\n      --tree               display output in tree format\n```\n\n### Options inherited from parent commands\n\n```\n  -o, --output string   set output format [table|markdown|html] (default \"table\")\n      --theme string    set theme (default \"default\")\n```\n\n## list tags\n\nList tags\n\n### Synopsis\n\nList tags.\n\n```\nlist tags [tags] [flags]\n```\n\n### Examples\n\n```\n  # List all tags\n  mani list tags\n```\n\n### Options\n\n```\n      --headers strings   specify columns to display [project, tag] (default [tag,project])\n  -h, --help              help for tags\n```\n\n### Options inherited from parent commands\n\n```\n  -o, --output string   set output format [table|markdown|html] (default \"table\")\n      --theme string    set theme (default \"default\")\n```\n\n## list tasks\n\nList tasks\n\n### Synopsis\n\nList tasks.\n\n```\nlist tasks [tasks] [flags]\n```\n\n### Examples\n\n```\n  # List all tasks\n  mani list tasks\n\n  # List tasks by name\n  mani list task <task>\n```\n\n### Options\n\n```\n      --headers strings   specify columns to display [task, description, target, spec] (default [task,description])\n  -h, --help              help for tasks\n```\n\n### Options inherited from parent commands\n\n```\n  -o, --output string   set output format [table|markdown|html] (default \"table\")\n      --theme string    set theme (default \"default\")\n```\n\n## describe projects\n\nDescribe projects\n\n### Synopsis\n\nDescribe projects.\n\n```\ndescribe projects [projects] [flags]\n```\n\n### Examples\n\n```\n  # Describe all projects\n  mani describe projects\n\n  # Describe projects by name\n  mani describe projects <project>\n\n  # Describe projects by tags\n  mani describe projects --tags <tag>\n\n  # Describe projects by paths\n  mani describe projects --paths <path>\n\n\t# Describe projects matching a tag expression\n\tmani run <task> --tags-expr '<tag-1> || <tag-2>'\n```\n\n### Options\n\n```\n  -a, --all                select all projects (default true)\n  -k, --cwd                select current working directory\n  -e, --edit               edit project\n  -h, --help               help for projects\n  -d, --paths strings      filter projects by paths\n  -t, --tags strings       filter projects by tags\n  -E, --tags-expr string   target projects by tags expression\n  -T, --target string      target projects by target name\n```\n\n### Options inherited from parent commands\n\n```\n      --theme string   set theme (default \"default\")\n```\n\n## describe tasks\n\nDescribe tasks\n\n### Synopsis\n\nDescribe tasks.\n\n```\ndescribe tasks [tasks] [flags]\n```\n\n### Examples\n\n```\n  # Describe all tasks\n  mani describe tasks\n\n  # Describe task <task>\n  mani describe task <task>\n```\n\n### Options\n\n```\n  -e, --edit   edit task\n  -h, --help   help for tasks\n```\n\n### Options inherited from parent commands\n\n```\n      --theme string   set theme (default \"default\")\n```\n\n## tui\n\nTUI\n\n### Synopsis\n\nRun TUI\n\n```\ntui [flags]\n```\n\n### Examples\n\n```\n  # Open tui\n  mani tui\n```\n\n### Options\n\n```\n  -h, --help               help for tui\n  -r, --reload-on-change   reload mani on config change\n      --theme string       set theme (default \"default\")\n```\n\n## check\n\nValidate config\n\n### Synopsis\n\nValidate config.\n\n```\ncheck [flags]\n```\n\n### Examples\n\n```\n  # Validate config\n  mani check\n```\n\n### Options\n\n```\n  -h, --help   help for check\n```\n\n## gen\n\nGenerate man page\n\n```\ngen\n```\n\n### Options\n\n```\n  -d, --dir string   directory to save manpage to (default \"./\")\n  -h, --help         help for gen\n```\n\n"
  },
  {
    "path": "docs/config.md",
    "content": "# Config\n\nThe mani.yaml config is based on the following concepts:\n\n- **projects** are directories, which may be git repositories, in which case they have an URL attribute\n- **tasks** are shell commands that you write and then run for selected **projects**\n- **specs** are configs that alter **task** execution and output\n- **targets** are configs that provide shorthand filtering of **projects** when executing **tasks**\n- **themes** are used to modify the output of `mani` commands\n- **env** are environment variables that can be defined globally, per project and per task\n\n**Specs**, **targets** and **themes** use a default object by default that the user can override to modify execution of mani commands.\n\nCheck the [files](#files) and [environment](#environment) section to see how the config file is loaded.\n\nBelow is a config file detailing all of the available options and their defaults.\n\n```yaml\n# Import projects/tasks/env/specs/themes/targets from other configs\nimport:\n  - ./some-dir/mani.yaml\n\n# Shell used for commands\n# If you use any other program than bash, zsh, sh, node, and python\n# then you have to provide the command flag if you want the command-line string evaluted\n# For instance: bash -c\nshell: bash\n\n# If set to true, mani will override the URL of any existing remote\n# and remove remotes not found in the config\nsync_remotes: false\n\n# If set to true, mani will remove worktrees that exist on disk\n# but are not defined in the config\nremove_orphaned_worktrees: false\n\n# Determines whether the .gitignore should be updated when syncing projects\nsync_gitignore: true\n\n# When running the TUI, specifies whether it should reload when the mani config is changed\nreload_tui_on_change: false\n\n# List of Projects\nprojects:\n  # Project name [required]\n  pinto:\n    # Determines if the project should be synchronized during 'mani sync'\n    sync: true\n\n    # Project path relative to the config file\n    # Defaults to project name if not specified\n    path: frontend/pinto\n\n    # Repository URL\n    url: git@github.com:alajmo/pinto\n\n    # Project description\n    desc: A vim theme editor\n\n    # Custom clone command\n    # Defaults to \"git clone URL PATH\"\n    clone: git clone git@github.com:alajmo/pinto --branch main\n\n    # Branch to use as primary HEAD when cloning\n    # Defaults to repository's primary HEAD\n    branch:\n\n    # When true, clones only the specified branch or primary HEAD\n    single_branch: false\n\n    # Project tags\n    tags: [dev]\n\n    # Remote repositories\n    # Key is the remote name, value is the URL\n    remotes:\n      foo: https://github.com/bar\n\n    # Git worktrees\n    # path: Required, relative to project directory (or absolute)\n    # branch: Optional, defaults to path basename\n    # Auto-discovered by 'mani init', created by 'mani sync'\n    worktrees:\n      - path: hotfix                    # branch defaults to \"hotfix\"\n      - path: feature-branch\n        branch: feature/awesome\n      - path: ../project-staging        # worktree outside project dir\n        branch: staging\n\n    # Project-specific environment variables\n    env:\n      # Simple string value\n      branch: main\n\n      # Shell command substitution\n      date: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n# List of Specs\nspecs:\n  default:\n    # Output format for task results\n    # Options: stream, table, html, markdown\n    output: stream\n\n    # Enable parallel task execution\n    parallel: false\n\n    # Maximum number of concurrent tasks when running in parallel\n    forks: 4\n\n    # When true, continues execution if a command fails in a multi-command task\n    ignore_errors: false\n\n    # When true, skips project entries in the config that don't exist\n    # on the filesystem without throwing an error\n    ignore_non_existing: false\n\n    # Hide projects with no command output\n    omit_empty_rows: false\n\n    # Hide columns with no data\n    omit_empty_columns: false\n\n    # Clear screen before task execution (TUI only)\n    clear_output: true\n\n# List of targets\ntargets:\n  default:\n    # Select all projects\n    all: false\n\n    # Select project in current working directory\n    cwd: false\n\n    # Select projects by name\n    projects: []\n\n    # Select projects by path\n    paths: []\n\n    # Select projects by tag\n    tags: []\n\n    # Select projects by tag expression\n    tags_expr: ''\n\n# Environment variables available to all tasks\nenv:\n  # Simple string value\n  AUTHOR: 'alajmo'\n\n# Shell command substitution\nDATE: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n\n# List of tasks\ntasks:\n  # Command name [required]\n  simple-2: echo \"hello world\"\n\n  # Command name [required]\n  simple-1:\n    cmd: |\n      echo \"hello world\"\n    desc: simple command 1\n\n  # Command name [required]\n  advanced-command:\n    # Task description\n    desc: complex task\n\n    # Task theme\n    theme: default\n\n    # Shell interpreter\n    shell: bash\n\n# List of themes\n# Styling Options:\n#   Fg (foreground color): Empty string (\"\"), hex color, or named color from W3C standard\n#   Bg (background color): Empty string (\"\"), hex color, or named color from W3C standard\n#   Format: Empty string (\"\"), \"lower\", \"title\", \"upper\"\n#   Attribute: Empty string (\"\"), \"bold\", \"italic\", \"underline\"\n#   Alignment: Empty string (\"\"), \"left\", \"center\", \"right\"\nthemes:\n  # Theme name [required]\n  default:\n    # Stream Output Configuration\n    stream:\n      # Include project name prefix for each line\n      prefix: true\n\n      # Colors to alternate between for each project prefix\n      prefix_colors:\n        ['#d787ff', '#00af5f', '#d75f5f', '#5f87d7', '#00af87', '#5f00ff']\n\n      # Add a header before each project\n      header: true\n\n      # String value that appears before the project name in the header\n      header_prefix: 'TASK'\n\n      # Fill remaining spaces with a character after the prefix\n      header_char: '*'\n\n    # Table Output Configuration\n    table:\n      # Table style\n      # Available options: ascii, light, bold, double, rounded\n      style: ascii\n\n      # Border options for table output\n      border:\n        around: false # Border around the table\n        columns: true # Vertical border between columns\n        header: true # Horizontal border between headers and rows\n        rows: false # Horizontal border between rows\n\n      header:\n        fg: '#d787ff'\n        attr: bold\n        format: ''\n\n      title_column:\n        fg: '#5f87d7'\n        attr: bold\n        format: ''\n\n    # Tree View Configuration\n    tree:\n      # Tree style\n      # Available options: ascii, light, bold, double, rounded, bullet-square, bullet-circle, bullet-star\n      style: light\n\n    # Block Display Configuration\n    block:\n      key:\n        fg: '#5f87d7'\n      separator:\n        fg: '#5f87d7'\n      value:\n        fg:\n      value_true:\n        fg: '#00af5f'\n      value_false:\n        fg: '#d75f5f'\n\n      # TUI Configuration\n      tui:\n        default:\n          fg:\n          bg:\n          attr:\n\n        border:\n          fg:\n        border_focus:\n          fg: '#d787ff'\n\n        title:\n          fg:\n          bg:\n          attr:\n          align: center\n        title_active:\n          fg: '#000000'\n          bg: '#d787ff'\n          attr:\n          align: center\n\n        button:\n          fg:\n          bg:\n          attr:\n          format:\n        button_active:\n          fg: '#080808'\n          bg: '#d787ff'\n          attr:\n          format:\n\n        table_header:\n          fg: '#d787ff'\n          bg:\n          attr: bold\n          align: left\n          format:\n\n        item:\n          fg:\n          bg:\n          attr:\n        item_focused:\n          fg: '#ffffff'\n          bg: '#262626'\n          attr:\n        item_selected:\n          fg: '#5f87d7'\n          bg:\n          attr:\n        item_dir:\n          fg: '#d787ff'\n          bg:\n          attr:\n        item_ref:\n          fg: '#d787ff'\n          bg:\n          attr:\n\n        search_label:\n          fg: '#d7d75f'\n          bg:\n          attr: bold\n        search_text:\n          fg:\n          bg:\n          attr:\n\n        filter_label:\n          fg: '#d7d75f'\n          bg:\n          attr: bold\n        filter_text:\n          fg:\n          bg:\n          attr:\n\n        shortcut_label:\n          fg: '#00af5f'\n          bg:\n          attr:\n        shortcut_text:\n          fg:\n          bg:\n          attr:\n\n```\n\n## Files\n\nWhen running a command, `mani` will check the current directory and all parent directories for the following files: `mani.yaml`, `mani.yml`, `.mani.yaml`, `.mani.yml` .\n\nAdditionally, it will import (if found) a config file from:\n\n- Linux: `$XDG_CONFIG_HOME/mani/config.yaml` or `$HOME/.config/mani/config.yaml` if `$XDG_CONFIG_HOME` is not set.\n- Darwin: `$HOME/Library/Application Support/mani/config.yaml`\n- Windows: `%AppData%\\mani`\n\nBoth the config and user config can be specified via flags or environments variables.\n\n## Environment\n\n```txt\nMANI_CONFIG\n    Override config file path\n\nMANI_USER_CONFIG\n    Override user config file path\n\nNO_COLOR\n    If this env variable is set (regardless of value) then all colors will be disabled\n```\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Contributing\n\nAll contributions are welcome, be it [filing bugs](https://github.com/alajmo/mani/issues), feature suggestions or helping developing `mani`.\n"
  },
  {
    "path": "docs/development.md",
    "content": "# Development\n\n## Build instructions\n\n### Prerequisites\n\n- [go 1.25 or above](https://golang.org/doc/install)\n- [goreleaser](https://goreleaser.com/install/)\n\n### Building\n\n```bash\n# Build mani for your platform target\nmake build\n\n# Build mani binaries and archives for all platforms using goreleaser\nmake build-all\n\n# Generate Manpage\nmake gen-man\n```\n\n## Developing\n\n```bash\n# Format code\nmake gofmt\n\n# Manage dependencies (download/remove unused)\nmake tidy\n\n# Lint code\nmake lint\n\n# Build mani and get an interactive docker shell with completion\nmake build-exec\n\n# Standing in _example directory you can run the following to debug faster\n(cd .. && make build-and-link && cd - && ../dist/mani run multi -p template-generator)\n```\n\n## Releasing\n\nThe following workflow is used for releasing a new `mani` version:\n\n1. Create pull request with changes\n2. Verify build works (especially windows build)\n   - `make build`\n   - `make build-all`\n3. Pass all integration and unit tests locally\n   - `make test-unit`\n   - `make test-integration`\n4. Update `config.man` and `config.md` if any config changes and generate manpage\n   - `make gen-man`\n5. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md`\n6. Squash-merge to main with `Release vx.y.z` and description of changes\n7. Run `make release`, which will:\n   - Create a git tag with release notes\n   - Trigger a build in Github that builds cross-platform binaries and generates release notes of changes between current and previous tag\n\n## Dependency Graph\n\nCreate SVG dependency graphs using graphviz and [goda](https://github.com/loov/goda)\n\n```bash\ngoda graph \"github.com/alajmo/mani/...\" | dot -Tsvg -o res/graph.svg\ngoda graph \"github.com/alajmo/mani:all\" | dot -Tsvg -o res/graph-full.svg\n```\n"
  },
  {
    "path": "docs/error-handling.md",
    "content": "# Error Handling\n\n## Ignoring Task Errors\n\nIf you wish to continue task execution even if an error is encountered, use the flag `--ignore-errors` or specify it in the task `spec`.\n\n- `ignore-errors` set to false\n\n  ```bash\n  $ mani run task-1 task-2 --all --ignore-errors=false\n     Project    | Task-1        | Task-2\n    ------------+---------------+--------\n     project-0  |               |\n                | exit status 1 |\n    ------------+---------------+--------\n     project-1  |               |\n                | exit status 1 |\n    ------------+---------------+--------\n     project-2  |               |\n                | exit status 1 |\n  ```\n\n- `ignore-errors` set to true\n  ```bash\n     Project    | Task-1        | Task-2\n    ------------+---------------+--------\n     project-0  |               | bar\n                | exit status 1 |\n    ------------+---------------+--------\n     project-1  |               | bar\n                | exit status 1 |\n    ------------+---------------+--------\n     project-2  |               | bar\n                | exit status 1 |\n  ```\n\n## Ignoring Non Existing Project\n\n- `--ignore-non-existing` set to false\n\n  ```bash\n  $ mani run task-1 --all\n    error: path `/home/test/project-1` does not exist\n  ```\n\n- `ignore-unreachable` set to true\n  ```bash\n  $ mani run task-1 --all --ignore-non-existing\n     Project    | Task-1\n    ------------+--------\n     project-0  | hello\n    ------------+--------\n     project-1  |\n    ------------+--------\n     project-2  | hello\n  ```\n"
  },
  {
    "path": "docs/examples.md",
    "content": "# Examples\n\nThis is an example of how to use `mani`. Save the following content to a file named `mani.yaml` and run `mani sync` to clone all repositories. If you already have your own repositories, you can omit the `projects` section.\nAfter setup, you can run any of the [commands](#commands) or check out [git-quick-stats.sh](https://git-quick-stats.sh/) for additional git statistics and run them via `mani` for multiple projects.\n\n### Config\n\n```yaml\nprojects:\n  example:\n    path: .\n    desc: A mani example\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto.git\n    desc: A vim theme editor\n    tags: [frontend, node]\n\n  dashgrid:\n    path: frontend/dashgrid\n    url: https://github.com/alajmo/dashgrid.git\n    desc: A highly customizable drag-and-drop grid\n    tags: [frontend, lib, node]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator.git\n    desc: A simple bash script used to manage boilerplates\n    tags: [cli, bash]\n    env:\n      branch: dev\n\nspecs:\n  custom:\n    output: table\n    parallel: true\n\ntargets:\n  all:\n    all: true\n\nthemes:\n  custom:\n    table:\n      border:\n        around: true\n        columns: true\n        header: true\n        rows: true\n\ntasks:\n  git-status:\n    desc: show working tree status\n    spec: custom\n    target: all\n    cmd: git status -s\n\n  git-last-commit-msg:\n    desc: show last commit\n    cmd: git log -1 --pretty=%B\n\n  git-last-commit-date:\n    desc: show last commit date\n    cmd: |\n      git log -1 --format=\"%cd (%cr)\" -n 1 --date=format:\"%d  %b %y\" \\\n      | sed 's/ //'\n\n  git-branch:\n    desc: show current git branch\n    cmd: git rev-parse --abbrev-ref HEAD\n\n  npm-install:\n    desc: run npm install in node repos\n    target:\n      tags: [node]\n    cmd: npm install\n\n  git-overview:\n    desc: show branch, local and remote diffs, last commit and date\n    spec: custom\n    target: all\n    theme: custom\n    commands:\n      - task: git-branch\n      - task: git-last-commit-msg\n      - task: git-last-commit-date\n```\n\n## Commands\n\n### List all Projects as Table or Tree:\n\n```bash\n$ mani list projects\n\n Project            | Tag                 | Description\n--------------------+---------------------+--------------------------------------------------\n example            |                     | A mani example\n pinto              | frontend, node      | A vim theme editor\n dashgrid           | frontend, lib, node | A highly customizable drag-and-drop grid\n template-generator | cli, bash           | A simple bash script used to manage boilerplates\n\n$ mani list projects --tree\n┌─ frontend\n│  ├─ pinto\n│  └─ dashgrid\n└─ template-generator\n```\n\n### Describe Task\n\n```bash\n$ mani describe task git-overview\n\nName: git-overview\nDescription: show branch, local and remote diffs, last commit and date\nTheme: custom\nTarget:\n    All: true\n    Cwd: false\n    Projects:\n    Paths:\n    Tags:\nSpec:\n    Output: table\n    Parallel: true\n    Forks: 4\n    IgnoreError: false\n    OmitEmptyRows: false\n    OmitEmptyColumns: false\nCommands:\n     - git-branch: show current git branch\n     - git-last-commit-msg: show last commit\n     - git-last-commit-date: show last commit date\n```\n\n### Run a Task Targeting Projects with Tag `node` and Output Table\n\n```bash\n$ mani run npm-install --tags node\n\nTASK [npm-install: run npm install in node repos] *********************************\n\npinto |\npinto | up to date, audited 911 packages in 928ms\npinto |\npinto | 71 packages are looking for funding\npinto |   run `npm fund` for details\npinto |\npinto | 15 vulnerabilities (9 moderate, 6 high)\npinto |\npinto | To address issues that do not require attention, run:\npinto |   npm audit fix\npinto |\npinto | To address all issues (including breaking changes), run:\npinto |   npm audit fix --force\npinto |\npinto | Run `npm audit` for details.\n\nTASK [npm-install: run npm install in node repos] *********************************\n\ndashgrid |\ndashgrid | up to date, audited 960 packages in 1s\ndashgrid |\ndashgrid | 87 packages are looking for funding\ndashgrid |   run `npm fund` for details\ndashgrid |\ndashgrid | 14 vulnerabilities (2 low, 2 moderate, 10 high)\ndashgrid |\ndashgrid | To address all issues possible (including breaking changes), run:\ndashgrid |   npm audit fix --force\ndashgrid |\ndashgrid | Some issues need review, and may require choosing\ndashgrid | a different dependency.\ndashgrid |\ndashgrid | Run `npm audit` for details.\n```\n\n### Run Custom Command for All Projects\n\n```bash\n$ mani exec --all --output table --parallel 'find . -type f | wc -l'\n\n Project            | Output\n--------------------+--------\n example            | 31016\n pinto              | 14444\n dashgrid           | 16527\n template-generator | 42\n```\n"
  },
  {
    "path": "docs/filtering-projects.md",
    "content": "# Filtering Projects\n\nProjects 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.\n\nAvailable options:\n\n- **cwd**: include only the project under the current working directory, ignoring all other filters\n- **all**: include all projects\n- **projects**: Filter by project names\n- **paths**: Filter by project paths\n- **tags**: Filter by project tags\n- **tags_expr**: Filter using tag logic expressions\n- **target**: Filter using target\n\nFor `mani sync/list/describe`:\n\n- No filters: Targets all projects\n- Multiple filters: Select intersection of `projects/paths/tags/tags_expr/target` filters\n\nFor `mani run/exec` the precedence is:\n\n1. Runtime flags (highest priority)\n2. Target flag configuration (`--target`)\n3. Task's default target data (lowest priority)\n\nThe 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`.\n\n## Tags Expression\n\nTag expressions allow filtering projects using boolean operations on their tags.\nThe expression is evaluated for each project's tags to determine if the project should be included.\n\nOperators (in precedence order):\n\n- (): Parentheses for grouping\n- !: NOT operator (logical negation)\n- &&: AND operator (logical conjunction)\n- ||: OR operator (logical disjunction)\n\nTags in expressions can contain any characters except:\n\n- Whitespace (spaces, tabs, newlines)\n- Reserved characters: `(`, `)`, `!`, `&`, `|`\n\nThis means tags can include letters, numbers, hyphens, underscores, dots, and other special characters like `@`, `#`, `$`, etc. For example: `my-tag`, `v1.0`, `frontend_v2`, `@scope/package`.\n\n### Example\n\nFor example, the expression:\n\n- (main && (dev || prod)) && !test\n\nrequires the projects to pass these conditions:\n\n- Must have \"main\" tag\n- Must have either \"dev\" OR \"prod\" tag\n- Must NOT have \"test\" tag\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\n`mani` is available on Linux and Mac, with partial support for Windows.\n\n* Binaries are available on the [release](https://github.com/alajmo/mani/releases) page\n\n* via cURL (Linux & macOS)\n  ```bash\n  curl -sfL https://raw.githubusercontent.com/alajmo/mani/main/install.sh | sh\n  ```\n\n* via Homebrew\n  ```bash\n  brew tap alajmo/mani\n  brew install mani\n  ```\n\n* via MacPorts\n  ```sh\n  sudo port install mani\n  ```\n\n* via Arch\n  ```sh\n  pacman -S mani\n  ```\n\n* via Nix\n  ```sh\n  nix-env -iA nixos.mani\n  ```\n\n* via Go\n  ```bash\n  go get -u github.com/alajmo/mani\n  ```\n\n## Building From Source\n\n1. Clone the repo\n2. Build and run the executable\n\n    ```bash\n    make build && ./dist/mani\n    ```\n"
  },
  {
    "path": "docs/introduction.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\n`mani` is a CLI tool that helps you manage multiple repositories. It's useful when you are working with microservices, multi-project systems, multiple libraries, or just a collection of repositories and want a central place for pulling all repositories and running commands across them.\n\n`mani` has many features:\n\n- Declarative configuration\n- Clone multiple repositories with a single command\n- Run custom or ad-hoc commands across multiple repositories\n- Built-in TUI\n- Flexible filtering\n- Customizable theme\n- Auto-completion support\n- Portable, no dependencies\n\n## Demo\n\n![demo](/img/demo.gif)\n\n## Example\n\nYou specify repositories and commands in a configuration file:\n\n```yaml title=\"mani.yaml\"\nprojects:\n  pinto:\n    url: https://github.com/alajmo/pinto.git\n    desc: A vim theme editor\n    tags: [frontend, node]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator.git\n    desc: A simple bash script used to manage boilerplates\n    tags: [cli, bash]\n    env:\n      branch: dev\n\ntasks:\n  git-status:\n    desc: Show working tree status\n    cmd: git status\n\n  git-create:\n    desc: Create branch\n    spec:\n      output: text\n    env:\n      branch: main\n    cmd: git checkout -b $branch\n```\n\nRun `mani sync` to clone the repositories:\n\n```bash\n$ mani sync\n✓ pinto\n✓ dashgrid\n\nAll projects synced\n```\n\nThen run commands across all or a subset of the repositories:\n\n```bash\n# Target repositories that have the tag 'node'\n$ mani run git-status --tags node\n\n┌──────────┬─────────────────────────────────────────────────┐\n│ Project  │ git-status                                      │\n├──────────┼─────────────────────────────────────────────────┤\n│ pinto    │ On branch master                                │\n│          │ Your branch is up to date with 'origin/master'. │\n│          │                                                 │\n│          │ nothing to commit, working tree clean           │\n└──────────┴─────────────────────────────────────────────────┘\n\n# Target project 'pinto'\n$ mani run git-create --projects pinto branch=dev\n\n[pinto] TASK [git-create: create branch] *******************\n\nSwitched to a new branch 'dev'\n```\n"
  },
  {
    "path": "docs/man-pages.md",
    "content": "# 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\n$ mani gen --dir /usr/local/share/man/man1/\nCreated /usr/local/share/man/man1/mani.1\n```\n"
  },
  {
    "path": "docs/output.md",
    "content": "# Output Format\n\n`mani` supports different output formats for tasks. By default it will use `stream` output, but it's possible to change this via the `--output` flag or specify it in the task `spec`.\n\nThe following output formats are available:\n\n- **stream** (default)\n\n  ```\n  TASK (1/2) [hello] ***********\n\n  test | world\n  test | bar\n\n  TASK (2/2) [foo] ***********\n\n  test | world\n  test | bar\n  ```\n\n- **table**\n  ```\n   Project  │ Hello │ Foo\n  ──────────┼───────┼───────\n   test     │ world │ bar\n  ──────────┼───────┼───────\n   test-2   │ world │ bar\n  ```\n- **html**\n  ```html\n  <table class=\"\">\n    <thead>\n      <tr>\n        <th>project</th>\n        <th>hello</th>\n        <th>foo</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>test</td>\n        <td>world</td>\n        <td>bar</td>\n      </tr>\n      <tr>\n        <td>test-2</td>\n        <td>world</td>\n        <td>bar</td>\n      </tr>\n    </tbody>\n  </table>\n  ```\n- **markdown**\n  ```markdown\n  | project | hello | foo |\n  | ------- | ----- | --- |\n  | test    | world | bar |\n  | test-2  | world | bar |\n  ```\n\n## Omit Empty Table Rows and Columns\n\nOmit empty outputs using `--omit-empty-rows` and `--omit-empty-columns` flags or task spec. Works for tables, markdown and html formats.\n\nSee below for an example:\n\n```bash\n$ mani run cmd-1 cmd-2 -s project-1,project-2 -o table\n\n  Project  │ Cmd-1 │ Cmd-2\n ──────────┼───────┼───────\n  test     │       │\n ──────────┼───────┼───────\n  test-2   │ world │\n\n$ mani run test -s project-1,project-2 -o table --omit-empty-rows --omit-empty-columns\n\nTASKS *******************************\n\n Project | Cmd-1\n---------+--------\n test-2  | world\n```\n"
  },
  {
    "path": "docs/project-background.md",
    "content": "# Project Background\n\nThis document contains a little bit of everything:\n\n- Background to `mani` and core design decisions used to develop `mani`\n- Comparisons with alternatives\n- Roadmap\n\n## Background\n\n`mani` came about because I needed a CLI tool to manage multiple repositories. So, the premise is, you have a bunch of repositories and want the following:\n\n1. a central place for your repositories, containing name, URL, and a small description of the repository\n2. ability to clone all repositories in 1 command\n3. ability to run ad-hoc and custom commands (perhaps `git status` to see working tree status) on 1, a subset, or all of the repositories\n4. ability to get an overview of 1, a subset, or all of the repositories and commands\n\nNow, there's plenty of CLI tools for running cloning multiple repositories, running commands over them, see [similar software](#similar-software), and while I've taken a lot of inspiration from them, there's some core design decision that led me to create `mani`, instead of forking or contributing to an existing solution.\n\n## Design\n\n### Config\n\nA lot of the alternatives to `mani` treat the config file (either using a custom format or JSON) as a state file that is interacted with via their executable.\nSo the way it works is, you would add a repository to the config file via `sometool add git@github.com/random/xyz`, and then to remove the repository, you'd have to open the config file and remove it manually, taking care to also update the `.gitignore` file.\n\nI think it's a missed opportunity to not let users edit the config file manually for the following reasons:\n\n1. The user can add additional metadata about the repositories\n2. The user can order the repositories to their liking to provide a better overview of the repositories, rather than using an alphabetical or random order\n3. It's seldom that you add new repositories, so it's not something that should be optimized for\n\nThat's why in `mani` you need to edit the config file to add or delete a repository. The exception is when you're setting up `mani` for the first time, then you want it to scan for existing repositories. As a bonus, it also updates your `.gitignore` file with the updated list of repositories.\n\n### Commands\n\nAnother missed opportunity is not to have built-in support for commands. For instance, [meta](https://github.com/mateodelnorte/meta), delegates this to 3rd party tools like `make`, which makes you lose out on a few benefits:\n\n1. Fewer tools for developers to learn (albeit `make` is something many are already familiar with)\n2. Fewer files to keep track of (1 file instead of 2)\n3. Better auto-completion and command discovery\n\nNote, you can still use `make` or regular script files, just call them from the `mani.yaml` config.\n\nSo what config format is best suited for this purpose? In my opinion, YAML is a suitable candidate. While it has its issues, I think its purpose as a human-readable config/state file works well. It has all the primitives you'd need in a config language, simple key/value entries, dictionaries, and lists, as well as supporting comments (something which JSON doesn't). We could create a custom format, but then users would have to learn that syntax, so in this case, YAML has a major advantage, almost all software developers are familiar with it.\n\n### Filtering\n\nWhen we run commands, we need a way to target specific repositories. To make it as flexible as possible, there are three ways to do it in `mani`:\n\n1. **Tag filtering**: target repositories which have a tag, for instance, add a tag `python` to all `python` repositories, then it's as simple as `mani run status -t python`\n2. **Directory filtering**: target repositories by which directory they belong to, `mani run status -d frontend`, will target all repositories that are in the `frontend` directory\n3. **Project name filtering**: target repositories by their name, `mani run status -p dashgrid`, will target the project `dashgrid`\n\n### General UX\n\nThese various features make using `mani` feel more effortless:\n\n- Automatically updating .gitignore when updating the config file\n- Rich auto-completion\n- Edit the `mani` config file via the `mani edit` command, which opens up the config file in your preferred editor\n- Most organizations/people use git, but not everyone uses it or even uses it in the same way, so it's important to provide escape hatches, where people can provide their own VCS and customize commands to clone repositories\n- Single binary (most alternatives require Python or Node.js runtime)\n- Pretty output when running commands or listing repositories/commands\n- Default tags/dirs/name filtering for commands\n- Export output as HTML/Markdown from list/run/exec commands\n\n## Similar Software\n\n- [gita](https://github.com/nosarthur/gita)\n- [gr](https://github.com/mixu/gr)\n- [meta](https://github.com/mateodelnorte/meta)\n- [mu-repo](https://github.com/fabioz/mu-repo)\n- [myrepos](https://myrepos.branchable.com/)\n- [repo](https://source.android.com/setup/develop/repo)\n- [vcstool](https://github.com/dirk-thomas/vcstool)\n\n"
  },
  {
    "path": "docs/roadmap.md",
    "content": "# Roadmap\n\n`mani` is under active development. Before **v1.0.0**, I want to finish the following tasks:\n\n- [ ] Bring changes from `sake`\n  - Refactor import logic and support recursive nesting of tasks\n  - Add new table format output (tasks in 1st column, output in 2nd, one table per project)\n- [ ] Allow user to edit mani config from command line or TUI\n"
  },
  {
    "path": "docs/shell-completion.mdx",
    "content": "# Shell Completion\n\nShell completion is available for `bash`, `zsh`, `fish` and `powershell`.\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<Tabs\n    defaultValue=\"bash\"\n    values={[\n        { label: 'bash', value: 'bash', },\n        { label: 'zsh', value: 'zsh', },\n        { label: 'fish', value: 'fish', },\n        { label: 'powershell', value: 'powershell', },\n    ]}\n>\n\n<TabItem value=\"bash\">\n\n```bash\nmani completion bash\n```\n\n</TabItem>\n\n<TabItem value='zsh'>\n\n```bash\nmani completion zsh\n```\n\n</TabItem>\n\n<TabItem value='fish'>\n\n```bash\nmani completion fish\n```\n\n</TabItem>\n\n<TabItem value='powershell'>\n\n```bash\nmani completion powershell\n```\n\n</TabItem>\n\n</Tabs>\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Usage\n\n## Initialize Mani\n\nRun the following command inside a directory containing your `git` repositories:\n\n```bash\nmani init\n```\n\nThis will generate:\n\n- `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)\n- `.gitignore`: (only when inside a git repo) Includes the projects specified in `mani.yaml` file. To opt out, use `mani init --sync-gitignore=false`.\n\nIt 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.\n\n## Example Commands\n\n```bash\n# List all projects\nmani list projects\n\n# Run git status across all projects\nmani exec --all git status\n\n# Run git status across all projects in parallel with output in table format\nmani exec --all --parallel --output table git status\n```\n\nNext up:\n\n- [Some more examples](/examples)\n- [Familiarize yourself with the mani.yaml config](/config)\n- [Checkout mani commands](/commands)\n"
  },
  {
    "path": "docs/variables.md",
    "content": "# Variables\n\n`mani` supports setting variables for both projects and tasks. Variables can be either strings or commands (encapsulated by $()) that will be evaluated once for each task.\n\n```yaml\nprojects:\n  pinto:\n    path: pinto\n    url: https://github.com/alajmo/pinto.git\n    env:\n      foo: bar\n\ntasks:\n  ping:\n    cmd: |\n      echo \"$msg\"\n      echo \"$date\"\n      echo \"$foo\"\n    env:\n      msg: text\n      date: $(date)\n```\n\n## Pass Variables from CLI\n\nTo pass variables from the command line, provide them as arguments. For example:\n\n```bash\nmani run msg option=123\n```\n\nThe environment variable option will then be available for use within the task.\n"
  },
  {
    "path": "examples/.gitignore",
    "content": "# mani #\ntemplate-generator\nfrontend/pinto\n# mani #\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\nThis is an example of how you can use `mani`. Simply save the following content to a file named `mani.yaml` and then run `mani sync` to clone all the repositories. If you already have your own repositories, just omit the `projects` section.\n\nYou can then run some of the [commands](#commands) or checkout [git-quick-stats.sh](https://git-quick-stats.sh/) for additional git statistics and run them via `mani` for multiple projects.\n\n### Config\n\n```yaml\nprojects:\n  example:\n    path: .\n    desc: A mani example\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto.git\n    desc: A vim theme editor\n    tags: [frontend, node]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator.git\n    desc: A simple bash script used to manage boilerplates\n    tags: [cli, bash]\n    env:\n      branch: dev\n\nspecs:\n  custom:\n    output: table\n    parallel: true\n    forks: 8\n\ntargets:\n  all:\n    all: true\n\nthemes:\n  custom:\n    table:\n      border:\n        around: true\n        header: true\n        columns: true\n        rows: true\n\ntasks:\n  git-status:\n    desc: Show working tree status\n    spec: custom\n    target: all\n    cmd: git status -s\n\n  git-last-commit-msg:\n    desc: Show last commit\n    cmd: git log -1 --pretty=%B\n\n  git-last-commit-date:\n    desc: Show last commit date\n    cmd: |\n      git log -1 --format=\"%cd (%cr)\" -n 1 --date=format:\"%d  %b %y\" \\\n      | sed 's/ //'\n\n  git-branch:\n    desc: Show current git branch\n    cmd: git rev-parse --abbrev-ref HEAD\n\n  npm-install:\n    desc: Run npm install in node repos\n    target:\n      tags: [node]\n    cmd: npm install\n\n  git-overview:\n    desc: Show branch, local and remote diffs, last commit and date\n    spec: custom\n    target: all\n    theme: custom\n    commands:\n      - task: git-branch\n      - task: git-last-commit-msg\n      - task: git-last-commit-date\n```\n\n## Commands\n\n### List All Projects as Table or Tree::\n\n```bash\n$ mani list projects\n\n Project            | Tag                 | Description\n--------------------+---------------------+--------------------------------------------------\n example            |                     | A mani example\n pinto              | frontend, node      | A vim theme editor\n dashgrid           | frontend, lib, node | A highly customizable drag-and-drop grid\n template-generator | cli, bash           | A simple bash script used to manage boilerplates\n\n$ mani list projects --tree\n┌─ frontend\n│  ├─ pinto\n│  └─ dashgrid\n└─ template-generator\n```\n\n### Describe Task\n\n```bash\n$ mani describe task git-overview\n\nName: git-overview\nDescription: show branch, local and remote diffs, last commit and date\nTheme: custom\nTarget:\n    All: true\n    Cwd: false\n    Projects:\n    Paths:\n    Tags:\nSpec:\n    Output: table\n    Parallel: true\n    Forks: 4\n    IgnoreErrors: false\n    IgnoreNonExisting: false\n    OmitEmptyRows: false\n    OmitEmptyColumns: false\nCommands:\n     - git-branch: show current git branch\n     - git-last-commit-msg: show last commit\n     - git-last-commit-date: show last commit date\n```\n\n### Run a Task Targeting Projects with Tag `node` and Output Table:\n\n```bash\n$ mani run npm-install --tags node\n\nTASK [npm-install: run npm install in node repos] *********************************\n\npinto |\npinto | up to date, audited 911 packages in 928ms\npinto |\npinto | 71 packages are looking for funding\npinto |   run `npm fund` for details\npinto |\npinto | 15 vulnerabilities (9 moderate, 6 high)\npinto |\npinto | To address issues that do not require attention, run:\npinto |   npm audit fix\npinto |\npinto | To address all issues (including breaking changes), run:\npinto |   npm audit fix --force\npinto |\npinto | Run `npm audit` for details.\n\nTASK [npm-install: run npm install in node repos] *********************************\n\ndashgrid |\ndashgrid | up to date, audited 960 packages in 1s\ndashgrid |\ndashgrid | 87 packages are looking for funding\ndashgrid |   run `npm fund` for details\ndashgrid |\ndashgrid | 14 vulnerabilities (2 low, 2 moderate, 10 high)\ndashgrid |\ndashgrid | To address all issues possible (including breaking changes), run:\ndashgrid |   npm audit fix --force\ndashgrid |\ndashgrid | Some issues need review, and may require choosing\ndashgrid | a different dependency.\ndashgrid |\ndashgrid | Run `npm audit` for details.\n```\n\n### Run Custom Command for All Projects\n\n```bash\n$ mani exec --all --output table --parallel 'find . -type f | wc -l'\n\n Project            | Output\n--------------------+--------\n example            | 31016\n pinto              | 14444\n dashgrid           | 16527\n template-generator | 42\n```\n"
  },
  {
    "path": "examples/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n    desc: A mani example\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto.git\n    desc: A vim theme editor\n    tags: [frontend, node]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator.git\n    desc: A simple bash script used to manage boilerplates\n    tags: [cli, bash]\n    env:\n      branch: dev\n\nspecs:\n  custom:\n    output: table\n    parallel: true\n\ntargets:\n  all:\n    all: true\n\nthemes:\n  custom:\n    table:\n      border:\n        around: true\n        columns: true\n        header: true\n        rows: true\n\ntasks:\n  git-status:\n    desc: show working tree status\n    spec: custom\n    target: all\n    cmd: git status\n\n  git-last-commit-msg:\n    desc: show last commit\n    cmd: git log -1 --pretty=%B\n\n  git-last-commit-date:\n    desc: show last commit date\n    cmd: |\n      git log -1 --format=\"%cd (%cr)\" -n 1 --date=format:\"%d  %b %y\" \\\n      | sed 's/ //'\n\n  git-branch:\n    desc: show current git branch\n    cmd: git rev-parse --abbrev-ref HEAD\n\n  npm-install:\n    desc: run npm install in node repos\n    target:\n      tags: [node]\n    cmd: npm install\n\n  git-overview:\n    desc: show branch, local and remote diffs, last commit and date\n    spec: custom\n    target: all\n    theme: custom\n    commands:\n      - task: git-branch\n      - task: git-last-commit-msg\n      - task: git-last-commit-date\n"
  },
  {
    "path": "go.mod",
    "content": "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.13.8\n\tgithub.com/gookit/color v1.6.0\n\tgithub.com/jedib0t/go-pretty/v6 v6.7.8\n\tgithub.com/jinzhu/copier v0.4.0\n\tgithub.com/kr/pretty v0.2.1\n\tgithub.com/otiai10/copy v1.6.0\n\tgithub.com/rivo/tview v0.42.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/theckman/yacspin v0.13.12\n\tgolang.org/x/sys v0.41.0\n\tgolang.org/x/term v0.40.0\n\tgolang.org/x/text v0.34.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/clipperhouse/uax29/v2 v2.6.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/gdamore/encoding v1.0.1 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=\ngithub.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=\ngithub.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=\ngithub.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=\ngithub.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=\ngithub.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=\ngithub.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ=\ngithub.com/otiai10/copy v1.6.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=\ngithub.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=\ngithub.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=\ngithub.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=\ngithub.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=\ngithub.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=\ngithub.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=\ngithub.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n# Credit to https://github.com/ducaale/xh for this install script.\n\nset -e\n\nif [ \"$(uname -s)\" = \"Darwin\" ] && [ \"$(uname -m)\" = \"x86_64\" ]; then\n    target=\"darwin_amd64\"\nelif [ \"$(uname -s)\" = \"Linux\" ] && [ \"$(uname -m)\" = \"x86_64\" ]; then\n    target=\"linux_amd64\"\nelif [ \"$(uname -s)\" = \"Linux\" ] && ( uname -m | grep -q -e '^arm' -e '^aarch' ); then\n    target=\"linux_arm64\"\nelse\n    echo \"Unsupported OS or architecture\"\n    exit 1\nfi\n\nfetch() {\n    if which curl > /dev/null; then\n        if [ \"$#\" -eq 2 ]; then curl -L -o \"$1\" \"$2\"; else curl -sSL \"$1\"; fi\n    elif which wget > /dev/null; then\n        if [ \"$#\" -eq 2 ]; then wget -O \"$1\" \"$2\"; else wget -nv -O - \"$1\"; fi\n    else\n        echo \"Can't find curl or wget, can't download package\"\n        exit 1\n    fi\n}\n\necho \"Detected target: $target\"\n\nurl=$(\n    fetch https://api.github.com/repos/alajmo/mani/releases/latest |\n    tac | tac | grep -wo -m1 \"https://.*$target.tar.gz\" || true\n)\n\nif ! test \"$url\"; then\n    echo \"Could not find release info\"\n    exit 1\nfi\n\necho \"Downloading mani...\"\n\ntemp_dir=$(mktemp -dt mani.XXXXXX)\ntrap 'rm -rf \"$temp_dir\"' EXIT INT TERM\ncd \"$temp_dir\"\n\nif ! fetch mani.tar.gz \"$url\"; then\n    echo \"Could not download tarball\"\n    exit 1\nfi\n\nuser_bin=\"$HOME/.local/bin\"\ncase $PATH in\n    *:\"$user_bin\":* | \"$user_bin\":* | *:\"$user_bin\")\n        default_bin=$user_bin\n        ;;\n    *)\n        default_bin='/usr/local/bin'\n        ;;\nesac\n\nprintf \"Install location [default: %s]: \" \"$default_bin\"\nread -r bindir < /dev/tty\nbindir=${bindir:-$default_bin}\n\nwhile ! test -d \"$bindir\"; do\n    echo \"Directory $bindir does not exist\"\n    printf \"Install location [default: %s]: \" \"$default_bin\"\n    read -r bindir < /dev/tty\n    bindir=${bindir:-$default_bin}\ndone\n\ntar xzf mani.tar.gz\n\nif test -w \"$bindir\"; then\n    mv mani \"$bindir/\"\nelse\n    sudo mv mani \"$bindir/\"\nfi\n\n$bindir/mani --version\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"github.com/alajmo/mani/cmd\"\n)\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "res/demo.md",
    "content": "# Demo\n\nTo generate a `demo.gif` use [vhs](https://github.com/charmbracelet/vhs).\n\nRequires:\n\n- ffmpeg\n- ttyd\n\n```\n# Stand in mani/res\nvhs demo.gif\n```\n\nTo update `demo.gif`, modify `demo.vhs`.\n"
  },
  {
    "path": "res/demo.vhs",
    "content": "Output demo.gif\n\nSet FontSize 28\nSet Width 1920\nSet Height 1080\n\nSleep 500ms\nType \"mani sync\"\nEnter\nSleep 2000ms\n\nType \"mani tui\"\nSleep 2000ms\nEnter\n\n# Select task\nSleep 1000ms\nType \"G\"\nSleep 2000ms\nSpace\n\n# Filter tags\nType \"3\"\nSleep 2000ms\nSpace\n\n# Select project\nType \"2\"\nSleep 1000ms\nType \"a\"\n\n# Run\nSleep 2000ms\nCtrl+R\n\nSleep 4s\n"
  },
  {
    "path": "res/mani.yaml",
    "content": "reload_tui_on_change: true\nsync_remotes: true\n\nprojects:\n  projects:\n    path: .\n\n  mani:\n    path: go/mani\n    url: https://github.com/alajmo/mani.git\n    remotes:\n      foo: https://github.com/alajmo/mani.git\n      bar: https://github.com/alajmo/mani.git\n    tags: [git, mani]\n\n  sake:\n    path: go/sake\n    url: https://github.com/alajmo/sake.git\n    tags: [git, sake]\n\ntasks:\n  current-branch:\n    desc: print current branch\n    cmd: git branch\n\n  num-branches:\n    desc: 'print # branches'\n    cmd: git branch | wc -l\n\n  num-commits:\n    desc: 'print # commits'\n    cmd: git rev-list --all --count\n\n  num-authors:\n    desc: 'print # authors'\n    cmd: git shortlog -s -n --all --no-merges | wc -l\n\n  print-overview:\n    desc: 'show # commits, # branches, # authors, last commit date'\n    commands:\n      - task: current-branch\n      - task: num-branches\n      - task: num-commits\n      - task: num-authors\n"
  },
  {
    "path": "scripts/release.sh",
    "content": "#!/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 | head -n-1 > release-changelog.md\n"
  },
  {
    "path": "test/README.md",
    "content": "# Test\n\n`mani` currently only has integration tests, which require `docker` to run. This is because `mani` mainly interacts with the filesystem, and whilst there are ways to mock the filesystem, it's simply easier (and fast enough) to spin up a `docker` container and do the work there.\n\nThe tests are based on something called \"golden files\", which are the expected output of the tests. It serves the benefit of working as documentation as well, since it becomes easy to see the desired output of the different `mani` commands.\n\nThere's some helpful scripts in the `scripts` directory that can be used to test and debug `mani`. These scripts should be run from the project directory.\n\nThe Docker test container includes a script `git` which only creates the project directories, it doesn't clone the actual repositories.\n\n## Directory Structure\n\n```sh\n.\n├── fixtures    # files needed for testing purposes\n├── images      # docker images used for testing and development\n├── integration # integration tests and golden files\n├── scripts     # scripts for development and testing\n└── tmp         # docker mounted volume that you can preview test output\n```\n\n## Prerequisites\n\n- [docker](https://docs.docker.com/get-docker/)\n- [golangci-lint](https://golangci-lint.run)\n\n## Testing & Development\n\nCheckout the below commands and the [Makefile](../Makefile) to test/debug `mani`.\n\n```sh\n# Run tests\n./test/scripts/test\n\n# Run specific tests, print stdout and build mani\n./test/scripts/test --debug --build --run TestInitCmd\n\n# Update Golden Files\n./test/scripts/test -u\n\n# Start an interactive shell inside docker\n./test/scripts/exec --shell bash|zsh|fish\n\n# Debug completion\nmani __complete list tags --projects \"\"\n\n# Stand in _example directory\n(cd ../ && make build-and-link && cd - && mani run status --cwd)\n```\n"
  },
  {
    "path": "test/fixtures/mani-advanced/.gitignore",
    "content": "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",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/fixtures/mani-empty/mani.yaml",
    "content": ""
  },
  {
    "path": "test/fixtures/mani-no-tasks/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-report\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n"
  },
  {
    "path": "test/images/alpine.exec.Dockerfile",
    "content": "FROM alpine:3.18.0 as build\n\nENV XDG_CACHE_HOME=/tmp/.cache\nENV GOPATH=${HOME}/go\nENV GO111MODULE=on\nENV PATH=\"/usr/local/go/bin:${PATH}\"\nENV USER=\"test\"\nENV HOME=\"/home/test\"\n\nCOPY --from=golang:1.20.5-alpine /usr/local/go/ /usr/local/go/\n\nRUN apk update\nRUN apk add --no-cache make build-base bash curl g++ git\n\nWORKDIR /opt\n\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN make build\n\nFROM alpine:3.15.4\n\nRUN apk update\nRUN apk add --no-cache sudo bash zsh fish bash-completion git\n\nCOPY --from=build /opt/dist/mani /usr/local/bin/mani\n\nRUN mani completion bash > /usr/share/bash-completion/completions/mani\n\nRUN addgroup -g 1000 -S test && adduser -u 1000 -S test -G test\nUSER test\n\nWORKDIR /home/test\n\n# Setup example directory\nCOPY --chown=test --from=build /opt/examples/mani.yaml /home/test/\n\nRUN echo 'fpath=( ~/.zsh/completion \"${fpath[@]}\" ); autoload -Uz compinit && compinit -i' > /home/test/.zshrc\nRUN mkdir -p /home/test/.zsh/completion ~/.config/fish/completions\nRUN mani completion zsh > /home/test/.zsh/completion/_mani\nRUN mani completion fish > ~/.config/fish/completions/mani.fish\nRUN echo 'source /etc/profile.d/bash_completion.sh' > /home/test/.bashrc\n"
  },
  {
    "path": "test/images/alpine.test.Dockerfile",
    "content": "FROM alpine:3.21.0\n\nENV GOCACHE=/go/cache\nENV GO111MODULE=on\nENV PATH=\"/usr/local/go/bin:${PATH}\"\nENV USER=\"test\"\nENV HOME=\"/home/test\"\n\nCOPY --from=golang:1.25.5-alpine /usr/local/go/ /usr/local/go/\n\nRUN apk update\nRUN apk add --no-cache make build-base bash curl g++ git\n\nRUN addgroup -g 1000 -S test && adduser -u 1000 -S test -G test\n\nWORKDIR /home/test\n\nCOPY --chown=test go.mod go.sum ./\nRUN go mod download\nCOPY --chown=test . .\nCOPY --chown=test ./test/scripts/git /usr/local/bin/git\nRUN make build-test && cp /home/test/dist/mani /usr/local/bin/mani\n\nUSER test\n"
  },
  {
    "path": "test/integration/describe_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestDescribe(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t// Projects\n\t\t{\n\t\t\tTestName:   \"Describe 0 projects when there's 0 projects\",\n\t\t\tInputFiles: []string{\"mani-empty/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe 0 projects on non-existent tag\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects --tags lala\",\n\t\t\tWantErr:    true,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe 0 projects on 2 non-matching tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects --tags frontend,cli\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe all projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe projects matching 1 tag\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects --tags frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe projects matching multiple tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects --tags misc,frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe 1 project\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe projects pinto\",\n\t\t\tWantErr:    false,\n\t\t},\n\n\t\t// Tasks\n\t\t{\n\t\t\tTestName:   \"Describe 0 tasks when no tasks exists \",\n\t\t\tInputFiles: []string{\"mani-no-tasks/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe tasks\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe all tasks\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe tasks\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"Describe 1 tasks\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani describe tasks status\",\n\t\t\tWantErr:    false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"describe/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/exec_test.go",
    "content": "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\tTestName:   \"Should fail to exec when no configuration file found\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd: `\n\t\t\t\tmani exec --all -o table ls\n\t\t\t`,\n\t\t\tWantErr: true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should exec in zero projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tmani exec -o table ls\n\t\t\t`,\n\t\t\tWantErr: true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should exec in all projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tmani exec --all -o table ls\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should exec when filtered on project name\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tmani exec -o table --projects pinto ls\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should exec when filtered on tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tmani exec -o table --tags frontend ls\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should exec when filtered on cwd\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tcd template-generator\n\t\t\t\tmani exec -o table --cwd pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should dry run exec\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\t\tmani sync\n\t\t\t\tmani exec -o table --dry-run --projects template-generator pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"exec/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/golden/describe/golden-0/mani.yaml",
    "content": ""
  },
  {
    "path": "test/integration/golden/describe/golden-0/stdout.golden",
    "content": "Index: 0\nName: Describe 0 projects when there's 0 projects\nWantErr: false\nCmd:\nmani describe projects\n\n---\nNo matching projects found\n"
  },
  {
    "path": "test/integration/golden/describe/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-1/stdout.golden",
    "content": "Index: 1\nName: Describe 0 projects on non-existent tag\nWantErr: true\nCmd:\nmani describe projects --tags lala\n\n---\nerror: cannot find tags `lala`\n"
  },
  {
    "path": "test/integration/golden/describe/golden-2/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-2/stdout.golden",
    "content": "Index: 2\nName: Describe 0 projects on 2 non-matching tags\nWantErr: false\nCmd:\nmani describe projects --tags frontend,cli\n\n---\nNo matching projects found\n"
  },
  {
    "path": "test/integration/golden/describe/golden-3/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-3/stdout.golden",
    "content": "Index: 3\nName: Describe all projects\nWantErr: false\nCmd:\nmani describe projects\n\n---\n\nname: example\nsync: true\npath: .\nurl: \nsingle_branch: false\n\n--\n\nname: pinto\nsync: true\npath: frontend/pinto\nurl: https://github.com/alajmo/pinto\nsingle_branch: false\ntags: frontend\n\n--\n\nname: dashgrid\nsync: true\npath: frontend/dashgrid\nurl: https://github.com/alajmo/dashgrid\nsingle_branch: false\ntags: frontend, misc\n\n--\n\nname: template-generator\nsync: true\nurl: https://github.com/alajmo/template-generator\nsingle_branch: false\ntags: cli\nenv: \n    branch: dev\n\n"
  },
  {
    "path": "test/integration/golden/describe/golden-4/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-4/stdout.golden",
    "content": "Index: 4\nName: Describe projects matching 1 tag\nWantErr: false\nCmd:\nmani describe projects --tags frontend\n\n---\n\nname: pinto\nsync: true\npath: frontend/pinto\nurl: https://github.com/alajmo/pinto\nsingle_branch: false\ntags: frontend\n\n--\n\nname: dashgrid\nsync: true\npath: frontend/dashgrid\nurl: https://github.com/alajmo/dashgrid\nsingle_branch: false\ntags: frontend, misc\n\n"
  },
  {
    "path": "test/integration/golden/describe/golden-5/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-5/stdout.golden",
    "content": "Index: 5\nName: Describe projects matching multiple tags\nWantErr: false\nCmd:\nmani describe projects --tags misc,frontend\n\n---\n\nname: dashgrid\nsync: true\npath: frontend/dashgrid\nurl: https://github.com/alajmo/dashgrid\nsingle_branch: false\ntags: frontend, misc\n\n"
  },
  {
    "path": "test/integration/golden/describe/golden-6/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-6/stdout.golden",
    "content": "Index: 6\nName: Describe 1 project\nWantErr: false\nCmd:\nmani describe projects pinto\n\n---\n\nname: pinto\nsync: true\npath: frontend/pinto\nurl: https://github.com/alajmo/pinto\nsingle_branch: false\ntags: frontend\n\n"
  },
  {
    "path": "test/integration/golden/describe/golden-7/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-report\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n"
  },
  {
    "path": "test/integration/golden/describe/golden-7/stdout.golden",
    "content": "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",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-8/stdout.golden",
    "content": "Index: 8\nName: Describe all tasks\nWantErr: false\nCmd:\nmani describe tasks\n\n---\n\nname: fetch\ndescription: Fetch git\ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    git fetch\n\n--\n\nname: status\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    git status\n\n--\n\nname: checkout\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\nenv: \n    branch: dev\ncmd: \n    git checkout $branch\n\n--\n\nname: create-branch\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    git checkout -b $branch\n\n--\n\nname: multi\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    echo \"1st line \"\n    echo \"2nd line\"\n\n--\n\nname: default-tags\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: frontend\n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    pwd\n\n--\n\nname: default-projects\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: dashgrid\n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    pwd\n\n--\n\nname: default-output\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: table\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    pwd\n\n--\n\nname: pwd\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    pwd\n\n--\n\nname: submarine\ndescription: Submarine test\ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: table\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    echo 0\ncommands: \n    - command-1 \n    - command-2 \n    - command-3 \n    - pwd \n\n"
  },
  {
    "path": "test/integration/golden/describe/golden-9/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/describe/golden-9/stdout.golden",
    "content": "Index: 9\nName: Describe 1 tasks\nWantErr: false\nCmd:\nmani describe tasks status\n\n---\n\nname: status\ndescription: \ntheme: default\ntarget: \n    all: false\n    cwd: false\n    projects: \n    paths: \n    tags: \n    tags_expr: \nspec: \n    output: stream\n    parallel: false\n    ignore_errors: false\n    omit_empty_rows: false\n    omit_empty_columns: false\ncmd: \n    git status\n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-0/stdout.golden",
    "content": "Index: 0\nName: Should fail to exec when no configuration file found\nWantErr: true\nCmd:\nmani exec --all -o table ls\n\n\n---\n\u001b[31merror\u001b[0m: cannot find any configuration file [mani.yaml mani.yml .mani.yaml .mani.yml] in current directory or any of the parent directories\n"
  },
  {
    "path": "test/integration/golden/exec/golden-1/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-1/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-1/stdout.golden",
    "content": "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\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\nerror: no matching projects found\n"
  },
  {
    "path": "test/integration/golden/exec/golden-2/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-2/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-2/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-2/stdout.golden",
    "content": "Index: 2\nName: Should exec in all projects\nWantErr: false\nCmd:\nmani sync\nmani exec --all -o table ls\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | output             \n--------------------+--------------------\n example            | frontend           \n                    | mani.yaml          \n                    | template-generator \n--------------------+--------------------\n pinto              | empty              \n--------------------+--------------------\n dashgrid           | empty              \n--------------------+--------------------\n template-generator | empty              \n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-3/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-3/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-3/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-3/stdout.golden",
    "content": "Index: 3\nName: Should exec when filtered on project name\nWantErr: false\nCmd:\nmani sync\nmani exec -o table --projects pinto ls\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project | output \n---------+--------\n pinto   | empty  \n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-4/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-4/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-4/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-4/stdout.golden",
    "content": "Index: 4\nName: Should exec when filtered on tags\nWantErr: false\nCmd:\nmani sync\nmani exec -o table --tags frontend ls\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project  | output \n----------+--------\n pinto    | empty  \n----------+--------\n dashgrid | empty  \n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-5/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-5/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-5/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-5/stdout.golden",
    "content": "Index: 5\nName: Should exec when filtered on cwd\nWantErr: false\nCmd:\nmani sync\ncd template-generator\nmani exec -o table --cwd pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | output                                                      \n--------------------+-------------------------------------------------------------\n template-generator | /home/test/test/tmp/golden/exec/golden-5/template-generator \n\n"
  },
  {
    "path": "test/integration/golden/exec/golden-6/.gitignore",
    "content": "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",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-6/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/exec/golden-6/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/exec/golden-6/stdout.golden",
    "content": "Index: 6\nName: Should dry run exec\nWantErr: false\nCmd:\nmani sync\nmani exec -o table --dry-run --projects template-generator pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | output \n--------------------+--------\n template-generator | pwd    \n\n"
  },
  {
    "path": "test/integration/golden/init/golden-0/mani.yaml",
    "content": "projects:\ntasks:\n  hello:\n    desc: Print Hello World\n    cmd: echo \"Hello World\"\n"
  },
  {
    "path": "test/integration/golden/init/golden-0/stdout.golden",
    "content": "Index: 0\nName: Initialize mani in empty directory\nWantErr: false\nCmd:\nmani init --color=false\n\n---\n\nInitialized mani repository in /home/test/test/tmp/golden/init/golden-0\n- Created mani.yaml\n"
  },
  {
    "path": "test/integration/golden/init/golden-1/.gitignore",
    "content": "# mani #\ntap-report\nnested/template-generator\n# mani #\n"
  },
  {
    "path": "test/integration/golden/init/golden-1/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/init/golden-1/mani.yaml",
    "content": "projects:\n  golden-1:\n    path: .\n    url: https://github.com/alajmo/pinto\n  \n  template-generator:\n    path: nested/template-generator\n    url: https://github.com/alajmo/template-generator\n  \n  tap-report:\n    url: https://github.com/alajmo/tap-report\n  \ntasks:\n  hello:\n    desc: Print Hello World\n    cmd: echo \"Hello World\"\n"
  },
  {
    "path": "test/integration/golden/init/golden-1/nameless/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/init/golden-1/nested/template-generator/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/init/golden-1/stdout.golden",
    "content": "Index: 1\nName: Initialize mani with auto-discovery\nWantErr: false\nCmd:\n(mkdir -p dashgrid && touch dashgrid/empty);\n(mkdir -p tap-report && touch tap-report/empty && cd tap-report && git init -b main && git remote add origin https://github.com/alajmo/tap-report);\n(mkdir -p nested/template-generator && touch nested/template-generator/empty && cd nested/template-generator && git init -b main && git remote add origin https://github.com/alajmo/template-generator);\n(mkdir nameless && touch nameless/empty);\n(git init -b main && git remote add origin https://github.com/alajmo/pinto)\nmani init --color=false\n\n\n---\nInitialized empty Git repository in /home/test/test/tmp/golden/init/golden-1/tap-report/.git/\nInitialized empty Git repository in /home/test/test/tmp/golden/init/golden-1/nested/template-generator/.git/\nInitialized empty Git repository in /home/test/test/tmp/golden/init/golden-1/.git/\n\nInitialized mani repository in /home/test/test/tmp/golden/init/golden-1\n- Created mani.yaml\n- Created .gitignore\n\nFollowing projects were added to mani.yaml\n\n Project            | Path                      \n--------------------+---------------------------\n golden-1           | .                         \n template-generator | nested/template-generator \n tap-report         | tap-report                \n"
  },
  {
    "path": "test/integration/golden/init/golden-2/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/init/golden-2/stdout.golden",
    "content": "Index: 2\nName: Throw error when initialize in existing mani directory\nWantErr: true\nCmd:\nmani init --color=false\n\n---\nerror: `/home/test/test/tmp/golden/init/golden-2` is already a mani directory\n\n"
  },
  {
    "path": "test/integration/golden/list/golden-0/mani.yaml",
    "content": ""
  },
  {
    "path": "test/integration/golden/list/golden-0/stdout.golden",
    "content": "Index: 0\nName: List 0 projects\nWantErr: false\nCmd:\nmani list projects\n\n---\nNo matching projects found\n"
  },
  {
    "path": "test/integration/golden/list/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-1/stdout.golden",
    "content": "Index: 1\nName: List 0 projects on non-existent tag\nWantErr: true\nCmd:\nmani list projects --tags lala\n\n---\nerror: cannot find tags `lala`\n"
  },
  {
    "path": "test/integration/golden/list/golden-10/mani.yaml",
    "content": ""
  },
  {
    "path": "test/integration/golden/list/golden-10/stdout.golden",
    "content": "Index: 10\nName: List empty projects tree\nWantErr: false\nCmd:\nmani list projects --tree\n\n---\n\n\n"
  },
  {
    "path": "test/integration/golden/list/golden-11/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-11/stdout.golden",
    "content": "Index: 11\nName: List full tree\nWantErr: false\nCmd:\nmani list projects --tree\n\n---\n┌─ .\n├─ frontend\n│  ├─ pinto\n│  └─ dashgrid\n└─ template-generator\n\n"
  },
  {
    "path": "test/integration/golden/list/golden-12/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-12/stdout.golden",
    "content": "Index: 12\nName: List tree filtered on tag\nWantErr: false\nCmd:\nmani list projects --tree --tags frontend\n\n---\n── frontend\n   ├─ pinto\n   └─ dashgrid\n\n"
  },
  {
    "path": "test/integration/golden/list/golden-13/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-13/stdout.golden",
    "content": "Index: 13\nName: List all tags\nWantErr: false\nCmd:\nmani list tags\n\n---\n\n Tag      | Project            \n----------+--------------------\n frontend | pinto              \n          | dashgrid           \n misc     | dashgrid           \n cli      | template-generator \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-14/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-14/stdout.golden",
    "content": "Index: 14\nName: List two tags\nWantErr: false\nCmd:\nmani list tags frontend misc\n\n---\n\n Tag      | Project  \n----------+----------\n frontend | pinto    \n          | dashgrid \n misc     | dashgrid \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-15/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-report\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n"
  },
  {
    "path": "test/integration/golden/list/golden-15/stdout.golden",
    "content": "Index: 15\nName: List 0 tasks when no tasks exists \nWantErr: false\nCmd:\nmani list tasks\n\n---\nNo tasks\n"
  },
  {
    "path": "test/integration/golden/list/golden-16/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-16/stdout.golden",
    "content": "Index: 16\nName: List all tasks\nWantErr: false\nCmd:\nmani list tasks\n\n---\n\n Task             | Description    \n------------------+----------------\n fetch            | Fetch git      \n status           |                \n checkout         |                \n create-branch    |                \n multi            |                \n default-tags     |                \n default-projects |                \n default-output   |                \n pwd              |                \n submarine        | Submarine test \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-17/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-17/stdout.golden",
    "content": "Index: 17\nName: List two args\nWantErr: false\nCmd:\nmani list tasks fetch status\n\n---\n\n Task   | Description \n--------+-------------\n fetch  | Fetch git   \n status |             \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-2/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-2/stdout.golden",
    "content": "Index: 2\nName: List 0 projects on 2 non-matching tags\nWantErr: false\nCmd:\nmani list projects --tags frontend,cli\n\n---\nNo matching projects found\n"
  },
  {
    "path": "test/integration/golden/list/golden-3/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-3/stdout.golden",
    "content": "Index: 3\nName: List multiple projects\nWantErr: false\nCmd:\nmani list projects\n\n---\n\n Project            | Tag            \n--------------------+----------------\n example            |                \n pinto              | frontend       \n dashgrid           | frontend, misc \n template-generator | cli            \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-4/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-4/stdout.golden",
    "content": "Index: 4\nName: List only project names and no description/tags\nWantErr: false\nCmd:\nmani list projects --output table --headers project\n\n---\n\n Project            \n--------------------\n example            \n pinto              \n dashgrid           \n template-generator \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-5/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-5/stdout.golden",
    "content": "Index: 5\nName: List projects matching 1 tag\nWantErr: false\nCmd:\nmani list projects --tags frontend\n\n---\n\n Project  | Tag            \n----------+----------------\n pinto    | frontend       \n dashgrid | frontend, misc \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-6/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-6/stdout.golden",
    "content": "Index: 6\nName: List projects matching multiple tags\nWantErr: false\nCmd:\nmani list projects --tags misc,frontend\n\n---\n\n Project  | Tag            \n----------+----------------\n dashgrid | frontend, misc \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-7/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-7/stdout.golden",
    "content": "Index: 7\nName: List two projects\nWantErr: false\nCmd:\nmani list projects pinto dashgrid\n\n---\n\n Project  | Tag            \n----------+----------------\n pinto    | frontend       \n dashgrid | frontend, misc \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-8/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-8/stdout.golden",
    "content": "Index: 8\nName: List projects matching 1 dir\nWantErr: false\nCmd:\nmani list projects --paths frontend\n\n---\n\n Project  | Tag            \n----------+----------------\n pinto    | frontend       \n dashgrid | frontend, misc \n\n"
  },
  {
    "path": "test/integration/golden/list/golden-9/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/list/golden-9/stdout.golden",
    "content": "Index: 9\nName: List 0 projects with no matching paths\nWantErr: true\nCmd:\nmani list projects --paths hello\n\n---\nerror: cannot find paths `hello`\n"
  },
  {
    "path": "test/integration/golden/run/golden-0/stdout.golden",
    "content": "Index: 0\nName: Should fail to run when no configuration file found\nWantErr: true\nCmd:\nmani run pwd --all\n\n\n---\n\u001b[31merror\u001b[0m: cannot find any configuration file [mani.yaml mani.yml .mani.yaml .mani.yml] in current directory or any of the parent directories\n"
  },
  {
    "path": "test/integration/golden/run/golden-1/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-1/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-1/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-1/stdout.golden",
    "content": "Index: 1\nName: Should run in zero projects\nWantErr: true\nCmd:\nmani sync\nmani run pwd -o table\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\nerror: no matching projects found\n"
  },
  {
    "path": "test/integration/golden/run/golden-10/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-10/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-10/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-10/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-10/stdout.golden",
    "content": "Index: 10\nName: Should run multiple commands\nWantErr: false\nCmd:\nmani sync\nmani run pwd multi -o table --all\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | pwd                                                         | multi     \n--------------------+-------------------------------------------------------------+-----------\n example            | /home/test/test/tmp/golden/run/golden-10                    | 1st line  \n                    |                                                             | 2nd line  \n--------------------+-------------------------------------------------------------+-----------\n pinto              | /home/test/test/tmp/golden/run/golden-10/frontend/pinto     | 1st line  \n                    |                                                             | 2nd line  \n--------------------+-------------------------------------------------------------+-----------\n dashgrid           | /home/test/test/tmp/golden/run/golden-10/frontend/dashgrid  | 1st line  \n                    |                                                             | 2nd line  \n--------------------+-------------------------------------------------------------+-----------\n template-generator | /home/test/test/tmp/golden/run/golden-10/template-generator | 1st line  \n                    |                                                             | 2nd line  \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-11/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-11/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-11/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-11/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-11/stdout.golden",
    "content": "Index: 11\nName: Should run sub-commands\nWantErr: false\nCmd:\nmani sync\nmani run submarine --all\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | command-1 | command-2 | command-3 | pwd                                                         | submarine \n--------------------+-----------+-----------+-----------+-------------------------------------------------------------+-----------\n example            | 1         | 2         | 3         | /home/test/test/tmp/golden/run/golden-11                    | 0         \n--------------------+-----------+-----------+-----------+-------------------------------------------------------------+-----------\n pinto              | 1         | 2         | 3         | /home/test/test/tmp/golden/run/golden-11/frontend/pinto     | 0         \n--------------------+-----------+-----------+-----------+-------------------------------------------------------------+-----------\n dashgrid           | 1         | 2         | 3         | /home/test/test/tmp/golden/run/golden-11/frontend/dashgrid  | 0         \n--------------------+-----------+-----------+-----------+-------------------------------------------------------------+-----------\n template-generator | 1         | 2         | 3         | /home/test/test/tmp/golden/run/golden-11/template-generator | 0         \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-2/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-2/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-2/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-2/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-2/stdout.golden",
    "content": "Index: 2\nName: Should run in all projects\nWantErr: false\nCmd:\nmani sync\nmani run --all pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\nTASK [pwd]\n\nexample | /home/test/test/tmp/golden/run/golden-2\n\nTASK [pwd]\n\npinto | /home/test/test/tmp/golden/run/golden-2/frontend/pinto\n\nTASK [pwd]\n\ndashgrid | /home/test/test/tmp/golden/run/golden-2/frontend/dashgrid\n\nTASK [pwd]\n\ntemplate-generator | /home/test/test/tmp/golden/run/golden-2/template-generator\n\n"
  },
  {
    "path": "test/integration/golden/run/golden-3/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-3/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-3/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-3/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-3/stdout.golden",
    "content": "Index: 3\nName: Should run when filtered on project\nWantErr: false\nCmd:\nmani sync\nmani run -o table --projects pinto pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project | pwd                                                    \n---------+--------------------------------------------------------\n pinto   | /home/test/test/tmp/golden/run/golden-3/frontend/pinto \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-4/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-4/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-4/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-4/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-4/stdout.golden",
    "content": "Index: 4\nName: Should run when filtered on tags\nWantErr: false\nCmd:\nmani sync\nmani run -o table --tags frontend pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project  | pwd                                                       \n----------+-----------------------------------------------------------\n pinto    | /home/test/test/tmp/golden/run/golden-4/frontend/pinto    \n----------+-----------------------------------------------------------\n dashgrid | /home/test/test/tmp/golden/run/golden-4/frontend/dashgrid \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-5/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-5/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-5/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-5/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-5/stdout.golden",
    "content": "Index: 5\nName: Should run when filtered on cwd\nWantErr: false\nCmd:\nmani sync\ncd template-generator\nmani run -o table --cwd pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | pwd                                                        \n--------------------+------------------------------------------------------------\n template-generator | /home/test/test/tmp/golden/run/golden-5/template-generator \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-6/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-6/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-6/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-6/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-6/stdout.golden",
    "content": "Index: 6\nName: Should run on default tags\nWantErr: false\nCmd:\nmani sync\nmani run -o table default-tags\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project  | default-tags                                              \n----------+-----------------------------------------------------------\n pinto    | /home/test/test/tmp/golden/run/golden-6/frontend/pinto    \n----------+-----------------------------------------------------------\n dashgrid | /home/test/test/tmp/golden/run/golden-6/frontend/dashgrid \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-7/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-7/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-7/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-7/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-7/stdout.golden",
    "content": "Index: 7\nName: Should run on default projects\nWantErr: false\nCmd:\nmani sync\nmani run -o table default-projects\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project  | default-projects                                          \n----------+-----------------------------------------------------------\n dashgrid | /home/test/test/tmp/golden/run/golden-7/frontend/dashgrid \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-8/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-8/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-8/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-8/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-8/stdout.golden",
    "content": "Index: 8\nName: Should print table when output set to table in task\nWantErr: false\nCmd:\nmani sync\nmani run default-output -p dashgrid\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project  | default-output                                            \n----------+-----------------------------------------------------------\n dashgrid | /home/test/test/tmp/golden/run/golden-8/frontend/dashgrid \n\n"
  },
  {
    "path": "test/integration/golden/run/golden-9/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/run/golden-9/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-9/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/run/golden-9/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/run/golden-9/stdout.golden",
    "content": "Index: 9\nName: Should dry run\nWantErr: false\nCmd:\nmani sync\nmani run --dry-run --projects template-generator -o table pwd\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n\n project            | pwd \n--------------------+-----\n template-generator | pwd \n\n"
  },
  {
    "path": "test/integration/golden/sync/golden-0/stdout.golden",
    "content": "Index: 0\nName: Throw error when trying to sync a non-existing mani repository\nWantErr: true\nCmd:\nmani sync\n\n\n---\n\u001b[31merror\u001b[0m: cannot find any configuration file [mani.yaml mani.yml .mani.yaml .mani.yml] in current directory or any of the parent directories\n"
  },
  {
    "path": "test/integration/golden/sync/golden-1/.gitignore",
    "content": "outside\n\n# mani #\ntemplate-generator\nfrontend/dashgrid\nfrontend/pinto\n# mani #\n\noutside\nfrontend/pinto-vim\n"
  },
  {
    "path": "test/integration/golden/sync/golden-1/frontend/dashgrid/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/sync/golden-1/frontend/pinto/empty",
    "content": ""
  },
  {
    "path": "test/integration/golden/sync/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/sync/golden-1/stdout.golden",
    "content": "Index: 1\nName: Should sync\nWantErr: false\nCmd:\nmani sync\n\n\n---\n\nProject [pinto]\n\n\nProject [dashgrid]\n\n\nProject [template-generator]\n\n\n\n Project            | Synced \n--------------------+--------\n example            | ✓      \n pinto              | ✓      \n dashgrid           | ✓      \n template-generator | ✓      \n\n"
  },
  {
    "path": "test/integration/golden/version/golden-0/stdout.golden",
    "content": "Index: 0\nName: Print version when no mani config is found\nWantErr: false\nCmd:\nmani --version\n\n---\nVersion: dev       \nCommit: none      \nDate: n/a       \n"
  },
  {
    "path": "test/integration/golden/version/golden-1/mani.yaml",
    "content": "projects:\n  example:\n    path: .\n\n  pinto:\n    path: frontend/pinto\n    url: https://github.com/alajmo/pinto\n    tags: [frontend]\n\n  dashgrid:\n    path: frontend/dashgrid/../dashgrid\n    url: https://github.com/alajmo/dashgrid\n    tags: [frontend, misc]\n\n  template-generator:\n    url: https://github.com/alajmo/template-generator\n    tags: [cli]\n    env:\n      branch: dev\n\nenv:\n  VERSION: v.1.2.3\n  TEST: $(echo \"Hello World\")\n  NO_COLOR: true\n\nspecs:\n  table:\n    output: table\n    parallel: false\n    ignore_errors: false\n\ntasks:\n  fetch:\n    desc: Fetch git\n    cmd: git fetch\n\n  status:\n    cmd: git status\n\n  checkout:\n    env:\n      branch: dev\n    cmd: git checkout $branch\n\n  create-branch:\n    cmd: git checkout -b $branch\n\n  multi:\n    cmd: | # Multi line command\n      echo \"1st line \"\n      echo \"2nd line\"\n\n  default-tags:\n    target:\n      tags: [frontend]\n    cmd: pwd\n\n  default-projects:\n    target:\n      projects: [dashgrid]\n    cmd: pwd\n\n  default-output:\n    spec:\n      output: table\n    cmd: pwd\n\n  pwd: pwd\n\n  submarine:\n    desc: Submarine test\n    cmd: echo 0\n    spec: table\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/integration/golden/version/golden-1/stdout.golden",
    "content": "Index: 1\nName: Print version when mani config is found\nWantErr: false\nCmd:\nmani --version\n\n---\nVersion: dev       \nCommit: none      \nDate: n/a       \n"
  },
  {
    "path": "test/integration/init_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestInit(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t{\n\t\t\tTestName:   \"Initialize mani in empty directory\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd:    \"mani init --color=false\",\n\t\t\tWantErr:    false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Initialize mani with auto-discovery\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd: `\n\t\t\t(mkdir -p dashgrid && touch dashgrid/empty);\n\t\t\t(mkdir -p tap-report && touch tap-report/empty && cd tap-report && git init -b main && git remote add origin https://github.com/alajmo/tap-report);\n\t\t\t(mkdir -p nested/template-generator && touch nested/template-generator/empty && cd nested/template-generator && git init -b main && git remote add origin https://github.com/alajmo/template-generator);\n\t\t\t(mkdir nameless && touch nameless/empty);\n\t\t\t(git init -b main && git remote add origin https://github.com/alajmo/pinto)\n\t\t\tmani init --color=false\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Throw error when initialize in existing mani directory\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani init --color=false\",\n\t\t\tWantErr:    true,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"init/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/list_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestList(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t// Projects\n\t\t{\n\t\t\tTestName:   \"List 0 projects\",\n\t\t\tInputFiles: []string{\"mani-empty/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List 0 projects on non-existent tag\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tags lala\",\n\t\t\tWantErr:    true,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List 0 projects on 2 non-matching tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tags frontend,cli\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List multiple projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List only project names and no description/tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --output table --headers project\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List projects matching 1 tag\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tags frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List projects matching multiple tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tags misc,frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List two projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects pinto dashgrid\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List projects matching 1 dir\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --paths frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List 0 projects with no matching paths\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --paths hello\",\n\t\t\tWantErr:    true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"List empty projects tree\",\n\t\t\tInputFiles: []string{\"mani-empty/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tree\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List full tree\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tree\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List tree filtered on tag\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list projects --tree --tags frontend\",\n\t\t\tWantErr:    false,\n\t\t},\n\n\t\t// Tags\n\t\t{\n\t\t\tTestName:   \"List all tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list tags\",\n\t\t\tGolden:     \"list/tags\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List two tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list tags frontend misc\",\n\t\t\tGolden:     \"list/tags-2-args\",\n\t\t\tWantErr:    false,\n\t\t},\n\n\t\t// Tasks\n\t\t{\n\t\t\tTestName:   \"List 0 tasks when no tasks exists \",\n\t\t\tInputFiles: []string{\"mani-no-tasks/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list tasks\",\n\t\t\tGolden:     \"list/tasks-empty\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List all tasks\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list tasks\",\n\t\t\tGolden:     \"list/tasks\",\n\t\t\tWantErr:    false,\n\t\t},\n\t\t{\n\t\t\tTestName:   \"List two args\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani list tasks fetch status\",\n\t\t\tGolden:     \"list/tasks-2-args\",\n\t\t\tWantErr:    false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"list/golden-%d\", i)\n\t\tcases[i].Index = i\n\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/main_test.go",
    "content": "package integration\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gookit/color\"\n\t\"github.com/kr/pretty\"\n\t\"github.com/otiai10/copy\"\n)\n\nvar tmpPath = \"/home/test/test/tmp\"\nvar rootDir = \"\"\n\nvar debug = flag.Bool(\"debug\", false, \"debug\")\nvar update = flag.Bool(\"update\", false, \"update golden files\")\nvar clean = flag.Bool(\"clean\", false, \"Clean tmp directory after run\")\n\nvar copyOpts = copy.Options{\n\tSkip: func(src string) (bool, error) {\n\t\treturn strings.HasSuffix(src, \".git\"), nil\n\t},\n}\n\ntype TemplateTest struct {\n\tTestName   string\n\tInputFiles []string\n\tTestCmd    string\n\tGolden     string\n\tIgnore     bool\n\tWantErr    bool\n\tIndex      int\n}\n\nfunc (tt TemplateTest) GoldenOutput(output []byte) []byte {\n\tout := string(output)\n\ttestCmd := strings.ReplaceAll(tt.TestCmd, \"\\t\", \"\")\n\ttestCmd = strings.TrimLeft(testCmd, \"\\n\")\n\tgolden := fmt.Sprintf(\n\t\t\"Index: %d\\nName: %s\\nWantErr: %t\\nCmd:\\n%s\\n\\n---\\n%s\",\n\t\ttt.Index, tt.TestName, tt.WantErr, testCmd, out,\n\t)\n\n\treturn []byte(golden)\n}\n\ntype TestFile struct {\n\tt    *testing.T\n\tname string\n\tdir  string\n}\n\nfunc NewGoldenFile(t *testing.T, name string) *TestFile {\n\treturn &TestFile{t: t, name: \"stdout.golden\", dir: filepath.Join(\"golden\", name)}\n}\n\nfunc (tf *TestFile) Dir() string {\n\ttf.t.Helper()\n\t_, filename, _, ok := runtime.Caller(0)\n\tif !ok {\n\t\ttf.t.Fatal(\"problems recovering caller information\")\n\t}\n\n\treturn filepath.Join(filepath.Dir(filename), tf.dir)\n}\n\nfunc (tf *TestFile) path() string {\n\ttf.t.Helper()\n\t_, filename, _, ok := runtime.Caller(0)\n\tif !ok {\n\t\ttf.t.Fatal(\"problems recovering caller information\")\n\t}\n\n\treturn filepath.Join(filepath.Dir(filename), tf.dir, tf.name)\n}\n\nfunc (tf *TestFile) Write(content string) {\n\ttf.t.Helper()\n\terr := os.MkdirAll(filepath.Dir(tf.path()), os.ModePerm)\n\tif err != nil {\n\t\ttf.t.Fatalf(\"could not create directory %s: %v\", tf.name, err)\n\t}\n\n\terr = os.WriteFile(tf.path(), []byte(content), 0644)\n\tif err != nil {\n\t\ttf.t.Fatalf(\"could not write %s: %v\", tf.name, err)\n\t}\n}\n\nfunc clearGolden(goldenDir string) {\n\t// Guard against accidentally deleting outside directory\n\tif strings.Contains(goldenDir, \"golden\") {\n\t\t_ = os.RemoveAll(goldenDir)\n\t}\n}\n\nfunc clearTmp() {\n\tdir, _ := os.ReadDir(path.Join(tmpPath, \"golden\"))\n\tfor _, d := range dir {\n\t\tf := path.Join(tmpPath, \"golden\", path.Join([]string{d.Name()}...))\n\t\t_ = os.RemoveAll(f)\n\t}\n}\n\nfunc diff(expected, actual any) []string {\n\treturn pretty.Diff(expected, actual)\n}\n\n// 1. Clean tmp directory\n// 2. Create mani binary\n// 3. cd into test/tmp\nfunc TestMain(m *testing.M) {\n\tclearTmp()\n\n\tvar wd, err = os.Getwd()\n\tif err != nil {\n\t\tlog.Fatalf(\"could not get wd\")\n\t}\n\trootDir = filepath.Dir(wd)\n\n\terr = os.Chdir(\"../..\")\n\tif err != nil {\n\t\tlog.Fatalf(\"could not change dir: %v\", err)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc printDirectoryContent(dir string) {\n\terr := filepath.Walk(dir,\n\t\tfunc(path string, info os.FileInfo, err error) error {\n\t\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\tif err != nil {\n\t\tlog.Fatalf(\"could not walk dir: %v\", err)\n\t}\n}\n\nfunc countFilesAndFolders(dir string) int {\n\tvar count = 0\n\terr := filepath.Walk(dir,\n\t\tfunc(path string, info os.FileInfo, err error) error {\n\t\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\tcount = count + 1\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\tif err != nil {\n\t\tlog.Fatalf(\"could not walk dir: %v\", err)\n\t}\n\n\treturn count\n}\n\nfunc Run(t *testing.T, tt TemplateTest) {\n\tlog.SetFlags(0)\n\tvar tmpDir = filepath.Join(tmpPath, \"golden\", tt.Golden)\n\tif _, err := os.Stat(tmpDir); os.IsNotExist(err) {\n\t\terr = os.MkdirAll(tmpDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"could not create directory at %s: %v\", tmpPath, err)\n\t\t}\n\t}\n\n\terr := os.Chdir(tmpDir)\n\tif err != nil {\n\t\tt.Fatalf(\"could not change dir: %v\", err)\n\t}\n\n\tvar fixturesDir = filepath.Join(rootDir, \"fixtures\")\n\n\tt.Cleanup(func() {\n\t\tif *clean {\n\t\t\tclearTmp()\n\t\t}\n\t})\n\n\t// Copy fixture files\n\tfor _, file := range tt.InputFiles {\n\t\tvar configPath = filepath.Join(fixturesDir, file)\n\t\terr := copy.Copy(configPath, filepath.Base(file), copyOpts)\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"\\x1b[31;1m%s\\x1b[0m\\n\", fmt.Sprintf(\"error: %s\", err))\n\t\t}\n\t}\n\n\t// Run test command\n\tcmd := exec.Command(\"sh\", \"-c\", tt.TestCmd)\n\tcmd.Env = os.Environ()\n\n\toutput, err := cmd.CombinedOutput()\n\t// TEST: Check we get error if we want error\n\tif (err != nil) != tt.WantErr {\n\t\tt.Fatalf(\"%s\\nexpected (err != nil) to be %v, but got %v. err: %v\", output, tt.WantErr, err != nil, err)\n\t}\n\n\tif *debug {\n\t\tfmt.Println(tt.TestCmd)\n\t\tfmt.Println(string(output))\n\t}\n\n\t// Save output from command as golden file\n\tgolden := NewGoldenFile(t, tt.Golden)\n\t// TODO\n\tactual := string(tt.GoldenOutput(output))\n\n\tvar goldenFile = path.Join(tmpDir, \"stdout.golden\")\n\t// Write output to tmp file which will be used to compare with golden files\n\t// TODO\n\terr = os.WriteFile(goldenFile, tt.GoldenOutput(output), 0644)\n\tif err != nil {\n\t\tt.Fatalf(\"could not write %s: %v\", goldenFile, err)\n\t}\n\n\tif *update {\n\t\tclearGolden(golden.Dir())\n\n\t\t// Write stdout of test command to golden file\n\t\tgolden.Write(actual)\n\n\t\terr := copy.Copy(tmpDir, golden.Dir(), copyOpts)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"\\x1b[31;1m%s\\x1b[0m\\n\", fmt.Sprintf(\"error: %s\", err))\n\t\t}\n\t} else {\n\t\terr := filepath.Walk(golden.Dir(), func(path string, info os.FileInfo, err error) error {\n\t\t\t// Skip project files, they require an empty file to be added to git\n\t\t\tif filepath.Base(path) == \"empty\" {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif info.IsDir() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif path == tmpDir {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error: %v\", err)\n\t\t\t}\n\n\t\t\ttmpPath := filepath.Join(tmpDir, filepath.Base(path))\n\n\t\t\tactual, err := os.ReadFile(tmpPath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error: %v\", err)\n\t\t\t}\n\n\t\t\texpected, err := os.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error: %v\", err)\n\t\t\t}\n\n\t\t\t// TEST: Check file content difference for each generated file\n\t\t\tif !tt.Ignore && !reflect.DeepEqual(actual, expected) {\n\t\t\t\tfmt.Println(color.FgGreen.Sprintf(\"EXPECTED:\"))\n\t\t\t\tfmt.Println(\"<---------------------\")\n\t\t\t\tfmt.Println(string(expected))\n\t\t\t\tfmt.Println(\"--------------------->\")\n\n\t\t\t\tfmt.Println()\n\n\t\t\t\tfmt.Println(color.FgRed.Sprintf(\"ACTUAL:\"))\n\t\t\t\tfmt.Println(\"<---------------------\")\n\t\t\t\tfmt.Println(string(actual))\n\t\t\t\tfmt.Println(\"--------------------->\")\n\n\t\t\t\tt.Fatalf(\"\\nfile: %v\\ndiff: %v\", color.FgBlue.Sprint(path), diff(expected, actual))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\t// TEST: Check the total amount of files and directories match\n\t\texpectedCount := countFilesAndFolders(golden.Dir())\n\t\tactualCount := countFilesAndFolders(tmpDir)\n\n\t\tif expectedCount != actualCount {\n\t\t\tfmt.Println(color.FgGreen.Sprintf(\"EXPECTED:\"))\n\t\t\tprintDirectoryContent(golden.Dir())\n\n\t\t\tfmt.Println(color.FgRed.Sprintf(\"ACTUAL:\"))\n\t\t\tprintDirectoryContent(tmpDir)\n\n\t\t\tt.Fatalf(\"\\nexpected count: %v\\nactual count: %v\", expectedCount, actualCount)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Error: %v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "test/integration/run_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestRun(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t{\n\t\t\tTestName:   \"Should fail to run when no configuration file found\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd: `\n\t\t\tmani run pwd --all\n\t\t\t`,\n\t\t\tWantErr: true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run in zero projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run pwd -o table\n\t\t\t`,\n\t\t\tWantErr: true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run in all projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run --all pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run when filtered on project\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run -o table --projects pinto pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run when filtered on tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run -o table --tags frontend pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run when filtered on cwd\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tcd template-generator\n\t\t\tmani run -o table --cwd pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run on default tags\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run -o table default-tags\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run on default projects\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run -o table default-projects\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should print table when output set to table in task\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run default-output -p dashgrid\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should dry run\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run --dry-run --projects template-generator -o table pwd\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run multiple commands\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run pwd multi -o table --all\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should run sub-commands\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\tmani run submarine --all\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"run/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/sync_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestSync(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t{\n\t\t\tTestName:   \"Throw error when trying to sync a non-existing mani repository\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\t`,\n\t\t\tWantErr: true,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Should sync\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\", \"mani-advanced/.gitignore\"},\n\t\t\tTestCmd: `\n\t\t\tmani sync\n\t\t\t`,\n\t\t\tWantErr: false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"sync/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/version_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestVersion(t *testing.T) {\n\tvar cases = []TemplateTest{\n\t\t{\n\t\t\tTestName:   \"Print version when no mani config is found\",\n\t\t\tInputFiles: []string{},\n\t\t\tTestCmd:    \"mani --version\",\n\t\t\tIgnore:     true,\n\t\t\tWantErr:    false,\n\t\t},\n\n\t\t{\n\t\t\tTestName:   \"Print version when mani config is found\",\n\t\t\tInputFiles: []string{\"mani-advanced/mani.yaml\"},\n\t\t\tTestCmd:    \"mani --version\",\n\t\t\tIgnore:     true,\n\t\t\tWantErr:    false,\n\t\t},\n\t}\n\n\tfor i, tt := range cases {\n\t\tcases[i].Golden = fmt.Sprintf(\"version/golden-%d\", i)\n\t\tcases[i].Index = i\n\t\tt.Run(tt.TestName, func(t *testing.T) {\n\t\t\tRun(t, cases[i])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/playground/.gitignore",
    "content": "# mani #\ntemplate-generator\nkaka\nfrontend/tap-report\nfrontend/dashgrid\n# mani #\n"
  },
  {
    "path": "test/playground/imports/many-projects.yaml",
    "content": "projects:\n  template-generator-1:\n    path: ../many/template-generator-1\n    url: https://github.com/alajmo/template-generator\n  template-generator-2:\n    path: ../many/template-generator-2\n    url: https://github.com/alajmo/template-generator\n  template-generator-3:\n    path: ../many/template-generator-3\n    url: https://github.com/alajmo/template-generator\n  template-generator-4:\n    path: ../many/template-generator-4\n    url: https://github.com/alajmo/template-generator\n  template-generator-5:\n    path: ../many/template-generator-5\n    url: https://github.com/alajmo/template-generator\n  template-generator-6:\n    path: ../many/template-generator-6\n    url: https://github.com/alajmo/template-generator\n  template-generator-7:\n    path: ../many/template-generator-7\n    url: https://github.com/alajmo/template-generator\n  template-generator-8:\n    path: ../many/template-generator-8\n    url: https://github.com/alajmo/template-generator\n  template-generator-9:\n    path: ../many/template-generator-9\n    url: https://github.com/alajmo/template-generator\n  template-generator-10:\n    path: ../many/template-generator-10\n    url: https://github.com/alajmo/template-generator\n  template-generator-11:\n    path: ../many/template-generator-11\n    url: https://github.com/alajmo/template-generator\n  template-generator-12:\n    path: ../many/template-generator-12\n    url: https://github.com/alajmo/template-generator\n  template-generator-13:\n    path: ../many/template-generator-13\n    url: https://github.com/alajmo/template-generator\n  template-generator-14:\n    path: ../many/template-generator-14\n    url: https://github.com/alajmo/template-generator\n  template-generator-15:\n    path: ../many/template-generator-15\n    url: https://github.com/alajmo/template-generator\n  template-generator-16:\n    path: ../many/template-generator-16\n    url: https://github.com/alajmo/template-generator\n  template-generator-17:\n    path: ../many/template-generator-17\n    url: https://github.com/alajmo/template-generator\n  template-generator-18:\n    path: ../many/template-generator-18\n    url: https://github.com/alajmo/template-generator\n  template-generator-19:\n    path: ../many/template-generator-19\n    url: https://github.com/alajmo/template-generator\n  template-generator-20:\n    path: ../many/template-generator-20\n    url: https://github.com/alajmo/template-generator\n  template-generator-21:\n    path: ../many/template-generator-21\n    url: https://github.com/alajmo/template-generator\n  template-generator-22:\n    path: ../many/template-generator-22\n    url: https://github.com/alajmo/template-generator\n  template-generator-23:\n    path: ../many/template-generator-23\n    url: https://github.com/alajmo/template-generator\n  template-generator-24:\n    path: ../many/template-generator-24\n    url: https://github.com/alajmo/template-generator\n  template-generator-25:\n    path: ../many/template-generator-25\n    url: https://github.com/alajmo/template-generator\n  template-generator-26:\n    path: ../many/template-generator-26\n    url: https://github.com/alajmo/template-generator\n  template-generator-27:\n    path: ../many/template-generator-27\n    url: https://github.com/alajmo/template-generator\n  template-generator-28:\n    path: ../many/template-generator-28\n    url: https://github.com/alajmo/template-generator\n  template-generator-29:\n    path: ../many/template-generator-29\n    url: https://github.com/alajmo/template-generator\n  template-generator-30:\n    path: ../many/template-generator-30\n    url: https://github.com/alajmo/template-generator\n  template-generator-31:\n    path: ../many/template-generator-31\n    url: https://github.com/alajmo/template-generator\n  template-generator-32:\n    path: ../many/template-generator-32\n    url: https://github.com/alajmo/template-generator\n  template-generator-33:\n    path: ../many/template-generator-33\n    url: https://github.com/alajmo/template-generator\n  template-generator-34:\n    path: ../many/template-generator-34\n    url: https://github.com/alajmo/template-generator\n  template-generator-35:\n    path: ../many/template-generator-35\n    url: https://github.com/alajmo/template-generator\n  template-generator-36:\n    path: ../many/template-generator-36\n    url: https://github.com/alajmo/template-generator\n  template-generator-37:\n    path: ../many/template-generator-37\n    url: https://github.com/alajmo/template-generator\n  template-generator-38:\n    path: ../many/template-generator-38\n    url: https://github.com/alajmo/template-generator\n  template-generator-39:\n    path: ../many/template-generator-39\n    url: https://github.com/alajmo/template-generator\n  template-generator-40:\n    path: ../many/template-generator-40\n    url: https://github.com/alajmo/template-generator\n  template-generator-41:\n    path: ../many/template-generator-41\n    url: https://github.com/alajmo/template-generator\n  template-generator-42:\n    path: ../many/template-generator-42\n    url: https://github.com/alajmo/template-generator\n  template-generator-43:\n    path: ../many/template-generator-43\n    url: https://github.com/alajmo/template-generator\n  template-generator-44:\n    path: ../many/template-generator-44\n    url: https://github.com/alajmo/template-generator\n  template-generator-45:\n    path: ../many/template-generator-45\n    url: https://github.com/alajmo/template-generator\n  template-generator-46:\n    path: ../many/template-generator-46\n    url: https://github.com/alajmo/template-generator\n  template-generator-47:\n    path: ../many/template-generator-47\n    url: https://github.com/alajmo/template-generator\n  template-generator-48:\n    path: ../many/template-generator-48\n    url: https://github.com/alajmo/template-generator\n  template-generator-49:\n    path: ../many/template-generator-49\n    url: https://github.com/alajmo/template-generator\n  template-generator-50:\n    path: ../many/template-generator-50\n    url: https://github.com/alajmo/template-generator\n  template-generator-51:\n    path: ../many/template-generator-51\n    url: https://github.com/alajmo/template-generator\n  template-generator-52:\n    path: ../many/template-generator-52\n    url: https://github.com/alajmo/template-generator\n  template-generator-53:\n    path: ../many/template-generator-53\n    url: https://github.com/alajmo/template-generator\n  template-generator-54:\n    path: ../many/template-generator-54\n    url: https://github.com/alajmo/template-generator\n  template-generator-55:\n    path: ../many/template-generator-55\n    url: https://github.com/alajmo/template-generator\n  template-generator-56:\n    path: ../many/template-generator-56\n    url: https://github.com/alajmo/template-generator\n  template-generator-57:\n    path: ../many/template-generator-57\n    url: https://github.com/alajmo/template-generator\n  template-generator-58:\n    path: ../many/template-generator-58\n    url: https://github.com/alajmo/template-generator\n  template-generator-59:\n    path: ../many/template-generator-59\n    url: https://github.com/alajmo/template-generator\n  template-generator-60:\n    path: ../many/template-generator-60\n    url: https://github.com/alajmo/template-generator\n  template-generator-61:\n    path: ../many/template-generator-61\n    url: https://github.com/alajmo/template-generator\n  template-generator-62:\n    path: ../many/template-generator-62\n    url: https://github.com/alajmo/template-generator\n  template-generator-63:\n    path: ../many/template-generator-63\n    url: https://github.com/alajmo/template-generator\n  template-generator-64:\n    path: ../many/template-generator-64\n    url: https://github.com/alajmo/template-generator\n  template-generator-65:\n    path: ../many/template-generator-65\n    url: https://github.com/alajmo/template-generator\n  template-generator-66:\n    path: ../many/template-generator-66\n    url: https://github.com/alajmo/template-generator\n  template-generator-67:\n    path: ../many/template-generator-67\n    url: https://github.com/alajmo/template-generator\n  template-generator-68:\n    path: ../many/template-generator-68\n    url: https://github.com/alajmo/template-generator\n  template-generator-69:\n    path: ../many/template-generator-69\n    url: https://github.com/alajmo/template-generator\n  template-generator-70:\n    path: ../many/template-generator-70\n    url: https://github.com/alajmo/template-generator\n  template-generator-71:\n    path: ../many/template-generator-71\n    url: https://github.com/alajmo/template-generator\n  template-generator-72:\n    path: ../many/template-generator-72\n    url: https://github.com/alajmo/template-generator\n  template-generator-73:\n    path: ../many/template-generator-73\n    url: https://github.com/alajmo/template-generator\n  template-generator-74:\n    path: ../many/template-generator-74\n    url: https://github.com/alajmo/template-generator\n  template-generator-75:\n    path: ../many/template-generator-75\n    url: https://github.com/alajmo/template-generator\n  template-generator-76:\n    path: ../many/template-generator-76\n    url: https://github.com/alajmo/template-generator\n  template-generator-77:\n    path: ../many/template-generator-77\n    url: https://github.com/alajmo/template-generator\n  template-generator-78:\n    path: ../many/template-generator-78\n    url: https://github.com/alajmo/template-generator\n  template-generator-79:\n    path: ../many/template-generator-79\n    url: https://github.com/alajmo/template-generator\n  template-generator-80:\n    path: ../many/template-generator-80\n    url: https://github.com/alajmo/template-generator\n  template-generator-81:\n    path: ../many/template-generator-81\n    url: https://github.com/alajmo/template-generator\n  template-generator-82:\n    path: ../many/template-generator-82\n    url: https://github.com/alajmo/template-generator\n  template-generator-83:\n    path: ../many/template-generator-83\n    url: https://github.com/alajmo/template-generator\n  template-generator-84:\n    path: ../many/template-generator-84\n    url: https://github.com/alajmo/template-generator\n  template-generator-85:\n    path: ../many/template-generator-85\n    url: https://github.com/alajmo/template-generator\n  template-generator-86:\n    path: ../many/template-generator-86\n    url: https://github.com/alajmo/template-generator\n  template-generator-87:\n    path: ../many/template-generator-87\n    url: https://github.com/alajmo/template-generator\n  template-generator-88:\n    path: ../many/template-generator-88\n    url: https://github.com/alajmo/template-generator\n  template-generator-89:\n    path: ../many/template-generator-89\n    url: https://github.com/alajmo/template-generator\n  template-generator-90:\n    path: ../many/template-generator-90\n    url: https://github.com/alajmo/template-generator\n  template-generator-91:\n    path: ../many/template-generator-91\n    url: https://github.com/alajmo/template-generator\n  template-generator-92:\n    path: ../many/template-generator-92\n    url: https://github.com/alajmo/template-generator\n  template-generator-93:\n    path: ../many/template-generator-93\n    url: https://github.com/alajmo/template-generator\n  template-generator-94:\n    path: ../many/template-generator-94\n    url: https://github.com/alajmo/template-generator\n  template-generator-95:\n    path: ../many/template-generator-95\n    url: https://github.com/alajmo/template-generator\n  template-generator-96:\n    path: ../many/template-generator-96\n    url: https://github.com/alajmo/template-generator\n  template-generator-97:\n    path: ../many/template-generator-97\n    url: https://github.com/alajmo/template-generator\n  template-generator-98:\n    path: ../many/template-generator-98\n    url: https://github.com/alajmo/template-generator\n  template-generator-99:\n    path: ../many/template-generator-99\n    url: https://github.com/alajmo/template-generator\n  template-generator-100:\n    path: ../many/template-generator-100\n    url: https://github.com/alajmo/template-generator\n  template-generator-101:\n    path: ../many/template-generator-101\n    url: https://github.com/alajmo/template-generator\n  template-generator-102:\n    path: ../many/template-generator-102\n    url: https://github.com/alajmo/template-generator\n  template-generator-103:\n    path: ../many/template-generator-103\n    url: https://github.com/alajmo/template-generator\n  template-generator-104:\n    path: ../many/template-generator-104\n    url: https://github.com/alajmo/template-generator\n  template-generator-105:\n    path: ../many/template-generator-105\n    url: https://github.com/alajmo/template-generator\n  template-generator-106:\n    path: ../many/template-generator-106\n    url: https://github.com/alajmo/template-generator\n  template-generator-107:\n    path: ../many/template-generator-107\n    url: https://github.com/alajmo/template-generator\n  template-generator-108:\n    path: ../many/template-generator-108\n    url: https://github.com/alajmo/template-generator\n  template-generator-109:\n    path: ../many/template-generator-109\n    url: https://github.com/alajmo/template-generator\n  template-generator-110:\n    path: ../many/template-generator-110\n    url: https://github.com/alajmo/template-generator\n  template-generator-111:\n    path: ../many/template-generator-111\n    url: https://github.com/alajmo/template-generator\n  template-generator-112:\n    path: ../many/template-generator-112\n    url: https://github.com/alajmo/template-generator\n  template-generator-113:\n    path: ../many/template-generator-113\n    url: https://github.com/alajmo/template-generator\n  template-generator-114:\n    path: ../many/template-generator-114\n    url: https://github.com/alajmo/template-generator\n  template-generator-115:\n    path: ../many/template-generator-115\n    url: https://github.com/alajmo/template-generator\n  template-generator-116:\n    path: ../many/template-generator-116\n    url: https://github.com/alajmo/template-generator\n  template-generator-117:\n    path: ../many/template-generator-117\n    url: https://github.com/alajmo/template-generator\n  template-generator-118:\n    path: ../many/template-generator-118\n    url: https://github.com/alajmo/template-generator\n  template-generator-119:\n    path: ../many/template-generator-119\n    url: https://github.com/alajmo/template-generator\n  template-generator-120:\n    path: ../many/template-generator-120\n    url: https://github.com/alajmo/template-generator\n  template-generator-121:\n    path: ../many/template-generator-121\n    url: https://github.com/alajmo/template-generator\n  template-generator-122:\n    path: ../many/template-generator-122\n    url: https://github.com/alajmo/template-generator\n  template-generator-123:\n    path: ../many/template-generator-123\n    url: https://github.com/alajmo/template-generator\n  template-generator-124:\n    path: ../many/template-generator-124\n    url: https://github.com/alajmo/template-generator\n  template-generator-125:\n    path: ../many/template-generator-125\n    url: https://github.com/alajmo/template-generator\n  template-generator-126:\n    path: ../many/template-generator-126\n    url: https://github.com/alajmo/template-generator\n  template-generator-127:\n    path: ../many/template-generator-127\n    url: https://github.com/alajmo/template-generator\n  template-generator-128:\n    path: ../many/template-generator-128\n    url: https://github.com/alajmo/template-generator\n  template-generator-129:\n    path: ../many/template-generator-129\n    url: https://github.com/alajmo/template-generator\n  template-generator-130:\n    path: ../many/template-generator-130\n    url: https://github.com/alajmo/template-generator\n  template-generator-131:\n    path: ../many/template-generator-131\n    url: https://github.com/alajmo/template-generator\n  template-generator-132:\n    path: ../many/template-generator-132\n    url: https://github.com/alajmo/template-generator\n  template-generator-133:\n    path: ../many/template-generator-133\n    url: https://github.com/alajmo/template-generator\n  template-generator-134:\n    path: ../many/template-generator-134\n    url: https://github.com/alajmo/template-generator\n  template-generator-135:\n    path: ../many/template-generator-135\n    url: https://github.com/alajmo/template-generator\n  template-generator-136:\n    path: ../many/template-generator-136\n    url: https://github.com/alajmo/template-generator\n  template-generator-137:\n    path: ../many/template-generator-137\n    url: https://github.com/alajmo/template-generator\n  template-generator-138:\n    path: ../many/template-generator-138\n    url: https://github.com/alajmo/template-generator\n  template-generator-139:\n    path: ../many/template-generator-139\n    url: https://github.com/alajmo/template-generator\n  template-generator-140:\n    path: ../many/template-generator-140\n    url: https://github.com/alajmo/template-generator\n  template-generator-141:\n    path: ../many/template-generator-141\n    url: https://github.com/alajmo/template-generator\n  template-generator-142:\n    path: ../many/template-generator-142\n    url: https://github.com/alajmo/template-generator\n  template-generator-143:\n    path: ../many/template-generator-143\n    url: https://github.com/alajmo/template-generator\n  template-generator-144:\n    path: ../many/template-generator-144\n    url: https://github.com/alajmo/template-generator\n  template-generator-145:\n    path: ../many/template-generator-145\n    url: https://github.com/alajmo/template-generator\n  template-generator-146:\n    path: ../many/template-generator-146\n    url: https://github.com/alajmo/template-generator\n  template-generator-147:\n    path: ../many/template-generator-147\n    url: https://github.com/alajmo/template-generator\n  template-generator-148:\n    path: ../many/template-generator-148\n    url: https://github.com/alajmo/template-generator\n  template-generator-149:\n    path: ../many/template-generator-149\n    url: https://github.com/alajmo/template-generator\n  template-generator-150:\n    path: ../many/template-generator-150\n    url: https://github.com/alajmo/template-generator\n  template-generator-151:\n    path: ../many/template-generator-151\n    url: https://github.com/alajmo/template-generator\n  template-generator-152:\n    path: ../many/template-generator-152\n    url: https://github.com/alajmo/template-generator\n  template-generator-153:\n    path: ../many/template-generator-153\n    url: https://github.com/alajmo/template-generator\n  template-generator-154:\n    path: ../many/template-generator-154\n    url: https://github.com/alajmo/template-generator\n  template-generator-155:\n    path: ../many/template-generator-155\n    url: https://github.com/alajmo/template-generator\n  template-generator-156:\n    path: ../many/template-generator-156\n    url: https://github.com/alajmo/template-generator\n  template-generator-157:\n    path: ../many/template-generator-157\n    url: https://github.com/alajmo/template-generator\n  template-generator-158:\n    path: ../many/template-generator-158\n    url: https://github.com/alajmo/template-generator\n  template-generator-159:\n    path: ../many/template-generator-159\n    url: https://github.com/alajmo/template-generator\n  template-generator-160:\n    path: ../many/template-generator-160\n    url: https://github.com/alajmo/template-generator\n  template-generator-161:\n    path: ../many/template-generator-161\n    url: https://github.com/alajmo/template-generator\n  template-generator-162:\n    path: ../many/template-generator-162\n    url: https://github.com/alajmo/template-generator\n  template-generator-163:\n    path: ../many/template-generator-163\n    url: https://github.com/alajmo/template-generator\n  template-generator-164:\n    path: ../many/template-generator-164\n    url: https://github.com/alajmo/template-generator\n  template-generator-165:\n    path: ../many/template-generator-165\n    url: https://github.com/alajmo/template-generator\n  template-generator-166:\n    path: ../many/template-generator-166\n    url: https://github.com/alajmo/template-generator\n  template-generator-167:\n    path: ../many/template-generator-167\n    url: https://github.com/alajmo/template-generator\n  template-generator-168:\n    path: ../many/template-generator-168\n    url: https://github.com/alajmo/template-generator\n  template-generator-169:\n    path: ../many/template-generator-169\n    url: https://github.com/alajmo/template-generator\n  template-generator-170:\n    path: ../many/template-generator-170\n    url: https://github.com/alajmo/template-generator\n  template-generator-171:\n    path: ../many/template-generator-171\n    url: https://github.com/alajmo/template-generator\n  template-generator-172:\n    path: ../many/template-generator-172\n    url: https://github.com/alajmo/template-generator\n  template-generator-173:\n    path: ../many/template-generator-173\n    url: https://github.com/alajmo/template-generator\n  template-generator-174:\n    path: ../many/template-generator-174\n    url: https://github.com/alajmo/template-generator\n  template-generator-175:\n    path: ../many/template-generator-175\n    url: https://github.com/alajmo/template-generator\n  template-generator-176:\n    path: ../many/template-generator-176\n    url: https://github.com/alajmo/template-generator\n  template-generator-177:\n    path: ../many/template-generator-177\n    url: https://github.com/alajmo/template-generator\n  template-generator-178:\n    path: ../many/template-generator-178\n    url: https://github.com/alajmo/template-generator\n  template-generator-179:\n    path: ../many/template-generator-179\n    url: https://github.com/alajmo/template-generator\n  template-generator-180:\n    path: ../many/template-generator-180\n    url: https://github.com/alajmo/template-generator\n  template-generator-181:\n    path: ../many/template-generator-181\n    url: https://github.com/alajmo/template-generator\n  template-generator-182:\n    path: ../many/template-generator-182\n    url: https://github.com/alajmo/template-generator\n  template-generator-183:\n    path: ../many/template-generator-183\n    url: https://github.com/alajmo/template-generator\n  template-generator-184:\n    path: ../many/template-generator-184\n    url: https://github.com/alajmo/template-generator\n  template-generator-185:\n    path: ../many/template-generator-185\n    url: https://github.com/alajmo/template-generator\n  template-generator-186:\n    path: ../many/template-generator-186\n    url: https://github.com/alajmo/template-generator\n  template-generator-187:\n    path: ../many/template-generator-187\n    url: https://github.com/alajmo/template-generator\n  template-generator-188:\n    path: ../many/template-generator-188\n    url: https://github.com/alajmo/template-generator\n  template-generator-189:\n    path: ../many/template-generator-189\n    url: https://github.com/alajmo/template-generator\n  template-generator-190:\n    path: ../many/template-generator-190\n    url: https://github.com/alajmo/template-generator\n  template-generator-191:\n    path: ../many/template-generator-191\n    url: https://github.com/alajmo/template-generator\n  template-generator-192:\n    path: ../many/template-generator-192\n    url: https://github.com/alajmo/template-generator\n  template-generator-193:\n    path: ../many/template-generator-193\n    url: https://github.com/alajmo/template-generator\n  template-generator-194:\n    path: ../many/template-generator-194\n    url: https://github.com/alajmo/template-generator\n  template-generator-195:\n    path: ../many/template-generator-195\n    url: https://github.com/alajmo/template-generator\n  template-generator-196:\n    path: ../many/template-generator-196\n    url: https://github.com/alajmo/template-generator\n  template-generator-197:\n    path: ../many/template-generator-197\n    url: https://github.com/alajmo/template-generator\n  template-generator-198:\n    path: ../many/template-generator-198\n    url: https://github.com/alajmo/template-generator\n  template-generator-199:\n    path: ../many/template-generator-199\n    url: https://github.com/alajmo/template-generator\n  template-generator-200:\n    path: ../many/template-generator-200\n    url: https://github.com/alajmo/template-generator\n  template-generator-201:\n    path: ../many/template-generator-201\n    url: https://github.com/alajmo/template-generator\n  template-generator-202:\n    path: ../many/template-generator-202\n    url: https://github.com/alajmo/template-generator\n  template-generator-203:\n    path: ../many/template-generator-203\n    url: https://github.com/alajmo/template-generator\n  template-generator-204:\n    path: ../many/template-generator-204\n    url: https://github.com/alajmo/template-generator\n  template-generator-205:\n    path: ../many/template-generator-205\n    url: https://github.com/alajmo/template-generator\n  template-generator-206:\n    path: ../many/template-generator-206\n    url: https://github.com/alajmo/template-generator\n  template-generator-207:\n    path: ../many/template-generator-207\n    url: https://github.com/alajmo/template-generator\n  template-generator-208:\n    path: ../many/template-generator-208\n    url: https://github.com/alajmo/template-generator\n  template-generator-209:\n    path: ../many/template-generator-209\n    url: https://github.com/alajmo/template-generator\n  template-generator-210:\n    path: ../many/template-generator-210\n    url: https://github.com/alajmo/template-generator\n  template-generator-211:\n    path: ../many/template-generator-211\n    url: https://github.com/alajmo/template-generator\n  template-generator-212:\n    path: ../many/template-generator-212\n    url: https://github.com/alajmo/template-generator\n  template-generator-213:\n    path: ../many/template-generator-213\n    url: https://github.com/alajmo/template-generator\n  template-generator-214:\n    path: ../many/template-generator-214\n    url: https://github.com/alajmo/template-generator\n  template-generator-215:\n    path: ../many/template-generator-215\n    url: https://github.com/alajmo/template-generator\n  template-generator-216:\n    path: ../many/template-generator-216\n    url: https://github.com/alajmo/template-generator\n  template-generator-217:\n    path: ../many/template-generator-217\n    url: https://github.com/alajmo/template-generator\n  template-generator-218:\n    path: ../many/template-generator-218\n    url: https://github.com/alajmo/template-generator\n  template-generator-219:\n    path: ../many/template-generator-219\n    url: https://github.com/alajmo/template-generator\n  template-generator-220:\n    path: ../many/template-generator-220\n    url: https://github.com/alajmo/template-generator\n  template-generator-221:\n    path: ../many/template-generator-221\n    url: https://github.com/alajmo/template-generator\n  template-generator-222:\n    path: ../many/template-generator-222\n    url: https://github.com/alajmo/template-generator\n  template-generator-223:\n    path: ../many/template-generator-223\n    url: https://github.com/alajmo/template-generator\n  template-generator-224:\n    path: ../many/template-generator-224\n    url: https://github.com/alajmo/template-generator\n  template-generator-225:\n    path: ../many/template-generator-225\n    url: https://github.com/alajmo/template-generator\n  template-generator-226:\n    path: ../many/template-generator-226\n    url: https://github.com/alajmo/template-generator\n  template-generator-227:\n    path: ../many/template-generator-227\n    url: https://github.com/alajmo/template-generator\n  template-generator-228:\n    path: ../many/template-generator-228\n    url: https://github.com/alajmo/template-generator\n  template-generator-229:\n    path: ../many/template-generator-229\n    url: https://github.com/alajmo/template-generator\n  template-generator-230:\n    path: ../many/template-generator-230\n    url: https://github.com/alajmo/template-generator\n  template-generator-231:\n    path: ../many/template-generator-231\n    url: https://github.com/alajmo/template-generator\n  template-generator-232:\n    path: ../many/template-generator-232\n    url: https://github.com/alajmo/template-generator\n  template-generator-233:\n    path: ../many/template-generator-233\n    url: https://github.com/alajmo/template-generator\n  template-generator-234:\n    path: ../many/template-generator-234\n    url: https://github.com/alajmo/template-generator\n  template-generator-235:\n    path: ../many/template-generator-235\n    url: https://github.com/alajmo/template-generator\n  template-generator-236:\n    path: ../many/template-generator-236\n    url: https://github.com/alajmo/template-generator\n  template-generator-237:\n    path: ../many/template-generator-237\n    url: https://github.com/alajmo/template-generator\n  template-generator-238:\n    path: ../many/template-generator-238\n    url: https://github.com/alajmo/template-generator\n  template-generator-239:\n    path: ../many/template-generator-239\n    url: https://github.com/alajmo/template-generator\n  template-generator-240:\n    path: ../many/template-generator-240\n    url: https://github.com/alajmo/template-generator\n  template-generator-241:\n    path: ../many/template-generator-241\n    url: https://github.com/alajmo/template-generator\n  template-generator-242:\n    path: ../many/template-generator-242\n    url: https://github.com/alajmo/template-generator\n  template-generator-243:\n    path: ../many/template-generator-243\n    url: https://github.com/alajmo/template-generator\n  template-generator-244:\n    path: ../many/template-generator-244\n    url: https://github.com/alajmo/template-generator\n  template-generator-245:\n    path: ../many/template-generator-245\n    url: https://github.com/alajmo/template-generator\n"
  },
  {
    "path": "test/playground/imports/projects.yaml",
    "content": "import:\n  - ./tasks.yaml\n\nprojects:\n  template-generator:\n    path: ../template-generator\n    url: git@github.com:alajmo/template-generator\n    tags: [cli,bash]\n\n"
  },
  {
    "path": "test/playground/imports/specs.yaml",
    "content": "specs:\n  default:\n    output: table\n    parallel: false\n    ignore_errors: false\n    ignore_non_existing: false\n    omit_empty_rows: false\n\n  advanced:\n    output: table\n    parallel: false\n    ignore_errors: true\n    omit_empty_rows: false\n\n  table:\n    output: table\n    parallel: false\n    ignore_errors: true\n    omit_empty_rows: false\n    omit_empty_columns: false\n"
  },
  {
    "path": "test/playground/imports/targets.yaml",
    "content": "targets:\n  default:\n    all: false\n    cwd: true\n\n  tap-report:\n    projects: [tap-report]\n\n  all:\n    all: true\n\n  root:\n    projects: [playground]\n    cwd: true\n\n"
  },
  {
    "path": "test/playground/imports/tasks.yaml",
    "content": "tasks:\n  hello:\n    desc: hello world\n    cmd: echo \"Hello World\"\n\n  pwd:\n    target: root\n    cmd: pwd\n"
  },
  {
    "path": "test/playground/imports/themes.yaml",
    "content": "themes:\n  default:\n    tree:\n      style: bold\n\n    stream:\n      prefix: true\n      header: true\n      header_char: '*'\n      header_prefix: 'TASK'\n      prefix_colors: ['blue', 'red']\n\n    table:\n      style: light\n\n      border:\n        around: true\n        columns: true\n        header: true\n        rows: false\n\n  advanced:\n    tree:\n      style: light\n\n    stream:\n      prefix: true\n      header: true\n      header_char: '*'\n      header_prefix: 'TASK'\n      colors: ['blue', 'red']\n\n    table:\n      style: ascii\n\n      border:\n        around: true\n        columns: true\n        header: true\n        rows: true\n"
  },
  {
    "path": "test/playground/mani.yaml",
    "content": "import:\n  # - ./imports/many-projects.yaml\n  - ./imports/projects.yaml\n  - ./imports/tasks.yaml\n  - ./imports/targets.yaml\n  - ./imports/specs.yaml\n  - ./imports/themes.yaml\n\nsync_remotes: true\n\nprojects:\n  playground:\n    path: .\n\n  dashgrid:\n    path: frontend/dashgrid\n    url: git+ssh://git@github.com/alajmo/dashgrid.git\n    clone: git clone $MANI_PROJECT_URL $MANI_PROJECT_PATH\n    tags: [frontend, node]\n    remotes:\n      foo: bar\n    env:\n      foo: bar\n\n  tap-report:\n    path: frontend/tap-report\n    url: https://github.com/alajmo/tap-report\n    tags: [frontend]\n\n  kaka:\n    path: kaka\n    url: git+ssh://git@github.com/alajmo/dashgrid.git\n    tags: [frontend, node]\n\n# GLOBAL ENVS\nenv:\n  VERSION: v0.1.0\n  GIT: git --no-pager\n  DATE: $(date -u +\"%Y-%m-%dT%H:%M:%S%Z\")\n  # NO_COLOR: true\n\n# TASKS\ntasks:\n  test:\n    desc: simple test task\n    cmd: echo \"$branch_name\"\n    spec:\n      ignore_errors: true\n      ignore_non_existing: true\n    env:\n      branch_name: main\n\n  git-status: git status\n\n  echo:\n    desc: Print hello world\n    cmd: echo \"hello world\"\n\n  ping:\n    desc: ping server\n    cmd: echo pong\n\n  sleep:\n    desc: Sleep 2 seconds\n    cmd: sleep 2 && echo \"slept 2 seconds\"\n\n  status:\n    desc: git status\n    target: tap-report\n    spec:\n      output: text\n    commands:\n      - mcd: git status\n      - cmd: git branch\n    cmd: echo done\n\n  stream:\n    desc: test text info\n    env:\n      BRANCH: main\n      FOO: bar\n    target: tap-report\n    spec:\n      output: text\n    commands:\n      - task: echo\n      - cmd: echo lala\n    cmd: echo hi\n\n  table:\n    env:\n      BRANCH: lala\n      FOO: bar\n    target: tap-report\n    spec:\n      output: table\n    commands:\n      - task: echo\n      - cmd: echo lala\n    cmd: echo hi\n\n  install:\n    desc: npm install\n    cmd: npm install\n\n  fail:\n    desc: fail on purpose\n    cmd: |\n      echo \"FAILED\"\n      exit 1\n  node:\n    target: all\n    shell: node\n    cmd: console.log(\"Running node.js example\")\n\n  random-data:\n    desc: generate random data\n    cmd: |\n      jot -r 1\n      echo \"RANDOM-DATA\"\n      jot -r 2\n      sleep 2\n      jot -r 2\n      sleep 2\n\n  many:\n    desc: run many tasks\n    env:\n      BRANCH: lala\n      HELLO: WORLD\n    target: all\n    spec:\n      ignore_errors: true\n\n    commands:\n      - task: node\n      - task: fail\n      - task: echo\n      - task: random-data\n      - task: ping\n\n  submarine:\n    desc: Submarine test\n    spec:\n      output: table\n\n    cmd: echo 0\n    commands:\n      - name: command-1\n        cmd: echo 1\n      - name: command-2\n        cmd: echo 2\n      - name: command-3\n        cmd: echo 3\n      - task: pwd\n"
  },
  {
    "path": "test/scripts/exec",
    "content": "#!/bin/bash\n\nset -e\nset -o pipefail\n\nAPPNAME=mani\nPROJECT_DIR=$(dirname \"$(cd \"$(dirname \"${0}\")\"; pwd -P)\")\n\nfunction help() {\n  cat >&2 << EOF\nThis script is debugger for mani.\n\nOptions:\n  --test|-t {case}     Run only cases which have specified pattern in the case names\n  --count|-c {count}   Run tests multiple times, the clean flag is necessary for this flag\n  --help|-h            Show this message\n\nExamples:\n\n  ./test/run.sh\n\nEOF\n}\n\nfunction parse_options() {\n  IMAGE=alpine\n  SHELL=bash\n  while [[ $# -gt 0 ]]; do\n    case \"${1}\" in\n      --image|-i)\n        IMAGE=\"${2}\"\n        shift && shift\n        ;;\n      --shell|-s)\n        SHELL=\"${2}\"\n        shift && shift\n        ;;\n      --help|-h)\n        help && exit 0\n        ;;\n      *)\n        printf \"Unknown flag: ${1}\\n\\n\"\n        help\n        exit 1\n        ;;\n    esac\n  done\n}\n\nfunction exec_docker() {\n  image=\"${APPNAME}/exec:${IMAGE}\"\n\n  shell=\n  case $SHELL in\n    zsh)\n      shell=\"/bin/zsh\"\n      ;;\n    fish)\n      shell=\"/usr/bin/fish\"\n      ;;\n    ps)\n      shell=\"/bin/ps\"\n      ;;\n    *)\n      shell=\"/bin/bash\"\n      ;;\n  esac\n\n  docker build                                          \\\n    --file \"$PROJECT_DIR/images/$IMAGE.exec.Dockerfile\" \\\n    --tag ${image}                                      \\\n    .\n\n  docker run                      \\\n    -it --rm                      \\\n    \"$image\"                      \\\n    \"$shell\"\n}\n\nfunction __main__() {\n  parse_options $@\n  exec_docker\n}\n\n__main__ $@\n"
  },
  {
    "path": "test/scripts/git",
    "content": "#!/bin/bash\n# Mock git, used for testing purposes.\n\ngit() {\n  if [[ $1 == \"clone\" ]]; then\n    mkdir -p \"$4/.git\"\n    touch \"$4/empty\"\n  # elif [[ $1 == \"init\" ]]; then\n  #   mkdir -p \"$3/.git\"\n  #   touch \"$3/empty\"\n  else\n    /usr/bin/git \"$@\"\n  fi\n}\n\ngit $@\n"
  },
  {
    "path": "test/scripts/test",
    "content": "#!/bin/bash\n\nset -e\nset -o pipefail\n\nAPPNAME=mani\nPROJECT_DIR=$(dirname \"$(cd \"$(dirname \"${0}\")\"; pwd -P)\")\n\nfunction help() {\n  cat >&2 << EOF\nThis script is used to run tests in docker\n\nOptions:\n  --run|-r <regexp>     Run only those tests matching the regular expression (wraps the go testflag -run)\n  --count|-c <number>   Run each test and benchmark n times (wraps the go testflag -count)\n  --clean               Clears the test/tmp directory after each run\n  --build               Build docker image\n  --update|-u           Update golden files\n  --debug|-d            Show stdout of the test commands\n  --help|-h             Show this message\n\nExamples:\n\n  ./test\n\n  ./test --debug --run TestInitCmd\n\nEOF\n}\n\nfunction parse_options() {\n  RUN=\n  COUNT=1\n  UPDATE_GOLDEN=\n  BUILD=\n  CLEAN=\n  DEBUG=\n  while [[ $# -gt 0 ]]; do\n    case \"${1}\" in\n      --build|-b)\n        BUILD=YES\n        shift\n        ;;\n      --debug|-d)\n        DEBUG=\"-debug\"\n        shift\n        ;;\n      --clean)\n        CLEAN=\"-clean\"\n        shift\n        ;;\n      --run|-r)\n        RUN=\"-run=${2}\"\n        shift && shift\n        ;;\n      --count|-c)\n        COUNT=\"${2}\"\n        shift && shift\n        ;;\n      --update|-u)\n        UPDATE_GOLDEN=\"-update\"\n        shift\n        ;;\n      --help|-h)\n        help && exit 0\n        ;;\n      *)\n        printf \"Unknown flag: ${1}\\n\\n\"\n        help\n        exit 1\n        ;;\n    esac\n  done\n}\n\nfunction run_tests() {\n  if [[ \"$COUNT\" -gt 1  ]]; then\n    CLEAN=\"-clean\"\n  fi\n\n  for runtime in `ls ${PROJECT_DIR}/images/*test.Dockerfile`; do\n    testcase=`basename ${runtime} | sed -e s/\\.test\\.Dockerfile$//`\n    image=\"${APPNAME}/test:${testcase}\"\n\n    local image_found=$(docker image inspect \"$image\" >/dev/null 2>&1 && echo yes)\n    if [[ \"$BUILD\" ||\n      -n \"$UPDATE_GOLDEN\" ||\n      -z \"$image_found\"\n          ]]; then\n\n        # Build test images\n        for dockerfile in `ls ${PROJECT_DIR}/images/*.test.Dockerfile`; do\n          testcase=`basename ${dockerfile} | sed -e s/\\.test\\.Dockerfile$//`\n          echo \"┌───────────── ${testcase}\"\n          echo \"│ [Docker] Building image...\"\n          docker build                        \\\n            --file ${dockerfile}              \\\n            --tag \"$image\"                    \\\n            . |                               \\\n            sed \"s/^/│ /\"\n          echo \"└───────────── ${testcase} [OK]\"\n        done\n    fi\n\n    echo \"┌───────────── ${testcase}\"\n    echo \"│ [Docker] Running tests...\"\n    docker run                                                                          \\\n      -t                                                                                \\\n      --user \"$(id -u):$(id -g)\"                                                        \\\n      --volume \"$PWD:/home/test\"                                                        \\\n\t\t\t--volume \"$(go env GOCACHE):/go/cache\" \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n      \"$image\"                                                                          \\\n      /bin/sh -c \"go test -v ./test/... $RUN -count=${COUNT} $CLEAN $DEBUG $UPDATE_GOLDEN\" | \\\n      sed \"s/^/│ [${testcase}] /\"\n    echo \"└───────────── ${testcase} [OK]\"\n  done\n}\n\nfunction __main__() {\n  parse_options $@\n  run_tests\n}\n\n__main__ $@\n"
  }
]