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
================================================
<img src="./docs/static/logo-wide.png" style="object-fit: cover; width: 100%;">
[](https://github.com/mactat/framed/actions/workflows/release.yaml) [](https://github.com/mactat/framed/actions/workflows/pr.yaml)      
# 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

## 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-<version>.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-<version>.tar.gz
```
3. This will extract the `framed` binary.
4. Open a terminal and navigate to the extracted directory:
```shell
cd framed-darwin-amd64-<version>
```
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-<version>.tar.gz` package from release page.
2. Extract the package using the following command in your terminal:
```shell
tar -xzf framed-linux-amd64-<version>.tar.gz
```
3. This will extract the `framed` binary.
4. Open a terminal and navigate to the extracted directory:
```shell
cd framed-linux-amd64-<version>
```
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-<version>.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 <template-file>
```
### 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>
```
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 <example-name>
```
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 <command>
```
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
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
SYMBOL INDEX (46 symbols across 12 files)
FILE: cmd/capture.go
function init (line 56) | func init() {
FILE: cmd/create.go
function init (line 40) | func init() {
FILE: cmd/import.go
function init (line 55) | func init() {
FILE: cmd/root.go
function Execute (line 27) | func Execute() {
function init (line 34) | func init() {}
FILE: cmd/verify.go
function init (line 89) | func init() {
FILE: cmd/visualize.go
function init (line 33) | func init() {
FILE: framed.go
function main (line 8) | func main() {
FILE: pkg/ext/decoder.go
type SingleDir (line 12) | type SingleDir struct
method UnmarshalYAML (line 27) | func (s *SingleDir) UnmarshalYAML(unmarshal func(interface{}) error) e...
type config (line 42) | type config struct
function ReadConfig (line 47) | func ReadConfig(path string) (config, []SingleDir) {
function traverseStructure (line 77) | func traverseStructure(dir *SingleDir, path string, dirsList *[]SingleDi...
FILE: pkg/ext/encoder.go
type SingleDirOut (line 12) | type SingleDirOut struct
type configOut (line 25) | type configOut struct
function ExportConfig (line 30) | func ExportConfig(name string, path string, subdirs []string, files []st...
function insertSubdirs (line 62) | func insertSubdirs(dirs *[]SingleDirOut, subdirs []string) {
function insertSingleDir (line 68) | func insertSingleDir(dirs *[]SingleDirOut, dir string) {
function containsDir (line 85) | func containsDir(dirs []SingleDirOut, name string) bool {
function getDir (line 94) | func getDir(dirs []SingleDirOut, name string) *SingleDirOut {
function insertFiles (line 103) | func insertFiles(root *SingleDirOut, files []string) {
function insertSingleFile (line 109) | func insertSingleFile(root *SingleDirOut, file string) {
function insertPatterns (line 126) | func insertPatterns(root *SingleDirOut, patterns map[string]string) {
function insertSinglePattern (line 132) | func insertSinglePattern(root *SingleDirOut, dir string, pattern string) {
FILE: pkg/ext/network.go
function ExampleToUrl (line 9) | func ExampleToUrl(example string) string {
function ImportFromUrl (line 13) | func ImportFromUrl(path string, url string) error {
FILE: pkg/ext/printer.go
function PrintOut (line 11) | func PrintOut(prompt string, text string) {
function VisualizeTemplate (line 16) | func VisualizeTemplate(template []SingleDir) {
function printDirectory (line 55) | func printDirectory(initString string, dirDepth int, connectorDir string...
function printFile (line 60) | func printFile(initString string, depth int, connector string, file stri...
FILE: pkg/ext/system.go
function CreateDir (line 13) | func CreateDir(path string) {
function CreateAllDirs (line 25) | func CreateAllDirs(dirList []SingleDir) {
function CreateFile (line 31) | func CreateFile(path string, name string) {
function CreateAllFiles (line 44) | func CreateAllFiles(dirList []SingleDir) {
function DirExists (line 56) | func DirExists(path string) bool {
function FileExists (line 68) | func FileExists(path string) bool {
function CountFiles (line 77) | func CountFiles(path string) int {
function HasDirs (line 91) | func HasDirs(path string) bool {
function CheckDepth (line 105) | func CheckDepth(path string) int {
function MatchPatternInDir (line 125) | func MatchPatternInDir(path string, pattern string) []string {
function CaptureSubDirs (line 150) | func CaptureSubDirs(path string, depth int) []string {
function CaptureAllFiles (line 170) | func CaptureAllFiles(path string, depth int) []string {
function CaptureRequiredPatterns (line 191) | func CaptureRequiredPatterns(path string, depth int) map[string]string {
function VerifyFiles (line 233) | func VerifyFiles(dir SingleDir, allGood *bool) {
function VerifyForbiddenPatterns (line 241) | func VerifyForbiddenPatterns(dir SingleDir, allGood *bool) {
function VerifyAllowedPatterns (line 251) | func VerifyAllowedPatterns(dir SingleDir, allGood *bool) {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (62K chars).
[
{
"path": ".dockerignore",
"chars": 63,
"preview": "README.md\npipelines/\ndockerfiles/\ndocs/\nbuild/\nLICENSE\nMakefile"
},
{
"path": ".github/FUNDING.yml",
"chars": 17,
"preview": "github: [mactat]\n"
},
{
"path": ".github/workflows/pr.yaml",
"chars": 653,
"preview": "name: pr\non:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n workflow_dispatch: {}\njobs:\n"
},
{
"path": ".github/workflows/release.yaml",
"chars": 795,
"preview": "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"
},
{
"path": ".gitignore",
"chars": 15,
"preview": "build/\n.vscode/"
},
{
"path": ".goreleaser.yaml",
"chars": 638,
"preview": "builds:\n - binary: framed\n goos:\n - darwin\n - linux\n - windows\n goarch:\n - amd64\n - arm6"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2023 Maciej Tatarski\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Makefile",
"chars": 2217,
"preview": "# Definitions\nROOT := $(PWD)\nGO_VERSION := 1.20.4\nALPINE_VERSION := 3.18\nOS\t\t\t\t"
},
{
"path": "README.md",
"chars": 9900,
"preview": "<img src=\"./docs/static/logo-wide.png\" style=\"object-fit: cover; width: 100%;\">\n\n[ {\n\tcmd.Ex"
},
{
"path": "framed.yaml",
"chars": 1218,
"preview": "# FRAMED Configuration\nname: framed\n\nstructure:\n name: root\n maxDepth: 5 # Disallow dirs deeper than 5\n files:\n"
},
{
"path": "go.mod",
"chars": 415,
"preview": "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"
},
{
"path": "go.sum",
"chars": 1979,
"preview": "github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng=\ngithub.com/TwiN/go-color v1.4.0/go.mod h"
},
{
"path": "pkg/ext/decoder.go",
"chars": 2350,
"preview": "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 Singl"
},
{
"path": "pkg/ext/encoder.go",
"chars": 3561,
"preview": "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 "
},
{
"path": "pkg/ext/network.go",
"chars": 523,
"preview": "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.githubu"
},
{
"path": "pkg/ext/printer.go",
"chars": 1449,
"preview": "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) "
},
{
"path": "pkg/ext/system.go",
"chars": 6228,
"preview": "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)"
},
{
"path": "test/test.bats",
"chars": 10776,
"preview": "setup() {\n # Load\n load 'test_helper/bats-support/load'\n load 'test_helper/bats-assert/load'\n load 'test_hel"
},
{
"path": "test/test.yaml",
"chars": 162,
"preview": "# FRAMED Configuration\nname: example\n\nstructure:\n name: root\n files:\n - test1.md\n dirs:\n - name: test2\n - na"
}
]
About this extraction
This page contains the full source code of the mactat/framed GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (54.8 KB), approximately 16.2k tokens, and a symbol index with 46 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.