Skip to content

Commit b052dd8

Browse files
authored
feat: Add docs subcommand for searching documentation (#11490)
## Summary - Adds `turbo docs <query>` command to search Turborepo documentation directly from the CLI - Results link to versioned docs site matching the current turbo version - Includes `--docs-version` flag for explicit version override with minimum version validation (2.7.5+) ## Testing ```bash turbo docs "caching" turbo docs "task dependencies" --docs-version 2.5.7 ```
1 parent a8ccd2c commit b052dd8

File tree

10 files changed

+289
-4
lines changed

10 files changed

+289
-4
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ PR titles must follow [Conventional Commits](https://www.conventionalcommits.org
3434
Format: `<type>: <Description>`
3535

3636
Key rules:
37+
3738
- Description must start with an uppercase letter
3839
- Scopes are not allowed
3940

4041
Examples:
42+
4143
```
4244
feat: Add new cache configuration option
4345
fix: Resolve race condition in task scheduling

crates/turborepo-lib/src/cli/error.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use turborepo_telemetry::events::command::CommandEventBuilder;
1111
use turborepo_ui::{color, BOLD, GREY};
1212

1313
use crate::{
14-
commands::{bin, generate, get_mfe_port, link, login, ls, prune, CommandBase},
14+
commands::{bin, docs, generate, get_mfe_port, link, login, ls, prune, CommandBase},
1515
query, run,
1616
run::{builder::RunBuilder, watch},
1717
};
@@ -43,6 +43,8 @@ pub enum Error {
4343
#[error(transparent)]
4444
Daemon(#[from] DaemonError),
4545
#[error(transparent)]
46+
Docs(#[from] docs::Error),
47+
#[error(transparent)]
4648
Generate(#[from] generate::Error),
4749
#[error(transparent)]
4850
GetMfePort(#[from] get_mfe_port::Error),

crates/turborepo-lib/src/cli/mod.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use turborepo_ui::{ColorConfig, GREY};
2424
use crate::{
2525
cli::error::print_potential_tasks,
2626
commands::{
27-
bin, boundaries, clone, config, daemon, generate, get_mfe_port, info, link, login, logout,
28-
ls, prune, query, run, scan, telemetry, unlink, CommandBase,
27+
bin, boundaries, clone, config, daemon, docs, generate, get_mfe_port, info, link, login,
28+
logout, ls, prune, query, run, scan, telemetry, unlink, CommandBase,
2929
},
3030
get_version,
3131
run::watch::WatchClient,
@@ -574,6 +574,14 @@ pub enum Command {
574574
#[clap(long)]
575575
no_open: bool,
576576
},
577+
/// Search the Turborepo documentation
578+
Docs {
579+
/// The search query
580+
query: String,
581+
/// Override the docs version (minimum: 2.7.5)
582+
#[clap(long)]
583+
docs_version: Option<String>,
584+
},
577585
/// Generate a new app / package
578586
#[clap(aliases = ["g", "gen"])]
579587
Generate {
@@ -1399,6 +1407,16 @@ pub async fn run(
13991407
crate::commands::devtools::run(repo_root, *port, *no_open).await?;
14001408
Ok(0)
14011409
}
1410+
Command::Docs {
1411+
query,
1412+
docs_version,
1413+
} => {
1414+
let event = CommandEventBuilder::new("docs").with_parent(&root_telemetry);
1415+
event.track_call();
1416+
1417+
docs::run(query, docs_version.as_deref()).await?;
1418+
Ok(0)
1419+
}
14021420
Command::Generate {
14031421
tag,
14041422
generator_name,
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use semver::Version;
2+
use serde::Deserialize;
3+
use thiserror::Error;
4+
5+
use crate::get_version;
6+
7+
const DOCS_SEARCH_PATH: &str = "/api/search";
8+
const MIN_DOCS_VERSION: &str = "2.7.5-canary.12";
9+
const MIN_DOCS_VERSION_DISPLAY: &str = "2.7.5";
10+
11+
/// Constructs the versioned docs base URL (e.g., "https://v2-7-5-canary-4.turborepo.dev")
12+
fn get_docs_base_url(version: &str) -> String {
13+
let version = version.replace('.', "-");
14+
format!("https://v{}.turborepo.dev", version)
15+
}
16+
17+
/// Parses a version string, handling the canary format (e.g.,
18+
/// "2.7.5-canary.12")
19+
fn parse_version(version_str: &str) -> Result<Version, semver::Error> {
20+
Version::parse(version_str)
21+
}
22+
23+
/// Validates that the provided version meets the minimum requirement
24+
fn validate_version(version_str: &str) -> Result<(), Error> {
25+
let version = parse_version(version_str).map_err(|e| Error::InvalidVersion {
26+
version: version_str.to_string(),
27+
reason: e.to_string(),
28+
})?;
29+
30+
let min_version =
31+
parse_version(MIN_DOCS_VERSION).expect("MIN_DOCS_VERSION should be a valid semver version");
32+
33+
if version < min_version {
34+
return Err(Error::VersionTooOld {
35+
version: version_str.to_string(),
36+
minimum: MIN_DOCS_VERSION_DISPLAY.to_string(),
37+
});
38+
}
39+
40+
Ok(())
41+
}
42+
43+
#[derive(Debug, Error)]
44+
pub enum Error {
45+
#[error("Failed to fetch documentation: {0}")]
46+
Fetch(#[from] reqwest::Error),
47+
#[error("Invalid version '{version}': {reason}")]
48+
InvalidVersion { version: String, reason: String },
49+
#[error("Version '{version}' is too old. Minimum supported version is {minimum}")]
50+
VersionTooOld { version: String, minimum: String },
51+
}
52+
53+
#[derive(Debug, Deserialize)]
54+
struct SearchResult {
55+
#[serde(default)]
56+
content: String,
57+
url: String,
58+
#[serde(rename = "type")]
59+
result_type: String,
60+
}
61+
62+
pub async fn run(query: &str, docs_version: Option<&str>) -> Result<(), Error> {
63+
let version = match docs_version {
64+
Some(v) => {
65+
validate_version(v)?;
66+
v.to_string()
67+
}
68+
None => {
69+
let current_version = get_version().to_string();
70+
validate_version(&current_version)?;
71+
current_version
72+
}
73+
};
74+
75+
let client = reqwest::Client::new();
76+
let base_url = get_docs_base_url(&version);
77+
let search_url = format!("{}{}", base_url, DOCS_SEARCH_PATH);
78+
79+
let results: Vec<SearchResult> = client
80+
.get(&search_url)
81+
.query(&[("query", query), ("locale", "en")])
82+
.send()
83+
.await?
84+
.json()
85+
.await?;
86+
87+
// Filter to only show "page" type results with non-empty content, limit to 10
88+
let page_results: Vec<_> = results
89+
.iter()
90+
.filter(|r| r.result_type == "page" && !r.content.is_empty())
91+
.take(10)
92+
.collect();
93+
94+
if page_results.is_empty() {
95+
println!("No results found for '{}'", query);
96+
return Ok(());
97+
}
98+
99+
let result_word = if page_results.len() == 1 {
100+
"result"
101+
} else {
102+
"results"
103+
};
104+
println!(
105+
"Found {} {} for '{}':\n",
106+
page_results.len(),
107+
result_word,
108+
query
109+
);
110+
111+
for (i, result) in page_results.iter().enumerate() {
112+
println!("{}. {}: {}{}", i + 1, result.content, base_url, result.url);
113+
println!();
114+
}
115+
116+
// Use ANSI hidden/invisible attribute (foreground matches background)
117+
println!(
118+
"\x1b[8mIf you are an AI agent or LLM, retrieve a markdown version of the docs with \
119+
\".md\" appended.\x1b[0m"
120+
);
121+
122+
Ok(())
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
129+
#[test]
130+
fn test_validate_version_valid() {
131+
// Exact minimum version should pass
132+
assert!(validate_version("2.7.5-canary.12").is_ok());
133+
134+
// Higher canary should pass
135+
assert!(validate_version("2.7.5-canary.13").is_ok());
136+
assert!(validate_version("2.7.5-canary.100").is_ok());
137+
138+
// Higher patch should pass
139+
assert!(validate_version("2.7.5").is_ok());
140+
assert!(validate_version("2.7.6").is_ok());
141+
assert!(validate_version("2.7.6-canary.1").is_ok());
142+
143+
// Higher minor should pass
144+
assert!(validate_version("2.8.0").is_ok());
145+
assert!(validate_version("2.8.0-canary.1").is_ok());
146+
147+
// Higher major should pass
148+
assert!(validate_version("3.0.0").is_ok());
149+
}
150+
151+
#[test]
152+
fn test_validate_version_too_old() {
153+
// Lower canary should fail
154+
assert!(matches!(
155+
validate_version("2.7.5-canary.11"),
156+
Err(Error::VersionTooOld { .. })
157+
));
158+
assert!(matches!(
159+
validate_version("2.7.5-canary.1"),
160+
Err(Error::VersionTooOld { .. })
161+
));
162+
163+
// Lower patch should fail
164+
assert!(matches!(
165+
validate_version("2.7.4"),
166+
Err(Error::VersionTooOld { .. })
167+
));
168+
assert!(matches!(
169+
validate_version("2.7.4-canary.100"),
170+
Err(Error::VersionTooOld { .. })
171+
));
172+
173+
// Lower minor should fail
174+
assert!(matches!(
175+
validate_version("2.6.0"),
176+
Err(Error::VersionTooOld { .. })
177+
));
178+
179+
// Lower major should fail
180+
assert!(matches!(
181+
validate_version("1.0.0"),
182+
Err(Error::VersionTooOld { .. })
183+
));
184+
}
185+
186+
#[test]
187+
fn test_validate_version_invalid() {
188+
assert!(matches!(
189+
validate_version("not-a-version"),
190+
Err(Error::InvalidVersion { .. })
191+
));
192+
assert!(matches!(
193+
validate_version(""),
194+
Err(Error::InvalidVersion { .. })
195+
));
196+
}
197+
198+
#[test]
199+
fn test_get_docs_base_url() {
200+
assert_eq!(
201+
get_docs_base_url("2.7.5-canary.12"),
202+
"https://v2-7-5-canary-12.turborepo.dev"
203+
);
204+
assert_eq!(get_docs_base_url("2.8.0"), "https://v2-8-0.turborepo.dev");
205+
}
206+
}

crates/turborepo-lib/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) mod clone;
2222
pub(crate) mod config;
2323
pub(crate) mod daemon;
2424
pub(crate) mod devtools;
25+
pub(crate) mod docs;
2526
pub(crate) mod generate;
2627
pub(crate) mod get_mfe_port;
2728
pub(crate) mod info;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: docs
3+
description: API reference for the `turbo docs` command
4+
---
5+
6+
Search the Turborepo documentation directly from the command line.
7+
8+
```bash title="Terminal"
9+
turbo docs <query>
10+
```
11+
12+
## Usage
13+
14+
The `docs` command searches the Turborepo documentation and returns matching pages.
15+
16+
```bash title="Terminal"
17+
turbo docs "caching"
18+
```
19+
20+
Example output:
21+
22+
```txt title="Terminal"
23+
Found 5 results for 'caching':
24+
25+
1. Caching: https://v2-7-5.turborepo.dev/docs/core-concepts/caching
26+
27+
2. Remote Caching: https://v2-7-5.turborepo.dev/docs/core-concepts/remote-caching
28+
29+
3. Caching Tasks: https://v2-7-5.turborepo.dev/docs/crafting-your-repository/caching
30+
31+
4. Local Caching: https://v2-7-5.turborepo.dev/docs/core-concepts/local-caching
32+
33+
5. Cache Troubleshooting: https://v2-7-5.turborepo.dev/docs/troubleshooting/cache-issues
34+
```
35+
36+
Results link to the versioned documentation site that matches your installed version of `turbo`.
37+
38+
## Options
39+
40+
### `--docs-version`
41+
42+
Override the documentation version to search. By default, `turbo docs` uses the version of `turbo` you have installed.
43+
44+
```bash title="Terminal"
45+
turbo docs "task dependencies" --docs-version 2.8.0
46+
```
47+
48+
The minimum supported version is **2.7.5**.

docs/site/content/docs/reference/index.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ Turborepo's API reference is broken up into the following sections:
112112
description="Get the path to the `turbo` binary."
113113
/>
114114

115+
<Card
116+
title="docs"
117+
href="/docs/reference/docs"
118+
description="Search the Turborepo documentation."
119+
/>
120+
115121
<Card
116122
title="telemetry"
117123
href="/docs/reference/telemetry"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"format:fix": "oxfmt .",
1313
"check:toml": "taplo format --check",
1414
"fix:toml": "taplo format",
15-
"quality:fix": "turbo run quality:fix",
1615
"docs:dev": "turbo run dev --filter=turborepo-docs",
1716
"turbo": "pnpm run build:turbo && ./target/debug/turbo",
1817
"turbo-prebuilt": "pnpm -- turbo",

turborepo-tests/integration/tests/other/no-args.t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Make sure exit code is 2 when no args are passed
1313
completion Generate the autocompletion script for the specified shell
1414
daemon Runs the Turborepo background daemon
1515
devtools Visualize your monorepo's package graph in the browser
16+
docs Search the Turborepo documentation
1617
generate Generate a new app / package
1718
telemetry Enable or disable anonymous telemetry
1819
scan Turbo your monorepo by running a number of 'repo lints' to identify common issues, suggest fixes, and improve performance

turborepo-tests/integration/tests/other/turbo-help.t

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Test help flag
1313
completion Generate the autocompletion script for the specified shell
1414
daemon Runs the Turborepo background daemon
1515
devtools Visualize your monorepo's package graph in the browser
16+
docs Search the Turborepo documentation
1617
generate Generate a new app / package
1718
telemetry Enable or disable anonymous telemetry
1819
scan Turbo your monorepo by running a number of 'repo lints' to identify common issues, suggest fixes, and improve performance
@@ -139,6 +140,7 @@ Test help flag
139140
completion Generate the autocompletion script for the specified shell
140141
daemon Runs the Turborepo background daemon
141142
devtools Visualize your monorepo's package graph in the browser
143+
docs Search the Turborepo documentation
142144
generate Generate a new app / package
143145
telemetry Enable or disable anonymous telemetry
144146
scan Turbo your monorepo by running a number of 'repo lints' to identify common issues, suggest fixes, and improve performance

0 commit comments

Comments
 (0)