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}