what-stack
Universal Rust library for detecting project roots and technology stacks from generic inputs such as image names, project directories, process names, executable paths, and command-line arguments.
Goals
- Standalone library API with no PortLens, socket, Docker-client, or CLI types.
- Zero default runtime dependencies on Windows; Unix uses
libconly for robust home-directory lookup. - No async runtime, regex engine, logging facade, serde model, or subprocesses.
- Best-effort detection that prefers explicit evidence over port-number guesses.
Install
[]
= "0.1"
Quick Start
use Path;
use ;
// One-off detection from strings. Every label carries a kind.
let postgres = detect_from_image.expect;
assert_eq!;
assert_eq!;
assert_eq!;
// Cached detection for a scan of many processes. The home ceiling defaults
// to `what_stack::home_dir()`; use `StackDetector::with_home` to override it.
let mut detector = new;
let root = detector.detect_project_root;
let label = detector.detect_stack;
if let Some = label
// Drop cached filesystem results before the next scan.
detector.clear;
API
| Item | Purpose |
|---|---|
StackLabel |
Label text plus StackKind. as_str(), kind(), Display, AsRef<str>, == "text", into_cow(). |
StackKind |
Runtime, Framework, Tool, Database, Service (non-exhaustive). |
StackDetector |
Cached detection. new() / Default (home ceiling from home_dir()), with_home(Option<PathBuf>), detect_project_root, detect_stack, clear, home. |
StackInput |
StackInput::new(process_name) with .image(), .project_root(), .exe_name(), .exe_path(). |
ProjectInput |
ProjectInput::new() with .cwd(), .exe(), .cmd(). |
detect_from_image |
Container or artifact image name. |
detect_from_process |
Process executable name. |
detect_from_process_names |
Process name with an executable-name fallback. |
detect_from_config |
Config files in one project-root directory. |
find_project_root |
Upward marker walk from one directory, with an optional home ceiling. |
resolve_project_root |
Uncached cwd, executable, and argument fallback walk. |
project_name |
Display name from a project-root path. |
home_dir |
Current user's home directory (sudo-aware on Unix). |
MAX_WALK_DEPTH |
Maximum directories tested per upward walk. |
Setters accept either a value or an Option, so .exe_path(path) and .exe_path(maybe_path) both work.
Detection Sources
what-stack detects stacks from:
- Image names, for example
postgres:16,redis/redis-stack,oven/bun,php:8.3-apache, ormcr.microsoft.com/dotnet/aspnet. Most language runtime images (node,python,php) match only their exact name. Database and service images, plus theopenjdk,eclipse-temurin, anddotnetruntime images, match a name prefix followed by the end of the name or a separator (redis-sentinelis Redis,redisinsightis not), and companion images such aspostgres-exporter,opensearch-dashboards, ormysql-workbenchare not labeled as the service. - Project config files, for example
next.config.mjs,vite.config.ts,Cargo.toml,go.mod,pom.xml,build.gradle,composer.json(withartisanfor Laravel),deno.json, and.csproj. Ruby projects needGemfileplusconfig.ru(Rack), and alsobin/railsfor Rails. - Python entry and dependency files, including Django, Flask, FastAPI, Starlette, Litestar, and generic Python fallback. Files are read up to 64 KiB, only when they are regular files; UTF-16 files with a byte-order mark and invalid UTF-8 are tolerated.
- Process names, including common runtimes, databases, web servers, search services, message brokers, and dev tools. Runtime version suffixes (
python3.12,php-fpm8.2) and titles truncated by Linux (next-server (v1,gunicorn: maste) are recognized. - Project markers found by walking upward from cwd, executable parent, or absolute command-line argument paths:
package.json,bun.lock,bun.lockb,deno.json,deno.jsonc,Cargo.toml,go.mod,go.work,pyproject.toml,requirements.txt,setup.py,Pipfile,pom.xml,build.gradle,build.gradle.kts,composer.json,Gemfile,mix.exs,.csproj, and.fsproj.
StackDetector::detect_stack uses this priority:
- Image name.
- Process name, when its label is final: a framework (
Rails), database (PostgreSQL), or service (Nginx). - Project config, when the process label is a runtime or tool (
Node.js,Vite), or when the process is unknown but its executable is inside the project. - Process name (runtime or tool).
So node in a Next.js folder is Next.js, while redis-server started from the same folder stays Redis. Config labels follow the process's language ecosystem: php in a Laravel project with vite.config.js is Laravel while node there is Vite, node next to deno.json stays Node.js, and gunicorn in a Python project with no recognized framework stays Gunicorn. There is no well-known-port fallback.
Config detection also follows the process ecosystem: a known runtime or tool accepts only config labels from its own ecosystem. In a Laravel project with vite.config.js, php is Laravel while node is Vite; python in a Next.js folder stays Python. An unknown process, and detect_from_config, use every rule in a fixed order.
Development
Install Rust stable and the supported lint targets:
Install local hooks:
.\scripts\install-hooks.ps1
Quality Gates
| Gate | Command | Purpose |
|---|---|---|
| 1 | cargo fmt --all -- --check |
Formatting |
| 2 | cargo clippy --locked --all-targets -- -D warnings |
Lints |
| 3 | cargo test --locked --lib --tests && cargo test --locked --doc |
Tests |
| 4 | cargo bench --locked --no-run |
Benchmarks compile |
| 5 | cargo build --locked |
Build |
| 6 | cargo doc --locked --no-deps |
Documentation |
| 7 | cargo deny check |
Dependency audit |
Benchmarks
This crate uses Gungraun for deterministic instruction-count benchmarks.
Contributing
See CONTRIBUTING.md.
License
MIT - Copyright (c) 2026 Ehsan Khan