fix(skills): embed skill content in the binary for single-binary installs
`skills get core` (the first step the agent-browser skill stub tells agents to run) failed with "Skills directory not found" on a GitHub-Release / install.sh install: only the binary is shipped, with no adjacent skills/ or skill-data/ the way an npm install bundles them, so find_package_root() returned nothing. Embed skills/ and skill-data/ into the binary via include_dir (168K) and, when no on-disk skill dirs are found, extract them once to a per-version cache dir ($CACHE/agent-browser/skills-<version>/) and serve from there. npm/dev installs still use the on-disk dirs unchanged. Verified: from an isolated dir (no skills/ nearby), `skills list` shows all 6 skills and `skills get core` serves content.
This commit is contained in:
Generated
+20
@@ -57,6 +57,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"image",
|
"image",
|
||||||
|
"include_dir",
|
||||||
"libc",
|
"libc",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1048,6 +1049,25 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
regex-lite = "0.1"
|
regex-lite = "0.1"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
include_dir = "0.7"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal", "process"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal", "process"] }
|
||||||
|
|||||||
+49
-6
@@ -1,3 +1,4 @@
|
|||||||
|
use include_dir::{include_dir, Dir};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -6,6 +7,12 @@ use std::process::exit;
|
|||||||
|
|
||||||
use crate::color;
|
use crate::color;
|
||||||
|
|
||||||
|
/// Skill content compiled into the binary so `skills get` works on a
|
||||||
|
/// single-binary install (GitHub Release / install.sh), where there is no
|
||||||
|
/// adjacent `skills/` or `skill-data/` on disk the way an npm install has.
|
||||||
|
static EMBEDDED_SKILLS: Dir = include_dir!("$CARGO_MANIFEST_DIR/../skills");
|
||||||
|
static EMBEDDED_SKILL_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/../skill-data");
|
||||||
|
|
||||||
struct SkillInfo {
|
struct SkillInfo {
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
@@ -63,6 +70,29 @@ fn find_package_root() -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the binary-embedded skill content to a per-version cache dir on
|
||||||
|
/// first use, returning a package root that contains `skills/` and
|
||||||
|
/// `skill-data/`. Fallback for single-binary installs (GitHub Release /
|
||||||
|
/// install.sh) that have no on-disk skill directories. Version-stamped so an
|
||||||
|
/// upgraded binary re-extracts fresh content.
|
||||||
|
fn embedded_skills_root() -> Option<PathBuf> {
|
||||||
|
let base = dirs::cache_dir()?
|
||||||
|
.join("agent-browser")
|
||||||
|
.join(concat!("skills-", env!("CARGO_PKG_VERSION")));
|
||||||
|
let marker = base.join(".extracted");
|
||||||
|
if !marker.exists() {
|
||||||
|
let _ = fs::create_dir_all(base.join("skills"));
|
||||||
|
let _ = fs::create_dir_all(base.join("skill-data"));
|
||||||
|
if EMBEDDED_SKILLS.extract(base.join("skills")).is_err()
|
||||||
|
|| EMBEDDED_SKILL_DATA.extract(base.join("skill-data")).is_err()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let _ = fs::write(&marker, env!("CARGO_PKG_VERSION"));
|
||||||
|
}
|
||||||
|
base.join("skills").is_dir().then_some(base)
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect all skill directories to search, respecting the env var override.
|
/// Collect all skill directories to search, respecting the env var override.
|
||||||
fn find_skills_dirs() -> Vec<PathBuf> {
|
fn find_skills_dirs() -> Vec<PathBuf> {
|
||||||
// Env var override: single directory, used as-is
|
// Env var override: single directory, used as-is
|
||||||
@@ -73,15 +103,28 @@ fn find_skills_dirs() -> Vec<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(root) = find_package_root() else {
|
// On-disk package root (npm install layout, or dev build walking up to repo).
|
||||||
return vec![];
|
if let Some(root) = find_package_root() {
|
||||||
};
|
let dirs: Vec<PathBuf> = SKILL_DIRS
|
||||||
|
|
||||||
SKILL_DIRS
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| root.join(d))
|
.map(|d| root.join(d))
|
||||||
.filter(|p| p.is_dir())
|
.filter(|p| p.is_dir())
|
||||||
.collect()
|
.collect();
|
||||||
|
if !dirs.is_empty() {
|
||||||
|
return dirs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: skill content compiled into the binary (single-binary install).
|
||||||
|
if let Some(root) = embedded_skills_root() {
|
||||||
|
return SKILL_DIRS
|
||||||
|
.iter()
|
||||||
|
.map(|d| root.join(d))
|
||||||
|
.filter(|p| p.is_dir())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse YAML frontmatter from a SKILL.md file. Returns (name, description, hidden).
|
/// Parse YAML frontmatter from a SKILL.md file. Returns (name, description, hidden).
|
||||||
|
|||||||
Reference in New Issue
Block a user