mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
The engine verified TLS against the Mozilla roots bundled through webpki-roots only, so behind a TLS-inspecting proxy (Aikido, Zscaler, Netskope) whose root lives in the OS trust store, `impeccable update` and `install` failed with `invalid peer certificate: UnknownIssuer` while curl and npm on the same machine succeeded. crates/context/src/http.rs builds one rustls ClientConfig per process: the OS trust store (rustls-native-certs: Keychain, Windows store, the OpenSSL paths on Linux) merged with the bundled roots. A union, not a replacement, so a container without ca-certificates or a store that fails to load still verifies exactly as before. SSL_CERT_FILE and SSL_CERT_DIR replace the OS store the way they do for OpenSSL and curl. Every HTTPS call site (bundle and signature downloads, /api/version, /api/commands, the roll API, image generation) builds its agent from this module; the plain-HTTP live-server calls on localhost are untouched. Verified against a local HTTPS server signed by a throwaway CA: trusted through SSL_CERT_FILE the update check reaches it; without it the same server is rejected as UnknownIssuer; with SSL_CERT_FILE pointing at that CA or at a missing file, impeccable.style still verifies through the bundled roots. cargo test --workspace and the oracle corpus (832) pass. Written with AI assistance (Claude Code). Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
44e825090e
commit
bd6964c35b
Generated
+78
-1
@@ -111,6 +111,22 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -590,12 +606,14 @@ dependencies = [
|
|||||||
"impeccable-core",
|
"impeccable-core",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rustls-native-certs",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -830,6 +848,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1063,7 +1087,7 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
"windows-sys",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1096,6 +1120,18 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1122,6 +1158,15 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1142,6 +1187,29 @@ dependencies = [
|
|||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "3.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -1603,6 +1671,15 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ impeccable detect [options] [file-or-dir-or-url...]
|
|||||||
|
|
||||||
- Node.js 22.18+ to run `npx impeccable`. The engine itself is a self-contained binary and needs no runtime; the skill installed into your harness calls it directly.
|
- Node.js 22.18+ to run `npx impeccable`. The engine itself is a self-contained binary and needs no runtime; the skill installed into your harness calls it directly.
|
||||||
- For URL scans, an installed Chrome, Chromium, or Edge (set `IMPECCABLE_BROWSER` to point at one).
|
- For URL scans, an installed Chrome, Chromium, or Edge (set `IMPECCABLE_BROWSER` to point at one).
|
||||||
|
- Behind a TLS-inspecting proxy, downloads trust your OS certificate store as well as the bundled Mozilla roots. Set `SSL_CERT_FILE` or `SSL_CERT_DIR` to use a specific CA bundle instead.
|
||||||
|
|
||||||
Binary lookup order: `IMPECCABLE_BIN`, the platform package, `~/.impeccable/bin/<version>/`, then a download of the pinned version into that cache. Set `IMPECCABLE_BIN` to a local build to skip all of that.
|
Binary lookup order: `IMPECCABLE_BIN`, the platform package, `~/.impeccable/bin/<version>/`, then a download of the pinned version into that cache. Set `IMPECCABLE_BIN` to a local build to skip all of that.
|
||||||
|
|
||||||
|
|||||||
@@ -21,5 +21,7 @@ once_cell = { workspace = true }
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
|
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
|
||||||
ureq = { version = "2", default-features = false, features = ["tls", "json"] }
|
ureq = { version = "2", default-features = false, features = ["tls", "json"] }
|
||||||
|
rustls-native-certs = "0.8"
|
||||||
|
webpki-roots = "1"
|
||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
unicode-normalization = "0.1.25"
|
unicode-normalization = "0.1.25"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fn card_base(env: &Env) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn agent(timeout: Duration) -> ureq::Agent {
|
fn agent(timeout: Duration) -> ureq::Agent {
|
||||||
ureq::AgentBuilder::new().timeout_connect(timeout).timeout(timeout).build()
|
crate::http::agent_builder().timeout_connect(timeout).timeout(timeout).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// URLSearchParams serialization (application/x-www-form-urlencoded).
|
/// URLSearchParams serialization (application/x-www-form-urlencoded).
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ fn fetch_latest_skill_version(env: &Env) -> Option<String> {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "https://impeccable.style".to_string());
|
.unwrap_or_else(|| "https://impeccable.style".to_string());
|
||||||
let host = host.strip_suffix('/').unwrap_or(&host).to_string();
|
let host = host.strip_suffix('/').unwrap_or(&host).to_string();
|
||||||
let agent = ureq::AgentBuilder::new()
|
let agent = crate::http::agent_builder()
|
||||||
.timeout(std::time::Duration::from_millis(FETCH_TIMEOUT_MS))
|
.timeout(std::time::Duration::from_millis(FETCH_TIMEOUT_MS))
|
||||||
.build();
|
.build();
|
||||||
let res = agent.get(&format!("{}/api/version", host)).call().ok()?;
|
let res = agent.get(&format!("{}/api/version", host)).call().ok()?;
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let agent = ureq::AgentBuilder::new().build();
|
let agent = crate::http::agent_builder().build();
|
||||||
let response = if !refs.is_empty() {
|
let response = if !refs.is_empty() {
|
||||||
let boundary = format!("----impeccable{:x}", crate::util::now_ms() as u64);
|
let boundary = format!("----impeccable{:x}", crate::util::now_ms() as u64);
|
||||||
let mut body: Vec<u8> = Vec::new();
|
let mut body: Vec<u8> = Vec::new();
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//! One TLS trust configuration for every HTTPS request the engine makes.
|
||||||
|
//!
|
||||||
|
//! `ureq`'s default rustls config trusts only the Mozilla roots compiled in
|
||||||
|
//! through `webpki-roots`. On a machine where an endpoint security agent
|
||||||
|
//! inspects TLS (Aikido, Zscaler, Netskope: routine in managed corporate
|
||||||
|
//! setups), every connection terminates at a proxy whose root lives in the
|
||||||
|
//! OS trust store and nowhere else, so `update` and `install` failed with
|
||||||
|
//! `invalid peer certificate: UnknownIssuer` while curl and npm on the same
|
||||||
|
//! machine succeeded (#757).
|
||||||
|
//!
|
||||||
|
//! The store built here is the union of the OS trust store
|
||||||
|
//! (`rustls-native-certs`: the macOS Keychain, the Windows store, the
|
||||||
|
//! OpenSSL paths on Linux) and the bundled Mozilla roots. A union, not a
|
||||||
|
//! replacement: a container without `ca-certificates`, or a store that
|
||||||
|
//! fails to load, verifies against the bundled roots exactly as before.
|
||||||
|
//! `SSL_CERT_FILE` / `SSL_CERT_DIR` stand in for the OS store, as they do
|
||||||
|
//! for OpenSSL and curl; the bundled roots stay either way.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use ureq::rustls::pki_types::CertificateDer;
|
||||||
|
use ureq::rustls::{self, ClientConfig, RootCertStore};
|
||||||
|
|
||||||
|
/// `ureq::AgentBuilder::new()` with the engine's trust store installed.
|
||||||
|
/// Every HTTPS call site builds its agent from this; the plain-HTTP calls
|
||||||
|
/// to the live server on localhost do not need it.
|
||||||
|
pub fn agent_builder() -> ureq::AgentBuilder {
|
||||||
|
ureq::AgentBuilder::new().tls_config(tls_config())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tls_config() -> Arc<ClientConfig> {
|
||||||
|
static CONFIG: Lazy<Arc<ClientConfig>> = Lazy::new(|| {
|
||||||
|
// Mirrors ureq's own default config (provider and protocol versions);
|
||||||
|
// only the root store differs.
|
||||||
|
let config =
|
||||||
|
ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
|
||||||
|
.with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
|
||||||
|
.expect("the ring provider supports TLS 1.2 and 1.3")
|
||||||
|
.with_root_certificates(root_store(rustls_native_certs::load_native_certs().certs))
|
||||||
|
.with_no_client_auth();
|
||||||
|
Arc::new(config)
|
||||||
|
});
|
||||||
|
CONFIG.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled Mozilla roots plus every parsable certificate in `native`.
|
||||||
|
/// Unparsable entries are dropped, so one broken certificate in the OS
|
||||||
|
/// store cannot take the bundled roots down with it.
|
||||||
|
fn root_store(native: Vec<CertificateDer<'static>>) -> RootCertStore {
|
||||||
|
let mut store = RootCertStore {
|
||||||
|
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||||
|
};
|
||||||
|
store.add_parsable_certificates(native);
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ureq::rustls::pki_types::pem::PemObject;
|
||||||
|
|
||||||
|
/// Self-signed CA minted for this test (P-256, v3, CA:TRUE): the shape
|
||||||
|
/// of the root a TLS-inspecting proxy installs into the OS store.
|
||||||
|
const PROXY_ROOT_PEM: &str = "-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBdTCCARugAwIBAgIJANhTZvQvv7HJMAoGCCqGSM49BAMCMB0xGzAZBgNVBAMM
|
||||||
|
EmltcGVjY2FibGUgdGVzdCBDQTAgFw0yNjA5MDcwNjQxMjdaGA8yMTI2MDgxNDA2
|
||||||
|
NDEyN1owHTEbMBkGA1UEAwwSaW1wZWNjYWJsZSB0ZXN0IENBMFkwEwYHKoZIzj0C
|
||||||
|
AQYIKoZIzj0DAQcDQgAEYVZtCOXaZsY71/0Roy62iBVcyx8UfMDkPbEbf/IEw5Bm
|
||||||
|
yNBfKTFS/8FbRBMWHXOwNE0Ns1BLVOB1oQ1XFC5Bz6NCMEAwDwYDVR0TAQH/BAUw
|
||||||
|
AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFONzBxi7ewOfuP6cBIIqsxu
|
||||||
|
3pEiMAoGCCqGSM49BAMCA0gAMEUCIQD98Q0ZRe8ceuopnUwQKYleZd5IzfWhhpmO
|
||||||
|
tB0WGTOG3QIgdJa8gBPU9Y6WsrursItsnUeGTYHKDCZZ6MjlekLFuoc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
";
|
||||||
|
|
||||||
|
fn bundled() -> usize {
|
||||||
|
webpki_roots::TLS_SERVER_ROOTS.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundled_roots_alone_when_the_os_store_is_empty() {
|
||||||
|
assert_eq!(root_store(Vec::new()).len(), bundled());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn os_store_root_joins_the_bundled_roots() {
|
||||||
|
let proxy = CertificateDer::from_pem_slice(PROXY_ROOT_PEM.as_bytes()).unwrap();
|
||||||
|
assert_eq!(root_store(vec![proxy]).len(), bundled() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unparsable_os_store_entry_is_dropped() {
|
||||||
|
let junk = CertificateDer::from(b"not a certificate".to_vec());
|
||||||
|
assert_eq!(root_store(vec![junk]).len(), bundled());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_builds_from_this_hosts_store() {
|
||||||
|
// Runs the real rustls-native-certs load: it must not panic, and the
|
||||||
|
// shared config must be accepted by a ureq agent.
|
||||||
|
let _agent = agent_builder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
pub mod jsp;
|
pub mod jsp;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod url;
|
pub mod url;
|
||||||
|
pub mod http;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
pub mod hook_markers;
|
pub mod hook_markers;
|
||||||
pub mod target_args;
|
pub mod target_args;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const RATIO_GUARD_FLOOR: u64 = 1024 * 1024;
|
|||||||
/// through `download_file`, which streams to disk. Reads are capped at
|
/// through `download_file`, which streams to disk. Reads are capped at
|
||||||
/// [`MAX_DOWNLOAD_BYTES`]; a longer response is an error, not a truncation.
|
/// [`MAX_DOWNLOAD_BYTES`]; a longer response is an error, not a truncation.
|
||||||
pub fn download(url: &str) -> Result<Vec<u8>, String> {
|
pub fn download(url: &str) -> Result<Vec<u8>, String> {
|
||||||
let agent = ureq::AgentBuilder::new()
|
let agent = impeccable_context::http::agent_builder()
|
||||||
.timeout_connect(std::time::Duration::from_secs(30))
|
.timeout_connect(std::time::Duration::from_secs(30))
|
||||||
.build();
|
.build();
|
||||||
match agent.get(url).call() {
|
match agent.get(url).call() {
|
||||||
@@ -79,7 +79,7 @@ pub struct FetchResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ureq_fetch(url: &str) -> Result<FetchResponse, String> {
|
fn ureq_fetch(url: &str) -> Result<FetchResponse, String> {
|
||||||
let agent = ureq::AgentBuilder::new()
|
let agent = impeccable_context::http::agent_builder()
|
||||||
.timeout_connect(std::time::Duration::from_secs(30))
|
.timeout_connect(std::time::Duration::from_secs(30))
|
||||||
.timeout(std::time::Duration::from_secs(120))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
.redirects(0)
|
.redirects(0)
|
||||||
|
|||||||
@@ -352,6 +352,8 @@ content, and failures fetching either asset exit nonzero, including when
|
|||||||
installed skill or hook files. Explicit `IMPECCABLE_BUNDLE_PATH` and `link`
|
installed skill or hook files. Explicit `IMPECCABLE_BUNDLE_PATH` and `link`
|
||||||
retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNING.md).
|
retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNING.md).
|
||||||
|
|
||||||
|
**TLS trust (#757):** every HTTPS request the engine makes (bundle and signature downloads, `/api/version`, `/api/commands`, the roll API, image generation) verifies against the OS trust store plus the bundled Mozilla roots, built once in `crates/context/src/http.rs`. `SSL_CERT_FILE` / `SSL_CERT_DIR` replace the OS store, never the bundled roots. The live server calls on localhost are plain HTTP and unaffected.
|
||||||
|
|
||||||
- **Invoked from**: README.md ("npx impeccable install / update"), README.npm.md Quick Start (`npx impeccable skills install`, `... install -y --providers=claude,codex --scope=project`, `... update`, `... install --no-hooks`, `... link --source=.impeccable --providers=claude,cursor`, `... skills help`), `README.md:360` (hook consent explanation).
|
- **Invoked from**: README.md ("npx impeccable install / update"), README.npm.md Quick Start (`npx impeccable skills install`, `... install -y --providers=claude,codex --scope=project`, `... update`, `... install --no-hooks`, `... link --source=.impeccable --providers=claude,cursor`, `... skills help`), `README.md:360` (hook consent explanation).
|
||||||
- `run(args)`: `args[0]` ∈ `undefined|help|--help|-h` → `showHelp()`; `install` → `install(rest)`; `link`; `update`; `check` (ignores flags); else `stderr> Unknown skills command: ${sub}` + `Run 'impeccable --help' for available commands.`, `exit 1`.
|
- `run(args)`: `args[0]` ∈ `undefined|help|--help|-h` → `showHelp()`; `install` → `install(rest)`; `link`; `update`; `check` (ignores flags); else `stderr> Unknown skills command: ${sub}` + `Run 'impeccable --help' for available commands.`, `exit 1`.
|
||||||
- Constants: `API_BASE = 'https://impeccable.style'`; `PROVIDER_DIRS = ['.claude','.cursor','.dsh','.gemini','.agents','.agent','.github','.grok','.hermes','.kiro','.opencode','.pi','.qoder','.trae','.trae-cn','.rovodev','.vibe']`; aliases (`agent`→`.agent`, `agents`/`codex`→`.agents`, `antigravity`→`.agent`, `claude`/`claude-code`→`.claude`, `copilot`/`github`→`.github`, `cursor`, `deepseek`/`deepseek-harness`/`dsh`→`.dsh`, `gemini`, `grok`/`grok-build`/`xai`→`.grok`, `hermes`, `kiro`, `opencode`, `pi`, `qoder`, `rovo-dev`/`rovodev`→`.rovodev`, `trae`, `trae-cn`, `vibe`); leading `.` stripped and lowercased before alias lookup; a literal PROVIDER_DIR value is accepted as-is. `DEFAULT_TARGETS = ['.claude','.agents']`. User-scope skill dir overrides: `.agent`→`~/.gemini/config/skills`, `.dsh`→`$DSH_HOME/skills` (only when DSH_HOME under home) else `~/.dsh/skills`, `.hermes`→`$HERMES_HOME/skills` (only when HERMES_HOME under home) else `~/.hermes/skills`, `.pi`→`~/.pi/agent/skills`, `.opencode`→`$OPENCODE_CONFIG_DIR|$XDG_CONFIG_HOME/opencode|~/.config/opencode` + `/skills`; others `~/<provider>/skills`. Project scope: `<root>/<provider>/skills`.
|
- Constants: `API_BASE = 'https://impeccable.style'`; `PROVIDER_DIRS = ['.claude','.cursor','.dsh','.gemini','.agents','.agent','.github','.grok','.hermes','.kiro','.opencode','.pi','.qoder','.trae','.trae-cn','.rovodev','.vibe']`; aliases (`agent`→`.agent`, `agents`/`codex`→`.agents`, `antigravity`→`.agent`, `claude`/`claude-code`→`.claude`, `copilot`/`github`→`.github`, `cursor`, `deepseek`/`deepseek-harness`/`dsh`→`.dsh`, `gemini`, `grok`/`grok-build`/`xai`→`.grok`, `hermes`, `kiro`, `opencode`, `pi`, `qoder`, `rovo-dev`/`rovodev`→`.rovodev`, `trae`, `trae-cn`, `vibe`); leading `.` stripped and lowercased before alias lookup; a literal PROVIDER_DIR value is accepted as-is. `DEFAULT_TARGETS = ['.claude','.agents']`. User-scope skill dir overrides: `.agent`→`~/.gemini/config/skills`, `.dsh`→`$DSH_HOME/skills` (only when DSH_HOME under home) else `~/.dsh/skills`, `.hermes`→`$HERMES_HOME/skills` (only when HERMES_HOME under home) else `~/.hermes/skills`, `.pi`→`~/.pi/agent/skills`, `.opencode`→`$OPENCODE_CONFIG_DIR|$XDG_CONFIG_HOME/opencode|~/.config/opencode` + `/skills`; others `~/<provider>/skills`. Project scope: `<root>/<provider>/skills`.
|
||||||
|
|||||||
Reference in New Issue
Block a user