diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53b4f62..4df2ba9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,8 @@ name: CI on: push: - branches: [main] + branches-ignore: + - "dependabot/**" # Skip dependabot branches (they create PRs) pull_request: concurrency: @@ -21,18 +22,19 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" cache: true - name: Lint uses: golangci/golangci-lint-action@v7 with: - version: v2.1.6 + version: v2.8.0 - name: Test run: go test -race -coverprofile=coverage.txt -timeout=2m ./... - name: Upload coverage to Codecov + if: github.ref == 'refs/heads/main' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e786e3..2894db6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - 'v*' + - "v*" workflow_dispatch: # Manual trigger for re-running failed releases (requires existing tag) @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" cache: true - name: Run GoReleaser diff --git a/.gitignore b/.gitignore index e887019..863ce65 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Test binary, built with `go test -c` *.test +# GoReleaser output +dist/ + # Output of the go coverage tool *.out coverage.out diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 79dc6b1..fc5e2f6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -11,8 +11,9 @@ before: - go test ./... builds: + # CLI binary (stacktower) - for end users - id: stacktower - main: ./main.go + main: ./cmd/stacktower binary: stacktower env: - CGO_ENABLED=0 @@ -25,12 +26,15 @@ builds: - arm64 ldflags: - -s -w - - -X main.version={{.Version}} - - -X main.commit={{.Commit}} - - -X main.date={{.Date}} + - -X github.com/matzehuels/stacktower/pkg/buildinfo.Version={{.Version}} + - -X github.com/matzehuels/stacktower/pkg/buildinfo.Commit={{.Commit}} + - -X github.com/matzehuels/stacktower/pkg/buildinfo.Date={{.Date}} archives: + # CLI archive (for Homebrew, manual download) - id: stacktower + builds: + - stacktower formats: - tar.gz name_template: >- @@ -50,7 +54,7 @@ archives: - README.md checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: version_template: "{{ incpatch .Version }}-next" @@ -60,11 +64,11 @@ changelog: use: github filters: exclude: - - '^docs:' - - '^test:' - - '^chore:' - - '^ci:' - - 'README' + - "^docs:" + - "^test:" + - "^chore:" + - "^ci:" + - "README" - Merge pull request - Merge branch groups: diff --git a/Makefile b/Makefile index 1df93cc..ace9c06 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,53 @@ -.PHONY: all build clean fmt lint test cover e2e e2e-test e2e-real e2e-parse blog blog-diagrams blog-showcase install-tools snapshot release help +.PHONY: all build clean fmt fmt-check lint test cover vuln e2e blog install-tools snapshot release help + +# ============================================================================= +# Variables +# ============================================================================= BINARY := stacktower +VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +LDFLAGS := -X github.com/matzehuels/stacktower/pkg/buildinfo.Version=$(VERSION) \ + -X github.com/matzehuels/stacktower/pkg/buildinfo.Commit=$(COMMIT) \ + -X github.com/matzehuels/stacktower/pkg/buildinfo.Date=$(DATE) + +# ============================================================================= +# Default Target +# ============================================================================= all: check build +# ============================================================================= +# Build Targets +# ============================================================================= + +build: + @echo "Building CLI (bin/$(BINARY))..." + @go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/stacktower + +install: + @echo "Installing CLI..." + @go install -ldflags "$(LDFLAGS)" ./cmd/stacktower + +clean: + @rm -rf bin/ dist/ coverage.out output/ tmp/ + +# ============================================================================= +# Quality Checks +# ============================================================================= + check: fmt lint test vuln fmt: @gofmt -s -w . - @goimports -w -local stacktower . + @goimports -w -local github.com/matzehuels/stacktower . + +fmt-check: + @echo "Checking formatting..." + @test -z "$$(gofmt -l .)" || (echo "Files not formatted:"; gofmt -l .; exit 1) + @test -z "$$(goimports -l -local github.com/matzehuels/stacktower .)" || (echo "Imports not formatted:"; goimports -l -local github.com/matzehuels/stacktower .; exit 1) + @echo "Formatting OK" lint: @golangci-lint run @@ -20,23 +59,33 @@ cover: @go test -race -coverprofile=coverage.out ./... @go tool cover -func=coverage.out -build: - @go build -o bin/$(BINARY) . +vuln: + @govulncheck ./... -install: - @go install . +install-tools: + @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + @go install golang.org/x/tools/cmd/goimports@latest + @go install golang.org/x/vuln/cmd/govulncheck@latest + +# ============================================================================= +# End-to-End Tests +# ============================================================================= e2e: build - @./scripts/test_e2e.sh all + @./scripts/test_cli_e2e.sh all e2e-test: build - @./scripts/test_e2e.sh test + @./scripts/test_cli_e2e.sh test e2e-real: build - @./scripts/test_e2e.sh real + @./scripts/test_cli_e2e.sh real e2e-parse: build - @./scripts/test_e2e.sh parse + @./scripts/test_cli_e2e.sh parse + +# ============================================================================= +# Blog Assets +# ============================================================================= blog: blog-diagrams blog-showcase @@ -46,13 +95,9 @@ blog-diagrams: build blog-showcase: build @./scripts/blog_showcase.sh -install-tools: - @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - @go install golang.org/x/tools/cmd/goimports@latest - @go install golang.org/x/vuln/cmd/govulncheck@latest - -vuln: - @govulncheck ./... +# ============================================================================= +# Release +# ============================================================================= snapshot: @goreleaser release --snapshot --clean --skip=publish @@ -60,23 +105,42 @@ snapshot: release: @goreleaser release --clean -clean: - @rm -rf bin/ dist/ coverage.out +# ============================================================================= +# Help +# ============================================================================= help: - @echo "make - Run checks and build" - @echo "make check - Format, lint, test, vulncheck (same as CI)" - @echo "make fmt - Format code" - @echo "make lint - Run golangci-lint" - @echo "make test - Run tests" - @echo "make cover - Run tests with coverage" - @echo "make build - Build binary" - @echo "make e2e - Run all end-to-end tests" - @echo "make e2e-test - Render examples/test/*.json" - @echo "make e2e-real - Render examples/real/*.json" - @echo "make e2e-parse - Parse packages to examples/real/" - @echo "make blog - Generate all blogpost diagrams" - @echo "make blog-diagrams - Generate blogpost example diagrams" - @echo "make blog-showcase - Generate blogpost showcase diagrams" - @echo "make vuln - Check for vulnerabilities" - @echo "make clean - Remove build artifacts" + @echo "Stacktower Makefile" + @echo "" + @echo "BUILDING:" + @echo " make build - Build CLI binary (bin/stacktower)" + @echo " make install - Install CLI to GOPATH" + @echo "" + @echo "QUALITY:" + @echo " make check - Run all checks (fmt, lint, test, vuln)" + @echo " make fmt - Format code" + @echo " make fmt-check - Check formatting (CI-style, no writes)" + @echo " make lint - Run golangci-lint" + @echo " make test - Run tests" + @echo " make cover - Run tests with coverage" + @echo " make vuln - Check for vulnerabilities" + @echo "" + @echo "TESTING:" + @echo " make e2e - Run all CLI end-to-end tests" + @echo " make e2e-test - Run test examples" + @echo " make e2e-real - Run real package examples" + @echo " make e2e-parse - Run parse tests" + @echo "" + @echo "BLOG:" + @echo " make blog - Generate all blog assets" + @echo " make blog-diagrams - Generate blog diagrams" + @echo " make blog-showcase - Generate blog showcase" + @echo "" + @echo "RELEASE:" + @echo " make snapshot - Build release locally (no publish)" + @echo " make release - Build and publish release" + @echo "" + @echo "OTHER:" + @echo " make clean - Remove build artifacts" + @echo " make install-tools - Install development tools" + @echo " make help - Show this help" diff --git a/README.md b/README.md index e161774..b31a74e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Release](https://img.shields.io/github/v/release/matzehuels/stacktower)](https://github.com/matzehuels/stacktower/releases) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -Inspired by [XKCD #2347](https://xkcd.com/2347/), Stacktower renders dependency graphs as **physical towers** where blocks rest on what they depend on. Your application sits at the top, supported by libraries below—all the way down to that one critical package maintained by *some dude in Nebraska*. +Inspired by [XKCD #2347](https://xkcd.com/2347/), Stacktower renders dependency graphs as **physical towers** where blocks rest on what they depend on. Your application sits at the top, supported by libraries below—all the way down to that one critical package maintained by _some dude in Nebraska_.

FastAPI dependency tower @@ -36,7 +36,7 @@ go install github.com/matzehuels/stacktower@latest ```bash git clone https://github.com/matzehuels/stacktower.git cd stacktower -go build -o stacktower . +go build -o bin/stacktower ./cmd/stacktower ``` ## Quick Start @@ -88,6 +88,7 @@ stacktower parse go examples/manifest/go.mod -o deps.json When the argument exists on disk or matches a known manifest filename, Stacktower automatically parses it as a manifest. The project name (root node) is auto-detected from the manifest or a sibling file: + - **Cargo.toml**: `[package].name` - **go.mod**: `module` directive - **package.json**: `name` field @@ -103,27 +104,6 @@ stacktower parse python requirements.txt --name="my-project" -o deps.json stacktower parse ruby Gemfile -n my-rails-app -o deps.json ``` -#### Explicit Mode - -Force registry or manifest parsing when auto-detection isn't enough: - -```bash -# Force registry lookup -stacktower parse python registry pypi fastapi -stacktower parse java registry maven org.springframework:spring-core -stacktower parse go registry goproxy github.com/spf13/cobra - -# Force manifest type -stacktower parse python manifest poetry examples/manifest/poetry.lock -stacktower parse python manifest requirements examples/manifest/requirements.txt -stacktower parse rust manifest cargo examples/manifest/Cargo.toml -stacktower parse javascript manifest package examples/manifest/package.json -stacktower parse ruby manifest gemfile examples/manifest/Gemfile -stacktower parse php manifest composer examples/manifest/composer.json -stacktower parse java manifest pom examples/manifest/pom.xml -stacktower parse go manifest gomod examples/manifest/go.mod -``` - #### Metadata Enrichment By default, Stacktower enriches packages with GitHub metadata (stars, maintainers, last commit) for richer visualizations. Set `GITHUB_TOKEN` to enable this: @@ -132,8 +112,8 @@ By default, Stacktower enriches packages with GitHub metadata (stars, maintainer export GITHUB_TOKEN=your_token stacktower parse python fastapi -o fastapi.json -# Disable enrichment if you don't have a token -stacktower parse python fastapi --enrich=false -o fastapi.json +# Skip enrichment if you don't have a token +stacktower parse python fastapi --skip-enrich -o fastapi.json ``` ### Rendering @@ -144,6 +124,14 @@ The `render` command generates visualizations from parsed JSON graphs: stacktower render [flags] ``` +This is a shortcut that combines `layout` and `visualize` in one step. For more control, you can run them separately: + +```bash +# Two-step workflow with intermediate layout +stacktower layout examples/real/flask.json -o flask.layout.json +stacktower visualize flask.layout.json -o flask.svg +``` + #### Visualization Types ```bash @@ -155,9 +143,6 @@ stacktower render examples/real/serde.json --style simple --randomize=false --po # Traditional node-link diagram (uses Graphviz DOT) stacktower render examples/real/yargs.json -t nodelink -o yargs.svg - -# Multiple types at once (outputs flask_tower.svg, flask_nodelink.svg) -stacktower render examples/real/flask.json -t tower,nodelink -o flask ``` #### Output Formats @@ -177,18 +162,16 @@ stacktower render examples/real/flask.json -f png -o flask.png # Multiple formats at once (outputs flask.svg, flask.json, flask.pdf) stacktower render examples/real/flask.json -f svg,json,pdf -o flask - -# Combine multiple types and formats -stacktower render examples/real/flask.json -t tower,nodelink -f svg,json ``` Output path behavior: + - **No `-o`**: Derives from input (`input.json` → `input.`) - **Single format**: Uses exact path (`-o out.svg` → `out.svg`) -- **Multiple formats**: Strips extension, adds format (`-o out.svg -f svg,json` → `out.svg`, `out.json`) -- **Multiple types**: Adds type suffix (`-t tower,nodelink` → `out_tower.svg`, `out_nodelink.svg`) +- **Multiple formats**: Strips extension, adds format (`-o out -f svg,json` → `out.svg`, `out.json`) > **Note:** PDF and PNG output requires [librsvg](https://wiki.gnome.org/Projects/LibRsvg): +> > - macOS: `brew install librsvg` > - Linux: `apt install librsvg2-bin` @@ -213,51 +196,45 @@ stacktower render examples/test/diamond.json -o diamond.svg ### Global Options -| Flag | Description | -|------|-------------| +| Flag | Description | +| ----------------- | -------------------------------------------------------- | | `-v`, `--verbose` | Enable debug logging (search space info, timing details) | ### Parse Options -| Flag | Description | -|------|-------------| -| `-o`, `--output` | Output file (stdout if empty) | -| `-n`, `--name` | Project name for manifest parsing (auto-detected from manifest if not set) | -| `--max-depth N` | Maximum dependency depth (default: 10) | -| `--max-nodes N` | Maximum packages to fetch (default: 5000) | -| `--enrich` | Enrich with GitHub metadata (default: true, requires `GITHUB_TOKEN`) | -| `--refresh` | Bypass cache | +| Flag | Description | +| ---------------- | -------------------------------------------------------------------------- | +| `-o`, `--output` | Output file (stdout if empty) | +| `-n`, `--name` | Project name for manifest parsing (auto-detected from manifest if not set) | +| `--max-depth N` | Maximum dependency depth (default: 10) | +| `--max-nodes N` | Maximum packages to fetch (default: 5000) | +| `--skip-enrich` | Skip metadata enrichment (GitHub descriptions, etc.) | +| `--no-cache` | Disable caching | ### Render Options -| Flag | Description | -|------|-------------| -| `-o`, `--output` | Output file or base path for multiple types/formats | -| `-t`, `--type` | Visualization type(s): `tower` (default), `nodelink` (comma-separated) | -| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) | -| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) | +| Flag | Description | +| ---------------- | --------------------------------------------------------------------------------------------------------------------- | +| `-o`, `--output` | Output file or base path for multiple formats | +| `-t`, `--type` | Visualization type: `tower` (default), `nodelink` | +| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) | +| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) | #### Tower Options -| Flag | Description | -|------|-------------| -| `--width N` | Frame width in pixels (default: 800) | -| `--height N` | Frame height in pixels (default: 600) | -| `--style handdrawn\|simple` | Visual style (default: handdrawn) | -| `--randomize` | Vary block widths to visualize load-bearing structure (default: true) | -| `--merge` | Merge subdivider blocks into continuous towers (default: true) | -| `--popups` | Enable hover popups with package metadata (default: true) | -| `--nebraska` | Show "Nebraska guy" maintainer ranking panel | -| `--edges` | Show dependency edges as dashed lines | -| `--ordering optimal\|barycentric` | Crossing minimization algorithm (default: optimal) | -| `--ordering-timeout N` | Timeout for optimal search in seconds (default: 60) | -| `--top-down` | Width flows from roots down; by default width flows from sinks up | - -#### Node-Link Options - -| Flag | Description | -|------|-------------| -| `--detailed` | Show all node metadata in labels | +| Flag | Description | +| --------------------------------- | --------------------------------------------------------------------- | +| `--width N` | Frame width in pixels (default: 800) | +| `--height N` | Frame height in pixels (default: 600) | +| `--style handdrawn\|simple` | Visual style (default: handdrawn) | +| `--randomize` | Vary block widths to visualize load-bearing structure (default: true) | +| `--merge` | Merge subdivider blocks into continuous towers (default: true) | +| `--popups` | Enable hover popups with package metadata (default: true) | +| `--nebraska` | Show "Nebraska guy" maintainer ranking panel | +| `--edges` | Show dependency edges as dashed lines | +| `--ordering optimal\|barycentric` | Crossing minimization algorithm (default: optimal) | +| `--ordering-timeout N` | Timeout for optimal search in seconds (default: 60) | +| `--no-cache` | Disable caching | ## JSON Format @@ -267,11 +244,7 @@ The render layer accepts a simple JSON format, making it easy to visualize **any ```json { - "nodes": [ - { "id": "app" }, - { "id": "lib-a" }, - { "id": "lib-b" } - ], + "nodes": [{ "id": "app" }, { "id": "lib-a" }, { "id": "lib-b" }], "edges": [ { "from": "app", "to": "lib-a" }, { "from": "lib-a", "to": "lib-b" } @@ -281,34 +254,34 @@ The render layer accepts a simple JSON format, making it easy to visualize **any ### Required Fields -| Field | Type | Description | -|-------|------|-------------| -| `nodes[].id` | string | Unique node identifier (displayed as label) | -| `edges[].from` | string | Source node ID | -| `edges[].to` | string | Target node ID | +| Field | Type | Description | +| -------------- | ------ | ------------------------------------------- | +| `nodes[].id` | string | Unique node identifier (displayed as label) | +| `edges[].from` | string | Source node ID | +| `edges[].to` | string | Target node ID | ### Optional Fields -| Field | Type | Description | -|-------|------|-------------| -| `nodes[].row` | int | Pre-assigned layer (computed automatically if omitted) | -| `nodes[].kind` | string | Internal use: `"subdivider"` or `"auxiliary"` | -| `nodes[].meta` | object | Freeform metadata for display features | +| Field | Type | Description | +| -------------- | ------ | ------------------------------------------------------ | +| `nodes[].row` | int | Pre-assigned layer (computed automatically if omitted) | +| `nodes[].kind` | string | Internal use: `"subdivider"` or `"auxiliary"` | +| `nodes[].meta` | object | Freeform metadata for display features | ### Recognized `meta` Keys These keys are read by specific render flags. All are optional—missing keys simply disable the corresponding feature. -| Key | Type | Used By | -|-----|------|---------| -| `repo_url` | string | Clickable blocks, `--popups`, `--nebraska` | -| `repo_stars` | int | `--popups` | -| `repo_owner` | string | `--nebraska` | -| `repo_maintainers` | []string | `--nebraska` | -| `repo_last_commit` | string (date) | `--popups`, brittle detection | -| `repo_last_release` | string (date) | `--popups` | -| `repo_archived` | bool | `--popups`, brittle detection | -| `summary` | string | `--popups` (fallback: `description`) | +| Key | Type | Used By | +| ------------------- | ------------- | ------------------------------------------ | +| `repo_url` | string | Clickable blocks, `--popups`, `--nebraska` | +| `repo_stars` | int | `--popups` | +| `repo_owner` | string | `--nebraska` | +| `repo_maintainers` | []string | `--nebraska` | +| `repo_last_commit` | string (date) | `--popups`, brittle detection | +| `repo_last_release` | string (date) | `--popups` | +| `repo_archived` | bool | `--popups`, brittle detection | +| `summary` | string | `--popups` (fallback: `description`) | The `--detailed` flag (node-link only) displays **all** meta keys in the node label. @@ -323,16 +296,34 @@ The `--detailed` flag (node-link only) displays **all** meta keys in the node la The ordering step is where the magic happens. Stacktower uses an optimal search algorithm that guarantees minimum crossings for small-to-medium graphs. For larger graphs, it gracefully falls back after a configurable timeout. +## GitHub Authentication + +For parsing repositories directly from GitHub, you can authenticate using the device flow: + +```bash +# Login with GitHub (opens browser for device authorization) +stacktower github login + +# Check current session +stacktower github whoami + +# Logout +stacktower github logout + +# Parse a manifest from a GitHub repository +stacktower parse github owner/repo -o deps.json +``` + ## Environment Variables -| Variable | Description | -|----------|-------------| -| `GITHUB_TOKEN` | GitHub API token for `--enrich` metadata | -| `GITLAB_TOKEN` | GitLab API token for `--enrich` metadata | +| Variable | Description | +| -------------- | ------------------------------------------------ | +| `GITHUB_TOKEN` | GitHub API token for metadata enrichment | +| `GITLAB_TOKEN` | GitLab API token for metadata enrichment | ## Caching -HTTP responses are cached in `~/.cache/stacktower/` with a 24-hour TTL. Use `--refresh` to bypass the cache for a single request. +HTTP responses are cached in `~/.cache/stacktower/` with a 24-hour TTL. Use `--no-cache` to disable caching for a single request. ```bash # Clear the entire cache @@ -348,10 +339,10 @@ Stacktower can be used as a Go library for programmatic graph visualization. ```go import ( - "github.com/matzehuels/stacktower/pkg/dag" - "github.com/matzehuels/stacktower/pkg/dag/transform" - "github.com/matzehuels/stacktower/pkg/render/tower/layout" - "github.com/matzehuels/stacktower/pkg/render/tower/sink" + "github.com/matzehuels/stacktower/pkg/core/dag" + "github.com/matzehuels/stacktower/pkg/core/dag/transform" + "github.com/matzehuels/stacktower/pkg/core/render/tower/layout" + "github.com/matzehuels/stacktower/pkg/core/render/tower/sink" ) // Build a graph @@ -369,10 +360,12 @@ svg := sink.RenderSVG(l, sink.WithGraph(g), sink.WithPopups()) 📚 **[Full API documentation on pkg.go.dev](https://pkg.go.dev/github.com/matzehuels/stacktower)** Key packages: -- [`pkg/dag`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/dag) — DAG data structure and crossing algorithms -- [`pkg/dag/transform`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/dag/transform) — Graph normalization pipeline -- [`pkg/render/tower`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/render/tower) — Layout, ordering, and rendering -- [`pkg/deps`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/deps) — Dependency resolution from registries + +- [`pkg/core/dag`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag) — DAG data structure and crossing algorithms +- [`pkg/core/dag/transform`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag/transform) — Graph normalization pipeline +- [`pkg/core/render/tower`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/render/tower) — Layout, ordering, and rendering +- [`pkg/core/deps`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/deps) — Dependency resolution from registries +- [`pkg/pipeline`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/pipeline) — Complete parse → layout → render pipeline ## Contributing @@ -386,16 +379,16 @@ make check # Run all CI checks locally (fmt, lint, test, vuln) make build # Build binary to bin/stacktower ``` -| Command | Description | -|---------|-------------| -| `make check` | Format, lint, test, vulncheck (same as CI) | -| `make fmt` | Format code with gofmt and goimports | -| `make lint` | Run golangci-lint | -| `make test` | Run tests with race detector | -| `make cover` | Run tests with coverage report | -| `make vuln` | Check for known vulnerabilities | -| `make e2e` | Run end-to-end tests | -| `make snapshot` | Build release locally (no publish) | +| Command | Description | +| --------------- | ------------------------------------------ | +| `make check` | Format, lint, test, vulncheck (same as CI) | +| `make fmt` | Format code with gofmt and goimports | +| `make lint` | Run golangci-lint | +| `make test` | Run tests with race detector | +| `make cover` | Run tests with coverage report | +| `make vuln` | Check for known vulnerabilities | +| `make e2e` | Run end-to-end tests | +| `make snapshot` | Build release locally (no publish) | Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/). diff --git a/blogpost/android-chrome-192x192.png b/blogpost/android-chrome-192x192.png index 2706f78..b842284 100644 Binary files a/blogpost/android-chrome-192x192.png and b/blogpost/android-chrome-192x192.png differ diff --git a/blogpost/android-chrome-512x512.png b/blogpost/android-chrome-512x512.png index 2f2243e..025ccdb 100644 Binary files a/blogpost/android-chrome-512x512.png and b/blogpost/android-chrome-512x512.png differ diff --git a/blogpost/apple-touch-icon.png b/blogpost/apple-touch-icon.png index 3aaa75c..571ab67 100644 Binary files a/blogpost/apple-touch-icon.png and b/blogpost/apple-touch-icon.png differ diff --git a/blogpost/favicon-16x16.png b/blogpost/favicon-16x16.png index 5844882..6c43ad9 100644 Binary files a/blogpost/favicon-16x16.png and b/blogpost/favicon-16x16.png differ diff --git a/blogpost/favicon-32x32.png b/blogpost/favicon-32x32.png index bf7dc7d..74bf318 100644 Binary files a/blogpost/favicon-32x32.png and b/blogpost/favicon-32x32.png differ diff --git a/blogpost/favicon.ico b/blogpost/favicon.ico index 9f9dd82..e6cbece 100644 Binary files a/blogpost/favicon.ico and b/blogpost/favicon.ico differ diff --git a/blogpost/plots/showcase/javascript/ioredis.svg b/blogpost/plots/showcase/javascript/ioredis.svg index 561daab..0e706e2 100644 --- a/blogpost/plots/showcase/javascript/ioredis.svg +++ b/blogpost/plots/showcase/javascript/ioredis.svg @@ -1,13 +1,19 @@ - + + @@ -26,48 +32,49 @@ - @ioredis/commands + @ioredis/commands - cluster-key-slot + cluster-key-slot - debug + debug - denque + denque - ioredis + ioredis - lodash.defaults + lodash.defaults - lodash.isarguments + lodash.isarguments - ms + ms - redis-errors + redis-errors - redis-parser + redis-parser - standard-as-callback + standard-as-callback + diff --git a/blogpost/plots/showcase/javascript/ioredis_mobile.svg b/blogpost/plots/showcase/javascript/ioredis_mobile.svg index f643b90..4cb99de 100644 --- a/blogpost/plots/showcase/javascript/ioredis_mobile.svg +++ b/blogpost/plots/showcase/javascript/ioredis_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -26,48 +32,49 @@ - @ioredis/commands + @ioredis/commands - cluster-key-slot + cluster-key-slot - debug + debug - denque + denque - ioredis + ioredis - lodash.defaults + lodash.defaults - lodash.isarguments + lodash.isarguments - ms + ms - redis-errors + redis-errors - redis-parser + redis-parser - standard-as-callback + standard-as-callback + diff --git a/blogpost/plots/showcase/javascript/knex.svg b/blogpost/plots/showcase/javascript/knex.svg index aeb772c..21e330c 100644 --- a/blogpost/plots/showcase/javascript/knex.svg +++ b/blogpost/plots/showcase/javascript/knex.svg @@ -1,13 +1,19 @@ - + + @@ -45,92 +51,93 @@ - colorette + colorette - commander + commander - debug + debug - escalade + escalade - esm + esm - function-bind + function-bind - get-package-type + get-package-type - getopts + getopts - hasown + hasown - interpret + interpret - is-core-module + is-core-module - knex + knex - lodash + lodash - ms + ms - path-parse + path-parse - pg-connection-string + pg-connection-string - rechoir + rechoir - resolve + resolve - resolve-from + resolve-from - supports-preserve-symlinks-flag + supports-preserve-symlinks-flag - tarn + tarn - tildify + tildify + diff --git a/blogpost/plots/showcase/javascript/knex_mobile.svg b/blogpost/plots/showcase/javascript/knex_mobile.svg index b4639f4..602a20f 100644 --- a/blogpost/plots/showcase/javascript/knex_mobile.svg +++ b/blogpost/plots/showcase/javascript/knex_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -45,92 +51,93 @@ - colorette + colorette - commander + commander - debug + debug - escalade + escalade - esm + esm - function-bind + function-bind - get-package-type + get-package-type - getopts + getopts - hasown + hasown - interpret + interpret - is-core-module + is-core-module - knex + knex - lodash + lodash - ms + ms - path-parse + path-parse - pg-connection-string + pg-connection-string - rechoir + rechoir - resolve + resolve - resolve-from + resolve-from - supports-preserve-symlinks-flag + supports-preserve-symlinks-flag - tarn + tarn - tildify + tildify + diff --git a/blogpost/plots/showcase/javascript/mongoose.svg b/blogpost/plots/showcase/javascript/mongoose.svg index 9b73b09..56f6021 100644 --- a/blogpost/plots/showcase/javascript/mongoose.svg +++ b/blogpost/plots/showcase/javascript/mongoose.svg @@ -1,13 +1,19 @@ - + + @@ -33,76 +39,77 @@ - @mongodb-js/.. + @mongodb-js/.. - @types/webidl-conversions + @types/webidl-conversions - @types/whatw.. + @types/whatw.. - bson + bson - kareem + kareem - memory-pager + memory-pager - mongodb + mongodb - mongodb-connection-string-url + mongodb-connection-string-url - mongoose + mongoose - mpath + mpath - mquery + mquery - ms + ms - punycode + punycode - sift + sift - sparse-bitfi.. + sparse-bitfi.. - tr46 + tr46 - webidl-conversions + webidl-conversions - whatwg-url + whatwg-url + diff --git a/blogpost/plots/showcase/javascript/mongoose_mobile.svg b/blogpost/plots/showcase/javascript/mongoose_mobile.svg index 4949b43..972d38f 100644 --- a/blogpost/plots/showcase/javascript/mongoose_mobile.svg +++ b/blogpost/plots/showcase/javascript/mongoose_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -33,76 +39,77 @@ - @mongodb-js/saslprep + @mongodb-js/saslprep - @types/webidl-conversions + @types/webidl-conversions - @types/whatwg-url + @types/whatwg-url - bson + bson - kareem + kareem - memory-pager + memory-pager - mongodb + mongodb - mongodb-connection-string-url + mongodb-connection-string-url - mongoose + mongoose - mpath + mpath - mquery + mquery - ms + ms - punycode + punycode - sift + sift - sparse-bitfield + sparse-bitfield - tr46 + tr46 - webidl-conversions + webidl-conversions - whatwg-url + whatwg-url + diff --git a/blogpost/plots/showcase/javascript/pino.svg b/blogpost/plots/showcase/javascript/pino.svg index ec71f96..c96ed38 100644 --- a/blogpost/plots/showcase/javascript/pino.svg +++ b/blogpost/plots/showcase/javascript/pino.svg @@ -1,13 +1,19 @@ - + + @@ -28,56 +34,57 @@ - @pinojs/redact + @pinojs/redact - atomic-sleep + atomic-sleep - on-exit-leak-free + on-exit-leak-free - pino + pino - pino-abstract-transport + pino-abstract-transport - pino-std-serializers + pino-std-serializers - process-warning + process-warning - quick-format-unescaped + quick-format-unescaped - real-require + real-require - safe-stable-stringify + safe-stable-stringify - sonic-boom + sonic-boom - split2 + split2 - thread-stream + thread-stream + diff --git a/blogpost/plots/showcase/javascript/pino_mobile.svg b/blogpost/plots/showcase/javascript/pino_mobile.svg index ddfeb8d..4fcf616 100644 --- a/blogpost/plots/showcase/javascript/pino_mobile.svg +++ b/blogpost/plots/showcase/javascript/pino_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -28,56 +34,57 @@ - @pinojs/redact + @pinojs/redact - atomic-sleep + atomic-sleep - on-exit-leak-free + on-exit-leak-free - pino + pino - pino-abstract-transport + pino-abstract-transport - pino-std-serializers + pino-std-serializers - process-warning + process-warning - quick-format-unescaped + quick-format-unescaped - real-require + real-require - safe-stable-stringify + safe-stable-stringify - sonic-boom + sonic-boom - split2 + split2 - thread-stream + thread-stream + diff --git a/blogpost/plots/showcase/javascript/yup.svg b/blogpost/plots/showcase/javascript/yup.svg index 59b2772..abd092e 100644 --- a/blogpost/plots/showcase/javascript/yup.svg +++ b/blogpost/plots/showcase/javascript/yup.svg @@ -1,13 +1,19 @@ - + + @@ -19,28 +25,29 @@ - property-expr + property-expr - tagged-tag + tagged-tag - tiny-case + tiny-case - toposort + toposort - type-fest + type-fest - yup + yup + diff --git a/blogpost/plots/showcase/javascript/yup_mobile.svg b/blogpost/plots/showcase/javascript/yup_mobile.svg index c976667..b741e14 100644 --- a/blogpost/plots/showcase/javascript/yup_mobile.svg +++ b/blogpost/plots/showcase/javascript/yup_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -19,28 +25,29 @@ - property-expr + property-expr - tagged-tag + tagged-tag - tiny-case + tiny-case - toposort + toposort - type-fest + type-fest - yup + yup + diff --git a/blogpost/plots/showcase/python/fastapi.svg b/blogpost/plots/showcase/python/fastapi.svg index 70d66e2..73b1b03 100644 --- a/blogpost/plots/showcase/python/fastapi.svg +++ b/blogpost/plots/showcase/python/fastapi.svg @@ -1,13 +1,19 @@ - + + @@ -21,48 +27,49 @@ - annotated-doc + annotated-doc - annotated-types + annotated-types - anyio + anyio - exceptiongroup + exceptiongroup - fastapi + fastapi - idna + idna - pydantic + pydantic - pydantic-core + pydantic-core - starlette + starlette - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/fastapi_mobile.svg b/blogpost/plots/showcase/python/fastapi_mobile.svg index f7caddc..d4b1da3 100644 --- a/blogpost/plots/showcase/python/fastapi_mobile.svg +++ b/blogpost/plots/showcase/python/fastapi_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -21,48 +27,49 @@ - annotated-doc + annotated-doc - annotated-types + annotated-types - anyio + anyio - exceptiongroup + exceptiongroup - fastapi + fastapi - idna + idna - pydantic + pydantic - pydantic-core + pydantic-core - starlette + starlette - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/openai.svg b/blogpost/plots/showcase/python/openai.svg index 4e58c57..063bdf8 100644 --- a/blogpost/plots/showcase/python/openai.svg +++ b/blogpost/plots/showcase/python/openai.svg @@ -1,13 +1,19 @@ - + + @@ -29,76 +35,77 @@ - annotated-types + annotated-types - anyio + anyio - certifi + certifi - colorama + colorama - distro + distro - exceptiongroup + exceptiongroup - h11 + h11 - httpcore + httpcore - httpx + httpx - idna + idna - jiter + jiter - openai + openai - pydantic + pydantic - pydantic-core + pydantic-core - sniffio + sniffio - tqdm + tqdm - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/openai_mobile.svg b/blogpost/plots/showcase/python/openai_mobile.svg index df7836b..e48975b 100644 --- a/blogpost/plots/showcase/python/openai_mobile.svg +++ b/blogpost/plots/showcase/python/openai_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -29,76 +35,77 @@ - annotated-types + annotated-types - anyio + anyio - certifi + certifi - colorama + colorama - distro + distro - exceptiongroup + exceptiongroup - h11 + h11 - httpcore + httpcore - httpx + httpx - idna + idna - jiter + jiter - openai + openai - pydantic + pydantic - pydantic-core + pydantic-core - sniffio + sniffio - tqdm + tqdm - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/pydantic.svg b/blogpost/plots/showcase/python/pydantic.svg index f34cbdd..48beb55 100644 --- a/blogpost/plots/showcase/python/pydantic.svg +++ b/blogpost/plots/showcase/python/pydantic.svg @@ -1,13 +1,19 @@ - + + @@ -15,24 +21,25 @@ - annotated-ty.. + annotated-ty.. - pydantic + pydantic - pydantic-core + pydantic-core - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/pydantic_mobile.svg b/blogpost/plots/showcase/python/pydantic_mobile.svg index 8bb6dc1..b7dc374 100644 --- a/blogpost/plots/showcase/python/pydantic_mobile.svg +++ b/blogpost/plots/showcase/python/pydantic_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -15,24 +21,25 @@ - annotated-types + annotated-types - pydantic + pydantic - pydantic-core + pydantic-core - typing-extensions + typing-extensions - typing-inspection + typing-inspection + diff --git a/blogpost/plots/showcase/python/requests.svg b/blogpost/plots/showcase/python/requests.svg index 6ddeeed..5fff762 100644 --- a/blogpost/plots/showcase/python/requests.svg +++ b/blogpost/plots/showcase/python/requests.svg @@ -1,13 +1,19 @@ - + + @@ -15,24 +21,25 @@ - certifi + certifi - charset-normalizer + charset-normalizer - idna + idna - requests + requests - urllib3 + urllib3 + diff --git a/blogpost/plots/showcase/python/requests_mobile.svg b/blogpost/plots/showcase/python/requests_mobile.svg index 1abb8b3..abe07d3 100644 --- a/blogpost/plots/showcase/python/requests_mobile.svg +++ b/blogpost/plots/showcase/python/requests_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -15,24 +21,25 @@ - certifi + certifi - charset-normalizer + charset-normalizer - idna + idna - requests + requests - urllib3 + urllib3 + diff --git a/blogpost/plots/showcase/python/typer.svg b/blogpost/plots/showcase/python/typer.svg index 9643a1d..dd8202b 100644 --- a/blogpost/plots/showcase/python/typer.svg +++ b/blogpost/plots/showcase/python/typer.svg @@ -1,13 +1,19 @@ - + + @@ -19,40 +25,41 @@ - click + click - colorama + colorama - markdown-it-py + markdown-it-py - mdurl + mdurl - pygments + pygments - rich + rich - shellingham + shellingham - typer + typer - typing-extensions + typing-extensions + diff --git a/blogpost/plots/showcase/python/typer_mobile.svg b/blogpost/plots/showcase/python/typer_mobile.svg index 6ab6877..c0fcd78 100644 --- a/blogpost/plots/showcase/python/typer_mobile.svg +++ b/blogpost/plots/showcase/python/typer_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -19,40 +25,41 @@ - click + click - colorama + colorama - markdown-it-py + markdown-it-py - mdurl + mdurl - pygments + pygments - rich + rich - shellingham + shellingham - typer + typer - typing-extensions + typing-extensions + diff --git a/blogpost/plots/showcase/rust/diesel.svg b/blogpost/plots/showcase/rust/diesel.svg index cf6518d..f9e8331 100644 --- a/blogpost/plots/showcase/rust/diesel.svg +++ b/blogpost/plots/showcase/rust/diesel.svg @@ -1,13 +1,19 @@ - + + @@ -28,68 +34,69 @@ - darling + darling - darling_core + darling_core - darling_macro + darling_macro - diesel + diesel - diesel_derives + diesel_derives - diesel_table_macro_syntax + diesel_table_macro_syntax - downcast-rs + downcast-rs - dsl_auto_type + dsl_auto_type - either + either - fnv + fnv - heck + heck - ident_case + ident_case - proc-macro2 + proc-macro2 - quote + quote - syn + syn - unicode-ident + unicode-ident + diff --git a/blogpost/plots/showcase/rust/diesel_mobile.svg b/blogpost/plots/showcase/rust/diesel_mobile.svg index 705ce68..a201a85 100644 --- a/blogpost/plots/showcase/rust/diesel_mobile.svg +++ b/blogpost/plots/showcase/rust/diesel_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -28,68 +34,69 @@ - darling + darling - darling_core + darling_core - darling_macro + darling_macro - diesel + diesel - diesel_derives + diesel_derives - diesel_table_macro_syntax + diesel_table_macro_syntax - downcast-rs + downcast-rs - dsl_auto_type + dsl_auto_type - either + either - fnv + fnv - heck + heck - ident_case + ident_case - proc-macro2 + proc-macro2 - quote + quote - syn + syn - unicode-ident + unicode-ident + diff --git a/blogpost/plots/showcase/rust/hyper.svg b/blogpost/plots/showcase/rust/hyper.svg index 9618e60..8d0bb33 100644 --- a/blogpost/plots/showcase/rust/hyper.svg +++ b/blogpost/plots/showcase/rust/hyper.svg @@ -1,13 +1,19 @@ - + + @@ -17,32 +23,33 @@ - bytes + bytes - http + http - http-body + http-body - hyper + hyper - itoa + itoa - pin-project-lite + pin-project-lite - tokio + tokio + diff --git a/blogpost/plots/showcase/rust/hyper_mobile.svg b/blogpost/plots/showcase/rust/hyper_mobile.svg index 9741c86..dabe756 100644 --- a/blogpost/plots/showcase/rust/hyper_mobile.svg +++ b/blogpost/plots/showcase/rust/hyper_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -17,32 +23,33 @@ - bytes + bytes - http + http - http-body + http-body - hyper + hyper - itoa + itoa - pin-project-lite + pin-project-lite - tokio + tokio + diff --git a/blogpost/plots/showcase/rust/rayon.svg b/blogpost/plots/showcase/rust/rayon.svg index 584c2df..34fda5c 100644 --- a/blogpost/plots/showcase/rust/rayon.svg +++ b/blogpost/plots/showcase/rust/rayon.svg @@ -1,13 +1,19 @@ - + + @@ -16,28 +22,29 @@ - crossbeam-deque + crossbeam-deque - crossbeam-epoch + crossbeam-epoch - crossbeam-utils + crossbeam-utils - either + either - rayon + rayon - rayon-core + rayon-core + diff --git a/blogpost/plots/showcase/rust/rayon_mobile.svg b/blogpost/plots/showcase/rust/rayon_mobile.svg index 1afde0d..dbd09f8 100644 --- a/blogpost/plots/showcase/rust/rayon_mobile.svg +++ b/blogpost/plots/showcase/rust/rayon_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -16,28 +22,29 @@ - crossbeam-deque + crossbeam-deque - crossbeam-epoch + crossbeam-epoch - crossbeam-utils + crossbeam-utils - either + either - rayon + rayon - rayon-core + rayon-core + diff --git a/blogpost/plots/showcase/rust/serde.svg b/blogpost/plots/showcase/rust/serde.svg index 4c71fd3..d96274a 100644 --- a/blogpost/plots/showcase/rust/serde.svg +++ b/blogpost/plots/showcase/rust/serde.svg @@ -1,13 +1,19 @@ - + + @@ -16,28 +22,29 @@ - proc-macro2 + proc-macro2 - quote + quote - serde + serde - serde_derive + serde_derive - syn + syn - unicode-ident + unicode-ident + diff --git a/blogpost/plots/showcase/rust/serde_mobile.svg b/blogpost/plots/showcase/rust/serde_mobile.svg index 175d8e5..c89f417 100644 --- a/blogpost/plots/showcase/rust/serde_mobile.svg +++ b/blogpost/plots/showcase/rust/serde_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -16,28 +22,29 @@ - proc-macro2 + proc-macro2 - quote + quote - serde + serde - serde_derive + serde_derive - syn + syn - unicode-ident + unicode-ident + diff --git a/blogpost/plots/showcase/rust/ureq.svg b/blogpost/plots/showcase/rust/ureq.svg index 8efae5f..d4c2e6f 100644 --- a/blogpost/plots/showcase/rust/ureq.svg +++ b/blogpost/plots/showcase/rust/ureq.svg @@ -1,13 +1,19 @@ - + + @@ -21,44 +27,45 @@ - base64 + base64 - bytes + bytes - http + http - httparse + httparse - itoa + itoa - log + log - percent-encoding + percent-encoding - ureq + ureq - ureq-proto + ureq-proto - utf-8 + utf-8 + diff --git a/blogpost/plots/showcase/rust/ureq_mobile.svg b/blogpost/plots/showcase/rust/ureq_mobile.svg index a7fa313..0e2053a 100644 --- a/blogpost/plots/showcase/rust/ureq_mobile.svg +++ b/blogpost/plots/showcase/rust/ureq_mobile.svg @@ -1,13 +1,19 @@ - + + @@ -21,44 +27,45 @@ - base64 + base64 - bytes + bytes - http + http - httparse + httparse - itoa + itoa - log + log - percent-encoding + percent-encoding - ureq + ureq - ureq-proto + ureq-proto - utf-8 + utf-8 + diff --git a/cmd/stacktower/main.go b/cmd/stacktower/main.go new file mode 100644 index 0000000..e22cdbc --- /dev/null +++ b/cmd/stacktower/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + "github.com/matzehuels/stacktower/internal/cli" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + if err := run(ctx); err != nil { + if errors.Is(err, context.Canceled) { + os.Exit(130) // Standard shell convention for SIGINT + } + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + var verbose bool + + // Create CLI - verbose flag will be handled via PersistentPreRun + // Create a temporary CLI to build the root command structure + c := cli.New(os.Stderr, cli.LogInfo) + root := c.RootCommand() + + root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose logging") + + // Recreate CLI with correct log level before command execution + originalPreRun := root.PersistentPreRunE + root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + level := cli.LogInfo + if verbose { + level = cli.LogDebug + } + // Update the CLI's logger + c.SetLogLevel(level) + + if originalPreRun != nil { + return originalPreRun(cmd, args) + } + return nil + } + + return root.ExecuteContext(ctx) +} diff --git a/examples/real/cobra.json b/examples/real/cobra.json index 4f52522..5590028 100644 --- a/examples/real/cobra.json +++ b/examples/real/cobra.json @@ -1,217 +1,135 @@ { "nodes": [ { - "id": "golang.org/x/crypto", + "id": "github.com/cpuguy83/go-md2man/v2", "meta": { - "version": "v0.46.0" + "version": "v2.0.7" } }, { - "id": "golang.org/x/term", + "id": "github.com/creack/pty", "meta": { - "version": "v0.38.0" + "version": "v1.1.24" } }, { - "id": "github.com/rogpeppe/go-internal", + "id": "github.com/google/go-cmp", "meta": { - "version": "v1.14.1" + "version": "v0.7.0" } }, { - "id": "golang.org/x/mod", + "id": "github.com/inconshreveable/mousetrap", "meta": { - "version": "v0.31.0" + "version": "v1.1.0" } }, { - "id": "github.com/creack/pty", + "id": "github.com/kr/pretty", "meta": { - "version": "v1.1.24" + "version": "v0.3.1" } }, { - "id": "github.com/google/go-cmp", + "id": "github.com/kr/text", "meta": { - "version": "v0.7.0" + "version": "v0.2.0" } }, { - "id": "github.com/yuin/goldmark", + "id": "github.com/rogpeppe/go-internal", "meta": { - "version": "v1.7.13" + "version": "v1.14.1" } }, { - "id": "golang.org/x/tools", + "id": "github.com/russross/blackfriday/v2", "meta": { - "version": "v0.40.0" + "version": "v2.1.0" } }, { - "id": "github.com/cpuguy83/go-md2man/v2", + "id": "github.com/spf13/cobra", "meta": { - "repo_archived": false, - "repo_language": "Go", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-11-20", - "repo_maintainers": [ - "Roasbeef", - "guggero", - "yyforyongyu", - "cfromknecht", - "halseth" - ], - "repo_owner": "lightningnetwork", - "repo_stars": 8074, - "repo_topics": [ - "bitcoin", - "blockchain", - "cryptocurrency", - "cryptography", - "lightning", - "lightning-network", - "micropayments", - "payments", - "peer-to-peer", - "protocol" - ], - "repo_url": "https://github.com/lightningnetwork/lnd", - "version": "v2.0.7" + "version": "v1.10.2" } }, { - "id": "github.com/inconshreveable/mousetrap", + "id": "github.com/spf13/pflag", "meta": { - "version": "v1.1.0" + "version": "v1.0.10" } }, { - "id": "github.com/russross/blackfriday/v2", + "id": "github.com/yuin/goldmark", "meta": { - "version": "v2.1.0" + "version": "v1.7.16" } }, { - "id": "gopkg.in/check.v1", + "id": "go.yaml.in/yaml/v3", "meta": { - "version": "v1.0.0-20201130134442-10cb98267c6c" + "version": "v3.0.4" } }, { - "id": "github.com/kr/pretty", + "id": "golang.org/x/crypto", "meta": { - "version": "v0.3.1" + "version": "v0.47.0" } }, { - "id": "golang.org/x/text", + "id": "golang.org/x/mod", "meta": { "version": "v0.32.0" } }, { - "id": "github.com/kr/text", + "id": "golang.org/x/net", "meta": { - "version": "v0.2.0" + "version": "v0.49.0" } }, { - "id": "golang.org/x/sys", + "id": "golang.org/x/sync", "meta": { - "version": "v0.39.0" + "version": "v0.19.0" } }, { - "id": "golang.org/x/telemetry", + "id": "golang.org/x/sys", "meta": { - "version": "v0.0.0-20251222180846-3f2a21fb04ff" + "version": "v0.40.0" } }, { - "id": "github.com/spf13/cobra", + "id": "golang.org/x/telemetry", "meta": { - "repo_archived": false, - "repo_language": "TypeScript", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-12-16", - "repo_maintainers": [ - "torkelo", - "bergquist", - "ryantxu", - "marefr" - ], - "repo_owner": "grafana", - "repo_stars": 71404, - "repo_topics": [ - "alerting", - "analytics", - "business-intelligence", - "dashboard", - "data-visualization", - "elasticsearch", - "go", - "grafana", - "hacktoberfest", - "influxdb", - "metrics", - "monitoring", - "mysql", - "postgres", - "prometheus" - ], - "repo_url": "https://github.com/grafana/grafana", - "version": "v1.10.2" + "version": "v0.0.0-20260128140115-fcf36f686977" } }, { - "id": "github.com/spf13/pflag", + "id": "golang.org/x/term", "meta": { - "repo_archived": false, - "repo_language": "Go", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-11-20", - "repo_maintainers": [ - "Roasbeef", - "guggero", - "yyforyongyu", - "cfromknecht", - "halseth" - ], - "repo_owner": "lightningnetwork", - "repo_stars": 8074, - "repo_topics": [ - "bitcoin", - "blockchain", - "cryptocurrency", - "cryptography", - "lightning", - "lightning-network", - "micropayments", - "payments", - "peer-to-peer", - "protocol" - ], - "repo_url": "https://github.com/lightningnetwork/lnd", - "version": "v1.0.10" + "version": "v0.39.0" } }, { - "id": "go.yaml.in/yaml/v3", + "id": "golang.org/x/text", "meta": { - "version": "v3.0.4" + "version": "v0.33.0" } }, { - "id": "golang.org/x/net", + "id": "golang.org/x/tools", "meta": { - "version": "v0.48.0" + "version": "v0.41.0" } }, { - "id": "golang.org/x/sync", + "id": "gopkg.in/check.v1", "meta": { - "version": "v0.19.0" + "version": "v1.0.0-20201130134442-10cb98267c6c" } } ], @@ -232,14 +150,14 @@ "from": "github.com/spf13/cobra", "to": "go.yaml.in/yaml/v3" }, - { - "from": "github.com/cpuguy83/go-md2man/v2", - "to": "github.com/russross/blackfriday/v2" - }, { "from": "go.yaml.in/yaml/v3", "to": "gopkg.in/check.v1" }, + { + "from": "github.com/cpuguy83/go-md2man/v2", + "to": "github.com/russross/blackfriday/v2" + }, { "from": "gopkg.in/check.v1", "to": "github.com/kr/pretty" @@ -296,18 +214,6 @@ "from": "golang.org/x/mod", "to": "golang.org/x/tools" }, - { - "from": "golang.org/x/telemetry", - "to": "golang.org/x/mod" - }, - { - "from": "golang.org/x/telemetry", - "to": "golang.org/x/sync" - }, - { - "from": "golang.org/x/telemetry", - "to": "golang.org/x/sys" - }, { "from": "golang.org/x/net", "to": "golang.org/x/crypto" @@ -325,8 +231,16 @@ "to": "golang.org/x/text" }, { - "from": "golang.org/x/text", - "to": "golang.org/x/tools" + "from": "golang.org/x/telemetry", + "to": "golang.org/x/mod" + }, + { + "from": "golang.org/x/telemetry", + "to": "golang.org/x/sync" + }, + { + "from": "golang.org/x/telemetry", + "to": "golang.org/x/sys" }, { "from": "golang.org/x/crypto", @@ -340,6 +254,10 @@ "from": "golang.org/x/crypto", "to": "golang.org/x/term" }, + { + "from": "golang.org/x/text", + "to": "golang.org/x/tools" + }, { "from": "golang.org/x/term", "to": "golang.org/x/sys" diff --git a/examples/real/com.google.guava_guava.json b/examples/real/com.google.guava_guava.json index 73aa99b..ec28b4a 100644 --- a/examples/real/com.google.guava_guava.json +++ b/examples/real/com.google.guava_guava.json @@ -1,155 +1,39 @@ { "nodes": [ { - "id": "com.google.guava:guava", + "id": "com.google.errorprone:error_prone_annotations", "meta": { - "repo_archived": false, - "repo_language": "Java", - "repo_last_commit": "2025-12-18", - "repo_last_release": "2025-06-26", - "repo_maintainers": [ - "Randgalt", - "cammckenzie", - "tisonkun", - "dragonsinth", - "kezhuw" - ], - "repo_owner": "apache", - "repo_stars": 3165, - "repo_topics": [ - "consensus", - "curator", - "database", - "java", - "zookeeper" - ], - "repo_url": "https://github.com/apache/curator", - "version": "33.4.8-jre" + "version": "2.39.0" } }, { "id": "com.google.guava:failureaccess", "meta": { - "repo_archived": false, - "repo_language": "Scala", - "repo_last_commit": "2025-12-23", - "repo_maintainers": [ - "dongjoon-hyun", - "HyukjinKwon", - "zhengruifeng", - "mateiz", - "rxin" - ], - "repo_owner": "apache", - "repo_stars": 42526, - "repo_topics": [ - "big-data", - "java", - "jdbc", - "python", - "r", - "scala", - "spark", - "sql" - ], - "repo_url": "https://github.com/apache/spark", "version": "1.0.3" } }, { - "id": "com.google.guava:listenablefuture", + "id": "com.google.guava:guava", "meta": { - "repo_archived": false, - "repo_language": "Java", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-12-17", - "repo_maintainers": [ - "gsmet", - "geoand", - "stuartwdouglas", - "gastaldi" - ], - "repo_owner": "quarkusio", - "repo_stars": 15356, - "repo_topics": [ - "cloud-native", - "hacktoberfest", - "java", - "kubernetes", - "reactive" - ], - "repo_url": "https://github.com/quarkusio/quarkus", - "version": "9999.0-empty-to-avoid-conflict-with-guava" + "version": "33.4.8-jre" } }, { - "id": "org.jspecify:jspecify", + "id": "com.google.guava:listenablefuture", "meta": { - "repo_archived": false, - "repo_language": "Java", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-09-16", - "repo_maintainers": [ - "codahale", - "joschi", - "arteam", - "jplock" - ], - "repo_owner": "dropwizard", - "repo_stars": 8577, - "repo_topics": [ - "dropwizard", - "hibernate", - "java", - "jax-rs", - "jdbi", - "jersey-framework", - "jersey2", - "jetty", - "rest", - "restful-webservices", - "web-framework" - ], - "repo_url": "https://github.com/dropwizard/dropwizard", - "version": "1.0.0" + "version": "9999.0-empty-to-avoid-conflict-with-guava" } }, { - "id": "com.google.errorprone:error_prone_annotations", + "id": "com.google.j2objc:j2objc-annotations", "meta": { - "repo_archived": false, - "repo_language": "Java", - "repo_last_commit": "2025-12-19", - "repo_maintainers": [ - "normanmaurer", - "trustin", - "Scottmitch", - "netty-project-bot", - "chrisvest" - ], - "repo_owner": "netty", - "repo_stars": 34658, - "repo_url": "https://github.com/netty/netty", - "version": "2.39.0" + "version": "3.0.0" } }, { - "id": "com.google.j2objc:j2objc-annotations", + "id": "org.jspecify:jspecify", "meta": { - "repo_archived": false, - "repo_language": "Java", - "repo_last_commit": "2025-12-19", - "repo_maintainers": [ - "normanmaurer", - "trustin", - "Scottmitch", - "netty-project-bot", - "chrisvest" - ], - "repo_owner": "netty", - "repo_stars": 34658, - "repo_url": "https://github.com/netty/netty", - "version": "3.0.0" + "version": "1.0.0" } } ], diff --git a/examples/real/console.json b/examples/real/console.json index ed1eb03..f513009 100644 --- a/examples/real/console.json +++ b/examples/real/console.json @@ -1,102 +1,21 @@ { "nodes": [ { - "id": "symfony/polyfill-mbstring", - "meta": { - "author": "Nicolas Grekas", - "description": "Symfony polyfill for the Mbstring extension", - "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-08-19", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "stof", - "IonBazan", - "keradus" - ], - "repo_owner": "symfony", - "repo_stars": 7859, - "repo_topics": [ - "compatibility", - "component", - "javascript", - "mbstring", - "polyfill", - "portable", - "shim", - "symfony", - "symfony-component", - "symfony-polyfill" - ], - "repo_url": "https://github.com/symfony/polyfill-mbstring", - "version": "v1.33.0" - } - }, - { - "id": "symfony/service-contracts", + "id": "psr/container", "meta": { - "author": "Nicolas Grekas", - "description": "Generic abstractions related to writing services", + "author": "PHP-FIG", + "description": "Common Container Interface (PHP FIG PSR-11)", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-12-13", - "repo_last_release": "2020-09-08", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "derrabus", - "kbond", - "xabbuh" - ], - "repo_owner": "symfony", - "repo_stars": 2630, - "repo_topics": [ - "contract", - "php", - "service", - "symfony", - "symfony-contract" - ], - "repo_url": "https://github.com/symfony/service-contracts", - "version": "v3.6.1" + "version": "2.0.2" } }, { - "id": "symfony/string", + "id": "symfony/console", "meta": { - "author": "Nicolas Grekas", - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "author": "Fabien Potencier", + "description": "Eases the creation of beautiful and testable command line interfaces", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-12-19", - "repo_last_release": "2025-12-07", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "derrabus", - "xabbuh", - "fancyweb" - ], - "repo_owner": "symfony", - "repo_stars": 1781, - "repo_topics": [ - "component", - "grapheme", - "i18n", - "php", - "string", - "symfony", - "symfony-component", - "unicode", - "utf-8", - "utf8" - ], - "repo_url": "https://github.com/symfony/string", - "version": "v8.0.1" + "version": "v8.0.4" } }, { @@ -105,62 +24,15 @@ "author": "Nicolas Grekas", "description": "A generic function and convention to trigger deprecation notices", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-05-25", - "repo_last_release": "2020-09-08", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "derrabus", - "xabbuh", - "cedric-anne" - ], - "repo_owner": "symfony", - "repo_stars": 2108, - "repo_topics": [ - "contract", - "deprecation", - "php", - "symfony", - "symfony-contract" - ], - "repo_url": "https://github.com/symfony/deprecation-contracts", "version": "v3.6.0" } }, { - "id": "symfony/polyfill-intl-normalizer", + "id": "symfony/polyfill-ctype", "meta": { - "author": "Nicolas Grekas", - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "author": "Gert de Pagter", + "description": "Symfony polyfill for ctype functions", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-08-19", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "derrabus", - "stof", - "DavidPrevot" - ], - "repo_owner": "symfony", - "repo_stars": 2071, - "repo_topics": [ - "compatibility", - "component", - "intl", - "javascript", - "normalizer", - "polyfill", - "portable", - "shim", - "symfony", - "symfony-component", - "symfony-polyfill" - ], - "repo_url": "https://github.com/symfony/polyfill-intl-normalizer", "version": "v1.33.0" } }, @@ -170,122 +42,43 @@ "author": "Nicolas Grekas", "description": "Symfony polyfill for intl's grapheme_* functions", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-08-19", - "repo_maintainers": [ - "nicolas-grekas", - "fabpot", - "gharlan", - "Ayesh", - "azjezz" - ], - "repo_owner": "symfony", - "repo_stars": 1733, - "repo_topics": [ - "compatibility", - "component", - "grapheme", - "intl", - "javascript", - "polyfill", - "portable", - "shim", - "symfony", - "symfony-component", - "symfony-polyfill" - ], - "repo_url": "https://github.com/symfony/polyfill-intl-grapheme", "version": "v1.33.0" } }, { - "id": "psr/container", + "id": "symfony/polyfill-intl-normalizer", "meta": { - "author": "PHP-FIG", - "description": "Common Container Interface (PHP FIG PSR-11)", + "author": "Nicolas Grekas", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2024-02-05", - "repo_last_release": "2021-11-05", - "repo_maintainers": [ - "mnapoli", - "moufmouf", - "Ocramius", - "weierophinney", - "Jean85" - ], - "repo_owner": "php-fig", - "repo_stars": 10023, - "repo_url": "https://github.com/php-fig/container", - "version": "2.0.2" + "version": "v1.33.0" } }, { - "id": "symfony/polyfill-ctype", + "id": "symfony/polyfill-mbstring", "meta": { - "author": "Gert de Pagter", - "description": "Symfony polyfill for ctype functions", + "author": "Nicolas Grekas", + "description": "Symfony polyfill for the Mbstring extension", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-08-19", - "repo_maintainers": [ - "nicolas-grekas", - "BackEndTea", - "fabpot", - "cedric-anne", - "GrahamCampbell" - ], - "repo_owner": "symfony", - "repo_stars": 4053, - "repo_topics": [ - "compatibility", - "component", - "ctype", - "javascript", - "polyfill", - "portable", - "symfony", - "symfony-component", - "symfony-polyfill" - ], - "repo_url": "https://github.com/symfony/polyfill-ctype", "version": "v1.33.0" } }, { - "id": "symfony/console", + "id": "symfony/service-contracts", "meta": { - "author": "Fabien Potencier", - "description": "Eases the creation of beautiful and testable command line interfaces", + "author": "Nicolas Grekas", + "description": "Generic abstractions related to writing services", "license": "MIT", - "repo_archived": false, - "repo_language": "PHP", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-12-07", - "repo_maintainers": [ - "fabpot", - "nicolas-grekas", - "xabbuh", - "derrabus", - "chalasr" - ], - "repo_owner": "symfony", - "repo_stars": 9834, - "repo_topics": [ - "cli", - "command-line", - "component", - "console", - "php", - "symfony", - "symfony-component", - "terminal" - ], - "repo_url": "https://github.com/symfony/console", - "version": "v8.0.1" + "version": "v3.6.1" + } + }, + { + "id": "symfony/string", + "meta": { + "author": "Nicolas Grekas", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "license": "MIT", + "version": "v8.0.4" } } ], @@ -302,14 +95,6 @@ "from": "symfony/console", "to": "symfony/string" }, - { - "from": "symfony/service-contracts", - "to": "psr/container" - }, - { - "from": "symfony/service-contracts", - "to": "symfony/deprecation-contracts" - }, { "from": "symfony/string", "to": "symfony/polyfill-intl-normalizer" @@ -325,6 +110,14 @@ { "from": "symfony/string", "to": "symfony/polyfill-intl-grapheme" + }, + { + "from": "symfony/service-contracts", + "to": "psr/container" + }, + { + "from": "symfony/service-contracts", + "to": "symfony/deprecation-contracts" } ] } diff --git a/examples/real/flask.json b/examples/real/flask.json index 98e303e..94942f3 100644 --- a/examples/real/flask.json +++ b/examples/real/flask.json @@ -1,254 +1,78 @@ { "nodes": [ { - "id": "importlib-metadata", + "id": "blinker", "meta": { - "description": "Read metadata from Python packages", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-21", - "repo_last_release": "2025-12-21", - "repo_maintainers": [ - "jaraco", - "warsaw", - "abravalheri", - "jherland", - "sjma3" - ], - "repo_owner": "python", - "repo_stars": 138, - "repo_url": "https://github.com/python/importlib_metadata", - "version": "8.7.1" + "author": "Jason Kirtland", + "description": "Fast, simple object-to-object and broadcast signaling", + "license": "MIT License", + "version": "1.9.0" } }, { - "id": "itsdangerous", + "id": "click", "meta": { - "description": "Safely pass data to untrusted environments and back.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-06-14", - "repo_last_release": "2024-04-16", - "repo_maintainers": [ - "davidism", - "mitsuhiko" - ], - "repo_owner": "pallets", - "repo_stars": 3093, - "repo_topics": [ - "hmac", - "itsdangerous", - "pallets", - "python", - "security", - "serialization" - ], - "repo_url": "https://github.com/pallets/itsdangerous", - "version": "2.2.0" + "description": "Composable command line interface toolkit", + "version": "8.3.1" } }, { - "id": "werkzeug", + "id": "colorama", "meta": { - "description": "The comprehensive WSGI web application library.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-02", - "repo_last_release": "2025-11-29", - "repo_maintainers": [ - "mitsuhiko", - "davidism", - "untitaker", - "DasIch" - ], - "repo_owner": "pallets", - "repo_stars": 6825, - "repo_topics": [ - "http", - "pallets", - "python", - "werkzeug", - "wsgi" - ], - "repo_url": "https://github.com/pallets/werkzeug", - "version": "3.1.4" + "description": "Cross-platform colored terminal text.", + "license": "BSD License", + "version": "0.4.6" } }, { - "id": "colorama", + "id": "flask", "meta": { - "description": "Cross-platform colored terminal text.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-07-09", - "repo_maintainers": [ - "tartley", - "wiggin15", - "hugovk", - "njsmith", - "jdufresne" - ], - "repo_owner": "tartley", - "repo_stars": 3758, - "repo_url": "https://github.com/tartley/colorama", - "version": "0.4.6" + "description": "A simple framework for building complex web applications.", + "version": "3.1.2" } }, { - "id": "jinja2", + "id": "importlib-metadata", "meta": { - "description": "A very fast and expressive template engine.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-06-14", - "repo_last_release": "2025-03-05", - "repo_maintainers": [ - "mitsuhiko", - "davidism", - "untitaker" - ], - "repo_owner": "pallets", - "repo_stars": 11342, - "repo_topics": [ - "jinja", - "jinja2", - "pallets", - "python", - "template-engine", - "templates" - ], - "repo_url": "https://github.com/pallets/jinja", - "version": "3.1.6" + "description": "Read metadata from Python packages", + "version": "8.7.1" } }, { - "id": "markupsafe", + "id": "itsdangerous", "meta": { - "description": "Safely add untrusted strings to HTML/XML markup.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-09-27", - "repo_last_release": "2025-09-27", - "repo_maintainers": [ - "davidism", - "mitsuhiko" - ], - "repo_owner": "pallets", - "repo_stars": 682, - "repo_topics": [ - "html", - "html-escape", - "jinja", - "markupsafe", - "pallets", - "python", - "template-engine" - ], - "repo_url": "https://github.com/pallets/markupsafe", - "version": "3.0.3" + "description": "Safely pass data to untrusted environments and back.", + "license": "BSD License", + "version": "2.2.0" } }, { - "id": "zipp", + "id": "jinja2", "meta": { - "description": "Backport of pathlib-compatible object wrapper for zip files", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-10-19", - "repo_last_release": "2025-06-08", - "repo_maintainers": [ - "jaraco", - "DimitriPapadopoulos", - "layday", - "Avasam", - "barneygale" - ], - "repo_owner": "jaraco", - "repo_stars": 67, - "repo_url": "https://github.com/jaraco/zipp", - "version": "3.23.0" + "description": "A very fast and expressive template engine.", + "license": "BSD License", + "version": "3.1.6" } }, { - "id": "flask", + "id": "markupsafe", "meta": { - "description": "A simple framework for building complex web applications.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-28", - "repo_last_release": "2025-08-19", - "repo_maintainers": [ - "davidism", - "mitsuhiko", - "untitaker", - "rduplain", - "greyli" - ], - "repo_owner": "pallets", - "repo_stars": 70955, - "repo_topics": [ - "flask", - "jinja", - "pallets", - "python", - "web-framework", - "werkzeug", - "wsgi" - ], - "repo_url": "https://github.com/pallets/flask", - "version": "3.1.2" + "description": "Safely add untrusted strings to HTML/XML markup.", + "version": "3.0.3" } }, { - "id": "blinker", + "id": "werkzeug", "meta": { - "author": "Jason Kirtland", - "description": "Fast, simple object-to-object and broadcast signaling", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-19", - "repo_last_release": "2024-11-08", - "repo_maintainers": [ - "davidism", - "jek", - "Secrus", - "pgjones" - ], - "repo_owner": "pallets-eco", - "repo_stars": 2008, - "repo_topics": [ - "blinker", - "python", - "signals" - ], - "repo_url": "https://github.com/pallets-eco/blinker", - "version": "1.9.0" + "description": "The comprehensive WSGI web application library.", + "version": "3.1.5" } }, { - "id": "click", + "id": "zipp", "meta": { - "description": "Composable command line interface toolkit", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-10", - "repo_last_release": "2025-11-15", - "repo_maintainers": [ - "davidism", - "mitsuhiko", - "Rowlando13", - "untitaker" - ], - "repo_owner": "pallets", - "repo_stars": 17074, - "repo_topics": [ - "cli", - "click", - "pallets", - "python" - ], - "repo_url": "https://github.com/pallets/click", - "version": "8.3.1" + "description": "Backport of pathlib-compatible object wrapper for zip files", + "version": "3.23.0" } } ], @@ -281,10 +105,6 @@ "from": "flask", "to": "werkzeug" }, - { - "from": "werkzeug", - "to": "markupsafe" - }, { "from": "jinja2", "to": "markupsafe" @@ -296,6 +116,10 @@ { "from": "importlib-metadata", "to": "zipp" + }, + { + "from": "werkzeug", + "to": "markupsafe" } ] } diff --git a/examples/real/htmd.json b/examples/real/htmd.json deleted file mode 100644 index 03ec20f..0000000 --- a/examples/real/htmd.json +++ /dev/null @@ -1,449 +0,0 @@ -{ - "nodes": [ - { - "id": "click", - "meta": { - "description": "Composable command line interface toolkit", - "version": "8.3.1" - } - }, - { - "id": "csscompressor", - "meta": { - "author": "Yury Selivanov", - "description": "A python port of YUI CSS Compressor", - "license": "BSD", - "version": "0.9.5" - } - }, - { - "id": "feedwerk", - "meta": { - "author": "phil", - "description": "The atom feed generator from werkzeug.", - "license": "BSD-3-Clause", - "version": "1.2.0" - } - }, - { - "id": "flask", - "meta": { - "description": "A simple framework for building complex web applications.", - "version": "3.1.2" - } - }, - { - "id": "pyyaml", - "meta": { - "author": "Kirill Simonov", - "description": "YAML parser and emitter for Python", - "license": "MIT", - "version": "6.0.3" - } - }, - { - "id": "frozen-flask", - "meta": { - "description": "Freezes a Flask application into a set of static files.", - "license": "Copyright (c) 2010-2012 by Simon Sapin and contributors. See AUTHORS\n for more details.\n \n Some rights reserved.\n \n Redistribution and use in source and binary forms, with or without\n modification, are permitted provided that the following conditions are\n met:\n \n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n \n * Redistributions in binary form must reproduce the above\n copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided\n with the distribution.\n \n * The names of the contributors may not be used to endorse or\n promote products derived from this software without specific\n prior written permission.\n \n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", - "version": "1.0.2" - } - }, - { - "id": "htmlmin2", - "meta": { - "author": "Dave Mankoff", - "description": "An HTML Minifier", - "license": "BSD", - "version": "0.1.13" - } - }, - { - "id": "beautifulsoup4", - "meta": { - "description": "Screen-scraping library", - "license": "MIT License", - "version": "4.14.3" - } - }, - { - "id": "soupsieve", - "meta": { - "description": "A modern CSS selector implementation for Beautiful Soup.", - "version": "2.8.1" - } - }, - { - "id": "typing-extensions", - "meta": { - "description": "Backported and Experimental Type Hints for Python 3.9+", - "version": "4.15.0" - } - }, - { - "id": "jinja2", - "meta": { - "description": "A very fast and expressive template engine.", - "version": "3.1.6" - } - }, - { - "id": "zipp", - "meta": { - "description": "Backport of pathlib-compatible object wrapper for zip files", - "version": "3.23.0" - } - }, - { - "id": "markdown", - "meta": { - "author": "Manfred Stienstra, Yuri Takhteyev", - "description": "Python implementation of John Gruber's Markdown.", - "version": "3.10" - } - }, - { - "id": "jsmin", - "meta": { - "author": "Dave St.Germain", - "description": "JavaScript minifier.", - "license": "MIT License", - "version": "3.0.1" - } - }, - { - "id": "htmd", - "meta": { - "virtual": true - } - }, - { - "id": "colorama", - "meta": { - "description": "Cross-platform colored terminal text.", - "version": "0.4.6" - } - }, - { - "id": "blinker", - "meta": { - "author": "Jason Kirtland", - "description": "Fast, simple object-to-object and broadcast signaling", - "version": "1.9.0" - } - }, - { - "id": "importlib-metadata", - "meta": { - "description": "Read metadata from Python packages", - "version": "8.7.0" - } - }, - { - "id": "flask-flatpages", - "meta": { - "author": "Simon Sapin, Igor Davydenko, Padraic Calpin", - "description": "Provides flat static pages to a Flask application", - "version": "0.8.3" - } - }, - { - "id": "pytz", - "meta": { - "author": "Stuart Bishop", - "description": "World timezone definitions, modern and historical", - "license": "MIT", - "version": "2025.2" - } - }, - { - "id": "watchdog", - "meta": { - "author": "Mickaël Schoentgen", - "description": "Filesystem events monitoring", - "license": "Apache-2.0", - "version": "6.0.0" - } - }, - { - "id": "werkzeug", - "meta": { - "description": "The comprehensive WSGI web application library.", - "version": "3.1.4" - } - }, - { - "id": "markupsafe", - "meta": { - "description": "Safely add untrusted strings to HTML/XML markup.", - "version": "3.0.3" - } - }, - { - "id": "itsdangerous", - "meta": { - "description": "Safely pass data to untrusted environments and back.", - "version": "2.2.0" - } - }, - { - "id": "six", - "meta": { - "author": "Benjamin Peterson", - "description": "Python 2 and 3 compatibility utilities", - "license": "MIT", - "version": "1.17.0" - } - }, - { - "id": "pygments", - "meta": { - "description": "Pygments is a syntax highlighting package written in Python.", - "license": "BSD-2-Clause", - "version": "2.19.2" - } - } - ], - "edges": [ - { - "from": "beautifulsoup4", - "to": "soupsieve" - }, - { - "from": "beautifulsoup4", - "to": "typing-extensions" - }, - { - "from": "htmd", - "to": "beautifulsoup4" - }, - { - "from": "click", - "to": "colorama" - }, - { - "from": "htmd", - "to": "click" - }, - { - "from": "htmd", - "to": "csscompressor" - }, - { - "from": "feedwerk", - "to": "werkzeug" - }, - { - "from": "feedwerk", - "to": "markupsafe" - }, - { - "from": "werkzeug", - "to": "markupsafe" - }, - { - "from": "htmd", - "to": "feedwerk" - }, - { - "from": "flask", - "to": "blinker" - }, - { - "from": "flask", - "to": "click" - }, - { - "from": "flask", - "to": "importlib-metadata" - }, - { - "from": "flask", - "to": "itsdangerous" - }, - { - "from": "flask", - "to": "jinja2" - }, - { - "from": "flask", - "to": "markupsafe" - }, - { - "from": "flask", - "to": "werkzeug" - }, - { - "from": "werkzeug", - "to": "markupsafe" - }, - { - "from": "click", - "to": "colorama" - }, - { - "from": "jinja2", - "to": "markupsafe" - }, - { - "from": "importlib-metadata", - "to": "zipp" - }, - { - "from": "importlib-metadata", - "to": "typing-extensions" - }, - { - "from": "htmd", - "to": "flask" - }, - { - "from": "flask-flatpages", - "to": "flask" - }, - { - "from": "flask-flatpages", - "to": "jinja2" - }, - { - "from": "flask-flatpages", - "to": "markdown" - }, - { - "from": "flask-flatpages", - "to": "pyyaml" - }, - { - "from": "flask-flatpages", - "to": "pytz" - }, - { - "from": "flask-flatpages", - "to": "six" - }, - { - "from": "flask", - "to": "blinker" - }, - { - "from": "flask", - "to": "click" - }, - { - "from": "flask", - "to": "importlib-metadata" - }, - { - "from": "flask", - "to": "itsdangerous" - }, - { - "from": "flask", - "to": "jinja2" - }, - { - "from": "flask", - "to": "markupsafe" - }, - { - "from": "flask", - "to": "werkzeug" - }, - { - "from": "jinja2", - "to": "markupsafe" - }, - { - "from": "werkzeug", - "to": "markupsafe" - }, - { - "from": "importlib-metadata", - "to": "zipp" - }, - { - "from": "importlib-metadata", - "to": "typing-extensions" - }, - { - "from": "click", - "to": "colorama" - }, - { - "from": "htmd", - "to": "flask-flatpages" - }, - { - "from": "frozen-flask", - "to": "flask" - }, - { - "from": "flask", - "to": "blinker" - }, - { - "from": "flask", - "to": "click" - }, - { - "from": "flask", - "to": "importlib-metadata" - }, - { - "from": "flask", - "to": "itsdangerous" - }, - { - "from": "flask", - "to": "jinja2" - }, - { - "from": "flask", - "to": "markupsafe" - }, - { - "from": "flask", - "to": "werkzeug" - }, - { - "from": "importlib-metadata", - "to": "zipp" - }, - { - "from": "importlib-metadata", - "to": "typing-extensions" - }, - { - "from": "werkzeug", - "to": "markupsafe" - }, - { - "from": "jinja2", - "to": "markupsafe" - }, - { - "from": "click", - "to": "colorama" - }, - { - "from": "htmd", - "to": "frozen-flask" - }, - { - "from": "htmd", - "to": "htmlmin2" - }, - { - "from": "htmd", - "to": "jsmin" - }, - { - "from": "htmd", - "to": "pygments" - }, - { - "from": "htmd", - "to": "watchdog" - } - ] -} diff --git a/examples/real/openai.json b/examples/real/openai.json index 35215c3..2ec9be2 100644 --- a/examples/real/openai.json +++ b/examples/real/openai.json @@ -1,380 +1,121 @@ { "nodes": [ { - "id": "h11", + "id": "annotated-types", "meta": { - "author": "Nathaniel J. Smith", - "description": "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1", - "license": "MIT", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-04-24", - "repo_maintainers": [ - "njsmith", - "pgjones", - "Lukasa", - "cdeler", - "lovelydinosaur" - ], - "repo_owner": "python-hyper", - "repo_stars": 538, - "repo_topics": [ - "http", - "python", - "sans-io" - ], - "repo_url": "https://github.com/python-hyper/h11", - "version": "0.16.0" + "description": "Reusable constraint types to use with typing.Annotated", + "license": "MIT License", + "version": "0.7.0" } }, { - "id": "exceptiongroup", + "id": "anyio", "meta": { - "description": "Backport of PEP 654 (exception groups)", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-21", - "repo_last_release": "2025-11-21", - "repo_maintainers": [ - "agronholm", - "jakkdl", - "Zac-HD", - "cfbolz" - ], - "repo_owner": "agronholm", - "repo_stars": 48, - "repo_url": "https://github.com/agronholm/exceptiongroup", - "version": "1.3.1" + "description": "High-level concurrency and networking framework on top of asyncio or Trio", + "version": "4.12.1" } }, { - "id": "httpx", + "id": "certifi", "meta": { - "description": "The next generation HTTP client.", - "license": "BSD-3-Clause", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-17", - "repo_last_release": "2024-12-06", - "repo_maintainers": [ - "lovelydinosaur", - "florimondmanca", - "sethmlarson", - "karpetrosyan" - ], - "repo_owner": "encode", - "repo_stars": 14857, - "repo_topics": [ - "asyncio", - "http", - "python", - "trio" - ], - "repo_url": "https://github.com/encode/httpx", - "version": "0.28.1" + "author": "Kenneth Reitz", + "description": "Python package for providing Mozilla's CA Bundle.", + "license": "Mozilla Public License 2.0 (MPL 2.0)", + "version": "2026.1.4" } }, { - "id": "jiter", + "id": "colorama", "meta": { - "description": "Fast iterable JSON parser.", - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-11-09", - "repo_last_release": "2025-11-09", - "repo_maintainers": [ - "samuelcolvin", - "davidhewitt", - "Viicos", - "jessekrubin", - "msimacek" - ], - "repo_owner": "pydantic", - "repo_stars": 481, - "repo_topics": [ - "json", - "json-parser", - "pydantic", - "rust" - ], - "repo_url": "https://github.com/pydantic/jiter", - "version": "0.12.0" + "description": "Cross-platform colored terminal text.", + "license": "BSD License", + "version": "0.4.6" } }, { - "id": "pydantic-core", + "id": "distro", "meta": { - "description": "Core functionality for Pydantic validation and serialization", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-10", - "repo_last_release": "2025-11-04", - "repo_maintainers": [ - "samuelcolvin", - "davidhewitt", - "adriangb", - "sydney-runkle" - ], - "repo_owner": "pydantic", - "repo_stars": 1739, - "repo_topics": [ - "json-schema", - "parsing", - "pydantic", - "rust", - "schema", - "validation" - ], - "repo_url": "https://github.com/pydantic/pydantic-core", - "version": "2.41.5" + "author": "Nir Cohen", + "description": "Distro - an OS platform information API", + "license": "Apache Software License", + "version": "1.9.0" } }, { - "id": "typing-inspection", + "id": "exceptiongroup", "meta": { - "description": "Runtime typing introspection tools", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-10-01", - "repo_last_release": "2025-10-01", - "repo_maintainers": [ - "Viicos", - "samuelcolvin", - "cdce8p" - ], - "repo_owner": "pydantic", - "repo_stars": 57, - "repo_url": "https://github.com/pydantic/typing-inspection", - "version": "0.4.2" + "description": "Backport of PEP 654 (exception groups)", + "license": "MIT License", + "version": "1.3.1" } }, { - "id": "anyio", + "id": "h11", "meta": { - "description": "High-level concurrency and networking framework on top of asyncio or Trio", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-11-30", - "repo_maintainers": [ - "agronholm", - "graingert", - "gschaffner", - "davidbrochart" - ], - "repo_owner": "agronholm", - "repo_stars": 2331, - "repo_topics": [ - "async-await", - "asyncio", - "trio" - ], - "repo_url": "https://github.com/agronholm/anyio", - "version": "4.12.0" + "author": "Nathaniel J. Smith", + "description": "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1", + "license": "MIT License", + "version": "0.16.0" } }, { - "id": "idna", + "id": "httpcore", "meta": { - "description": "Internationalized Domain Names in Applications (IDNA)", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-10-20", - "repo_last_release": "2025-10-12", - "repo_maintainers": [ - "kjd", - "hugovk", - "jdufresne", - "jribbens", - "slingamn" - ], - "repo_owner": "kjd", - "repo_stars": 273, - "repo_topics": [ - "dns", - "hacktoberfest", - "idna", - "python", - "unicode" - ], - "repo_url": "https://github.com/kjd/idna", - "version": "3.11" + "description": "A minimal low-level HTTP client.", + "license": "BSD License", + "version": "1.0.9" } }, { - "id": "certifi", + "id": "httpx", "meta": { - "author": "Kenneth Reitz", - "description": "Python package for providing Mozilla's CA Bundle.", - "license": "MPL-2.0", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-22", - "repo_maintainers": [ - "Lukasa", - "alex", - "sigmavirus24" - ], - "repo_owner": "certifi", - "repo_stars": 943, - "repo_url": "https://github.com/certifi/python-certifi", - "version": "2025.11.12" + "description": "The next generation HTTP client.", + "license": "BSD License", + "version": "0.28.1" } }, { - "id": "httpcore", + "id": "idna", "meta": { - "description": "A minimal low-level HTTP client.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-06", - "repo_last_release": "2025-04-24", - "repo_maintainers": [ - "lovelydinosaur", - "florimondmanca", - "karpetrosyan", - "cdeler" - ], - "repo_owner": "encode", - "repo_stars": 527, - "repo_url": "https://github.com/encode/httpcore", - "version": "1.0.9" + "description": "Internationalized Domain Names in Applications (IDNA)", + "version": "3.11" } }, { - "id": "colorama", + "id": "jiter", "meta": { - "description": "Cross-platform colored terminal text.", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-07-09", - "repo_maintainers": [ - "tartley", - "wiggin15", - "hugovk", - "njsmith", - "jdufresne" - ], - "repo_owner": "tartley", - "repo_stars": 3758, - "repo_url": "https://github.com/tartley/colorama", - "version": "0.4.6" + "description": "Fast iterable JSON parser.", + "license": "MIT License", + "version": "0.12.0" } }, { "id": "openai", "meta": { "description": "The official Python library for the openai API", - "license": "Apache-2.0", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-19", - "repo_last_release": "2025-12-19", - "repo_maintainers": [ - "stainless-bot", - "RobertCraigie", - "hallacy", - "rachellim" - ], - "repo_owner": "openai", - "repo_stars": 29537, - "repo_topics": [ - "openai", - "python" - ], - "repo_url": "https://github.com/openai/openai-python", - "version": "2.14.0" + "license": "Apache Software License", + "version": "2.16.0" } }, { - "id": "distro", + "id": "pydantic", "meta": { - "author": "Nir Cohen", - "description": "Distro - an OS platform information API", - "license": "Apache License, Version 2.0", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-14", - "repo_last_release": "2024-01-14", - "repo_maintainers": [ - "nir0s", - "andy-maier", - "jdufresne", - "HorlogeSkynet", - "SethMichaelLarson" - ], - "repo_owner": "python-distro", - "repo_stars": 279, - "repo_topics": [ - "distribution", - "distro", - "hacktoberfest", - "linux", - "linux-distribution", - "python", - "python-library" - ], - "repo_url": "https://github.com/python-distro/distro", - "version": "1.9.0" + "description": "Data validation using Python type hints", + "version": "2.12.5" } }, { - "id": "pydantic", + "id": "pydantic-core", "meta": { - "description": "Data validation using Python type hints", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-21", - "repo_last_release": "2025-11-26", - "repo_maintainers": [ - "samuelcolvin", - "Viicos", - "sydney-runkle", - "dmontagu" - ], - "repo_owner": "pydantic", - "repo_stars": 26251, - "repo_topics": [ - "hints", - "json-schema", - "parsing", - "pydantic", - "python", - "python310", - "python311", - "python312", - "python313", - "python39", - "validation" - ], - "repo_url": "https://github.com/pydantic/pydantic", - "version": "2.12.5" + "description": "Core functionality for Pydantic validation and serialization", + "version": "2.41.5" } }, { "id": "sniffio", "meta": { "description": "Sniff out which async library your code is running under", - "license": "MIT OR Apache-2.0", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2024-02-25", - "repo_maintainers": [ - "njsmith", - "graingert", - "pquentin", - "agronholm", - "hugovk" - ], - "repo_owner": "python-trio", - "repo_stars": 144, - "repo_topics": [ - "async", - "asyncio", - "python", - "trio" - ], - "repo_url": "https://github.com/python-trio/sniffio", + "license": "Apache Software License", "version": "1.3.1" } }, @@ -383,85 +124,21 @@ "meta": { "description": "Fast, Extensible Progress Meter", "license": "MPL-2.0 AND MIT", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-05-22", - "repo_last_release": "2024-11-27", - "repo_maintainers": [ - "casperdcl", - "lrq3000", - "altendky", - "hadim", - "richardsheridan" - ], - "repo_owner": "tqdm", - "repo_stars": 30795, - "repo_topics": [ - "cli", - "closember", - "console", - "discord", - "gui", - "jupyter", - "keras", - "meter", - "pandas", - "parallel", - "progress", - "progress-bar", - "progressbar", - "progressmeter", - "python", - "rate", - "telegram", - "terminal", - "time", - "utilities" - ], - "repo_url": "https://github.com/tqdm/tqdm", - "version": "4.67.1" + "version": "4.67.2" } }, { "id": "typing-extensions", "meta": { "description": "Backported and Experimental Type Hints for Python 3.9+", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-12-02", - "repo_last_release": "2025-08-25", - "repo_maintainers": [ - "JelleZijlstra", - "AlexWaygood", - "srittau", - "brianschubert", - "Daraan" - ], - "repo_owner": "python", - "repo_stars": 543, - "repo_url": "https://github.com/python/typing_extensions", "version": "4.15.0" } }, { - "id": "annotated-types", + "id": "typing-inspection", "meta": { - "description": "Reusable constraint types to use with typing.Annotated", - "repo_archived": false, - "repo_language": "Python", - "repo_last_commit": "2025-11-10", - "repo_last_release": "2024-05-20", - "repo_maintainers": [ - "adriangb", - "samuelcolvin", - "Zac-HD", - "hugovk", - "Viicos" - ], - "repo_owner": "annotated-types", - "repo_stars": 585, - "repo_url": "https://github.com/annotated-types/annotated-types", - "version": "0.7.0" + "description": "Runtime typing introspection tools", + "version": "0.4.2" } } ], @@ -498,26 +175,6 @@ "from": "openai", "to": "typing-extensions" }, - { - "from": "httpx", - "to": "anyio" - }, - { - "from": "httpx", - "to": "certifi" - }, - { - "from": "httpx", - "to": "httpcore" - }, - { - "from": "httpx", - "to": "idna" - }, - { - "from": "tqdm", - "to": "colorama" - }, { "from": "anyio", "to": "exceptiongroup" @@ -546,29 +203,49 @@ "from": "pydantic", "to": "typing-inspection" }, - { - "from": "httpcore", - "to": "certifi" - }, - { - "from": "httpcore", - "to": "h11" - }, { "from": "exceptiongroup", "to": "typing-extensions" }, { - "from": "annotated-types", + "from": "typing-inspection", "to": "typing-extensions" }, { - "from": "typing-inspection", + "from": "annotated-types", "to": "typing-extensions" }, { "from": "pydantic-core", "to": "typing-extensions" + }, + { + "from": "httpx", + "to": "anyio" + }, + { + "from": "httpx", + "to": "certifi" + }, + { + "from": "httpx", + "to": "httpcore" + }, + { + "from": "httpx", + "to": "idna" + }, + { + "from": "tqdm", + "to": "colorama" + }, + { + "from": "httpcore", + "to": "certifi" + }, + { + "from": "httpcore", + "to": "h11" } ] } diff --git a/examples/real/rspec.json b/examples/real/rspec.json index 8c8bf71..5e8fbe3 100644 --- a/examples/real/rspec.json +++ b/examples/real/rspec.json @@ -1,70 +1,12 @@ { "nodes": [ - { - "id": "rspec-mocks", - "meta": { - "author": "Steven Baker, David Chelimsky, Myron Marston", - "description": "RSpec's 'test double' framework, with support for stubbing and mocking", - "downloads": 1086156206, - "license": "MIT", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-12", - "repo_maintainers": [ - "myronmarston", - "JonRowe", - "dchelimsky", - "alindeman", - "xaviershay" - ], - "repo_owner": "rspec", - "repo_stars": 81, - "repo_url": "https://github.com/rspec/rspec", - "version": "3.13.7" - } - }, - { - "id": "rspec-support", - "meta": { - "author": "David Chelimsky, Myron Marson, Jon Rowe, Sam Phippen, Xaviery Shay, Bradley Schaefer", - "description": "Support utilities for RSpec gems", - "downloads": 1076638938, - "license": "MIT", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-12", - "repo_maintainers": [ - "myronmarston", - "JonRowe", - "dchelimsky", - "alindeman", - "xaviershay" - ], - "repo_owner": "rspec", - "repo_stars": 81, - "repo_url": "https://github.com/rspec/rspec", - "version": "3.13.6" - } - }, { "id": "diff-lcs", "meta": { "author": "Austin Ziegler", "description": "Diff::LCS computes the difference between two Enumerable sequences using the\nMcIlroy-Hunt longest common subsequence (LCS) algorithm. It includes utilities\nto create a simple HTML diff output format and a standard diff-like tool.\n\nThis is release 1.6.1, providing a simple extension that allows for\nDiff::LCS::Change objects to be treated implicitly as arrays and fixes a number\nof formatting issues.\n\nRuby versions below 2.5 are soft-deprecated, which means that older versions are\nno longer part of the CI test suite. If any changes have been introduced that\nbreak those versions, bug reports and patches will be accepted, but it will be\nup to the reporter to verify any fixes prior to release. The next major release\nwill completely break compatibility.", - "downloads": 1116219630, + "downloads": 1128541527, "license": "MIT, Artistic-1.0-Perl, GPL-2.0-or-later", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-01", - "repo_maintainers": [ - "halostatue", - "tiendo1011", - "Annih", - "JonRowe" - ], - "repo_owner": "halostatue", - "repo_stars": 300, - "repo_url": "https://github.com/halostatue/diff-lcs", "version": "1.6.2" } }, @@ -73,21 +15,8 @@ "meta": { "author": "Steven Baker, David Chelimsky, Myron Marston", "description": "BDD for Ruby", - "downloads": 938043191, + "downloads": 946513541, "license": "MIT", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-12", - "repo_maintainers": [ - "myronmarston", - "JonRowe", - "dchelimsky", - "alindeman", - "xaviershay" - ], - "repo_owner": "rspec", - "repo_stars": 81, - "repo_url": "https://github.com/rspec/rspec", "version": "3.13.2" } }, @@ -96,21 +25,8 @@ "meta": { "author": "Steven Baker, David Chelimsky, Chad Humphries, Myron Marston", "description": "BDD for Ruby. RSpec runner and example groups.", - "downloads": 1091901505, + "downloads": 1102951587, "license": "MIT", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-12", - "repo_maintainers": [ - "myronmarston", - "JonRowe", - "dchelimsky", - "alindeman", - "xaviershay" - ], - "repo_owner": "rspec", - "repo_stars": 81, - "repo_url": "https://github.com/rspec/rspec", "version": "3.13.6" } }, @@ -119,23 +35,30 @@ "meta": { "author": "Steven Baker, David Chelimsky, Myron Marston", "description": "rspec-expectations provides a simple, readable API to express expected outcomes of a code example.", - "downloads": 1092953359, + "downloads": 1103861051, "license": "MIT", - "repo_archived": false, - "repo_language": "Ruby", - "repo_last_commit": "2025-12-12", - "repo_maintainers": [ - "myronmarston", - "JonRowe", - "dchelimsky", - "alindeman", - "xaviershay" - ], - "repo_owner": "rspec", - "repo_stars": 81, - "repo_url": "https://github.com/rspec/rspec", "version": "3.13.5" } + }, + { + "id": "rspec-mocks", + "meta": { + "author": "Steven Baker, David Chelimsky, Myron Marston", + "description": "RSpec's 'test double' framework, with support for stubbing and mocking", + "downloads": 1097081204, + "license": "MIT", + "version": "3.13.7" + } + }, + { + "id": "rspec-support", + "meta": { + "author": "David Chelimsky, Myron Marson, Jon Rowe, Sam Phippen, Xaviery Shay, Bradley Schaefer", + "description": "Support utilities for RSpec gems", + "downloads": 1087843559, + "license": "MIT", + "version": "3.13.7" + } } ], "edges": [ @@ -151,10 +74,6 @@ "from": "rspec", "to": "rspec-mocks" }, - { - "from": "rspec-core", - "to": "rspec-support" - }, { "from": "rspec-expectations", "to": "diff-lcs" @@ -170,6 +89,10 @@ { "from": "rspec-mocks", "to": "rspec-support" + }, + { + "from": "rspec-core", + "to": "rspec-support" } ] } diff --git a/examples/real/serde.json b/examples/real/serde.json index 15337f1..4d8359b 100644 --- a/examples/real/serde.json +++ b/examples/real/serde.json @@ -1,183 +1,60 @@ { "nodes": [ { - "id": "serde_core", + "id": "proc-macro2", "meta": { - "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", - "downloads": 57875040, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-09-27", - "repo_maintainers": [ - "dtolnay", - "erickt", - "Mingun", - "oli-obk", - "mitsuhiko" - ], - "repo_owner": "serde-rs", - "repo_stars": 10241, - "repo_topics": [ - "derive", - "no-std", - "rust", - "serde" - ], - "repo_url": "https://github.com/serde-rs/serde", - "version": "1.0.228" + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "downloads": 919778105, + "version": "1.0.106" } }, { - "id": "serde_derive", + "id": "quote", "meta": { - "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", - "downloads": 694132674, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-09-27", - "repo_maintainers": [ - "dtolnay", - "erickt", - "Mingun", - "oli-obk", - "mitsuhiko" - ], - "repo_owner": "serde-rs", - "repo_stars": 10241, - "repo_topics": [ - "derive", - "no-std", - "rust", - "serde" - ], - "repo_url": "https://github.com/serde-rs/serde", + "description": "Quasi-quoting macro quote!(...)", + "downloads": 899531495, + "version": "1.0.44" + } + }, + { + "id": "serde", + "meta": { + "description": "A generic serialization/deserialization framework", + "downloads": 798236839, "version": "1.0.228" } }, { - "id": "proc-macro2", + "id": "serde_core", "meta": { - "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", - "downloads": 862284054, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-20", - "repo_last_release": "2025-10-23", - "repo_maintainers": [ - "dtolnay", - "alexcrichton", - "mystor", - "bjorn3", - "SergioBenitez" - ], - "repo_owner": "dtolnay", - "repo_stars": 885, - "repo_url": "https://github.com/dtolnay/proc-macro2", - "version": "1.0.103" + "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", + "downloads": 89424375, + "version": "1.0.228" } }, { - "id": "quote", + "id": "serde_derive", "meta": { - "description": "Quasi-quoting macro quote!(...)", - "downloads": 843664819, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-20", - "repo_last_release": "2025-11-06", - "repo_maintainers": [ - "dtolnay", - "mystor", - "alexcrichton", - "dishmaker", - "SimonSapin" - ], - "repo_owner": "dtolnay", - "repo_stars": 1503, - "repo_topics": [ - "proc-macro", - "rust", - "syn" - ], - "repo_url": "https://github.com/dtolnay/quote", - "version": "1.0.42" + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", + "downloads": 740018551, + "version": "1.0.228" } }, { "id": "syn", "meta": { "description": "Parser for Rust source code", - "downloads": 1251312086, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-20", - "repo_last_release": "2025-11-23", - "repo_maintainers": [ - "dtolnay", - "mystor", - "alexcrichton", - "taiki-e", - "CAD97" - ], - "repo_owner": "dtolnay", - "repo_stars": 3245, - "repo_topics": [ - "proc-macro" - ], - "repo_url": "https://github.com/dtolnay/syn", - "version": "2.0.111" + "downloads": 1331315986, + "version": "2.0.114" } }, { "id": "unicode-ident", "meta": { "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", - "downloads": 665270247, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-21", - "repo_last_release": "2025-10-30", - "repo_maintainers": [ - "dtolnay", - "Marcondiro", - "Jake-Shadle", - "sorairolake" - ], - "repo_owner": "dtolnay", - "repo_stars": 104, - "repo_url": "https://github.com/dtolnay/unicode-ident", + "downloads": 715962350, "version": "1.0.22" } - }, - { - "id": "serde", - "meta": { - "description": "A generic serialization/deserialization framework", - "downloads": 750943583, - "repo_archived": false, - "repo_language": "Rust", - "repo_last_commit": "2025-12-22", - "repo_last_release": "2025-09-27", - "repo_maintainers": [ - "dtolnay", - "erickt", - "Mingun", - "oli-obk", - "mitsuhiko" - ], - "repo_owner": "serde-rs", - "repo_stars": 10241, - "repo_topics": [ - "derive", - "no-std", - "rust", - "serde" - ], - "repo_url": "https://github.com/serde-rs/serde", - "version": "1.0.228" - } } ], "edges": [ diff --git a/examples/real/yargs.json b/examples/real/yargs.json index 0f63106..e884961 100644 --- a/examples/real/yargs.json +++ b/examples/real/yargs.json @@ -1,66 +1,39 @@ { "nodes": [ { - "id": "yargs", + "id": "ansi-regex", "meta": { - "description": "yargs the modern, pirate-themed, successor to optimist.", + "author": "Sindre Sorhus", + "description": "Regular expression for matching ANSI escape codes", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-12-20", - "repo_last_release": "2025-05-27", - "repo_maintainers": [ - "bcoe", - "maxrimue", - "nexdrew", - "mleguen" - ], - "repo_owner": "yargs", - "repo_stars": 11410, - "repo_url": "https://github.com/yargs/yargs", - "version": "18.0.0" + "version": "6.2.2" } }, { - "id": "y18n", + "id": "ansi-styles", "meta": { - "author": "Ben Coe", - "description": "the bare-bones internationalization library used by yargs", - "license": "ISC", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-12-15", - "repo_last_release": "2021-04-07", - "repo_maintainers": [ - "greenkeeperio-bot", - "maxrimue", - "bcoe" - ], - "repo_owner": "yargs", - "repo_stars": 151, - "repo_url": "https://github.com/yargs/y18n", - "version": "5.0.8" + "author": "Sindre Sorhus", + "description": "ANSI escape codes for styling strings in the terminal", + "license": "MIT", + "version": "6.2.3" } }, { - "id": "yargs-parser", + "id": "cliui", "meta": { "author": "Ben Coe", - "description": "the mighty option parser used by yargs", + "description": "easily create complex multi-column command-line-interfaces", "license": "ISC", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-12-15", - "repo_last_release": "2025-05-26", - "repo_maintainers": [ - "bcoe", - "elas7", - "juergba" - ], - "repo_owner": "yargs", - "repo_stars": 520, - "repo_url": "https://github.com/yargs/yargs-parser", - "version": "22.0.0" + "version": "9.0.1" + } + }, + { + "id": "escalade", + "meta": { + "author": "Luke Edwards", + "description": "A tiny (183B to 210B) and fast utility to ascend parent directories", + "license": "MIT", + "version": "3.2.0" } }, { @@ -69,19 +42,6 @@ "author": "Stefan Penner", "description": "[![Build Status](https://travis-ci.org/stefanpenner/get-caller-file.svg?branch=master)](https://travis-ci.org/stefanpenner/get-caller-file) [![Build status](https://ci.appveyor.com/api/projects/status/ol2q94g1932cy14a/branch/master?svg=true)](https://ci.a", "license": "ISC", - "repo_archived": false, - "repo_language": "TypeScript", - "repo_last_commit": "2023-09-27", - "repo_maintainers": [ - "stefanpenner", - "zomarg", - "mportuga", - "locks", - "dyniper" - ], - "repo_owner": "stefanpenner", - "repo_stars": 37, - "repo_url": "https://github.com/stefanpenner/get-caller-file", "version": "2.0.5" } }, @@ -91,215 +51,87 @@ "author": "Sindre Sorhus", "description": "Determine the East Asian Width of a Unicode character", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-09-11", - "repo_last_release": "2025-09-09", - "repo_maintainers": [ - "sindresorhus", - "fisker" - ], - "repo_owner": "sindresorhus", - "repo_stars": 43, - "repo_url": "https://github.com/sindresorhus/get-east-asian-width", "version": "1.4.0" } }, { - "id": "wrap-ansi", + "id": "string-width", "meta": { "author": "Sindre Sorhus", - "description": "Wordwrap a string with ANSI escape codes", + "description": "Get the visual width of a string - the number of columns required to display it", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-09-08", - "repo_last_release": "2025-09-08", - "repo_maintainers": [ - "sindresorhus", - "bcoe", - "SamVerschueren", - "coreyfarrell", - "kevva" - ], - "repo_owner": "chalk", - "repo_stars": 130, - "repo_url": "https://github.com/chalk/wrap-ansi", - "version": "9.0.2" + "version": "8.1.1" } }, { - "id": "ansi-styles", + "id": "strip-ansi", "meta": { "author": "Sindre Sorhus", - "description": "ANSI escape codes for styling strings in the terminal", + "description": "Strip ANSI escape codes from a string", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-10-01", - "repo_last_release": "2025-09-08", - "repo_maintainers": [ - "sindresorhus", - "Qix-", - "Richienb", - "arthurvr", - "ExE-Boss" - ], - "repo_owner": "chalk", - "repo_stars": 447, - "repo_topics": [ - "chalk" - ], - "repo_url": "https://github.com/chalk/ansi-styles", - "version": "6.2.3" + "version": "7.1.2" } }, { - "id": "string-width", + "id": "wrap-ansi", "meta": { "author": "Sindre Sorhus", - "description": "Get the visual width of a string - the number of columns required to display it", + "description": "Wordwrap a string with ANSI escape codes", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-09-01", - "repo_last_release": "2025-09-01", - "repo_maintainers": [ - "sindresorhus", - "fisker", - "coreyfarrell", - "BendingBender", - "adam2k" - ], - "repo_owner": "sindresorhus", - "repo_stars": 513, - "repo_url": "https://github.com/sindresorhus/string-width", - "version": "8.1.0" + "version": "9.0.2" } }, { - "id": "cliui", + "id": "y18n", "meta": { "author": "Ben Coe", - "description": "easily create complex multi-column command-line-interfaces", + "description": "the bare-bones internationalization library used by yargs", "license": "ISC", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-12-15", - "repo_last_release": "2025-03-17", - "repo_maintainers": [ - "bcoe", - "greenkeeperio-bot", - "coreyfarrell", - "nexdrew" - ], - "repo_owner": "yargs", - "repo_stars": 383, - "repo_url": "https://github.com/yargs/cliui", - "version": "9.0.1" - } - }, - { - "id": "escalade", - "meta": { - "author": "Luke Edwards", - "description": "A tiny (183B to 210B) and fast utility to ascend parent directories", - "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2024-08-29", - "repo_last_release": "2024-08-29", - "repo_maintainers": [ - "lukeed", - "ljharb", - "NMinhNguyen", - "triannguyen" - ], - "repo_owner": "lukeed", - "repo_stars": 153, - "repo_url": "https://github.com/lukeed/escalade", - "version": "3.2.0" + "version": "5.0.8" } }, { - "id": "strip-ansi", + "id": "yargs", "meta": { - "author": "Sindre Sorhus", - "description": "Strip ANSI escape codes from a string", + "description": "yargs the modern, pirate-themed, successor to optimist.", "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-09-08", - "repo_last_release": "2025-09-08", - "repo_maintainers": [ - "sindresorhus", - "jbnicolai", - "LitoMore", - "rmg", - "kevva" - ], - "repo_owner": "chalk", - "repo_stars": 430, - "repo_url": "https://github.com/chalk/strip-ansi", - "version": "7.1.2" + "version": "18.0.0" } }, { - "id": "ansi-regex", + "id": "yargs-parser", "meta": { - "author": "Sindre Sorhus", - "description": "Regular expression for matching ANSI escape codes", - "license": "MIT", - "repo_archived": false, - "repo_language": "JavaScript", - "repo_last_commit": "2025-09-08", - "repo_last_release": "2025-09-08", - "repo_maintainers": [ - "sindresorhus", - "Qix-", - "Akim95", - "arjunmehta", - "llimllib" - ], - "repo_owner": "chalk", - "repo_stars": 198, - "repo_url": "https://github.com/chalk/ansi-regex", - "version": "6.2.2" + "author": "Ben Coe", + "description": "the mighty option parser used by yargs", + "license": "ISC", + "version": "22.0.0" } } ], "edges": [ { "from": "yargs", - "to": "string-width" + "to": "cliui" }, { "from": "yargs", - "to": "y18n" + "to": "escalade" }, { "from": "yargs", - "to": "yargs-parser" + "to": "get-caller-file" }, { "from": "yargs", - "to": "cliui" + "to": "string-width" }, { "from": "yargs", - "to": "escalade" + "to": "y18n" }, { "from": "yargs", - "to": "get-caller-file" - }, - { - "from": "string-width", - "to": "get-east-asian-width" - }, - { - "from": "string-width", - "to": "strip-ansi" + "to": "yargs-parser" }, { "from": "cliui", @@ -328,6 +160,14 @@ { "from": "wrap-ansi", "to": "string-width" + }, + { + "from": "string-width", + "to": "strip-ansi" + }, + { + "from": "string-width", + "to": "get-east-asian-width" } ] } diff --git a/go.mod b/go.mod index fd09b2e..a5ff5ba 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/matzehuels/stacktower -go 1.24.11 +go 1.25.6 require ( github.com/BurntSushi/toml v1.5.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/log v0.4.2 github.com/goccy/go-graphviz v0.2.9 github.com/spf13/cobra v1.10.1 @@ -12,11 +14,11 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/disintegration/imaging v1.6.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/flopp/go-findfont v0.1.0 // indirect github.com/fogleman/gg v1.3.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -24,7 +26,10 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -32,6 +37,6 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/image v0.33.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index 26acffa..f7364bb 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,22 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= @@ -21,6 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= @@ -37,8 +45,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -65,9 +79,10 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/internal/cli/auth.go b/internal/cli/auth.go new file mode 100644 index 0000000..bbca83d --- /dev/null +++ b/internal/cli/auth.go @@ -0,0 +1,245 @@ +package cli + +import ( + "context" + "fmt" + "net/url" + "os" + "os/exec" + "runtime" + "time" + + "github.com/spf13/cobra" + + "github.com/matzehuels/stacktower/pkg/integrations/github" + "github.com/matzehuels/stacktower/pkg/session" +) + +// sessionTTL is the duration for CLI sessions (30 days). +const sessionTTL = 30 * 24 * time.Hour + +// githubCommand creates the github command with subcommands. +func (c *CLI) githubCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "github", + Short: "GitHub integration commands", + Long: `Authenticate with GitHub and interact with your repositories. + +Use the device flow to authenticate without needing a web browser callback. +Your session is stored in ~/.config/stacktower/sessions/`, + } + + cmd.AddCommand(c.githubLoginCommand()) + cmd.AddCommand(c.githubLogoutCommand()) + cmd.AddCommand(c.githubWhoamiCommand()) + + return cmd +} + +// githubLoginCommand creates the login subcommand. +func (c *CLI) githubLoginCommand() *cobra.Command { + return &cobra.Command{ + Use: "login", + Short: "Authenticate with GitHub using device flow", + Long: `Start the GitHub device authorization flow. + +You'll be given a code to enter at https://github.com/login/device. +Once authorized, your session will be saved locally for future commands.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + if existing, _ := loadGitHubSession(ctx); existing != nil { + printInfo("Already logged in as @%s", existing.User.Login) + printDetail("Run 'stacktower github logout' first to re-authenticate") + return nil + } + + _, err := c.runGitHubLogin(ctx) + return err + }, + } +} + +// githubLogoutCommand creates the logout subcommand. +func (c *CLI) githubLogoutCommand() *cobra.Command { + return &cobra.Command{ + Use: "logout", + Short: "Remove stored GitHub credentials", + RunE: func(cmd *cobra.Command, args []string) error { + if err := deleteGitHubSession(cmd.Context()); err != nil { + return fmt.Errorf("delete session: %w", err) + } + printSuccess("Logged out") + return nil + }, + } +} + +// githubWhoamiCommand creates the whoami subcommand. +func (c *CLI) githubWhoamiCommand() *cobra.Command { + return &cobra.Command{ + Use: "whoami", + Short: "Show the currently authenticated GitHub user", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + sess, err := loadGitHubSession(ctx) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + spinner := newSpinnerWithContext(ctx, "Verifying session...") + spinner.Start() + + client := github.NewContentClient(sess.AccessToken) + user, err := client.FetchUser(ctx) + if err != nil { + spinner.StopWithError("Session invalid") + return fmt.Errorf("verify session: %w", err) + } + spinner.Stop() + + printSuccess("GitHub Session") + printKeyValue("Username", "@"+user.Login) + if user.Name != "" { + printKeyValue("Name", user.Name) + } + if user.Email != "" { + printKeyValue("Email", user.Email) + } + printKeyValue("Logged in", sess.CreatedAt.Format("Jan 2, 2006")) + printKeyValue("Expires", sess.ExpiresAt.Format("Jan 2, 2006")) + + return nil + }, + } +} + +// ============================================================================= +// Session Management +// ============================================================================= + +// loadGitHubSession loads the GitHub session from disk. +func loadGitHubSession(ctx context.Context) (*session.Session, error) { + store, err := session.NewCLIStore() + if err != nil { + return nil, fmt.Errorf("open session store: %w", err) + } + + sess, err := store.GetSession(ctx) + if err != nil { + return nil, fmt.Errorf("get session: %w", err) + } + if sess == nil { + return nil, fmt.Errorf("not logged in (run 'stacktower github login' first)") + } + + return sess, nil +} + +func saveGitHubSession(ctx context.Context, token *github.OAuthToken, user *github.User) (*session.Session, error) { + store, err := session.NewCLIStore() + if err != nil { + return nil, fmt.Errorf("open session store: %w", err) + } + + sess, err := session.New(token.AccessToken, user, sessionTTL) + if err != nil { + return nil, fmt.Errorf("create session: %w", err) + } + + if err := store.SaveSession(ctx, sess); err != nil { + return nil, fmt.Errorf("save session: %w", err) + } + + return sess, nil +} + +func deleteGitHubSession(ctx context.Context) error { + store, err := session.NewCLIStore() + if err != nil { + return fmt.Errorf("open session store: %w", err) + } + return store.DeleteSession(ctx) +} + +// ============================================================================= +// Device Flow Login +// ============================================================================= + +func (c *CLI) runGitHubLogin(ctx context.Context) (*session.Session, error) { + clientID := os.Getenv("GITHUB_CLIENT_ID") + if clientID == "" { + clientID = github.DefaultClientID + } + + oauthClient := github.NewOAuthClient(github.OAuthConfig{ClientID: clientID}) + + loginCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + deviceResp, err := oauthClient.RequestDeviceCode(loginCtx) + if err != nil { + return nil, fmt.Errorf("request device code: %w", err) + } + + printNewline() + fmt.Println(StyleTitle.Render("GitHub Device Authorization")) + printNewline() + printKeyValue("Code", StyleNumber.Render(deviceResp.UserCode)) + printKeyValue("URL", StyleLink.Render(deviceResp.VerificationURI)) + printNewline() + + if err := openBrowser(deviceResp.VerificationURI); err != nil { + printDetail("Copy the URL above and paste it in your browser") + } else { + printDetail("Opening browser...") + } + printInline("Waiting for authorization...") + + token, err := oauthClient.PollForToken(loginCtx, deviceResp.DeviceCode, deviceResp.Interval) + if err != nil { + fmt.Println() + return nil, fmt.Errorf("authorization failed: %w", err) + } + + contentClient := github.NewContentClient(token.AccessToken) + user, err := contentClient.FetchUser(loginCtx) + if err != nil { + return nil, fmt.Errorf("fetch user: %w", err) + } + + sess, err := saveGitHubSession(ctx, token, user) + if err != nil { + return nil, fmt.Errorf("save session: %w", err) + } + + fmt.Println() + printSuccess("Logged in as @%s", user.Login) + + return sess, nil +} + +func openBrowser(rawURL string) error { + parsed, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + if parsed.Scheme != "https" && parsed.Scheme != "http" { + return fmt.Errorf("URL scheme must be http or https, got %q", parsed.Scheme) + } + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", rawURL) + case "linux": + cmd = exec.Command("xdg-open", rawURL) + case "windows": + cmd = exec.Command("cmd", "/c", "start", rawURL) + default: + return fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } + return cmd.Start() +} diff --git a/internal/cli/cache.go b/internal/cli/cache.go index 01bdff5..67c515c 100644 --- a/internal/cli/cache.go +++ b/internal/cli/cache.go @@ -8,85 +8,84 @@ import ( "github.com/spf13/cobra" ) -// newCacheCmd creates the cache management command with subcommands for clearing and inspecting the cache. -// The cache stores HTTP responses from package registries to reduce network calls and improve performance. -func newCacheCmd() *cobra.Command { +// cacheCommand creates the cache management command. +func (c *CLI) cacheCommand() *cobra.Command { cmd := &cobra.Command{ Use: "cache", Short: "Manage the HTTP response cache", } - cmd.AddCommand(newCacheClearCmd()) - cmd.AddCommand(newCachePathCmd()) + cmd.AddCommand(c.cacheClearCommand()) + cmd.AddCommand(c.cachePathCommand()) return cmd } -// newCacheClearCmd creates the "cache clear" subcommand. -// It removes all non-directory files from the cache directory. -// If the cache directory does not exist, the command prints "Cache is empty" and succeeds. -// Failed removals are silently skipped; only successful deletions are counted. -func newCacheClearCmd() *cobra.Command { +// cacheClearCommand creates the "cache clear" subcommand. +func (c *CLI) cacheClearCommand() *cobra.Command { return &cobra.Command{ Use: "clear", Short: "Clear all cached HTTP responses", RunE: func(cmd *cobra.Command, args []string) error { dir, err := cacheDir() if err != nil { - return err + return fmt.Errorf("get cache dir: %w", err) } - entries, err := os.ReadDir(dir) - if os.IsNotExist(err) { - fmt.Println("Cache is empty") + if _, err := os.Stat(dir); os.IsNotExist(err) { + printInfo("Cache is empty") return nil } - if err != nil { - return err - } count := 0 - for _, entry := range entries { - if !entry.IsDir() { - if err := os.Remove(filepath.Join(dir, entry.Name())); err == nil { + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Skip errors, continue walking + } + if path == dir { + return nil + } + if !info.IsDir() { + if err := os.Remove(path); err == nil { count++ } } + return nil + }) + if err != nil { + return err } - fmt.Printf("Cleared %d cached entries from %s\n", count, dir) + // Clean up empty subdirectories + _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil || path == dir { + return nil + } + if info.IsDir() { + os.Remove(path) + } + return nil + }) + + printSuccess("Cleared %d cached entries", count) + printDetail("Directory: %s", dir) return nil }, } } -// newCachePathCmd creates the "cache path" subcommand. -// It prints the absolute path to the cache directory. -// The directory may not exist if no cached responses have been stored yet. -func newCachePathCmd() *cobra.Command { +// cachePathCommand creates the "cache path" subcommand. +func (c *CLI) cachePathCommand() *cobra.Command { return &cobra.Command{ Use: "path", Short: "Print the cache directory path", RunE: func(cmd *cobra.Command, args []string) error { dir, err := cacheDir() if err != nil { - return err + return fmt.Errorf("get cache dir: %w", err) } fmt.Println(dir) return nil }, } } - -// cacheDir returns the absolute path to the stacktower cache directory. -// The directory is located at $HOME/.cache/stacktower and follows XDG conventions. -// The directory is created on-demand by the deps package; this function only computes the path. -// -// It returns an error if the user's home directory cannot be determined. -func cacheDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, ".cache", "stacktower"), nil -} diff --git a/internal/cli/cache_test.go b/internal/cli/cache_test.go deleted file mode 100644 index e316631..0000000 --- a/internal/cli/cache_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package cli - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestCacheDir(t *testing.T) { - dir, err := cacheDir() - if err != nil { - t.Fatalf("cacheDir() error: %v", err) - } - - if dir == "" { - t.Error("cacheDir() returned empty string") - } - - // Should be under home directory - home, _ := os.UserHomeDir() - if !strings.HasPrefix(dir, home) { - t.Errorf("cacheDir() = %q, should be under home %q", dir, home) - } - - // Should end with "stacktower" - if !strings.HasSuffix(dir, "stacktower") { - t.Errorf("cacheDir() = %q, should end with 'stacktower'", dir) - } - - // Should contain ".cache" in path - if !strings.Contains(dir, ".cache") { - t.Errorf("cacheDir() = %q, should contain '.cache'", dir) - } -} - -func TestCacheDirStructure(t *testing.T) { - dir, err := cacheDir() - if err != nil { - t.Fatalf("cacheDir() error: %v", err) - } - - // Verify the expected structure: $HOME/.cache/stacktower - home, _ := os.UserHomeDir() - expected := filepath.Join(home, ".cache", "stacktower") - if dir != expected { - t.Errorf("cacheDir() = %q, want %q", dir, expected) - } -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..8661d9d --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,147 @@ +// Package cli implements the stacktower command-line interface. +package cli + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/log" + "github.com/spf13/cobra" + + "github.com/matzehuels/stacktower/pkg/buildinfo" + "github.com/matzehuels/stacktower/pkg/cache" + "github.com/matzehuels/stacktower/pkg/pipeline" +) + +// ============================================================================= +// Constants +// ============================================================================= + +const ( + // appName is the application name used for directories and display. + appName = "stacktower" + + // defaultOrderTimeout is the default timeout for optimal ordering search (seconds). + defaultOrderTimeout = 60 +) + +// Log levels exported for use in main.go. +const ( + LogDebug = log.DebugLevel + LogInfo = log.InfoLevel +) + +// ============================================================================= +// CLI - Central CLI State +// ============================================================================= + +// CLI holds shared state for all commands. +type CLI struct { + Logger *log.Logger +} + +// New creates a new CLI instance with a default logger. +func New(w io.Writer, level log.Level) *CLI { + return &CLI{ + Logger: log.NewWithOptions(w, log.Options{ + ReportTimestamp: true, + TimeFormat: "15:04:05.00", + Level: level, + }), + } +} + +// SetLogLevel updates the logger's level. +func (c *CLI) SetLogLevel(level log.Level) { + c.Logger.SetLevel(level) +} + +// RootCommand creates the root cobra command with all subcommands registered. +func (c *CLI) RootCommand() *cobra.Command { + root := &cobra.Command{ + Use: "stacktower", + Short: "Stacktower visualizes dependency graphs as towers", + Long: `Stacktower is a CLI tool for visualizing complex dependency graphs as tiered tower structures, making it easier to understand layering and flow.`, + Version: buildinfo.Version, + SilenceUsage: true, + } + + root.SetVersionTemplate(buildinfo.Template()) + + // Register all subcommands + root.AddCommand(c.parseCommand()) + root.AddCommand(c.layoutCommand()) + root.AddCommand(c.visualizeCommand()) + root.AddCommand(c.renderCommand()) + root.AddCommand(c.cacheCommand()) + root.AddCommand(c.pqtreeCommand()) + root.AddCommand(c.githubCommand()) + root.AddCommand(c.completionCommand()) + + return root +} + +// ============================================================================= +// Runner Factory +// ============================================================================= + +// newRunner creates a pipeline runner for CLI use. +func (c *CLI) newRunner(noCache bool) (*pipeline.Runner, error) { + cache, err := newCache(noCache) + if err != nil { + return nil, err + } + return pipeline.NewRunner(cache, nil, c.Logger), nil +} + +func newCache(noCache bool) (cache.Cache, error) { + if noCache { + return cache.NewNullCache(), nil + } + dir, err := cacheDir() + if err != nil { + return cache.NewNullCache(), nil + } + return cache.NewFileCache(dir) +} + +// ============================================================================= +// Paths +// ============================================================================= + +// cacheDir returns the cache directory using XDG standard (~/.cache/stacktower/). +func cacheDir() (string, error) { + if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" { + return filepath.Join(cacheHome, appName), nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".cache", appName), nil +} + +// ============================================================================= +// Options Helpers +// ============================================================================= + +// setCLIDefaults applies CLI-specific defaults on top of pipeline defaults. +func setCLIDefaults(opts *pipeline.Options) { + opts.SetLayoutDefaults() + opts.SetRenderDefaults() + // CLI-specific preferences (override pipeline defaults) + opts.Randomize = true + opts.Merge = true + opts.Normalize = true + opts.Popups = true +} + +// parseFormats parses a comma-separated format string into a slice. +func parseFormats(s string) []string { + if s == "" { + return []string{pipeline.FormatSVG} + } + return strings.Split(s, ",") +} diff --git a/internal/cli/completion.go b/internal/cli/completion.go new file mode 100644 index 0000000..a37d1b3 --- /dev/null +++ b/internal/cli/completion.go @@ -0,0 +1,69 @@ +package cli + +import ( + "os" + + "github.com/spf13/cobra" +) + +// completionCommand creates the completion command for generating shell completions. +func (c *CLI) completionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion scripts", + Long: `Generate shell completion scripts for stacktower. + +To load completions: + +Bash: + $ source <(stacktower completion bash) + + # To load completions for each session, execute once: + # Linux: + $ stacktower completion bash > /etc/bash_completion.d/stacktower + # macOS: + $ stacktower completion bash > $(brew --prefix)/etc/bash_completion.d/stacktower + +Zsh: + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ stacktower completion zsh > "${fpath[1]}/_stacktower" + + # You will need to start a new shell for this setup to take effect. + +Fish: + $ stacktower completion fish | source + + # To load completions for each session, execute once: + $ stacktower completion fish > ~/.config/fish/completions/stacktower.fish + +PowerShell: + PS> stacktower completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> stacktower completion powershell > stacktower.ps1 + # and source this file from your PowerShell profile. +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + return nil + }, + } + + return cmd +} diff --git a/internal/cli/layout.go b/internal/cli/layout.go new file mode 100644 index 0000000..a0d85a6 --- /dev/null +++ b/internal/cli/layout.go @@ -0,0 +1,113 @@ +package cli + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + + "github.com/matzehuels/stacktower/pkg/graph" + "github.com/matzehuels/stacktower/pkg/pipeline" +) + +// layoutCommand creates the layout command for computing visualization layouts. +func (c *CLI) layoutCommand() *cobra.Command { + var ( + output string + noCache bool + orderTimeout int + ) + opts := pipeline.Options{} + setCLIDefaults(&opts) + + cmd := &cobra.Command{ + Use: "layout [graph.json]", + Short: "Compute visualization layout from a dependency graph", + Long: `Compute visualization layout from a dependency graph. + +The layout command takes a graph.json file (produced by 'parse') and computes +the layout for visualization. The output is a layout.json file (same format as +'render -f json') that can be rendered to SVG/PNG/PDF using the 'visualize' command. + +Supports both tower (-t tower) and nodelink (-t nodelink) visualization types. + +Results are cached locally for faster subsequent runs.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return c.runLayout(cmd.Context(), args[0], opts, output, noCache, orderTimeout) + }, + } + + // Common flags + cmd.Flags().StringVarP(&output, "output", "o", "", "output file (default: