Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Verify formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-clean:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Test
run: go test -race ./...

- name: Build
run: go build ./...
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Compiled binaries
/termaid
termaid
/cmd/**/termaid

# Runtime output
/workdir/
run-*.log

# Editor / OS cruft
*.swp
.DS_Store
49 changes: 49 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
BINARY := termaid
PKG := ./cmd/termaid
PREFIX ?= $(HOME)/.local
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)

.PHONY: all build install test race vet fmt fmt-check tidy run clean help

all: build ## Build the binary (default)

build: ## Compile the termaid binary
go build -o $(BINARY) $(PKG)

install: ## Install termaid into $(PREFIX)/bin
install -d $(PREFIX)/bin
go build -o $(PREFIX)/bin/$(BINARY) $(PKG)
@echo "installed $(PREFIX)/bin/$(BINARY)"

test: ## Run the test suite
go test ./...

race: ## Run tests with the race detector
go test -race ./...

vet: ## Run go vet
go vet ./...

fmt: ## Format all Go sources
gofmt -w .

fmt-check: ## Fail if any file is not gofmt-clean
@unformatted=$$(gofmt -l .); \
if [ -n "$$unformatted" ]; then \
echo "not gofmt-clean:"; echo "$$unformatted"; exit 1; \
fi

tidy: ## Sync go.mod/go.sum
go mod tidy

run: build ## Build and launch the interactive TUI
./$(BINARY)

clean: ## Remove build artifacts and run output
rm -f $(BINARY)
rm -rf workdir
rm -f run-*.log

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
63 changes: 57 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Since this platform is a work in progress and was greatly inspired by trickest.i

### Prerequisites

- Go 1.22+
- Go 1.24+
- Python 3.x
- Node.js (for some tools)
- Common bug bounty tools (subfinder, httpx, nuclei, etc.)
Expand All @@ -48,13 +48,19 @@ The installer will:
### Manual Build

```bash
go mod tidy
go build -o termaid ./cmd/termaid
# or, with the Makefile:
make build # compile ./termaid
make install # install into ~/.local/bin
make test # run the test suite
```

The tool catalog (`assets/tools.yaml`) is embedded into the binary at build
time, so `termaid` runs correctly from any working directory.

## Usage

Launch the TUI:
Launch the interactive TUI:

```bash
./termaid
Expand All @@ -68,6 +74,42 @@ Launch the TUI:
4. **Create Workflow** - Open the visual workflow builder
5. **Exit** - Quit the application

## Command-Line (Headless) Mode

Termaid can be driven entirely from the command line, which makes it easy to
script and to run in CI. Running with no arguments launches the TUI; any
subcommand runs headlessly.

```bash
# Execute a workflow against a target, writing results under ./workdir
termaid run -d example.com -w workflow.json -o workdir -c 6

# Print the Mermaid diagram for a workflow (JSON or .mmd)
termaid preview -w workflow.json

# List the tool catalog (optionally filtered by category)
termaid tools
termaid tools -cat discovery

# Validate a workflow file's structure
termaid validate -w workflow.json

# Version / help
termaid version
termaid help
```

| Command | Flags | Description |
|------------|-----------------------------------------|----------------------------------------------|
| `run` | `-d` domain (required), `-w`, `-o`, `-c` | Execute a workflow headlessly |
| `preview` | `-w` | Print a workflow's Mermaid diagram |
| `tools` | `-cat` | List the embedded tool catalog |
| `validate` | `-w` | Check a workflow file for structural issues |

`termaid run` streams per-tool status to stdout and exits non-zero on a fatal
error, so it composes well with shell pipelines and CI steps. Press `Ctrl-C`
to cancel a run cleanly.

## Workflow Builder

The interactive workflow builder allows you to:
Expand Down Expand Up @@ -156,9 +198,18 @@ Workflows are JSON files with the following structure:

### Placeholders

- `{{domain}}` - Target domain
- `{{input}}` - Input file from previous layer
- `{{output}}` - Output file for current tool
Two equivalent placeholder styles are supported; use whichever you prefer.

| Canonical | Catalog form | Meaning |
|--------------|-------------------|----------------------------------------|
| `{{domain}}` | `$(target)` | Target domain (first line of the input) |
| `{{input}}` | `$(target_file)` | Input file produced by the previous layer |
| `{{output}}` | `$(output)` | Output file for the current tool |

If a tool's arguments contain no output placeholder, Termaid captures the
tool's **stdout** into its output file automatically — so tools that stream
results (e.g. `-o -`) work without any extra configuration. Each layer's tool
outputs are merged and de-duplicated before being handed to the next layer.

## Examples

Expand Down
11 changes: 11 additions & 0 deletions assets/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package assets embeds static resources (the tool catalog) so the compiled
// termaid binary is self-contained and can run from any working directory.
package assets

import _ "embed"

// ToolsYAML is the built-in tool catalog, embedded at build time from
// assets/tools.yaml. Callers may still override it with a user-supplied file.
//
//go:embed tools.yaml
var ToolsYAML []byte
2 changes: 1 addition & 1 deletion cmd/demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,4 @@ func createSampleWorkflow() *graph.DAG {
}

return dag
}
}
46 changes: 26 additions & 20 deletions cmd/responsive-demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ func main() {
for _, size := range testSizes {
fmt.Printf("📏 %s Screen (%dx%d) - %s\n", size.name, size.width, size.height, size.desc)
fmt.Println(strings.Repeat("─", 60))

layout := rm.CalculateLayout(size.width, size.height)

// Show layout breakdown
fmt.Printf("Screen Size Category: %v\n", getScreenSizeName(layout.ScreenSize))
fmt.Printf("Tools Panel: %dx%d (%.1f%% width, %.1f%% height)\n",
fmt.Printf("Tools Panel: %dx%d (%.1f%% width, %.1f%% height)\n",
layout.ToolsWidth, layout.ToolsHeight,
float64(layout.ToolsWidth)/float64(size.width)*100,
float64(layout.ToolsHeight)/float64(size.height)*100)
Expand All @@ -46,7 +46,7 @@ func main() {
layout.VisualWidth, layout.VisualHeight,
float64(layout.VisualWidth)/float64(size.width)*100,
float64(layout.VisualHeight)/float64(size.height)*100)

// Show configuration details
config := layout.Config
fmt.Printf("\nConfiguration:\n")
Expand All @@ -58,11 +58,11 @@ func main() {
fmt.Printf(" Max Mermaid Lines: %d\n", config.MaxMermaidLines)
fmt.Printf(" Vertical Scrolling: %t\n", config.UseVerticalScroll)
fmt.Printf(" Horizontal Scrolling: %t\n", config.UseHorizontalScroll)

// Visual layout representation
fmt.Printf("\nLayout Visualization:\n")
renderLayoutPreview(layout, size.width, size.height)

fmt.Println()
fmt.Println()
}
Expand Down Expand Up @@ -102,39 +102,45 @@ func main() {

func getScreenSizeName(screenSize tui.ScreenSize) string {
switch screenSize {
case 0: return "Tiny"
case 1: return "Small"
case 2: return "Medium"
case 3: return "Large"
case 4: return "XLarge"
default: return "Unknown"
case 0:
return "Tiny"
case 1:
return "Small"
case 2:
return "Medium"
case 3:
return "Large"
case 4:
return "XLarge"
default:
return "Unknown"
}
}

func renderLayoutPreview(layout tui.LayoutDimensions, totalW, totalH int) {
// Create a simple ASCII representation of the layout
fmt.Printf("┌%s┬%s┐\n",
strings.Repeat("─", layout.ToolsWidth/4),
fmt.Printf("┌%s┬%s┐\n",
strings.Repeat("─", layout.ToolsWidth/4),
strings.Repeat("─", layout.InputWidth/4))

fmt.Printf("│%s│%s│ ← Input (%dx%d)\n",
centerText("Tools", layout.ToolsWidth/4),
centerText("Input/Args", layout.InputWidth/4),
layout.InputWidth, layout.InputHeight)

fmt.Printf("│%s├%s┤\n",
centerText(fmt.Sprintf("%dx%d", layout.ToolsWidth, layout.ToolsHeight), layout.ToolsWidth/4),
strings.Repeat("─", layout.VisualWidth/4))

fmt.Printf("├%s┤%s│\n",
strings.Repeat("─", layout.HelpWidth/4),
centerText("Matrix Visual", layout.VisualWidth/4))

fmt.Printf("│%s│%s│ ← Visual (%dx%d)\n",
centerText("Help", layout.HelpWidth/4),
centerText(fmt.Sprintf("%dx%d", layout.VisualWidth, layout.VisualHeight), layout.VisualWidth/4),
layout.VisualWidth, layout.VisualHeight)

fmt.Printf("└%s┴%s┘\n",
strings.Repeat("─", layout.HelpWidth/4),
strings.Repeat("─", layout.VisualWidth/4))
Expand All @@ -151,4 +157,4 @@ func centerText(text string, width int) string {
leftPad := padding / 2
rightPad := padding - leftPad
return strings.Repeat(" ", leftPad) + text + strings.Repeat(" ", rightPad)
}
}
Loading
Loading