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:
leeguooooo
2026-06-01 17:24:34 +09:00
parent e70d841a94
commit 8f67cff3e1
3 changed files with 72 additions and 8 deletions
+20
View File
@@ -57,6 +57,7 @@ dependencies = [
"hex",
"hmac",
"image",
"include_dir",
"libc",
"regex-lite",
"reqwest",
@@ -1048,6 +1049,25 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "indexmap"
version = "2.13.0"
+1
View File
@@ -19,6 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex-lite = "0.1"
dirs = "5.0"
include_dir = "0.7"
base64 = "0.22"
getrandom = "0.2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal", "process"] }
+49 -6
View File
@@ -1,3 +1,4 @@
use include_dir::{include_dir, Dir};
use serde_json::json;
use std::env;
use std::fs;
@@ -6,6 +7,12 @@ use std::process::exit;
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 {
name: String,
description: String,
@@ -63,6 +70,29 @@ fn find_package_root() -> Option<PathBuf> {
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.
fn find_skills_dirs() -> Vec<PathBuf> {
// 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 {
return vec![];
};
SKILL_DIRS
// On-disk package root (npm install layout, or dev build walking up to repo).
if let Some(root) = find_package_root() {
let dirs: Vec<PathBuf> = SKILL_DIRS
.iter()
.map(|d| root.join(d))
.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).