Repository: mactat/framed Branch: master Commit: 4f18db334ce4 Files: 33 Total size: 54.8 KB Directory structure: gitextract__ybk0m0_/ ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── pr.yaml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ ├── capture.go │ ├── create.go │ ├── import.go │ ├── root.go │ ├── verify.go │ └── visualize.go ├── dockerfiles/ │ ├── dockerfile │ ├── goreleaser.dockerfile │ └── test.dockerfile ├── docs/ │ └── framed.tape ├── examples/ │ ├── golang.yaml │ ├── python.yaml │ └── structure.yaml ├── framed.go ├── framed.yaml ├── go.mod ├── go.sum ├── pkg/ │ └── ext/ │ ├── decoder.go │ ├── encoder.go │ ├── network.go │ ├── printer.go │ └── system.go └── test/ ├── test.bats └── test.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ README.md pipelines/ dockerfiles/ docs/ build/ LICENSE Makefile ================================================ FILE: .github/FUNDING.yml ================================================ github: [mactat] ================================================ FILE: .github/workflows/pr.yaml ================================================ name: pr on: pull_request: types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: {} jobs: pr: name: "Pull Request Checks" runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v3 - name: "Framed verify" uses: 'mactat/framed-action@v0.0.7' - name: "Functional tests" run: make test BUILD=true EXPORT=true shell: bash - name: Test Summary uses: test-summary/action@v2 with: paths: "results/test.xml" if: always() - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: v1.53 ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - run: git fetch --force --tags - name: Log in to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - uses: actions/setup-go@v4 with: go-version: stable - uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GORELEASER_GH_TOKEN }} ================================================ FILE: .gitignore ================================================ build/ .vscode/ ================================================ FILE: .goreleaser.yaml ================================================ builds: - binary: framed goos: - darwin - linux - windows goarch: - amd64 - arm64 env: - CGO_ENABLED=0 release: prerelease: auto dockers: - dockerfile: "dockerfiles/goreleaser.dockerfile" image_templates: - "mactat/framed:latest" - "mactat/framed:{{ .Tag }}" universal_binaries: - replace: true brews: - name: framed homepage: "https://github.com/mactat/framed" repository: owner: mactat name: homebrew-mactat commit_author: name: mactat email: maciektatarsk@gmail.com checksum: name_template: 'checksums.txt' ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Maciej Tatarski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Definitions ROOT := $(PWD) GO_VERSION := 1.20.4 ALPINE_VERSION := 3.18 OS := linux ARCH := amd64 BUILD := true .PHONY: version version: $(eval VERSION := $(shell git describe --tags --abbrev=0 2> /dev/null || git rev-parse --short HEAD)) @echo "Version: $(VERSION)" .PHONY: build-docker build-docker: docker build \ -t mactat/framed \ -f ./dockerfiles/dockerfile \ --build-arg GO_VERSION=$(GO_VERSION) \ --build-arg ALPINE_VERSION=$(ALPINE_VERSION) \ . .PHONY: release-docker release-docker: version build-docker docker tag mactat/framed mactat/framed:$(VERSION) docker tag mactat/framed mactat/framed:alpine-$(ALPINE_VERSION)-$(VERSION) docker push mactat/framed:$(VERSION) docker push mactat/framed:alpine-$(ALPINE_VERSION)-$(VERSION) docker push mactat/framed:latest .PHONY: build-in-docker build-in-docker: clean version docker run \ --rm \ -v $(ROOT):/app \ --env GOOS=$(OS) \ --env GOARCH=$(ARCH) \ golang:$(GO_VERSION)-alpine$(ALPINE_VERSION) \ /bin/sh -c "cd /app && go build -o ./build/ ./framed.go" sudo chown -R $(USER):$(USER) ./build cd ./build && \ tar -zcvf \ ./framed-$(OS)-$(ARCH)-$(VERSION).tar.gz \ ./framed$(if $(filter $(OS),windows),.exe) .PHONY: release-lin release-lin: $(MAKE) build-in-docker OS=linux ARCH=amd64 .PHONY: release-win release-win: $(MAKE) build-in-docker OS=windows ARCH=amd64 .PHONY: release-mac release-mac: $(MAKE) build-in-docker OS=darwin ARCH=amd64 .PHONY: build build: go build -o ./build/ ./framed.go .PHONY: test test: @if [ "$(BUILD)" = "true" ]; then\ $(MAKE) build-docker;\ fi docker build -f ./dockerfiles/test.dockerfile -t framed-test . @if [ "$(EXPORT)" = "true" ]; then\ mkdir -p ./results;\ docker run --rm framed-test /bin/sh -c "/test/bats/bin/bats -F junit /test/" > ./results/test.xml;\ else\ docker run --rm framed-test /bin/sh -c "/test/bats/bin/bats --pretty /test/";\ fi .PHONY: format format: go fmt ./... .PHONY: lint lint: docker run -t --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.53.3 golangci-lint run -v .PHONY: clean clean: rm -f ./build/framed rm -f ./build/framed.exe ================================================ FILE: README.md ================================================ [![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) # Framed - Files and Directories Reusability, Architecture, and Management Framed 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. To 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. ## Demo ![Demo](./docs/static/demo.gif) ## Features - **YAML Templates**: Framed uses YAML templates to define the entire project structure. - **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. - **Consistency Across Projects**: Framed offers a consistent way of organizing files and directories across different projects. ## Example configuration To get started with Framed, you can use the following example: ```yaml # Framed Configuration name: framed structure: name: root maxDepth: 5 # Disallow dirs deeper than 5 files: - README.md - framed.yaml - main.go - go.mod - go.sum - .gitignore dirs: - name: cmd allowedPatterns: - ".go" forbiddenPatterns: - "_test.go" # Disallow tests in /src - name: pipelines maxCount: 2 allowedPatterns: - ".yml" - ".yaml" # only yaml files allowed files: - pr.yaml - name: dockerfiles minCount: 1 # At least one file has to be there allowChildren: false # Allow subdirectories creation, default true allowedPatterns: - ".dockerfile" - name: docs maxCount: 10 # No more than 10 files per dir allowedPatterns: - ".md" - ".txt" dirs: - name: design - name: examples ``` ### Project Structure Definition Framed 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. ### Root-level Requirements The 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. ### Nested Structure The dirs section allows you to define nested directories within the project structure. Each subdirectory can have its own set of required files and directories. ### File Requirements You can specify file requirements using the files property. It ensures that specific files are present within the designated directory. ### File Patterns The 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. ### Forbidden Files The 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. ### Minimum File Count The 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. ### Maximum File Count The 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. ### Allowing Children The 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. ## Installation ### Brew Installation 1. Open a terminal and run the following command: ```shell brew tap mactat/mactat brew install framed ``` ### Darwin (macOS) Installation 1. Download the `framed-darwin-amd64-.tar.gz` package from release page. 2. Extract the package by double-clicking on the downloaded file or using a tool like `tar` in your terminal: ```shell tar -xzf framed-darwin-amd64-.tar.gz ``` 3. This will extract the `framed` binary. 4. Open a terminal and navigate to the extracted directory: ```shell cd framed-darwin-amd64- ``` 5. Make the binary executable by running the following command: ```shell chmod +x framed ``` 6. Move the `framed` binary to a directory in your system's `PATH` so that it can be accessed from anywhere. For example: ```shell sudo mv framed /usr/local/bin/ ``` 7. You can now use the `framed` command to execute the application. ### Linux Installation 1. Download the `framed-linux-amd64-.tar.gz` package from release page. 2. Extract the package using the following command in your terminal: ```shell tar -xzf framed-linux-amd64-.tar.gz ``` 3. This will extract the `framed` binary. 4. Open a terminal and navigate to the extracted directory: ```shell cd framed-linux-amd64- ``` 5. Make the binary executable by running the following command: ```shell chmod +x framed ``` 6. Move the `framed` binary to a directory in your system's `PATH` so that it can be accessed from anywhere. For example: ```shell sudo mv framed /usr/local/bin/ ``` 7. You can now use the `framed` command to execute the application. ### Windows Installation 1. Download the `framed-windows-amd64-.tar.gz` package from release page. 2. Extract the package using a file extraction tool like 7-Zip or WinRAR. 3. This will extract the `framed.exe` binary. 4. Move the `framed.exe` binary to a directory that is included in your system's `PATH`, such as `C:\Windows` or `C:\Windows\System32`. 5. You can now use the `framed` command to execute the application from the Command Prompt or PowerShell. **Please note that the exact steps may vary depending on your system configuration.** ## Usage **Note**: The following commands assume that you have already installed Framed and added it to your system's PATH environment variable. **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`. ### 1. Creating a Project Structure To create a new project structure using a YAML template, run the following command: ```bash framed create ``` If you also want to create required files, run the following command: ```bash framed create --files ``` ### 2. Capturing current project structure To capture the current project structure as a YAML template, run the following command: ```bash framed capture --output ``` ### 3. Test Project Structure (CI/CD) To test the project structure for consistency and compliance with the YAML template, run the following command: ```bash framed verify ``` For a complete list of available commands and usage examples, refer to the [documentation](link-to-full-docs). ### 4. Visualize Project Structure To visualize the project structure, run the following command: ```bash framed visualize ``` ### 5. Importing Project Structure To import the project structure from url, run the following command: ```bash framed import --url ``` url has to be pointing to a yaml file with valid structure. To import an example project structure, run the following command: ```bash framed import --example ``` Currently available examples: - python - golang See [examples](./examples) for more details. ## Running from docker To run framed from docker, run the following command: ```bash docker run --rm -v $(pwd):/app --user $(id -u):$(id -g) mactat/framed framed ``` example: ```bash docker run --rm -v $(pwd):/app --user $(id -u):$(id -g) mactat/framed framed import --example python ``` Images can be found on [dockerhub](https://hub.docker.com/r/mactat/framed). ## Github Action You can use framed as a github action to verify your project structure. Minimal example: ```yaml name: Verify Project Structure on: [push, pull_request] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Verify Project Structure uses: mactat/framed@0.0.7 with: template: './framed.yaml' # Optional, default is framed.yaml version: 'v0.0.8' # Optional, default is v0.0.8 ``` ## TODO - [ ] Add support from importing part of the structure from url or file like: ```yaml name: framed structure: name: root dirs: - name: other template: other.yaml # Use another template for this dir - name: another template: https://yourfile.com/framed.yaml # Share templates between projects ``` - [x] Add some tests - [ ] Add contributing guidelines - [ ] Add more examples - [x] Create a github action for running tests - [ ] Move remaining business logic to a separate package ================================================ FILE: cmd/capture.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "fmt" "framed/pkg/ext" "os" "strconv" "github.com/spf13/cobra" ) // captureCmd represents the capture command var captureCmd = &cobra.Command{ Use: "capture", Short: "Capture the current project structure as a YAML template", Long: `This command is capturing the current project structure as a YAML template. Example: framed capture --output ./framed.yaml --name my-project `, Run: func(cmd *cobra.Command, args []string) { output := cmd.Flag("output").Value.String() name := cmd.Flag("name").Value.String() depthStr := cmd.Flag("depth").Value.String() depth, err := strconv.Atoi(depthStr) if err != nil { ext.PrintOut("🚨 Invalid depth value: ", depthStr) os.Exit(1) } ext.PrintOut("📝 Name:", name+"\n") // capture subdirectories subdirs := ext.CaptureSubDirs(".", depth) ext.PrintOut("📂 Directories:", fmt.Sprintf("%v", len(subdirs))) // capture files files := ext.CaptureAllFiles(".", depth) ext.PrintOut("📄 Files:", fmt.Sprintf("%v", len(files))) // capture patterns patterns := ext.CaptureRequiredPatterns(".", depth) ext.PrintOut("🔁 Patterns:", fmt.Sprintf("%v", len(patterns))) // export config ext.ExportConfig(name, output, subdirs, files, patterns) ext.PrintOut("\n✅ Exported to file: ", output) }, } func init() { rootCmd.AddCommand(captureCmd) captureCmd.PersistentFlags().String("output", "./framed.yaml", "path to output file") captureCmd.PersistentFlags().String("name", "default", "name of the project") captureCmd.PersistentFlags().String("depth", "-1", "depth of the directory tree to capture") } ================================================ FILE: cmd/create.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "framed/pkg/ext" "github.com/spf13/cobra" ) // createCmd represents the create command var createCmd = &cobra.Command{ Use: "create", Short: "Create a new project structure using a YAML template", Long: `This command is creating a new project structure from a YAML template. Example: framed create --template ./framed.yaml --files true `, Run: func(cmd *cobra.Command, args []string) { path := cmd.Flag("template").Value.String() createFiles := cmd.Flag("files").Value.String() == "true" // read config _, dirList := ext.ReadConfig(path) // create directories ext.CreateAllDirs(dirList) // create files if createFiles { ext.CreateAllFiles(dirList) } }, } func init() { rootCmd.AddCommand(createCmd) createCmd.PersistentFlags().String("template", "./framed.yaml", "path to template file default") createCmd.PersistentFlags().Bool("files", false, "create required files") } ================================================ FILE: cmd/import.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "framed/pkg/ext" "os" "github.com/spf13/cobra" ) // importCmd represents the import command var importCmd = &cobra.Command{ Use: "import", Short: "Import the project structure", Long: `This command is importing the project structure from a YAML template. It can be imported from a template or from a remote URL. Example: framed import https://raw.githubusercontent.com/username/repo/master/framed.yaml or framed import --example python --output ./python.yaml `, Run: func(cmd *cobra.Command, args []string) { url := cmd.Flag("url").Value.String() example := cmd.Flag("example").Value.String() output := cmd.Flag("output").Value.String() if url != "" { err := ext.ImportFromUrl(output, url) if err != nil { ext.PrintOut("☠️ Failed to import from url ==>", url) os.Exit(1) } } if example != "" { err := ext.ImportFromUrl(output, ext.ExampleToUrl(example)) if err != nil { ext.PrintOut("☠️ Failed to import from example ==>", example) os.Exit(1) } } ext.PrintOut("✅ Saved to ==>", output) // try to load configTree, _ := ext.ReadConfig(output) ext.PrintOut("✅ Imported successfully ==>", configTree.Name) }, } func init() { rootCmd.AddCommand(importCmd) // Here you will define your flags and configuration settings. importCmd.PersistentFlags().String("url", "", "url to template file") importCmd.PersistentFlags().String("example", "", "example template file from github") importCmd.MarkFlagsMutuallyExclusive("url", "example") // path to file importCmd.PersistentFlags().String("output", "./framed.yaml", "path to template file") } ================================================ FILE: cmd/root.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "os" "github.com/spf13/cobra" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "framed", Short: "CLI tool for managing folder and files structures", Long: `FRAMED (Files and Directories Reusability, Architecture, and Management) is a powerful CLI tool written in Go 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 ensures that your projects adhere to the defined structure, enabling consistency and reusability. `, } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() {} ================================================ FILE: cmd/verify.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "fmt" "framed/pkg/ext" "os" "github.com/spf13/cobra" ) // testCmd represents the test command var testCmd = &cobra.Command{ Use: "verify", Short: "Verify the project rules and structure", Long: `This command is verifying the project structure for consistency and compliance with the YAML template. Example: framed verify --template ./framed.yaml `, Run: func(cmd *cobra.Command, args []string) { path := cmd.Flag("template").Value.String() // read config _, dirList := ext.ReadConfig(path) allGood := true // verify directories for _, dir := range dirList { if !ext.DirExists(dir.Path) { ext.PrintOut("❌ Directory not found ==>", dir.Path) allGood = false } // verify files if dir.Files != nil { ext.VerifyFiles(dir, &allGood) } // verify minCount numFiles := ext.CountFiles(dir.Path) if numFiles < dir.MinCount { ext.PrintOut("❌ Min count ("+fmt.Sprint(dir.MinCount)+") not met ==>", dir.Path) allGood = false } // verify maxCount if numFiles > dir.MaxCount { ext.PrintOut("❌ Max count ("+fmt.Sprint(dir.MaxCount)+") exceeded ==>", dir.Path) allGood = false } // verify childrenAllowed if !dir.AllowChildren { if ext.HasDirs(dir.Path) { ext.PrintOut("❌ Children not allowed ==>", dir.Path) allGood = false } } // verify maxDepth if ext.CheckDepth(dir.Path) > dir.MaxDepth { ext.PrintOut("❌ Max depth exceeded ("+fmt.Sprint(dir.MaxDepth)+") ==>", dir.Path) allGood = false } // Verify forbidden if dir.ForbiddenPatterns != nil { ext.VerifyForbiddenPatterns(dir, &allGood) } // Verify allowed patterns if dir.AllowedPatterns != nil { ext.VerifyAllowedPatterns(dir, &allGood) } } if allGood { fmt.Println("✅ Verified successfully!") } else { os.Exit(1) } }, } func init() { rootCmd.AddCommand(testCmd) testCmd.PersistentFlags().String("template", "./framed.yaml", "path to template file") } ================================================ FILE: cmd/visualize.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ // Package cmd represents the command line interface of the application package cmd import ( "framed/pkg/ext" "github.com/spf13/cobra" ) // visualizeCmd represents the visualize command var visualizeCmd = &cobra.Command{ Use: "visualize", Short: "Visualize the project structure", Long: `This command is visualizing the project structure from a YAML template. Example: framed visualize --template ./framed.yaml`, Run: func(cmd *cobra.Command, args []string) { path := cmd.Flag("template").Value.String() // read config _, dirList := ext.ReadConfig(path) // visualize template ext.VisualizeTemplate(dirList) }, } func init() { rootCmd.AddCommand(visualizeCmd) visualizeCmd.PersistentFlags().String("template", "./framed.yaml", "Path to template file default is ./framed.yaml") } ================================================ FILE: dockerfiles/dockerfile ================================================ ARG GO_VERSION=1.20.4 ARG ALPINE_VERSION=3.18 FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} as builder WORKDIR /app COPY go.mod go.sum /app/ RUN go mod download COPY framed.go /app/ COPY cmd /app/cmd COPY pkg /app/pkg RUN go build -o framed /app/framed.go FROM alpine:${ALPINE_VERSION} as release COPY --from=builder /app/framed /bin/framed WORKDIR /app CMD ["/bin/sh"] ================================================ FILE: dockerfiles/goreleaser.dockerfile ================================================ ARG ALPINE_VERSION=3.18 FROM alpine:${ALPINE_VERSION} as release COPY framed /bin/framed CMD [ "/bin/sh" ] ================================================ FILE: dockerfiles/test.dockerfile ================================================ FROM mactat/framed:latest as builder RUN apk add --no-cache git bash sudo yq ncurses ENV TERM=linux # clone bats RUN git clone https://github.com/bats-core/bats-core.git /test/bats RUN git clone https://github.com/bats-core/bats-support.git /test/test_helper/bats-support RUN git clone https://github.com/bats-core/bats-assert.git /test/test_helper/bats-assert RUN git clone https://github.com/bats-core/bats-file.git /test/test_helper/bats-file COPY /test/test.bats /test/test.yaml /test/ ================================================ FILE: docs/framed.tape ================================================ Output ./docs/static/demo.gif # Settings Set FontSize 20 Set Width 1200 Set Height 600 # Requirements Require framed # Setup Hide Type "mkdir -p /tmp/framed-demo" Enter Type "cd /tmp/framed-demo" Enter Ctrl+L Show # Flow Sleep 1s Type "echo 'Welcome to Framed!'" Enter Sleep 4s Ctrl+L Sleep 1s Type "tree" Enter Sleep 4s Ctrl+L Sleep 1s Type "framed import --example python" Enter Sleep 4s Ctrl+L Sleep 1s Type "tree" Enter Sleep 4s Ctrl+L Sleep 1s Type "cat framed.yaml" Enter Sleep 6s Ctrl+L Sleep 1s Type "framed verify" Enter Sleep 6s Ctrl+L Sleep 1s Type "framed create --files" Enter Sleep 6s Ctrl+L Sleep 1s Type "framed verify" Enter Sleep 4s Ctrl+L Sleep 1s Type "tree" Enter Sleep 5s Ctrl+L Sleep 1s Type "rm setup.py" Enter Sleep 1s Type "framed verify" Enter Sleep 6s Ctrl+L # Clean Hide Type "cd -" Enter Type "rm -r /tmp/framed-demo" Enter ================================================ FILE: examples/golang.yaml ================================================ # FRAMED Configuration name: golang_example structure: name: root files: - README.md - framed.yaml - go.mod - go.sum - LICENSE.md - .gitignore - Makefile dirs: - name: cmd minCount: 1 maxCount: 10 allowedPatterns: - ".go" - name: .github dirs: - name: workflows maxCount: 4 allowedPatterns: - ".yml" - ".yaml" files: - pr.yaml - release.yaml - name: dockerfiles minCount: 1 allowChildren: false allowedPatterns: - "dockerfile" - "Dockerfile" - name: docs maxCount: 10 # No more than 10 files per dir allowedPatterns: - ".md" - ".txt" - name: pkg allowedPatterns: - ".go" - name: examples - name: build ================================================ FILE: examples/python.yaml ================================================ # FRAMED Configuration name: python_example structure: name: root files: - README.md - setup.py - .gitignore dirs: - name: docs - name: tests files: - __init__.py - name: package_name files: - __init__.py allowedPatterns: - .py ================================================ FILE: examples/structure.yaml ================================================ # FRAMED Configuration name: structure structure: name: root dirs: - name: src dirs: - name: this dirs: - name: is files: - test.yaml - name: test - name: test - name: docs - name: dockerfiles - name: examples ================================================ FILE: framed.go ================================================ /* Copyright © 2023 Maciej Tatarski maciektatarski@gmail.com */ package main import "framed/cmd" func main() { cmd.Execute() } ================================================ FILE: framed.yaml ================================================ # FRAMED Configuration name: framed structure: name: root maxDepth: 5 # Disallow dirs deeper than 5 files: - README.md - framed.yaml - framed.go - go.mod - go.sum - .gitignore dirs: - name: cmd allowedPatterns: - ".go" forbiddenPatterns: - "_test.go" # Disallow tests in /cmd - name: pkg dirs: - name: ext allowedPatterns: - ".go" - name: .github dirs: - name: workflows maxCount: 4 allowedPatterns: - ".yml" - ".yaml" # only yaml files allowed files: - pr.yaml - release.yaml - name: dockerfiles minCount: 1 # At least one file has to be there allowChildren: false # Allow subdirectories creation, default true allowedPatterns: - "dockerfile" - name: docs maxCount: 10 # No more than 10 files per dir allowedPatterns: - ".md" - ".txt" - name: examples ================================================ FILE: go.mod ================================================ module framed go 1.20 require ( github.com/TwiN/go-color v1.4.0 github.com/creasty/defaults v1.7.0 github.com/spf13/cobra v1.7.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/spf13/pflag v1.0.5 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect ) ================================================ FILE: go.sum ================================================ github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng= github.com/TwiN/go-color v1.4.0/go.mod h1:0QTVEPlu+AoCyTrho7bXbVkrCkVpdQr7YF7PYWEtSxM= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: pkg/ext/decoder.go ================================================ package ext import ( "fmt" "os" "github.com/creasty/defaults" "gopkg.in/yaml.v3" ) // SingleDir struct type SingleDir struct { Name string `yaml:"name"` Path string `yaml:"path"` Files *[]string `yaml:"files"` Dirs *[]SingleDir `yaml:"dirs"` AllowedPatterns *[]string `yaml:"allowedPatterns"` ForbiddenPatterns *[]string `yaml:"forbiddenPatterns"` MinCount int `default:"0" yaml:"minCount"` MaxCount int `default:"1000" yaml:"maxCount"` MaxDepth int `default:"1000" yaml:"maxDepth"` AllowChildren bool `default:"true" yaml:"allowChildren"` } // UnmarshalYAML implements yaml.Unmarshaler interface // Meant for initializing default values func (s *SingleDir) UnmarshalYAML(unmarshal func(interface{}) error) error { err := defaults.Set(s) if err != nil { fmt.Println("Cannot set defaults!") os.Exit(1) } type plain SingleDir if err := unmarshal((*plain)(s)); err != nil { return err } return nil } type config struct { Name string `yaml:"name"` Structure *SingleDir `yaml:"structure"` } func ReadConfig(path string) (config, []SingleDir) { yamlFile, err := os.ReadFile(path) if err != nil { // add emoji PrintOut("☠️ Can not read file ==>", path) os.Exit(1) } PrintOut("✅ Loaded template from ==>", path) // Map to store the parsed YAML data var curConfig config // Unmarshal the YAML string into the data map err = yaml.Unmarshal([]byte(yamlFile), &curConfig) if err != nil { PrintOut("☠️ Can not decode file ==>", path) os.Exit(1) } if curConfig.Structure == nil { PrintOut("☠️ Can not find correct structure in ==>", path) os.Exit(1) } else { PrintOut("✅ Read structure for ==>", curConfig.Name) } dirList := []SingleDir{} traverseStructure(curConfig.Structure, ".", &dirList) return curConfig, dirList } func traverseStructure(dir *SingleDir, path string, dirsList *[]SingleDir) { // Change path if dir == nil { PrintOut("☠️ Can't traverse nil dir ==>", path) os.Exit(1) } dir.Path = path // add current dir to dirsList *dirsList = append(*dirsList, *dir) if dir.Dirs == nil { return } // traverse children for _, child := range *dir.Dirs { traverseStructure(&child, path+"/"+child.Name, dirsList) } } ================================================ FILE: pkg/ext/encoder.go ================================================ package ext import ( "fmt" "os" "strings" "gopkg.in/yaml.v3" ) // Consider implementing a custom Unmarshaler for SingleDirOut type SingleDirOut struct { Name string `yaml:"name"` Path string `yaml:"-"` Files *[]string `yaml:"files,omitempty"` Dirs *[]SingleDirOut `yaml:"dirs,omitempty"` AllowedPatterns *[]string `yaml:"allowedPatterns,omitempty"` ForbiddenPatterns *[]string `yaml:"forbiddenPatterns,omitempty"` MinCount int `yaml:"minCount,omitempty"` MaxCount int `yaml:"maxCount,omitempty"` MaxDepth int `yaml:"maxDepth,omitempty"` AllowChildren bool `yaml:"allowChildren,omitempty"` } type configOut struct { Name string `yaml:"name"` Structure *SingleDirOut `yaml:"structure"` } func ExportConfig(name string, path string, subdirs []string, files []string, patterns map[string]string) { // create config, files and dirs are empty config := configOut{ Name: name, Structure: &SingleDirOut{ Name: "root", Files: &[]string{}, Dirs: &[]SingleDirOut{}, }, } // add subdirs insertSubdirs(config.Structure.Dirs, subdirs) // add files insertFiles(config.Structure, files) // add patterns insertPatterns(config.Structure, patterns) // export config yamlFile, err := yaml.Marshal(config) if err != nil { fmt.Printf("Error while Marshaling. %v", err) } // Save to file err = os.WriteFile(path, yamlFile, 0644) if err != nil { fmt.Printf("Error while writing file. %v", err) } } func insertSubdirs(dirs *[]SingleDirOut, subdirs []string) { for subdir := range subdirs { insertSingleDir(dirs, subdirs[subdir]) } } func insertSingleDir(dirs *[]SingleDirOut, dir string) { subdirPath := strings.Split(dir, "/") curDirs := dirs for i := range subdirPath { if !containsDir(*curDirs, subdirPath[i]) { *curDirs = append(*curDirs, SingleDirOut{ Name: subdirPath[i], Files: &[]string{}, Dirs: &[]SingleDirOut{}, AllowedPatterns: &[]string{}, }) } // go deeper curDirs = getDir(*curDirs, subdirPath[i]).Dirs } } func containsDir(dirs []SingleDirOut, name string) bool { for _, dir := range dirs { if dir.Name == name { return true } } return false } func getDir(dirs []SingleDirOut, name string) *SingleDirOut { for _, dir := range dirs { if dir.Name == name { return &dir } } return nil } func insertFiles(root *SingleDirOut, files []string) { for file := range files { insertSingleFile(root, files[file]) } } func insertSingleFile(root *SingleDirOut, file string) { subdirPath := strings.Split(file, "/") if len(subdirPath) == 1 { *root.Files = append(*root.Files, subdirPath[0]) return } curDirs := root.Dirs curDir := root for i := 0; i < len(subdirPath)-1; i++ { curDir = getDir(*curDirs, subdirPath[i]) curDirs = curDir.Dirs } *curDir.Files = append(*curDir.Files, subdirPath[len(subdirPath)-1]) } func insertPatterns(root *SingleDirOut, patterns map[string]string) { for dir, pattern := range patterns { insertSinglePattern(root, dir, pattern) } } func insertSinglePattern(root *SingleDirOut, dir string, pattern string) { subdirPath := strings.Split(dir, "/") curDirs := root.Dirs curDir := root for i := range subdirPath { // go deeper curDir = getDir(*curDirs, subdirPath[i]) curDirs = curDir.Dirs } // insert pattern *curDir.AllowedPatterns = append(*curDir.AllowedPatterns, pattern) } ================================================ FILE: pkg/ext/network.go ================================================ package ext import ( "io" "net/http" "os" ) func ExampleToUrl(example string) string { return "https://raw.githubusercontent.com/mactat/framed/master/examples/" + example + ".yaml" } func ImportFromUrl(path string, url string) error { // Get the data resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Create the file out, err := os.Create(path) if err != nil { return err } defer out.Close() // Write the body to file _, err = io.Copy(out, resp.Body) return err } ================================================ FILE: pkg/ext/printer.go ================================================ package ext import ( "fmt" "os" "strings" "github.com/TwiN/go-color" ) func PrintOut(prompt string, text string) { fmt.Printf("%-35s %-35s\n", prompt, text) } // This is ugly but it works, it needs to be refactored. It also has some bugs in case of out of order directories. func VisualizeTemplate(template []SingleDir) { for dirNum, dir := range template { connectorDir := "├──" initString := "" depth := strings.Count(dir.Path, string(os.PathSeparator)) + 1 name := strings.Split(dir.Path, string(os.PathSeparator))[depth-1] dirDepth := depth if depth <= 2 { dirDepth = 1 } else { initString = "│" } if dirNum == len(template)-1 { connectorDir = "└──" } printDirectory(initString, dirDepth, connectorDir, name) if dir.Files == nil { continue } for num, file := range *dir.Files { connector := "├──" if depth > 1 { initString = "│" } if num == len(*dir.Files)-1 { connector = "└──" } printFile(initString, depth, connector, file) } } } func printDirectory(initString string, dirDepth int, connectorDir string, name string) { output := initString + strings.Repeat(" ", dirDepth-1) + connectorDir + " 📂 " + color.Ize(color.Blue, name) println(output) } func printFile(initString string, depth int, connector string, file string) { output := initString + strings.Repeat(" ", depth-1) + connector + " 📄 " + color.Ize(color.Green, file) println(output) } ================================================ FILE: pkg/ext/system.go ================================================ package ext import ( "errors" "fmt" "log" "os" "path/filepath" "regexp" "strings" ) func CreateDir(path string) { // Create directory if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { err := os.Mkdir(path, os.ModePerm) fmt.Printf("%-35s %-35s\n", "📂 Creating directory ==> ", path) if err != nil { log.Println(err) } } } func CreateAllDirs(dirList []SingleDir) { for _, dir := range dirList { CreateDir(dir.Path) } } func CreateFile(path string, name string) { // Check if file exists if _, err := os.Stat(path + "/" + name); errors.Is(err, os.ErrNotExist) { // Create file fmt.Printf("%-35s %-35s\n", "📄 Creating file ==> ", path+"/"+name) file, err := os.Create(path + "/" + name) if err != nil { log.Println(err) } defer file.Close() } } func CreateAllFiles(dirList []SingleDir) { for _, dir := range dirList { if dir.Files == nil { continue } for _, file := range *dir.Files { CreateFile(dir.Path, file) } } } // Check if directory exists on given path and is type dir func DirExists(path string) bool { if path == "." { return true } info, err := os.Stat(path) if os.IsNotExist(err) { return false } return info.IsDir() } // Check if file exists on given path and is type file func FileExists(path string) bool { info, err := os.Stat(path) if os.IsNotExist(err) { return false } return !info.IsDir() } // Count files in given directory func CountFiles(path string) int { files, err := os.ReadDir(path) if err != nil { log.Fatal(err) } filesCount := 0 for _, file := range files { if !file.IsDir() { filesCount++ } } return filesCount } func HasDirs(path string) bool { files, err := os.ReadDir(path) if err != nil { log.Fatal(err) } for _, file := range files { if file.IsDir() { return true } } return false } // Check depth of folder tree, exclude .git folder func CheckDepth(path string) int { maxDepth := 0 var depth int err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if info.IsDir() && info.Name() == ".git" { return filepath.SkipDir } else if info.IsDir() { depth = strings.Count(path, string(os.PathSeparator)) + 1 if depth > maxDepth { maxDepth = depth } } return nil }) if err != nil { log.Println(err) } return maxDepth } func MatchPatternInDir(path string, pattern string) []string { if pattern == "" { pattern = ".*" } // List all files in directory files, err := os.ReadDir(path) if err != nil { log.Fatal(err) } matched := []string{} for _, file := range files { if !file.IsDir() { match, err := regexp.MatchString(pattern, file.Name()) if err != nil { log.Fatal(err) } if match { matched = append(matched, file.Name()) } } } return matched } // Capture all subdirectories in given directory func CaptureSubDirs(path string, depth int) []string { var dirs []string err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if info.IsDir() && info.Name() == ".git" { return filepath.SkipDir } else if info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth { return filepath.SkipDir } else if info.IsDir() && info.Name() != "." { dirs = append(dirs, path) } return nil }) if err != nil { fmt.Printf("Cannot traverse dirs!") os.Exit(1) } return dirs } // Capture all files in given directory func CaptureAllFiles(path string, depth int) []string { var files []string err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if info.IsDir() && info.Name() == ".git" { return filepath.SkipDir } else if !info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth { return filepath.SkipDir } else if !info.IsDir() { files = append(files, path) } return nil }) if err != nil { fmt.Printf("Cannot traverse dirs!") os.Exit(1) } return files } // 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. // It should return map path -> extension func CaptureRequiredPatterns(path string, depth int) map[string]string { var rules = make(map[string]string) var dirs []string err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if info.IsDir() && info.Name() == ".git" { return filepath.SkipDir } else if info.IsDir() && depth > 0 && strings.Count(path, string(os.PathSeparator)) >= depth { return filepath.SkipDir } else if info.IsDir() && info.Name() != "." { dirs = append(dirs, path) } return nil }) if err != nil { fmt.Printf("Cannot traverse dirs!") os.Exit(1) } // Check files in dir, if all extensions are the same, save extension to map with dir path as key for _, dir := range dirs { files, err := os.ReadDir(dir) if err != nil { log.Fatal(err) } ext := "" for _, file := range files { if !file.IsDir() { extension := filepath.Ext(file.Name()) if ext == "" { ext = extension } else if ext != extension { ext = "" break } } } if ext != "" { rules[dir] = ext } } return rules } func VerifyFiles(dir SingleDir, allGood *bool) { for _, file := range *dir.Files { if !FileExists(dir.Path + "/" + file) { PrintOut("❌ File not found ==>", dir.Path+"/"+file) *allGood = false } } } func VerifyForbiddenPatterns(dir SingleDir, allGood *bool) { for _, pattern := range *dir.ForbiddenPatterns { matched := MatchPatternInDir(dir.Path, pattern) for _, match := range matched { PrintOut("❌ Forbidden pattern ("+pattern+") matched under ==>", dir.Path+"/"+match) *allGood = false } } } func VerifyAllowedPatterns(dir SingleDir, allGood *bool) { matchedCount := 0 for _, pattern := range *dir.AllowedPatterns { matched := MatchPatternInDir(dir.Path, pattern) matchedCount += len(matched) } if matchedCount != CountFiles(dir.Path) && len(*dir.AllowedPatterns) > 0 { patternsString := strings.Join(*dir.AllowedPatterns, " ") PrintOut("❌ Not all files match required pattern ("+patternsString+") under ==>", dir.Path) *allGood = false } } ================================================ FILE: test/test.bats ================================================ setup() { # Load load 'test_helper/bats-support/load' load 'test_helper/bats-assert/load' load 'test_helper/bats-file/load' # Vars TMP_DIR="/tmp/framed-test" VALID_URL="https://raw.githubusercontent.com/mactat/framed/master/examples/python.yaml" INVALID_URL="https://raw.githubusercontent.com/mactat/framed/master/examples/invalid.yaml" DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" # Commands mkdir -p $TMP_DIR cd $TMP_DIR all_commands=(import visualize create capture verify) commands_with_template=(visualize create verify) } teardown() { sudo rm -R $TMP_DIR } ### Basic @test "Run framed" { run framed --help assert_success assert_output --partial 'FRAMED (Files and Directories Reusability, Architecture, and Management)' } ### Import @test "Import from valid example" { run framed import --example python assert_success assert_output --partial '✅ Imported successfully' assert_file_exists "$TMP_DIR/framed.yaml" # assert that content is correct run cat $TMP_DIR/framed.yaml assert_output --partial 'name: python_example' } @test "Import from invalid example" { run framed import --example invalid assert_failure assert_output --partial '☠️ Can not find correct structure' } @test "Import from valid url" { run framed import --url $VALID_URL assert_success assert_output --partial '✅ Imported successfully' assert_file_exists "$TMP_DIR/framed.yaml" # assert that content is correct run cat $TMP_DIR/framed.yaml assert_output --partial 'name: python_example' } @test "Import from invalid url" { run framed import --url $INVALID_URL assert_failure assert_output --partial '☠️ Can not find correct structure' } @test "Import with specified output" { run framed import --example python --output $TMP_DIR/test.yaml assert_success assert_output --partial '✅ Imported successfully' assert_file_exists "$TMP_DIR/test.yaml" # assert that content is correct run cat $TMP_DIR/test.yaml assert_output --partial 'name: python_example' } ### Visualize @test "Visualize show correct structure" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" # test run framed visualize assert_success assert_output --partial '✅ Read structure' assert_output --partial 'test1' assert_output --partial 'test2' assert_output --partial 'test3' assert_output --partial 'test4' } ### Create @test "Create creates correct dirs" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" # test run framed create assert_success assert_output --partial '✅ Read structure' assert_output --partial '📂 Creating directory' assert_dir_exists "$(pwd)/test2" assert_dir_exists "$(pwd)/test3" } @test "Create creates correct files in dirs" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" # test run framed create --files assert_success assert_output --partial '✅ Read structure' assert_output --partial '📂 Creating directory' assert_output --partial '📄 Creating file' assert_dir_exists "$(pwd)/test2" assert_dir_exists "$(pwd)/test3" assert_file_exists "$(pwd)/test1.md" assert_file_exists "$(pwd)/test3/test4.md" } @test "Validate validates correct structure" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success # test run framed verify assert_success assert_output --partial '✅ Read structure' assert_output --partial '✅ Verified successfully!' } ### Validate @test "Verify should spot missing file" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success rm "$(pwd)/test1.md" # test run framed verify assert_failure assert_output --partial '❌ File not found' assert_output --partial 'test1.md' } @test "Verify should spot missing dir" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success rm -R "$(pwd)/test2" # test run framed verify assert_failure assert_output --partial '❌ Directory not found' assert_output --partial 'test2' } @test "Verify should spot missing file and dir" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success rm "$(pwd)/test1.md" rm -R "$(pwd)/test2" # test run framed verify assert_failure assert_output --partial '❌ File not found' assert_output --partial 'test1.md' assert_output --partial '❌ Directory not found' assert_output --partial 'test2' } @test "Verify should spot wrong pattern if allowedPatterns is set" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].allowedPatterns[0] = "md"' "$(pwd)/framed.yaml" touch "$(pwd)/test2/test3.txt" # test run framed verify assert_failure assert_output --partial '❌ Not all files match required pattern' assert_output --partial 'test2' assert_output --partial 'md' } @test "Verify should spot wrong pattern if forbiddenPatterns is set" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].forbiddenPatterns[0] = "md"' "$(pwd)/framed.yaml" touch "$(pwd)/test2/test3.md" # test run framed verify assert_failure assert_output --partial '❌ Forbidden pattern (md) matched' assert_output --partial 'test2' } @test "Verify should spot if there is less files than minCount" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].minCount = 2' "$(pwd)/framed.yaml" # test run framed verify assert_failure assert_output --partial '❌ Min count (2) not met' assert_output --partial 'test2' } @test "Verify should spot if there is more files than maxCount" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].maxCount = 1' "$(pwd)/framed.yaml" touch "$(pwd)/test2/test3.md" touch "$(pwd)/test2/test4.md" # test run framed verify assert_failure assert_output --partial '❌ Max count (1) exceeded' assert_output --partial 'test2' } @test "Verify should spot when maxDepth is exceeded" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].maxDepth = 1' "$(pwd)/framed.yaml" mkdir "$(pwd)/test2/test3" # test run framed verify assert_failure assert_output --partial '❌ Max depth exceeded (1)' assert_output --partial 'test2' } @test "Verify should spot when there are subdirs with children not allowed" { # setup cp $DIR/test.yaml $TMP_DIR/framed.yaml assert_file_exists "$(pwd)/framed.yaml" run framed create --files assert_success yq -i '.structure.dirs[0].allowChildren = false' "$(pwd)/framed.yaml" mkdir "$(pwd)/test2/test3" # test run framed verify assert_failure assert_output --partial '❌ Children not allowed' assert_output --partial 'test2' } ### Capture @test "Capture should fail if depth flag is not integer" { # test run framed capture --depth invalid_value assert_failure assert_output --partial '🚨 Invalid depth value' assert_output --partial 'invalid_value' } @test "Capture should export correct name when name flag is set" { # test run framed capture --name test_name assert_success assert_output --partial '✅ Exported to file' # verify assert_file_exists "$(pwd)/framed.yaml" run yq eval '.name' "$(pwd)/framed.yaml" assert_success assert_output 'test_name' } @test "Capture should save file to correct path when output flag is set" { # test run framed capture --output "$(pwd)/test.yaml" assert_success assert_output --partial '✅ Exported to file' # verify assert_file_exists "$(pwd)/test.yaml" } @test "Capture should capture correct structure without depth flag" { # setup mkdir "$(pwd)/test1" touch "$(pwd)/test1/test2.md" # test run framed capture assert_success assert_output --partial '📝 Name: default' assert_output --partial '📂 Directories: 1' assert_output --partial '📄 Files: 1' assert_output --partial '🔁 Patterns: 1' assert_output --partial '✅ Exported to file' # verify assert_file_exists "$(pwd)/framed.yaml" run yq eval '.name' "$(pwd)/framed.yaml" assert_success assert_output 'default' run yq eval '.structure.dirs.[0].name' "$(pwd)/framed.yaml" assert_success assert_output 'test1' run yq eval '.structure.dirs.[0].files.[0]' "$(pwd)/framed.yaml" assert_success assert_output 'test2.md' run yq eval '.structure.dirs.[0].allowedPatterns.[0]' "$(pwd)/framed.yaml" assert_success assert_output '.md' } @test "Capture should capture correct depth when flag is provided" { # setup mkdir "$(pwd)/test1" mkdir "$(pwd)/test1/test2" mkdir "$(pwd)/test1/test2/test3" touch "$(pwd)/test1/test2/test3/test4.md" touch "$(pwd)/test6.md" # test run framed capture --depth 1 assert_success assert_output --partial '📂 Directories: 1' assert_output --partial '📄 Files: 1' run yq eval '.structure.dirs' "$(pwd)/framed.yaml" assert_success assert_output --partial 'test1' refute_output --partial 'test2' refute_output --partial 'test3' refute_output --partial 'test4.md' run yq eval '.structure.files' "$(pwd)/framed.yaml" assert_success assert_output --partial 'test6.md' } ### All @test "All commands fails when file not exist" { for command in "${commands_with_template[@]}" do run framed $command assert_failure assert_output --partial '☠️ Can not read file' done } ================================================ FILE: test/test.yaml ================================================ # FRAMED Configuration name: example structure: name: root files: - test1.md dirs: - name: test2 - name: test3 files: - test4.md