[
  {
    "path": ".dockerignore",
    "content": "README.md\npipelines/\ndockerfiles/\ndocs/\nbuild/\nLICENSE\nMakefile"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [mactat]\n"
  },
  {
    "path": ".github/workflows/pr.yaml",
    "content": "name: pr\non:\n  pull_request:\n    types: [opened, synchronize, reopened, ready_for_review]\n  workflow_dispatch: {}\njobs:\n  pr:\n    name: \"Pull Request Checks\"\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: actions/checkout@v3\n      - name: \"Framed verify\"\n        uses: 'mactat/framed-action@v0.0.7'\n      - name: \"Functional tests\"\n        run: make test BUILD=true EXPORT=true\n        shell: bash\n      - name: Test Summary\n        uses: test-summary/action@v2\n        with:\n          paths: \"results/test.xml\"\n        if: always()\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v3\n        with:\n          version: v1.53"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: release\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+'\n\npermissions:\n  contents: write\n\njobs:\n\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - run: git fetch --force --tags\n      - name: Log in to Docker Hub\n        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_TOKEN }}\n      - uses: actions/setup-go@v4\n        with:\n          go-version: stable\n      - uses: goreleaser/goreleaser-action@v5\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GORELEASER_GH_TOKEN }}"
  },
  {
    "path": ".gitignore",
    "content": "build/\n.vscode/"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "builds:\n  - binary: framed\n    goos:\n      - darwin\n      - linux\n      - windows\n    goarch:\n      - amd64\n      - arm64\n    env:\n      - CGO_ENABLED=0\n\nrelease:\n  prerelease: auto\n\ndockers:\n  - dockerfile: \"dockerfiles/goreleaser.dockerfile\"\n    image_templates:\n    - \"mactat/framed:latest\"\n    - \"mactat/framed:{{ .Tag }}\"\n\nuniversal_binaries:\n  - replace: true\n\nbrews:\n    - name: framed\n      homepage: \"https://github.com/mactat/framed\"\n      repository:\n        owner: mactat\n        name: homebrew-mactat\n      commit_author:\n        name: mactat\n        email: maciektatarsk@gmail.com\n\nchecksum:\n  name_template: 'checksums.txt'"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Maciej Tatarski\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 all\ncopies 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 THE\nSOFTWARE."
  },
  {
    "path": "Makefile",
    "content": "# Definitions\nROOT                    := $(PWD)\nGO_VERSION              := 1.20.4\nALPINE_VERSION          := 3.18\nOS\t\t\t\t\t  \t:= linux\nARCH                    := amd64\nBUILD                   := true\n\n.PHONY: version\nversion:\n\t$(eval VERSION := $(shell git describe --tags --abbrev=0 2> /dev/null || git rev-parse --short HEAD))\n\t@echo \"Version: $(VERSION)\"\n\n.PHONY: build-docker\nbuild-docker:\n\tdocker build \\\n\t-t mactat/framed \\\n\t-f ./dockerfiles/dockerfile \\\n\t--build-arg GO_VERSION=$(GO_VERSION) \\\n\t--build-arg ALPINE_VERSION=$(ALPINE_VERSION) \\\n\t.\n\n.PHONY: release-docker\nrelease-docker: version build-docker\n\tdocker tag mactat/framed mactat/framed:$(VERSION)\n\tdocker tag mactat/framed mactat/framed:alpine-$(ALPINE_VERSION)-$(VERSION)\n\tdocker push mactat/framed:$(VERSION)\n\tdocker push mactat/framed:alpine-$(ALPINE_VERSION)-$(VERSION)\n\tdocker push mactat/framed:latest\n\n.PHONY: build-in-docker\nbuild-in-docker: clean version\n\tdocker run \\\n\t\t--rm \\\n\t\t-v $(ROOT):/app \\\n\t\t--env GOOS=$(OS) \\\n\t\t--env GOARCH=$(ARCH) \\\n\t\tgolang:$(GO_VERSION)-alpine$(ALPINE_VERSION) \\\n\t\t/bin/sh -c \"cd /app && go build -o ./build/ ./framed.go\"\n\tsudo chown -R $(USER):$(USER) ./build\n\tcd ./build && \\\n\ttar -zcvf \\\n\t  ./framed-$(OS)-$(ARCH)-$(VERSION).tar.gz \\\n\t  ./framed$(if $(filter $(OS),windows),.exe)\n\n.PHONY: release-lin\nrelease-lin:\n\t$(MAKE) build-in-docker OS=linux ARCH=amd64\n\n.PHONY: release-win\nrelease-win:\n\t$(MAKE) build-in-docker OS=windows ARCH=amd64\n\n.PHONY: release-mac\nrelease-mac:\n\t$(MAKE) build-in-docker OS=darwin ARCH=amd64\n\n.PHONY: build\nbuild:\n\tgo build -o ./build/ ./framed.go\n\n.PHONY: test\ntest:\n\t@if [ \"$(BUILD)\" = \"true\" ]; then\\\n        $(MAKE) build-docker;\\\n    fi\n\tdocker build -f ./dockerfiles/test.dockerfile -t framed-test .\n\t@if [ \"$(EXPORT)\" = \"true\" ]; then\\\n\t\tmkdir -p ./results;\\\n\t\tdocker run --rm framed-test /bin/sh -c \"/test/bats/bin/bats -F junit /test/\" > ./results/test.xml;\\\n\telse\\\n\t\tdocker run --rm framed-test /bin/sh -c \"/test/bats/bin/bats --pretty /test/\";\\\n\tfi\n.PHONY: format\nformat:\n\tgo fmt ./...\n\n.PHONY: lint\nlint:\n\tdocker run -t --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.53.3 golangci-lint run -v\n\n.PHONY: clean\nclean:\n\trm -f ./build/framed\n\trm -f ./build/framed.exe\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"./docs/static/logo-wide.png\" style=\"object-fit: cover; width: 100%;\">\n\n[![onTag](https://github.com/mactat/framed/actions/workflows/release.yaml/badge.svg)](https://github.com/mactat/framed/actions/workflows/release.yaml) [![pr](https://github.com/mactat/framed/actions/workflows/pr.yaml/badge.svg?branch=master)](https://github.com/mactat/framed/actions/workflows/pr.yaml) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/mactat/framed) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/mactat/framed) ![Docker Pulls](https://img.shields.io/docker/pulls/mactat/framed) ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/mactat/framed?label=docker-version)  ![GitHub](https://img.shields.io/github/license/mactat/framed) ![GitHub Repo stars](https://img.shields.io/github/stars/mactat/framed?style=social)\n\n# Framed - Files and Directories Reusability, Architecture, and Management\n\nFramed is a CLI tool that simplifies the organization and management of files and directories in a reusable and architectural manner. It provides YAML templates for defining project structures and enables workflows based on those.\n\nTo always be in sync with the YAML template, Framed provides a built-in test command that can be used in CI/CD pipelines to verify the project structure.\n\n## Demo\n\n![Demo](./docs/static/demo.gif)\n\n## Features\n\n- **YAML Templates**: Framed uses YAML templates to define the entire project structure.\n\n- **Always in Sync**: Framed provides a built-in test command that can be used in CI/CD pipelines to verify the project structure and ensure that it is always in sync with the YAML template.\n\n- **Consistency Across Projects**: Framed offers a consistent way of organizing files and directories across different projects.\n\n## Example configuration\n\nTo get started with Framed, you can use the following example:\n\n```yaml\n# Framed Configuration\nname: framed\n\nstructure:\n  name: root\n  maxDepth: 5 # Disallow dirs deeper than 5\n  files:\n    - README.md\n    - framed.yaml\n    - main.go\n    - go.mod\n    - go.sum\n    - .gitignore\n  dirs:\n    - name: cmd\n      allowedPatterns:\n        - \".go\"\n      forbiddenPatterns:\n        - \"_test.go\" # Disallow tests in /src\n    - name: pipelines\n      maxCount: 2\n      allowedPatterns:\n        - \".yml\"\n        - \".yaml\" # only yaml files allowed\n      files:\n        - pr.yaml\n    - name: dockerfiles\n      minCount: 1 # At least one file has to be there\n      allowChildren: false # Allow subdirectories creation, default true\n      allowedPatterns:\n        - \".dockerfile\"\n    - name: docs\n      maxCount: 10 # No more than 10 files per dir\n      allowedPatterns:\n        - \".md\"\n        - \".txt\"\n      dirs:\n        - name: design\n    - name: examples\n```\n\n### Project Structure Definition\n\nFramed allows you to define the desired structure of your project using a YAML-based configuration file. The configuration specifies the required files and directories that should exist in the project.\n\n### Root-level Requirements\n\nThe structure section defines the files that are required at the root level of the project. These files must be present for the project to be considered valid.\n\n### Nested Structure\n\nThe dirs section allows you to define nested directories within the project structure. Each subdirectory can have its own set of required files and directories.\n\n### File Requirements\n\nYou can specify file requirements using the files property. It ensures that specific files are present within the designated directory.\n\n### File Patterns\n\nThe allowedPatterns property enables you to define file patterns using glob syntax. This allows for more flexible matching of files based on their extensions or naming conventions.\n\n### Forbidden Files\n\nThe forbiddenPatterns property lets you specify file patterns that are not allowed within a directory. This can be useful for enforcing certain naming conventions or excluding specific types of files.\n\n### Minimum File Count\n\nThe minCount property allows you to set a minimum count for files within a directory. It ensures that a certain number of files must be present in the directory.\n\n### Maximum File Count\n\nThe maxCount property allows you to set a maximum count for files within a directory. It limits the number of files that can exist within the directory.\n\n### Allowing Children\n\nThe allowChildren property, when set to true, permits the presence of additional directories within a specified directory. This provides flexibility for organizing files and directories within the project.\n\n## Installation\n\n### Brew Installation\n\n1. Open a terminal and run the following command:\n\n   ```shell\n   brew tap mactat/mactat\n   brew install framed\n   ```\n\n### Darwin (macOS) Installation\n\n1. Download the `framed-darwin-amd64-<version>.tar.gz` package from release page.\n\n2. Extract the package by double-clicking on the downloaded file or using a tool like `tar` in your terminal:\n\n   ```shell\n   tar -xzf framed-darwin-amd64-<version>.tar.gz\n   ```\n\n3. This will extract the `framed` binary.\n\n4. Open a terminal and navigate to the extracted directory:\n\n   ```shell\n   cd framed-darwin-amd64-<version>\n   ```\n\n5. Make the binary executable by running the following command:\n\n   ```shell\n   chmod +x framed\n   ```\n\n6. Move the `framed` binary to a directory in your system's `PATH` so that it can be accessed from anywhere. For example:\n\n   ```shell\n   sudo mv framed /usr/local/bin/\n   ```\n\n7. You can now use the `framed` command to execute the application.\n\n### Linux Installation\n\n1. Download the `framed-linux-amd64-<version>.tar.gz` package from release page.\n\n2. Extract the package using the following command in your terminal:\n\n   ```shell\n   tar -xzf framed-linux-amd64-<version>.tar.gz\n   ```\n\n3. This will extract the `framed` binary.\n\n4. Open a terminal and navigate to the extracted directory:\n\n   ```shell\n   cd framed-linux-amd64-<version>\n   ```\n\n5. Make the binary executable by running the following command:\n\n   ```shell\n   chmod +x framed\n   ```\n\n6. Move the `framed` binary to a directory in your system's `PATH` so that it can be accessed from anywhere. For example:\n\n   ```shell\n   sudo mv framed /usr/local/bin/\n   ```\n\n7. You can now use the `framed` command to execute the application.\n\n### Windows Installation\n\n1. Download the `framed-windows-amd64-<version>.tar.gz` package from release page.\n\n2. Extract the package using a file extraction tool like 7-Zip or WinRAR.\n\n3. This will extract the `framed.exe` binary.\n\n4. Move the `framed.exe` binary to a directory that is included in your system's `PATH`, such as `C:\\Windows` or `C:\\Windows\\System32`.\n\n5. You can now use the `framed` command to execute the application from the Command Prompt or PowerShell.\n\n**Please note that the exact steps may vary depending on your system configuration.**\n\n## Usage\n\n**Note**: The following commands assume that you have already installed Framed and added it to your system's PATH environment variable.\n\n**Note**: By default template file is `framed.yaml`. You can specify a different template file using the `--template` flag f.e `--template path/to/my-template.yaml`.\n\n### 1. Creating a Project Structure\n\nTo create a new project structure using a YAML template, run the following command:\n\n```bash\nframed create\n```\n\nIf you also want to create required files, run the following command:\n\n```bash\nframed create --files\n```\n\n### 2. Capturing current project structure\n\nTo capture the current project structure as a YAML template, run the following command:\n\n```bash\nframed capture --output <template-file>\n```\n\n### 3. Test Project Structure (CI/CD)\n\nTo test the project structure for consistency and compliance with the YAML template, run the following command:\n\n```bash\nframed verify\n```\n\nFor a complete list of available commands and usage examples, refer to the [documentation](link-to-full-docs).\n\n### 4. Visualize Project Structure\n\nTo visualize the project structure, run the following command:\n\n```bash\nframed visualize\n```\n\n### 5. Importing Project Structure\n\nTo import the project structure from url, run the following command:\n\n```bash\nframed import --url <url>\n```\n\nurl has to be pointing to a yaml file with valid structure.\n\nTo import an example project structure, run the following command:\n\n```bash\nframed import --example <example-name>\n```\n\nCurrently available examples:\n\n- python\n- golang\n\nSee [examples](./examples) for more details.\n\n## Running from docker\n\nTo run framed from docker, run the following command:\n\n```bash\ndocker run --rm -v $(pwd):/app --user $(id -u):$(id -g) mactat/framed framed <command>\n```\n\nexample:\n\n```bash\ndocker run --rm -v $(pwd):/app --user $(id -u):$(id -g) mactat/framed framed import --example python\n```\n\nImages can be found on [dockerhub](https://hub.docker.com/r/mactat/framed).\n\n\n## Github Action\n\nYou can use framed as a github action to verify your project structure. Minimal example:\n\n  ```yaml\n  name: Verify Project Structure\n  on: [push, pull_request]\n  jobs:\n    verify:\n      runs-on: ubuntu-latest\n      steps:\n        - uses: actions/checkout@v2\n        - name: Verify Project Structure\n          uses: mactat/framed@0.0.7\n          with:\n            template: './framed.yaml' # Optional, default is framed.yaml\n            version: 'v0.0.8'         # Optional, default is v0.0.8\n  ```\n\n## TODO\n\n- [ ] Add support from importing part of the structure from url or file like:\n\n  ```yaml\n  name: framed\n\n  structure:\n    name: root\n    dirs:\n      - name: other\n        template: other.yaml # Use another template for this dir\n      - name: another\n        template: https://yourfile.com/framed.yaml # Share templates between projects\n  ```\n\n- [x] Add some tests\n- [ ] Add contributing guidelines\n- [ ] Add more examples\n- [x] Create a github action for running tests\n- [ ] Move remaining business logic to a separate package\n"
  },
  {
    "path": "cmd/capture.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"framed/pkg/ext\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// captureCmd represents the capture command\nvar captureCmd = &cobra.Command{\n\tUse:   \"capture\",\n\tShort: \"Capture the current project structure as a YAML template\",\n\tLong: `This command is capturing the current project structure as a YAML template.\n\nExample:\nframed capture --output ./framed.yaml --name my-project\n`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\toutput := cmd.Flag(\"output\").Value.String()\n\t\tname := cmd.Flag(\"name\").Value.String()\n\t\tdepthStr := cmd.Flag(\"depth\").Value.String()\n\t\tdepth, err := strconv.Atoi(depthStr)\n\t\tif err != nil {\n\t\t\text.PrintOut(\"🚨 Invalid depth value: \", depthStr)\n\t\t\tos.Exit(1)\n\t\t}\n\t\text.PrintOut(\"📝 Name:\", name+\"\\n\")\n\n\t\t// capture subdirectories\n\t\tsubdirs := ext.CaptureSubDirs(\".\", depth)\n\t\text.PrintOut(\"📂 Directories:\", fmt.Sprintf(\"%v\", len(subdirs)))\n\n\t\t// capture files\n\t\tfiles := ext.CaptureAllFiles(\".\", depth)\n\t\text.PrintOut(\"📄 Files:\", fmt.Sprintf(\"%v\", len(files)))\n\n\t\t// capture patterns\n\t\tpatterns := ext.CaptureRequiredPatterns(\".\", depth)\n\t\text.PrintOut(\"🔁 Patterns:\", fmt.Sprintf(\"%v\", len(patterns)))\n\n\t\t// export config\n\t\text.ExportConfig(name, output, subdirs, files, patterns)\n\t\text.PrintOut(\"\\n✅ Exported to file: \", output)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(captureCmd)\n\n\tcaptureCmd.PersistentFlags().String(\"output\", \"./framed.yaml\", \"path to output file\")\n\n\tcaptureCmd.PersistentFlags().String(\"name\", \"default\", \"name of the project\")\n\n\tcaptureCmd.PersistentFlags().String(\"depth\", \"-1\", \"depth of the directory tree to capture\")\n}\n"
  },
  {
    "path": "cmd/create.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"framed/pkg/ext\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// createCmd represents the create command\nvar createCmd = &cobra.Command{\n\tUse:   \"create\",\n\tShort: \"Create a new project structure using a YAML template\",\n\tLong: `This command is creating a new project structure from a YAML template.\n\nExample:\nframed create --template ./framed.yaml --files true\n\t`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tpath := cmd.Flag(\"template\").Value.String()\n\t\tcreateFiles := cmd.Flag(\"files\").Value.String() == \"true\"\n\n\t\t// read config\n\t\t_, dirList := ext.ReadConfig(path)\n\n\t\t// create directories\n\t\text.CreateAllDirs(dirList)\n\n\t\t// create files\n\t\tif createFiles {\n\t\t\text.CreateAllFiles(dirList)\n\t\t}\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(createCmd)\n\n\tcreateCmd.PersistentFlags().String(\"template\", \"./framed.yaml\", \"path to template file default\")\n\n\tcreateCmd.PersistentFlags().Bool(\"files\", false, \"create required files\")\n}\n"
  },
  {
    "path": "cmd/import.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"framed/pkg/ext\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// importCmd represents the import command\nvar importCmd = &cobra.Command{\n\tUse:   \"import\",\n\tShort: \"Import the project structure\",\n\tLong: `This command is importing the project structure from a YAML template. It can be imported from a template or from a remote URL.\nExample:\nframed import https://raw.githubusercontent.com/username/repo/master/framed.yaml\nor\nframed import --example python --output ./python.yaml\n`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\turl := cmd.Flag(\"url\").Value.String()\n\t\texample := cmd.Flag(\"example\").Value.String()\n\t\toutput := cmd.Flag(\"output\").Value.String()\n\n\t\tif url != \"\" {\n\t\t\terr := ext.ImportFromUrl(output, url)\n\t\t\tif err != nil {\n\t\t\t\text.PrintOut(\"☠️ Failed to import from url ==>\", url)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\n\t\tif example != \"\" {\n\t\t\terr := ext.ImportFromUrl(output, ext.ExampleToUrl(example))\n\t\t\tif err != nil {\n\t\t\t\text.PrintOut(\"☠️ Failed to import from example ==>\", example)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\n\t\text.PrintOut(\"✅ Saved to ==>\", output)\n\n\t\t// try to load\n\t\tconfigTree, _ := ext.ReadConfig(output)\n\t\text.PrintOut(\"✅ Imported successfully ==>\", configTree.Name)\n\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(importCmd)\n\n\t// Here you will define your flags and configuration settings.\n\timportCmd.PersistentFlags().String(\"url\", \"\", \"url to template file\")\n\timportCmd.PersistentFlags().String(\"example\", \"\", \"example template file from github\")\n\timportCmd.MarkFlagsMutuallyExclusive(\"url\", \"example\")\n\n\t// path to file\n\timportCmd.PersistentFlags().String(\"output\", \"./framed.yaml\", \"path to template file\")\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:   \"framed\",\n\tShort: \"CLI tool for managing folder and files structures\",\n\tLong: `FRAMED (Files and Directories Reusability, Architecture, and Management)\nis a powerful CLI tool written in Go that simplifies the organization and management\nof files and directories in a reusable and architectural manner. It provides YAML\ntemplates for defining project structures and ensures that your projects adhere to \nthe defined structure, enabling consistency and reusability.\n\n\t`,\n}\n\nfunc Execute() {\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {}\n"
  },
  {
    "path": "cmd/verify.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"framed/pkg/ext\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// testCmd represents the test command\nvar testCmd = &cobra.Command{\n\tUse:   \"verify\",\n\tShort: \"Verify the project rules and structure\",\n\tLong: `This command is verifying the project structure for consistency and compliance with the YAML template.\n\t\nExample:\nframed verify --template ./framed.yaml\n`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tpath := cmd.Flag(\"template\").Value.String()\n\t\t// read config\n\t\t_, dirList := ext.ReadConfig(path)\n\n\t\tallGood := true\n\t\t// verify directories\n\t\tfor _, dir := range dirList {\n\t\t\tif !ext.DirExists(dir.Path) {\n\t\t\t\text.PrintOut(\"❌ Directory not found ==>\", dir.Path)\n\t\t\t\tallGood = false\n\t\t\t}\n\n\t\t\t// verify files\n\t\t\tif dir.Files != nil {\n\t\t\t\text.VerifyFiles(dir, &allGood)\n\t\t\t}\n\n\t\t\t// verify minCount\n\t\t\tnumFiles := ext.CountFiles(dir.Path)\n\t\t\tif numFiles < dir.MinCount {\n\t\t\t\text.PrintOut(\"❌ Min count (\"+fmt.Sprint(dir.MinCount)+\") not met ==>\", dir.Path)\n\t\t\t\tallGood = false\n\t\t\t}\n\n\t\t\t// verify maxCount\n\t\t\tif numFiles > dir.MaxCount {\n\t\t\t\text.PrintOut(\"❌ Max count (\"+fmt.Sprint(dir.MaxCount)+\") exceeded ==>\", dir.Path)\n\t\t\t\tallGood = false\n\t\t\t}\n\n\t\t\t// verify childrenAllowed\n\t\t\tif !dir.AllowChildren {\n\t\t\t\tif ext.HasDirs(dir.Path) {\n\t\t\t\t\text.PrintOut(\"❌ Children not allowed ==>\", dir.Path)\n\t\t\t\t\tallGood = false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// verify maxDepth\n\t\t\tif ext.CheckDepth(dir.Path) > dir.MaxDepth {\n\t\t\t\text.PrintOut(\"❌ Max depth exceeded (\"+fmt.Sprint(dir.MaxDepth)+\") ==>\", dir.Path)\n\t\t\t\tallGood = false\n\t\t\t}\n\n\t\t\t// Verify forbidden\n\t\t\tif dir.ForbiddenPatterns != nil {\n\t\t\t\text.VerifyForbiddenPatterns(dir, &allGood)\n\t\t\t}\n\n\t\t\t// Verify allowed patterns\n\t\t\tif dir.AllowedPatterns != nil {\n\t\t\t\text.VerifyAllowedPatterns(dir, &allGood)\n\t\t\t}\n\t\t}\n\n\t\tif allGood {\n\t\t\tfmt.Println(\"✅ Verified successfully!\")\n\t\t} else {\n\t\t\tos.Exit(1)\n\t\t}\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(testCmd)\n\n\ttestCmd.PersistentFlags().String(\"template\", \"./framed.yaml\", \"path to template file\")\n}\n"
  },
  {
    "path": "cmd/visualize.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\n\n// Package cmd represents the command line interface of the application\npackage cmd\n\nimport (\n\t\"framed/pkg/ext\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// visualizeCmd represents the visualize command\nvar visualizeCmd = &cobra.Command{\n\tUse:   \"visualize\",\n\tShort: \"Visualize the project structure\",\n\tLong: `This command is visualizing the project structure from a YAML template.\n\nExample:\nframed visualize --template ./framed.yaml`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tpath := cmd.Flag(\"template\").Value.String()\n\n\t\t// read config\n\t\t_, dirList := ext.ReadConfig(path)\n\n\t\t// visualize template\n\t\text.VisualizeTemplate(dirList)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(visualizeCmd)\n\n\tvisualizeCmd.PersistentFlags().String(\"template\", \"./framed.yaml\", \"Path to template file default is ./framed.yaml\")\n}\n"
  },
  {
    "path": "dockerfiles/dockerfile",
    "content": "ARG GO_VERSION=1.20.4\nARG ALPINE_VERSION=3.18\n\nFROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} as builder\n\nWORKDIR /app\nCOPY go.mod go.sum /app/\nRUN go mod download\nCOPY framed.go /app/\nCOPY cmd /app/cmd\nCOPY pkg /app/pkg\nRUN go build -o framed /app/framed.go\n\nFROM alpine:${ALPINE_VERSION} as release\n\nCOPY --from=builder /app/framed /bin/framed\n\nWORKDIR /app\nCMD [\"/bin/sh\"]"
  },
  {
    "path": "dockerfiles/goreleaser.dockerfile",
    "content": "ARG ALPINE_VERSION=3.18\n\nFROM alpine:${ALPINE_VERSION} as release\nCOPY framed /bin/framed\nCMD [ \"/bin/sh\" ]"
  },
  {
    "path": "dockerfiles/test.dockerfile",
    "content": "FROM mactat/framed:latest as builder\n\nRUN apk add --no-cache git bash sudo yq ncurses\n\nENV TERM=linux\n\n# clone bats\nRUN git clone https://github.com/bats-core/bats-core.git /test/bats\nRUN git clone https://github.com/bats-core/bats-support.git /test/test_helper/bats-support\nRUN git clone https://github.com/bats-core/bats-assert.git /test/test_helper/bats-assert\nRUN git clone https://github.com/bats-core/bats-file.git /test/test_helper/bats-file\n\nCOPY /test/test.bats /test/test.yaml /test/\n\n\n"
  },
  {
    "path": "docs/framed.tape",
    "content": "Output ./docs/static/demo.gif\n\n# Settings\n\nSet FontSize 20\nSet Width 1200\nSet Height 600\n\n# Requirements\n\nRequire framed\n\n# Setup\n\nHide\nType \"mkdir -p /tmp/framed-demo\"\nEnter\nType \"cd /tmp/framed-demo\"\nEnter\nCtrl+L\nShow\n\n# Flow\n\nSleep 1s\nType \"echo 'Welcome to Framed!'\"\nEnter\nSleep 4s\nCtrl+L\n\nSleep 1s\nType \"tree\"\nEnter\nSleep 4s\nCtrl+L\n\nSleep 1s\nType \"framed import --example python\"\nEnter\nSleep 4s\nCtrl+L\n\nSleep 1s\nType \"tree\"\nEnter\nSleep 4s\nCtrl+L\n\nSleep 1s\nType \"cat framed.yaml\"\nEnter\nSleep 6s\nCtrl+L\n\nSleep 1s\nType \"framed verify\"\nEnter\nSleep 6s\nCtrl+L\n\nSleep 1s\nType \"framed create --files\"\nEnter\nSleep 6s\nCtrl+L\n\nSleep 1s\nType \"framed verify\"\nEnter\nSleep 4s\nCtrl+L\n\nSleep 1s\nType \"tree\"\nEnter\nSleep 5s\nCtrl+L\n\nSleep 1s\nType \"rm setup.py\"\nEnter\nSleep 1s\nType \"framed verify\"\nEnter\nSleep 6s\nCtrl+L\n\n# Clean\nHide\nType \"cd -\"\nEnter\nType \"rm -r /tmp/framed-demo\"\nEnter"
  },
  {
    "path": "examples/golang.yaml",
    "content": "# FRAMED Configuration\nname: golang_example\n\nstructure:\n    name: root\n    files:\n        - README.md\n        - framed.yaml\n        - go.mod\n        - go.sum\n        - LICENSE.md\n        - .gitignore\n        - Makefile\n    dirs:\n        - name: cmd\n          minCount: 1\n          maxCount: 10\n          allowedPatterns:\n              - \".go\"\n        - name: .github\n          dirs:\n              - name: workflows\n                maxCount: 4\n                allowedPatterns:\n                    - \".yml\"\n                    - \".yaml\"\n                files:\n                    - pr.yaml\n                    - release.yaml\n        - name: dockerfiles\n          minCount: 1\n          allowChildren: false\n          allowedPatterns:\n              - \"dockerfile\"\n              - \"Dockerfile\"\n        - name: docs\n          maxCount: 10 # No more than 10 files per dir\n          allowedPatterns:\n              - \".md\"\n              - \".txt\"\n        - name: pkg\n          allowedPatterns:\n              - \".go\"\n        - name: examples\n        - name: build\n"
  },
  {
    "path": "examples/python.yaml",
    "content": "# FRAMED Configuration\nname: python_example\n\nstructure:\n  name: root\n  files:\n    - README.md\n    - setup.py\n    - .gitignore\n  dirs:\n    - name: docs\n    - name: tests\n      files:\n        - __init__.py\n    - name: package_name\n      files:\n        - __init__.py\n      allowedPatterns:\n        - .py\n"
  },
  {
    "path": "examples/structure.yaml",
    "content": "# FRAMED Configuration\nname: structure\n\nstructure:\n  name: root\n  dirs:\n    - name: src\n      dirs:\n        - name: this\n          dirs:\n            - name: is\n              files:\n                - test.yaml\n            - name: test\n    - name: test\n    - name: docs\n    - name: dockerfiles\n    - name: examples\n"
  },
  {
    "path": "framed.go",
    "content": "/*\nCopyright © 2023 Maciej Tatarski maciektatarski@gmail.com\n*/\npackage main\n\nimport \"framed/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "framed.yaml",
    "content": "# FRAMED Configuration\nname: framed\n\nstructure:\n    name: root\n    maxDepth: 5 # Disallow dirs deeper than 5\n    files:\n        - README.md\n        - framed.yaml\n        - framed.go\n        - go.mod\n        - go.sum\n        - .gitignore\n    dirs:\n        - name: cmd\n          allowedPatterns:\n              - \".go\"\n          forbiddenPatterns:\n              - \"_test.go\" # Disallow tests in /cmd\n        - name: pkg\n          dirs:\n              - name: ext\n                allowedPatterns:\n                    - \".go\"\n        - name: .github\n          dirs:\n              - name: workflows\n                maxCount: 4\n                allowedPatterns:\n                    - \".yml\"\n                    - \".yaml\" # only yaml files allowed\n                files:\n                    - pr.yaml\n                    - release.yaml\n        - name: dockerfiles\n          minCount: 1 # At least one file has to be there\n          allowChildren: false # Allow subdirectories creation, default true\n          allowedPatterns:\n              - \"dockerfile\"\n        - name: docs\n          maxCount: 10 # No more than 10 files per dir\n          allowedPatterns:\n              - \".md\"\n              - \".txt\"\n        - name: examples\n"
  },
  {
    "path": "go.mod",
    "content": "module framed\n\ngo 1.20\n\nrequire (\n\tgithub.com/TwiN/go-color v1.4.0\n\tgithub.com/creasty/defaults v1.7.0\n\tgithub.com/spf13/cobra v1.7.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng=\ngithub.com/TwiN/go-color v1.4.0/go.mod h1:0QTVEPlu+AoCyTrho7bXbVkrCkVpdQr7YF7PYWEtSxM=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=\ngithub.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=\ngithub.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\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": "pkg/ext/decoder.go",
    "content": "package ext\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/creasty/defaults\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// SingleDir struct\ntype SingleDir struct {\n\tName              string       `yaml:\"name\"`\n\tPath              string       `yaml:\"path\"`\n\tFiles             *[]string    `yaml:\"files\"`\n\tDirs              *[]SingleDir `yaml:\"dirs\"`\n\tAllowedPatterns   *[]string    `yaml:\"allowedPatterns\"`\n\tForbiddenPatterns *[]string    `yaml:\"forbiddenPatterns\"`\n\tMinCount          int          `default:\"0\" yaml:\"minCount\"`\n\tMaxCount          int          `default:\"1000\" yaml:\"maxCount\"`\n\tMaxDepth          int          `default:\"1000\" yaml:\"maxDepth\"`\n\tAllowChildren     bool         `default:\"true\" yaml:\"allowChildren\"`\n}\n\n// UnmarshalYAML implements yaml.Unmarshaler interface\n// Meant for initializing default values\nfunc (s *SingleDir) UnmarshalYAML(unmarshal func(interface{}) error) error {\n\terr := defaults.Set(s)\n\tif err != nil {\n\t\tfmt.Println(\"Cannot set defaults!\")\n\t\tos.Exit(1)\n\t}\n\n\ttype plain SingleDir\n\tif err := unmarshal((*plain)(s)); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype config struct {\n\tName      string     `yaml:\"name\"`\n\tStructure *SingleDir `yaml:\"structure\"`\n}\n\nfunc ReadConfig(path string) (config, []SingleDir) {\n\tyamlFile, err := os.ReadFile(path)\n\tif err != nil {\n\t\t// add emoji\n\t\tPrintOut(\"☠️ Can not read file ==>\", path)\n\t\tos.Exit(1)\n\t}\n\tPrintOut(\"✅ Loaded template from  ==>\", path)\n\t// Map to store the parsed YAML data\n\tvar curConfig config\n\n\t// Unmarshal the YAML string into the data map\n\terr = yaml.Unmarshal([]byte(yamlFile), &curConfig)\n\tif err != nil {\n\t\tPrintOut(\"☠️ Can not decode file ==>\", path)\n\t\tos.Exit(1)\n\t}\n\n\tif curConfig.Structure == nil {\n\t\tPrintOut(\"☠️ Can not find correct structure in ==>\", path)\n\t\tos.Exit(1)\n\t} else {\n\t\tPrintOut(\"✅ Read structure for ==>\", curConfig.Name)\n\t}\n\n\tdirList := []SingleDir{}\n\ttraverseStructure(curConfig.Structure, \".\", &dirList)\n\treturn curConfig, dirList\n}\n\nfunc traverseStructure(dir *SingleDir, path string, dirsList *[]SingleDir) {\n\t// Change path\n\tif dir == nil {\n\t\tPrintOut(\"☠️  Can't traverse nil dir ==>\", path)\n\t\tos.Exit(1)\n\t}\n\tdir.Path = path\n\n\t// add current dir to dirsList\n\t*dirsList = append(*dirsList, *dir)\n\n\tif dir.Dirs == nil {\n\t\treturn\n\t}\n\t// traverse children\n\tfor _, child := range *dir.Dirs {\n\t\ttraverseStructure(&child, path+\"/\"+child.Name, dirsList)\n\t}\n}\n"
  },
  {
    "path": "pkg/ext/encoder.go",
    "content": "package ext\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Consider implementing a custom Unmarshaler for SingleDirOut\ntype SingleDirOut struct {\n\tName              string          `yaml:\"name\"`\n\tPath              string          `yaml:\"-\"`\n\tFiles             *[]string       `yaml:\"files,omitempty\"`\n\tDirs              *[]SingleDirOut `yaml:\"dirs,omitempty\"`\n\tAllowedPatterns   *[]string       `yaml:\"allowedPatterns,omitempty\"`\n\tForbiddenPatterns *[]string       `yaml:\"forbiddenPatterns,omitempty\"`\n\tMinCount          int             `yaml:\"minCount,omitempty\"`\n\tMaxCount          int             `yaml:\"maxCount,omitempty\"`\n\tMaxDepth          int             `yaml:\"maxDepth,omitempty\"`\n\tAllowChildren     bool            `yaml:\"allowChildren,omitempty\"`\n}\n\ntype configOut struct {\n\tName      string        `yaml:\"name\"`\n\tStructure *SingleDirOut `yaml:\"structure\"`\n}\n\nfunc ExportConfig(name string, path string, subdirs []string, files []string, patterns map[string]string) {\n\t// create config, files and dirs are empty\n\tconfig := configOut{\n\t\tName: name,\n\t\tStructure: &SingleDirOut{\n\t\t\tName:  \"root\",\n\t\t\tFiles: &[]string{},\n\t\t\tDirs:  &[]SingleDirOut{},\n\t\t},\n\t}\n\t// add subdirs\n\tinsertSubdirs(config.Structure.Dirs, subdirs)\n\n\t// add files\n\tinsertFiles(config.Structure, files)\n\n\t// add patterns\n\tinsertPatterns(config.Structure, patterns)\n\n\t// export config\n\tyamlFile, err := yaml.Marshal(config)\n\tif err != nil {\n\t\tfmt.Printf(\"Error while Marshaling. %v\", err)\n\t}\n\n\t// Save to file\n\terr = os.WriteFile(path, yamlFile, 0644)\n\tif err != nil {\n\t\tfmt.Printf(\"Error while writing file. %v\", err)\n\t}\n}\n\nfunc insertSubdirs(dirs *[]SingleDirOut, subdirs []string) {\n\tfor subdir := range subdirs {\n\t\tinsertSingleDir(dirs, subdirs[subdir])\n\t}\n}\n\nfunc insertSingleDir(dirs *[]SingleDirOut, dir string) {\n\tsubdirPath := strings.Split(dir, \"/\")\n\tcurDirs := dirs\n\tfor i := range subdirPath {\n\t\tif !containsDir(*curDirs, subdirPath[i]) {\n\t\t\t*curDirs = append(*curDirs, SingleDirOut{\n\t\t\t\tName:            subdirPath[i],\n\t\t\t\tFiles:           &[]string{},\n\t\t\t\tDirs:            &[]SingleDirOut{},\n\t\t\t\tAllowedPatterns: &[]string{},\n\t\t\t})\n\t\t}\n\t\t// go deeper\n\t\tcurDirs = getDir(*curDirs, subdirPath[i]).Dirs\n\t}\n}\n\nfunc containsDir(dirs []SingleDirOut, name string) bool {\n\tfor _, dir := range dirs {\n\t\tif dir.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getDir(dirs []SingleDirOut, name string) *SingleDirOut {\n\tfor _, dir := range dirs {\n\t\tif dir.Name == name {\n\t\t\treturn &dir\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc insertFiles(root *SingleDirOut, files []string) {\n\tfor file := range files {\n\t\tinsertSingleFile(root, files[file])\n\t}\n}\n\nfunc insertSingleFile(root *SingleDirOut, file string) {\n\tsubdirPath := strings.Split(file, \"/\")\n\n\tif len(subdirPath) == 1 {\n\t\t*root.Files = append(*root.Files, subdirPath[0])\n\t\treturn\n\t}\n\tcurDirs := root.Dirs\n\tcurDir := root\n\tfor i := 0; i < len(subdirPath)-1; i++ {\n\t\tcurDir = getDir(*curDirs, subdirPath[i])\n\t\tcurDirs = curDir.Dirs\n\t}\n\t*curDir.Files = append(*curDir.Files, subdirPath[len(subdirPath)-1])\n\n}\n\nfunc insertPatterns(root *SingleDirOut, patterns map[string]string) {\n\tfor dir, pattern := range patterns {\n\t\tinsertSinglePattern(root, dir, pattern)\n\t}\n}\n\nfunc insertSinglePattern(root *SingleDirOut, dir string, pattern string) {\n\tsubdirPath := strings.Split(dir, \"/\")\n\tcurDirs := root.Dirs\n\tcurDir := root\n\tfor i := range subdirPath {\n\t\t// go deeper\n\t\tcurDir = getDir(*curDirs, subdirPath[i])\n\t\tcurDirs = curDir.Dirs\n\t}\n\t// insert pattern\n\t*curDir.AllowedPatterns = append(*curDir.AllowedPatterns, pattern)\n}\n"
  },
  {
    "path": "pkg/ext/network.go",
    "content": "package ext\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc ExampleToUrl(example string) string {\n\treturn \"https://raw.githubusercontent.com/mactat/framed/master/examples/\" + example + \".yaml\"\n}\n\nfunc ImportFromUrl(path string, url string) error {\n\t// Get the data\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Create the file\n\tout, err := os.Create(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t// Write the body to file\n\t_, err = io.Copy(out, resp.Body)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/ext/printer.go",
    "content": "package ext\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/TwiN/go-color\"\n)\n\nfunc PrintOut(prompt string, text string) {\n\tfmt.Printf(\"%-35s %-35s\\n\", prompt, text)\n}\n\n// This is ugly but it works, it needs to be refactored. It also has some bugs in case of out of order directories.\nfunc VisualizeTemplate(template []SingleDir) {\n\tfor dirNum, dir := range template {\n\t\tconnectorDir := \"├──\"\n\t\tinitString := \"\"\n\t\tdepth := strings.Count(dir.Path, string(os.PathSeparator)) + 1\n\t\tname := strings.Split(dir.Path, string(os.PathSeparator))[depth-1]\n\n\t\tdirDepth := depth\n\t\tif depth <= 2 {\n\t\t\tdirDepth = 1\n\t\t} else {\n\t\t\tinitString = \"│\"\n\t\t}\n\n\t\tif dirNum == len(template)-1 {\n\t\t\tconnectorDir = \"└──\"\n\t\t}\n\n\t\tprintDirectory(initString, dirDepth, connectorDir, name)\n\n\t\tif dir.Files == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor num, file := range *dir.Files {\n\t\t\tconnector := \"├──\"\n\t\t\tif depth > 1 {\n\t\t\t\tinitString = \"│\"\n\t\t\t}\n\n\t\t\tif num == len(*dir.Files)-1 {\n\t\t\t\tconnector = \"└──\"\n\t\t\t}\n\n\t\t\tprintFile(initString, depth, connector, file)\n\t\t}\n\t}\n}\n\nfunc printDirectory(initString string, dirDepth int, connectorDir string, name string) {\n\toutput := initString + strings.Repeat(\"    \", dirDepth-1) + connectorDir + \" 📂 \" + color.Ize(color.Blue, name)\n\tprintln(output)\n}\n\nfunc printFile(initString string, depth int, connector string, file string) {\n\toutput := initString + strings.Repeat(\"    \", depth-1) + connector + \" 📄 \" + color.Ize(color.Green, file)\n\tprintln(output)\n}\n"
  },
  {
    "path": "pkg/ext/system.go",
    "content": "package ext\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc CreateDir(path string) {\n\t// Create directory\n\tif _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {\n\t\terr := os.Mkdir(path, os.ModePerm)\n\t\tfmt.Printf(\"%-35s %-35s\\n\", \"📂 Creating directory ==> \", path)\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t}\n\t}\n\n}\n\nfunc CreateAllDirs(dirList []SingleDir) {\n\tfor _, dir := range dirList {\n\t\tCreateDir(dir.Path)\n\t}\n}\n\nfunc CreateFile(path string, name string) {\n\t// Check if file exists\n\tif _, err := os.Stat(path + \"/\" + name); errors.Is(err, os.ErrNotExist) {\n\t\t// Create file\n\t\tfmt.Printf(\"%-35s %-35s\\n\", \"📄 Creating file      ==> \", path+\"/\"+name)\n\t\tfile, err := os.Create(path + \"/\" + name)\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t}\n\t\tdefer file.Close()\n\t}\n}\n\nfunc CreateAllFiles(dirList []SingleDir) {\n\tfor _, dir := range dirList {\n\t\tif dir.Files == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, file := range *dir.Files {\n\t\t\tCreateFile(dir.Path, file)\n\t\t}\n\t}\n}\n\n// Check if directory exists on given path and is type dir\nfunc DirExists(path string) bool {\n\tif path == \".\" {\n\t\treturn true\n\t}\n\tinfo, err := os.Stat(path)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn info.IsDir()\n}\n\n// Check if file exists on given path and is type file\nfunc FileExists(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\n// Count files in given directory\nfunc CountFiles(path string) int {\n\tfiles, err := os.ReadDir(path)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfilesCount := 0\n\tfor _, file := range files {\n\t\tif !file.IsDir() {\n\t\t\tfilesCount++\n\t\t}\n\t}\n\treturn filesCount\n}\n\nfunc HasDirs(path string) bool {\n\tfiles, err := os.ReadDir(path)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Check depth of folder tree, exclude .git folder\nfunc CheckDepth(path string) int {\n\tmaxDepth := 0\n\tvar depth int\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\treturn filepath.SkipDir\n\t\t} else if info.IsDir() {\n\t\t\tdepth = strings.Count(path, string(os.PathSeparator)) + 1\n\t\t\tif depth > maxDepth {\n\t\t\t\tmaxDepth = depth\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\treturn maxDepth\n}\n\nfunc MatchPatternInDir(path string, pattern string) []string {\n\tif pattern == \"\" {\n\t\tpattern = \".*\"\n\t}\n\t// List all files in directory\n\tfiles, err := os.ReadDir(path)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tmatched := []string{}\n\tfor _, file := range files {\n\t\tif !file.IsDir() {\n\t\t\tmatch, err := regexp.MatchString(pattern, file.Name())\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\tif match {\n\t\t\t\tmatched = append(matched, file.Name())\n\t\t\t}\n\t\t}\n\t}\n\treturn matched\n}\n\n// Capture all subdirectories in given directory\nfunc CaptureSubDirs(path string, depth int) []string {\n\tvar dirs []string\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\treturn filepath.SkipDir\n\t\t} else if info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth {\n\t\t\treturn filepath.SkipDir\n\t\t} else if info.IsDir() && info.Name() != \".\" {\n\t\t\tdirs = append(dirs, path)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tfmt.Printf(\"Cannot traverse dirs!\")\n\t\tos.Exit(1)\n\t}\n\treturn dirs\n}\n\n// Capture all files in given directory\nfunc CaptureAllFiles(path string, depth int) []string {\n\tvar files []string\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\treturn filepath.SkipDir\n\t\t} else if !info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth {\n\t\t\treturn filepath.SkipDir\n\t\t} else if !info.IsDir() {\n\t\t\tfiles = append(files, path)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tfmt.Printf(\"Cannot traverse dirs!\")\n\t\tos.Exit(1)\n\t}\n\treturn files\n}\n\n// Capture rules for files with same extension in given directory. If all files in subdirectory have the same extension, save the extension to map with directory path as key.\n// It should return map path -> extension\nfunc CaptureRequiredPatterns(path string, depth int) map[string]string {\n\tvar rules = make(map[string]string)\n\tvar dirs []string\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() && info.Name() == \".git\" {\n\t\t\treturn filepath.SkipDir\n\t\t} else if info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth {\n\t\t\treturn filepath.SkipDir\n\t\t} else if info.IsDir() && info.Name() != \".\" {\n\t\t\tdirs = append(dirs, path)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tfmt.Printf(\"Cannot traverse dirs!\")\n\t\tos.Exit(1)\n\t}\n\t// Check files in dir, if all extensions are the same, save extension to map with dir path as key\n\tfor _, dir := range dirs {\n\t\tfiles, err := os.ReadDir(dir)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\text := \"\"\n\t\tfor _, file := range files {\n\t\t\tif !file.IsDir() {\n\t\t\t\textension := filepath.Ext(file.Name())\n\t\t\t\tif ext == \"\" {\n\t\t\t\t\text = extension\n\t\t\t\t} else if ext != extension {\n\t\t\t\t\text = \"\"\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ext != \"\" {\n\t\t\trules[dir] = ext\n\t\t}\n\t}\n\treturn rules\n}\n\nfunc VerifyFiles(dir SingleDir, allGood *bool) {\n\tfor _, file := range *dir.Files {\n\t\tif !FileExists(dir.Path + \"/\" + file) {\n\t\t\tPrintOut(\"❌ File not found      ==>\", dir.Path+\"/\"+file)\n\t\t\t*allGood = false\n\t\t}\n\t}\n}\nfunc VerifyForbiddenPatterns(dir SingleDir, allGood *bool) {\n\tfor _, pattern := range *dir.ForbiddenPatterns {\n\t\tmatched := MatchPatternInDir(dir.Path, pattern)\n\t\tfor _, match := range matched {\n\t\t\tPrintOut(\"❌ Forbidden pattern (\"+pattern+\") matched under ==>\", dir.Path+\"/\"+match)\n\t\t\t*allGood = false\n\t\t}\n\t}\n}\n\nfunc VerifyAllowedPatterns(dir SingleDir, allGood *bool) {\n\tmatchedCount := 0\n\tfor _, pattern := range *dir.AllowedPatterns {\n\t\tmatched := MatchPatternInDir(dir.Path, pattern)\n\t\tmatchedCount += len(matched)\n\t}\n\tif matchedCount != CountFiles(dir.Path) && len(*dir.AllowedPatterns) > 0 {\n\t\tpatternsString := strings.Join(*dir.AllowedPatterns, \" \")\n\t\tPrintOut(\"❌ Not all files match required pattern (\"+patternsString+\") under ==>\", dir.Path)\n\t\t*allGood = false\n\t}\n}\n"
  },
  {
    "path": "test/test.bats",
    "content": "setup() {\n    # Load\n    load 'test_helper/bats-support/load'\n    load 'test_helper/bats-assert/load'\n    load 'test_helper/bats-file/load'\n\n    # Vars\n    TMP_DIR=\"/tmp/framed-test\"\n    VALID_URL=\"https://raw.githubusercontent.com/mactat/framed/master/examples/python.yaml\"\n    INVALID_URL=\"https://raw.githubusercontent.com/mactat/framed/master/examples/invalid.yaml\"\n    DIR=\"$( cd \"$( dirname \"$BATS_TEST_FILENAME\" )\" >/dev/null 2>&1 && pwd )\"\n\n    # Commands\n    mkdir -p $TMP_DIR\n    cd $TMP_DIR\n    all_commands=(import visualize create capture verify)\n    commands_with_template=(visualize create verify)\n}\n\nteardown() {\n    sudo rm -R $TMP_DIR\n}\n\n### Basic\n\n@test \"Run framed\" {\n    run framed --help\n    assert_success\n    assert_output --partial 'FRAMED (Files and Directories Reusability, Architecture, and Management)'\n}\n\n\n### Import\n\n@test \"Import from valid example\" {\n\n    run framed import --example python\n    assert_success\n    assert_output --partial '✅ Imported successfully'\n    assert_file_exists \"$TMP_DIR/framed.yaml\"\n\n    # assert that content is correct\n    run cat $TMP_DIR/framed.yaml\n    assert_output --partial 'name: python_example'\n}\n\n@test \"Import from invalid example\" {\n    run framed import --example invalid\n    assert_failure\n    assert_output --partial '☠️ Can not find correct structure'\n}\n\n@test \"Import from valid url\" {\n    run framed import --url $VALID_URL\n    assert_success\n    assert_output --partial '✅ Imported successfully'\n    assert_file_exists \"$TMP_DIR/framed.yaml\"\n\n    # assert that content is correct\n    run cat $TMP_DIR/framed.yaml\n    assert_output --partial 'name: python_example'\n}\n\n@test \"Import from invalid url\" {\n    run framed import --url $INVALID_URL\n    assert_failure\n    assert_output --partial '☠️ Can not find correct structure'\n}\n\n@test \"Import with specified output\" {\n    run framed import --example python --output $TMP_DIR/test.yaml\n    assert_success\n    assert_output --partial '✅ Imported successfully'\n    assert_file_exists \"$TMP_DIR/test.yaml\"\n\n    # assert that content is correct\n    run cat $TMP_DIR/test.yaml\n    assert_output --partial 'name: python_example'\n}\n\n\n### Visualize\n\n@test \"Visualize show correct structure\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n\n    # test\n    run framed visualize\n    assert_success\n    assert_output --partial '✅ Read structure'\n    assert_output --partial 'test1'\n    assert_output --partial 'test2'\n    assert_output --partial 'test3'\n    assert_output --partial 'test4'\n}\n\n\n### Create\n\n@test \"Create creates correct dirs\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n\n    # test\n    run framed create\n    assert_success\n    assert_output --partial '✅ Read structure'\n    assert_output --partial '📂 Creating directory'\n    assert_dir_exists \"$(pwd)/test2\"\n    assert_dir_exists \"$(pwd)/test3\"\n}\n\n@test \"Create creates correct files in dirs\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n\n    # test\n    run framed create --files\n    assert_success\n    assert_output --partial '✅ Read structure'\n    assert_output --partial '📂 Creating directory'\n    assert_output --partial '📄 Creating file'\n    assert_dir_exists \"$(pwd)/test2\"\n    assert_dir_exists \"$(pwd)/test3\"\n    assert_file_exists \"$(pwd)/test1.md\"\n    assert_file_exists \"$(pwd)/test3/test4.md\"\n}\n\n@test \"Validate validates correct structure\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    # test\n    run framed verify\n    assert_success\n    assert_output --partial '✅ Read structure'\n    assert_output --partial '✅ Verified successfully!'\n}\n\n\n### Validate\n\n@test \"Verify should spot missing file\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n    rm \"$(pwd)/test1.md\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ File not found'\n    assert_output --partial 'test1.md'\n}\n\n@test \"Verify should spot missing dir\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n    rm -R \"$(pwd)/test2\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Directory not found'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot missing file and dir\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n    rm \"$(pwd)/test1.md\"\n    rm -R \"$(pwd)/test2\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ File not found'\n    assert_output --partial 'test1.md'\n    assert_output --partial '❌ Directory not found'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot wrong pattern if allowedPatterns is set\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].allowedPatterns[0] = \"md\"' \"$(pwd)/framed.yaml\"\n    touch \"$(pwd)/test2/test3.txt\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Not all files match required pattern'\n    assert_output --partial 'test2'\n    assert_output --partial 'md'\n}\n\n@test \"Verify should spot wrong pattern if forbiddenPatterns is set\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].forbiddenPatterns[0] = \"md\"' \"$(pwd)/framed.yaml\"\n    touch \"$(pwd)/test2/test3.md\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Forbidden pattern (md) matched'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot if there is less files than minCount\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].minCount = 2' \"$(pwd)/framed.yaml\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Min count (2) not met'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot if there is more files than maxCount\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].maxCount = 1' \"$(pwd)/framed.yaml\"\n    touch \"$(pwd)/test2/test3.md\"\n    touch \"$(pwd)/test2/test4.md\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Max count (1) exceeded'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot when maxDepth is exceeded\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].maxDepth = 1' \"$(pwd)/framed.yaml\"\n    mkdir \"$(pwd)/test2/test3\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Max depth exceeded (1)'\n    assert_output --partial 'test2'\n}\n\n@test \"Verify should spot when there are subdirs with children not allowed\" {\n    # setup\n    cp $DIR/test.yaml $TMP_DIR/framed.yaml\n    assert_file_exists \"$(pwd)/framed.yaml\"\n    run framed create --files\n    assert_success\n\n    yq -i '.structure.dirs[0].allowChildren = false' \"$(pwd)/framed.yaml\"\n    mkdir \"$(pwd)/test2/test3\"\n\n    # test\n    run framed verify\n    assert_failure\n    assert_output --partial '❌ Children not allowed'\n    assert_output --partial 'test2'\n}\n\n\n### Capture\n\n@test \"Capture should fail if depth flag is not integer\" {\n    # test\n    run framed capture --depth invalid_value\n    assert_failure\n    assert_output --partial '🚨 Invalid depth value'\n    assert_output --partial 'invalid_value'\n}\n\n@test \"Capture should export correct name when name flag is set\" {\n    # test\n    run framed capture --name test_name\n    assert_success\n    assert_output --partial '✅ Exported to file'\n\n    # verify\n    assert_file_exists \"$(pwd)/framed.yaml\"\n\n    run yq eval '.name' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output 'test_name'\n}\n\n@test \"Capture should save file to correct path when output flag is set\" {\n    # test\n    run framed capture --output \"$(pwd)/test.yaml\"\n    assert_success\n    assert_output --partial '✅ Exported to file'\n\n    # verify\n    assert_file_exists \"$(pwd)/test.yaml\"\n}\n\n@test \"Capture should capture correct structure without depth flag\" {\n    # setup\n    mkdir \"$(pwd)/test1\"\n    touch \"$(pwd)/test1/test2.md\"\n\n    # test\n    run framed capture\n    assert_success\n    assert_output --partial '📝 Name:                             default'\n    assert_output --partial '📂 Directories:                      1'\n    assert_output --partial '📄 Files:                            1'\n    assert_output --partial '🔁 Patterns:                         1'\n    assert_output --partial '✅ Exported to file'\n\n    # verify\n    assert_file_exists \"$(pwd)/framed.yaml\"\n\n    run yq eval '.name' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output 'default'\n\n    run yq eval '.structure.dirs.[0].name' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output 'test1'\n\n    run yq eval '.structure.dirs.[0].files.[0]' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output 'test2.md'\n\n    run yq eval '.structure.dirs.[0].allowedPatterns.[0]' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output '.md'\n}\n\n@test \"Capture should capture correct depth when flag is provided\" {\n    # setup\n    mkdir \"$(pwd)/test1\"\n    mkdir \"$(pwd)/test1/test2\"\n    mkdir \"$(pwd)/test1/test2/test3\"\n    touch \"$(pwd)/test1/test2/test3/test4.md\"\n    touch \"$(pwd)/test6.md\"\n\n    # test\n    run framed capture --depth 1\n    assert_success\n    assert_output --partial '📂 Directories:                      1'\n    assert_output --partial '📄 Files:                            1'\n    run yq eval '.structure.dirs' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output --partial 'test1'\n    refute_output --partial 'test2'\n    refute_output --partial 'test3'\n    refute_output --partial 'test4.md'\n\n    run yq eval '.structure.files' \"$(pwd)/framed.yaml\"\n    assert_success\n    assert_output --partial 'test6.md'\n\n}\n\n\n### All\n\n@test \"All commands fails when file not exist\" {\n    for command in \"${commands_with_template[@]}\"\n    do\n        run framed $command\n        assert_failure\n        assert_output --partial '☠️ Can not read file'\n    done\n}"
  },
  {
    "path": "test/test.yaml",
    "content": "# FRAMED Configuration\nname: example\n\nstructure:\n  name: root\n  files:\n    - test1.md\n  dirs:\n    - name: test2\n    - name: test3\n      files:\n        - test4.md\n"
  }
]