Skip to content

Commit 55f1c6d

Browse files
committed
feat(telemetry): add security audit API integration and enhance git clone error handling
- Add `urlencoding` dependency to workspace - Extend `Error::GitClone` with `is_timeout` and `is_auth_error` flags for structured error handling - Implement 60-second clone timeout matching TS `CLONE_TIMEOUT_MS` with detailed timeout/auth error messages - Set `GIT_TERMINAL_PROMPT=0` to suppress interactive credential prompts during clone - Refactor telemetry from POST JSON to GET query parameters matching TS implementation
1 parent 46da673 commit 55f1c6d

16 files changed

Lines changed: 417 additions & 108 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ default-members = ["skill"]
44
resolver = "3"
55

66
[workspace.package]
7-
version = "0.4.5"
7+
version = "0.5.0"
88
edition = "2024"
99
license = "MIT OR Apache-2.0"
1010
repository = "https://github.com/qntx/skill"
1111
description = "Blazing-fast Vercel Skills CLI, reborn in Rust. 100% command parity, zero compromises."
1212

1313
[workspace.dependencies]
14-
skill = { version = "0.4", path = "skill", features = ["network", "telemetry"] }
14+
skill = { version = "0.5", path = "skill", features = ["network", "telemetry"] }
1515

1616
# Async runtime
1717
tokio = { version = "1.50.0", default-features = false }

skill/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ regex = { workspace = true }
1919
dirs = { workspace = true }
2020
pathdiff = { workspace = true }
2121
tempfile = { workspace = true }
22+
urlencoding = { workspace = true }
2223
reqwest = { workspace = true, optional = true }
2324

2425
[target.'cfg(windows)'.dependencies]

skill/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ pub enum Error {
2929
url: String,
3030
/// Error description.
3131
message: String,
32+
/// Whether the clone timed out.
33+
is_timeout: bool,
34+
/// Whether the error is an authentication failure.
35+
is_auth_error: bool,
3236
},
3337

3438
/// An HTTP request failed.

skill/src/git.rs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
//! Git operations for cloning skill repositories.
22
//!
3-
//! Uses the system `git` command (like the `TypeScript` `simple-git` reference)
4-
//! for maximum compatibility.
3+
//! Uses the system `git` command with a 60-second timeout, matching the
4+
//! the TS `simple-git` reference implementation.
55
66
use std::path::PathBuf;
77

88
use tempfile::TempDir;
99

1010
use crate::error::{Error, Result};
1111

12+
/// Clone timeout matching the TS `CLONE_TIMEOUT_MS`.
13+
const CLONE_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(1);
14+
1215
/// Clone a git repository to a temporary directory.
1316
///
1417
/// Returns the [`TempDir`] which will be cleaned up on drop.
1518
///
1619
/// # Errors
1720
///
18-
/// Returns an error if `git clone` fails.
21+
/// Returns [`Error::GitClone`] with `is_timeout` / `is_auth_error` flags
22+
/// for structured error handling by callers.
1923
pub async fn clone_repo(url: &str, git_ref: Option<&str>) -> Result<TempDir> {
2024
let temp_dir = TempDir::new().map_err(|e| Error::io(PathBuf::from("/tmp"), e))?;
2125

@@ -31,16 +35,59 @@ pub async fn clone_repo(url: &str, git_ref: Option<&str>) -> Result<TempDir> {
3135

3236
cmd.arg(url).arg(temp_dir.path());
3337

34-
let output = cmd.output().await.map_err(|e| Error::GitClone {
35-
url: url.to_owned(),
36-
message: format!("failed to run git: {e}"),
37-
})?;
38+
// Suppress interactive credential prompts (matching TS GIT_TERMINAL_PROMPT=0)
39+
cmd.env("GIT_TERMINAL_PROMPT", "0");
40+
41+
let output = match tokio::time::timeout(CLONE_TIMEOUT, cmd.output()).await {
42+
Ok(Ok(output)) => output,
43+
Ok(Err(e)) => {
44+
return Err(Error::GitClone {
45+
url: url.to_owned(),
46+
message: format!("failed to run git: {e}"),
47+
is_timeout: false,
48+
is_auth_error: false,
49+
});
50+
}
51+
Err(_elapsed) => {
52+
return Err(Error::GitClone {
53+
url: url.to_owned(),
54+
message: concat!(
55+
"Clone timed out after 60s. This often happens with private repos ",
56+
"that require authentication.\n",
57+
" Ensure you have access and your SSH keys or credentials are configured:\n",
58+
" - For SSH: ssh-add -l (to check loaded keys)\n",
59+
" - For HTTPS: gh auth status (if using GitHub CLI)",
60+
)
61+
.to_owned(),
62+
is_timeout: true,
63+
is_auth_error: false,
64+
});
65+
}
66+
};
3867

3968
if !output.status.success() {
40-
let stderr = String::from_utf8_lossy(&output.stderr);
69+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
70+
let is_auth = stderr.contains("Authentication failed")
71+
|| stderr.contains("could not read Username")
72+
|| stderr.contains("Permission denied")
73+
|| stderr.contains("Repository not found");
74+
75+
let message = if is_auth {
76+
format!(
77+
"Authentication failed for {url}.\n\
78+
\x20 - For private repos, ensure you have access\n\
79+
\x20 - For SSH: Check your keys with 'ssh -T git@github.com'\n\
80+
\x20 - For HTTPS: Run 'gh auth login' or configure git credentials"
81+
)
82+
} else {
83+
stderr
84+
};
85+
4186
return Err(Error::GitClone {
4287
url: url.to_owned(),
43-
message: stderr.to_string(),
88+
message,
89+
is_timeout: false,
90+
is_auth_error: is_auth,
4491
});
4592
}
4693

skill/src/source.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
5959
local_path: Some(resolved),
6060
git_ref: None,
6161
skill_filter: None,
62+
is_private: None,
6263
};
6364
}
6465

@@ -75,6 +76,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
7576
local_path: None,
7677
git_ref: Some(git_ref.to_owned()),
7778
skill_filter: None,
79+
is_private: None,
7880
};
7981
}
8082

@@ -90,6 +92,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
9092
local_path: None,
9193
git_ref: Some(git_ref.to_owned()),
9294
skill_filter: None,
95+
is_private: None,
9396
};
9497
}
9598

@@ -104,6 +107,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
104107
local_path: None,
105108
git_ref: None,
106109
skill_filter: None,
110+
is_private: None,
107111
};
108112
}
109113

@@ -122,6 +126,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
122126
local_path: None,
123127
git_ref: Some(git_ref.to_owned()),
124128
skill_filter: None,
129+
is_private: None,
125130
};
126131
}
127132
}
@@ -140,6 +145,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
140145
local_path: None,
141146
git_ref: Some(git_ref.to_owned()),
142147
skill_filter: None,
148+
is_private: None,
143149
};
144150
}
145151
}
@@ -155,6 +161,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
155161
local_path: None,
156162
git_ref: None,
157163
skill_filter: None,
164+
is_private: None,
158165
};
159166
}
160167
}
@@ -175,6 +182,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
175182
local_path: None,
176183
git_ref: None,
177184
skill_filter: Some(skill_filter.to_owned()),
185+
is_private: None,
178186
};
179187
}
180188

@@ -194,6 +202,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
194202
local_path: None,
195203
git_ref: None,
196204
skill_filter: None,
205+
is_private: None,
197206
};
198207
}
199208

@@ -206,6 +215,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
206215
local_path: None,
207216
git_ref: None,
208217
skill_filter: None,
218+
is_private: None,
209219
};
210220
}
211221

@@ -217,6 +227,7 @@ pub fn parse_source(input: &str) -> ParsedSource {
217227
local_path: None,
218228
git_ref: None,
219229
skill_filter: None,
230+
is_private: None,
220231
}
221232
}
222233

0 commit comments

Comments
 (0)