Skip to main content

what_stack/
lib.rs

1//! Detect project roots and technology stacks from generic process metadata.
2//!
3//! `what-stack` is a small standalone library. It does not depend on Docker,
4//! socket collection, async runtimes, logging, serialization, CLI parsing, or
5//! any application-specific types. Callers pass ordinary strings and paths:
6//! image names, project directories, process names, executable paths, working
7//! directories, and command-line arguments.
8//!
9//! # Detection Model
10//!
11//! The crate exposes focused single-purpose functions plus [`StackDetector`]
12//! for repeated lookups with caching:
13//!
14//! - [`detect_from_image`] parses container or artifact image names.
15//! - [`detect_from_config`] scans one project-root directory for known project
16//!   files.
17//! - [`detect_from_process`] maps known runtime, server, database, and tool
18//!   executable names; [`detect_from_process_names`] adds an executable-name
19//!   fallback.
20//! - [`find_project_root`] walks upward from one directory until it finds a
21//!   project marker.
22//! - [`resolve_project_root`] applies the project-root fallback order used by
23//!   process collectors: current working directory, executable parent, then
24//!   absolute command-line argument parents.
25//! - [`StackDetector`] combines those rules and caches filesystem results.
26//!
27//! Every detected [`StackLabel`] carries a [`StackKind`] (runtime, framework,
28//! tool, database, or service).
29//!
30//! High-level stack detection in [`StackDetector::detect_stack`] uses this
31//! priority:
32//!
33//! 1. Image label.
34//! 2. Process label, when it is final (framework, database, or service).
35//! 3. Project config label.
36//! 4. Process label (runtime or tool).
37//!
38//! Config labels are guarded: a project config is used only when the process
39//! is a known runtime or tool, or when the process is unknown but its
40//! executable belongs to the project. This keeps a `postgres` or
41//! `nginx` process started from a Next.js folder labeled as itself, and keeps
42//! unrelated helper shells from inheriting a project's framework label just
43//! because their working directory happens to be inside that project.
44//!
45//! Config labels are also ecosystem-aware: a known runtime or tool accepts
46//! only config labels from its own ecosystem. In a Laravel project with
47//! `vite.config.js`, `php` is `Laravel` and `node` is `Vite`; a `python`
48//! process in a Next.js folder stays `Python`.
49//!
50//! # Scope
51//!
52//! This crate only detects labels. It does not discover running processes,
53//! inspect network ports, query container engines, kill processes, read custom
54//! rule files, or format user-facing output.
55//!
56//! # Examples
57//!
58//! Direct image and process detection:
59//!
60//! ```
61//! use what_stack::{StackKind, detect_from_image, detect_from_process};
62//!
63//! let nginx = detect_from_image("ghcr.io/org/nginx:latest").expect("known image");
64//! assert_eq!(nginx, "Nginx");
65//! assert_eq!(nginx.kind(), StackKind::Service);
66//! assert_eq!(detect_from_process("node.exe").expect("known process"), "Node.js");
67//! ```
68//!
69//! Cached high-level detection:
70//!
71//! ```
72//! use what_stack::{StackDetector, StackInput};
73//!
74//! let mut detector = StackDetector::new();
75//! let label = detector.detect_stack(StackInput::new("").image("redis:7-alpine"));
76//!
77//! assert_eq!(label.expect("known image"), "Redis");
78//! ```
79
80mod config;
81mod detector;
82mod ecosystem;
83mod image;
84mod labels;
85mod process;
86mod project;
87mod text;
88mod types;
89
90pub use config::detect_from_config;
91pub use detector::StackDetector;
92pub use image::detect_from_image;
93pub use process::{detect_from_process, detect_from_process_names};
94pub use project::{
95    MAX_WALK_DEPTH, find_project_root, home_dir, project_name, resolve_project_root,
96};
97pub use types::{ProjectInput, StackInput, StackKind, StackLabel};
98
99/// Compiles the README examples as doctests.
100#[doc = include_str!("../README.md")]
101#[cfg(doctest)]
102pub struct ReadmeDoctests;
103
104#[cfg(test)]
105mod tests {
106    use std::collections::HashMap;
107
108    use super::*;
109
110    #[test]
111    fn every_label_text_has_exactly_one_kind_across_all_rules() {
112        let mut kinds: HashMap<&str, StackKind> = HashMap::new();
113
114        for label in labels::ALL {
115            let previous = kinds.insert(label.as_str(), label.kind());
116            assert!(
117                previous.is_none_or(|kind| kind == label.kind()),
118                "label {label} has conflicting kinds {previous:?} and {:?}",
119                label.kind()
120            );
121        }
122
123        assert!(
124            kinds.len() > 40,
125            "expected every built-in label to be scanned"
126        );
127    }
128
129    #[test]
130    fn stack_label_text_traits_ignore_kind_but_equality_does_not() {
131        let runtime = StackLabel::from_static("Vite", StackKind::Runtime);
132        let tool = StackLabel::new(String::from("Vite"), StackKind::Tool);
133
134        assert_eq!(runtime, "Vite");
135        assert_eq!("Vite", tool);
136        assert_eq!(runtime.as_str(), tool.as_ref());
137        assert_eq!(runtime.to_string(), "Vite");
138        assert_ne!(runtime, tool);
139        assert_eq!(String::from(tool.clone()), "Vite");
140        assert_eq!(tool.into_cow(), "Vite");
141    }
142
143    #[test]
144    fn builtin_labels_borrow_static_text() {
145        for label in labels::ALL {
146            assert!(
147                matches!(label.clone().into_cow(), std::borrow::Cow::Borrowed(_)),
148                "label {label} should not allocate"
149            );
150        }
151    }
152}