Skip to content

Commit f60019f

Browse files
authored
Add opensrc fetch subcommand to cache source without printing paths (#53)
Extracts the shared "ensure cached" logic into a new `core::fetcher` module used by both `path` (prints the cached path) and the new `fetch` command (prints fetch status + summary, supports `-q/--quiet`).
1 parent a7ab493 commit f60019f

8 files changed

Lines changed: 312 additions & 174 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,18 @@ See `~/.opensrc/sources.json` for the list of available packages and their versi
5454

5555
Use this source code when you need to understand how a package works internally, not just its types/interface.
5656

57+
### Fetching Source Code
58+
59+
To just cache a package's source without doing anything else, use `opensrc fetch`:
60+
61+
```bash
62+
opensrc fetch <package>
63+
opensrc fetch pypi:<package> crates:<package> <owner>/<repo>
64+
```
65+
5766
### Reading Source Code
5867

59-
Use `opensrc path` inside other commands to search, read, or explore a package's source:
68+
Use `opensrc path` inside other commands to search, read, or explore a package's source (fetches on cache miss):
6069

6170
```bash
6271
rg "pattern" $(opensrc path <package>)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use crate::core::fetcher::ensure_cached;
2+
3+
pub fn run(
4+
specs: &[String],
5+
cwd: Option<&str>,
6+
quiet: bool,
7+
) -> Result<(), Box<dyn std::error::Error>> {
8+
let cwd = cwd.unwrap_or(".");
9+
10+
let mut fetched = 0u32;
11+
let mut cached = 0u32;
12+
let mut had_errors = false;
13+
14+
for spec in specs {
15+
match ensure_cached(spec, cwd, !quiet) {
16+
Ok(outcome) => {
17+
if outcome.from_cache {
18+
cached += 1;
19+
if !quiet {
20+
println!(
21+
" ✓ {}@{} already cached ({})",
22+
outcome.name,
23+
outcome.version,
24+
outcome.path.display()
25+
);
26+
}
27+
} else {
28+
fetched += 1;
29+
if !quiet {
30+
if let Some(warn) = &outcome.warning {
31+
println!(" ⚠ {warn}");
32+
}
33+
println!(
34+
" ✓ Fetched {}@{} from {} ({})",
35+
outcome.name,
36+
outcome.version,
37+
outcome.source_label,
38+
outcome.path.display()
39+
);
40+
}
41+
}
42+
}
43+
Err(e) => {
44+
had_errors = true;
45+
eprintln!(" ✗ {spec}: {e}");
46+
}
47+
}
48+
}
49+
50+
if !quiet {
51+
let mut parts = Vec::new();
52+
if fetched > 0 {
53+
parts.push(format!("{fetched} fetched"));
54+
}
55+
if cached > 0 {
56+
parts.push(format!("{cached} already cached"));
57+
}
58+
if !parts.is_empty() {
59+
println!("\n{}", parts.join(", "));
60+
}
61+
}
62+
63+
if had_errors {
64+
return Err("Some sources could not be fetched".into());
65+
}
66+
67+
Ok(())
68+
}

packages/opensrc/cli/src/commands/list.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ pub fn run(json: bool) -> Result<(), Box<dyn std::error::Error>> {
77

88
if total == 0 {
99
println!("No sources cached yet.");
10-
println!("\nUse `opensrc path <package>` to fetch source code for a package.");
11-
println!("Use `opensrc path <owner>/<repo>` to fetch a GitHub repository.");
10+
println!("\nUse `opensrc fetch <package>` to cache source code for a package.");
11+
println!("Use `opensrc fetch <owner>/<repo>` to cache a GitHub repository.");
1212
println!("\nSupported registries:");
13-
println!(" • npm: opensrc path zod, opensrc path npm:react");
14-
println!(" • PyPI: opensrc path pypi:requests");
15-
println!(" • crates: opensrc path crates:serde");
13+
println!(" • npm: opensrc fetch zod, opensrc fetch npm:react");
14+
println!(" • PyPI: opensrc fetch pypi:requests");
15+
println!(" • crates: opensrc fetch crates:serde");
1616
return Ok(());
1717
}
1818

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod clean;
2+
pub mod fetch;
23
pub mod list;
34
pub mod path;
45
pub mod remove;
Lines changed: 3 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,4 @@
1-
use std::path::PathBuf;
2-
3-
use crate::core::cache::{
4-
get_absolute_path, get_package_info, get_repo_info, list_sources, now_iso, write_sources,
5-
PackageEntry, RepoEntry,
6-
};
7-
use crate::core::git::{fetch_repo_source, fetch_source};
8-
use crate::core::registries::repo::{parse_repo_spec, resolve_repo};
9-
use crate::core::registries::{
10-
detect_input_type, parse_package_spec, resolve_package, PackageSpec, Registry,
11-
};
12-
use crate::core::version::detect_installed_version;
13-
14-
fn log(verbose: bool, msg: &str) {
15-
if verbose {
16-
eprintln!("{msg}");
17-
}
18-
}
19-
20-
fn handle_package(spec: &str, cwd: &str, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
21-
let parsed = parse_package_spec(spec);
22-
let registry = parsed.registry;
23-
let name = parsed.name.clone();
24-
let mut version = parsed.version.clone();
25-
26-
// Check cache if version is specified
27-
if let Some(ref v) = version {
28-
if let Some(existing) = get_package_info(&name, registry) {
29-
if existing.version == *v {
30-
let abs = get_absolute_path(&existing.path);
31-
println!("{}", abs.display());
32-
return Ok(());
33-
}
34-
}
35-
}
36-
37-
// Detect installed version for npm
38-
if version.is_none() && registry == Registry::Npm {
39-
let detected = detect_installed_version(&name, &PathBuf::from(cwd));
40-
if let Some(v) = detected {
41-
version = Some(v.clone());
42-
if let Some(existing) = get_package_info(&name, registry) {
43-
if existing.version == v {
44-
let abs = get_absolute_path(&existing.path);
45-
println!("{}", abs.display());
46-
return Ok(());
47-
}
48-
}
49-
}
50-
}
51-
52-
log(
53-
verbose,
54-
&format!("Fetching {name} from {}...", registry.label()),
55-
);
56-
57-
let pkg_spec = PackageSpec {
58-
registry,
59-
name: name.clone(),
60-
version,
61-
};
62-
let resolved = resolve_package(&pkg_spec)?;
63-
log(verbose, &format!(" → Cloning at {}...", resolved.git_tag));
64-
65-
let result = fetch_source(&resolved);
66-
67-
if !result.success {
68-
return Err(format!("Failed: {}", result.error.as_deref().unwrap_or("unknown")).into());
69-
}
70-
71-
if let Some(ref warn) = result.error {
72-
log(verbose, &format!(" ⚠ {warn}"));
73-
}
74-
75-
// Update index
76-
let (mut packages, repos) = list_sources();
77-
let entry = PackageEntry {
78-
name: result.package.clone(),
79-
version: result.version.clone(),
80-
registry: result.registry.unwrap_or(Registry::Npm),
81-
path: result.path.clone(),
82-
fetched_at: now_iso(),
83-
};
84-
if let Some(idx) = packages
85-
.iter()
86-
.position(|p| p.name == entry.name && p.registry == entry.registry)
87-
{
88-
packages[idx] = entry;
89-
} else {
90-
packages.push(entry);
91-
}
92-
write_sources(packages, repos)?;
93-
94-
let abs = get_absolute_path(&result.path);
95-
log(verbose, &format!(" ✓ Cached at {}", abs.display()));
96-
println!("{}", abs.display());
97-
Ok(())
98-
}
99-
100-
fn handle_repo(spec: &str, _cwd: &str, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
101-
let repo_spec = match parse_repo_spec(spec) {
102-
Some(s) => s,
103-
None => {
104-
return Err(format!("Invalid repository format: {spec}").into());
105-
}
106-
};
107-
108-
let display = format!("{}/{}/{}", repo_spec.host, repo_spec.owner, repo_spec.repo);
109-
110-
// Check cache
111-
if let Some(ref r) = repo_spec.git_ref {
112-
if let Some(existing) = get_repo_info(&display) {
113-
if existing.version == *r {
114-
let abs = get_absolute_path(&existing.path);
115-
println!("{}", abs.display());
116-
return Ok(());
117-
}
118-
}
119-
}
120-
121-
log(
122-
verbose,
123-
&format!("Fetching {}/{}...", repo_spec.owner, repo_spec.repo),
124-
);
125-
let resolved = resolve_repo(&repo_spec)?;
126-
log(verbose, &format!(" → Cloning at {}...", resolved.git_ref));
127-
128-
let result = fetch_repo_source(&resolved);
129-
130-
if !result.success {
131-
return Err(format!("Failed: {}", result.error.as_deref().unwrap_or("unknown")).into());
132-
}
133-
134-
if let Some(ref warn) = result.error {
135-
log(verbose, &format!(" ⚠ {warn}"));
136-
}
137-
138-
// Update index
139-
let (packages, mut repos) = list_sources();
140-
let entry = RepoEntry {
141-
name: result.package.clone(),
142-
version: result.version.clone(),
143-
path: result.path.clone(),
144-
fetched_at: now_iso(),
145-
};
146-
if let Some(idx) = repos.iter().position(|r| r.name == entry.name) {
147-
repos[idx] = entry;
148-
} else {
149-
repos.push(entry);
150-
}
151-
write_sources(packages, repos)?;
152-
153-
let abs = get_absolute_path(&result.path);
154-
log(verbose, &format!(" ✓ Cached at {}", abs.display()));
155-
println!("{}", abs.display());
156-
Ok(())
157-
}
158-
159-
fn run_one(spec: &str, cwd: &str, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
160-
let input_type = detect_input_type(spec);
161-
162-
if input_type == "repo" {
163-
handle_repo(spec, cwd, verbose)
164-
} else {
165-
handle_package(spec, cwd, verbose)
166-
}
167-
}
1+
use crate::core::fetcher::ensure_cached;
1682

1693
pub fn run(
1704
specs: &[String],
@@ -173,7 +7,8 @@ pub fn run(
1737
) -> Result<(), Box<dyn std::error::Error>> {
1748
let cwd = cwd.unwrap_or(".");
1759
for spec in specs {
176-
run_one(spec, cwd, verbose)?;
10+
let outcome = ensure_cached(spec, cwd, verbose)?;
11+
println!("{}", outcome.path.display());
17712
}
17813
Ok(())
17914
}

0 commit comments

Comments
 (0)