diff --git a/README.md b/README.md index 36b742d..63e5fb7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ fastskill install --lock - [Quick Start](webdocs/quickstart.mdx) - [Installation](webdocs/installation.mdx) - [CLI Reference](webdocs/cli-reference/overview.mdx) -- [Registry Guide](docs/REGISTRY.md) +- [Registry Guide](webdocs/registry/overview.mdx) ## Crates diff --git a/crates/fastskill-cli/Cargo.toml b/crates/fastskill-cli/Cargo.toml index 29875a2..f152428 100644 --- a/crates/fastskill-cli/Cargo.toml +++ b/crates/fastskill-cli/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" [dependencies] # Core library -fastskill-core = { path = "../fastskill-core", features = ["filesystem-storage", "registry-publish", "hot-reload"] } +fastskill-core = { path = "../fastskill-core", default-features = false, features = ["filesystem-storage", "hot-reload"] } # CLI framework for AppBuilder, command registry, MCP server, and doctor cli-framework = { workspace = true, default-features = false, features = ["mcp-server", "mcp-install", "doctor", "testkit"] } @@ -88,9 +88,9 @@ dirs.workspace = true # Semantic versioning semver.workspace = true -# AWS S3 storage (for registry publishing) -aws-sdk-s3.workspace = true -aws-config.workspace = true +# AWS S3 storage (optional; only compiled with registry publishing) +aws-sdk-s3 = { workspace = true, optional = true } +aws-config = { workspace = true, optional = true } # Vendored OpenSSL — required for musl cross-compilation because cli-framework and # aikit-sdk depend on reqwest without default-features = false, which unifies @@ -99,6 +99,8 @@ aws-config.workspace = true openssl = { version = "0.10", optional = true, features = ["vendored"] } [features] +default = [] +registry-publish = ["fastskill-core/registry-publish", "dep:aws-sdk-s3", "dep:aws-config"] vendored-openssl = ["dep:openssl"] [dev-dependencies] diff --git a/crates/fastskill-cli/src/auth_config.rs b/crates/fastskill-cli/src/auth_config.rs index f7a8802..7044246 100644 --- a/crates/fastskill-cli/src/auth_config.rs +++ b/crates/fastskill-cli/src/auth_config.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; +#[cfg(feature = "registry-publish")] use std::time::{SystemTime, UNIX_EPOCH}; /// Authentication configuration structure @@ -150,6 +151,7 @@ fn decode_token_unsafe(token: &str) -> Option { } /// Check if a JWT token is expired or expiring soon +#[cfg(feature = "registry-publish")] fn is_token_expired_or_expiring_soon(token: &str, buffer_seconds: u64) -> CliResult { match decode_token_unsafe(token) { Some(claims) => { @@ -209,6 +211,7 @@ pub fn get_token_for_registry(registry_url: &str) -> CliResult> { } /// Get token with automatic refresh if needed +#[cfg(feature = "registry-publish")] pub async fn get_token_with_refresh(registry_url: &str) -> CliResult> { // Check environment variable first if let Ok(token) = std::env::var("FASTSKILL_API_TOKEN") { @@ -283,6 +286,7 @@ pub async fn get_token_with_refresh(registry_url: &str) -> CliResult CliResult { let client = reqwest::Client::new(); let token_url = format!("{}/auth/token", registry_url); diff --git a/crates/fastskill-cli/src/commands/add/install.rs b/crates/fastskill-cli/src/commands/add/install.rs index 46209a4..2b8ebef 100644 --- a/crates/fastskill-cli/src/commands/add/install.rs +++ b/crates/fastskill-cli/src/commands/add/install.rs @@ -376,6 +376,7 @@ version = "1.0.0" #[cfg(unix)] #[tokio::test] + #[allow(clippy::await_holding_lock)] async fn test_add_editable_local_creates_symlink() { let _lock = fastskill_core::test_utils::DIR_MUTEX .lock() @@ -440,6 +441,7 @@ skills_directory = ".claude/skills" } #[tokio::test] + #[allow(clippy::await_holding_lock)] async fn test_add_non_editable_local_creates_directory() { let _lock = fastskill_core::test_utils::DIR_MUTEX .lock() diff --git a/crates/fastskill-cli/src/commands/mod.rs b/crates/fastskill-cli/src/commands/mod.rs index e0378c2..a0d44a0 100644 --- a/crates/fastskill-cli/src/commands/mod.rs +++ b/crates/fastskill-cli/src/commands/mod.rs @@ -11,6 +11,7 @@ pub mod install; pub mod list; pub mod marketplace; pub mod package; +#[cfg(feature = "registry-publish")] pub mod publish; pub mod read; pub mod registry; diff --git a/crates/fastskill-cli/src/commands/publish.rs b/crates/fastskill-cli/src/commands/publish.rs index a04b22d..d35fca9 100644 --- a/crates/fastskill-cli/src/commands/publish.rs +++ b/crates/fastskill-cli/src/commands/publish.rs @@ -296,17 +296,11 @@ pub async fn execute_publish(args: PublishArgs) -> CliResult<()> { /// Publish to API with authentication async fn publish_to_api_with_auth(context: &PublishContext) -> CliResult<()> { let token = crate::auth_config::get_token_with_refresh(&context.target).await?; - let token_str = token.ok_or_else(|| { - CliError::Validation(format!( - "No authentication token found for registry: {}. Run `fastskill auth login` to authenticate.", - context.target - )) - })?; publish_to_api( &context.target, &context.packages, - Some(&token_str), + token.as_deref(), context.wait, context.max_wait, ) @@ -473,7 +467,7 @@ async fn publish_single_package( println!( "{}", messages::info(&format!( - "Check status with: GET {}/api/registry/publish/status/{}", + "Check status with: GET {}/api/v1/registry/publish/status/{}", api_url, response.job_id )) ); @@ -595,7 +589,7 @@ fn extract_api_url_from_repository(repo: &RepositoryDefinition) -> CliResult Option { }) } +#[cfg(feature = "registry-publish")] +use commands::publish; use commands::{ - add, analyze, auth, doctor, eval, init, install, list, marketplace, package, publish, read, - reindex, remove, repos, search, serve, skillopt, update, + add, analyze, auth, doctor, eval, init, install, list, marketplace, package, read, reindex, + remove, repos, search, serve, skillopt, update, }; #[tokio::main] @@ -208,16 +210,18 @@ fn build_app(builder: AppBuilder, state: Arc) -> anyhow::Result CliResult { - let url = format!("{}/api/registry/publish", self.base_url); + let url = format!("{}/api/v1/registry/publish", self.base_url); // Read package file let package_data = std::fs::read(package_path).map_err(CliError::Io)?; @@ -139,7 +139,10 @@ impl ApiClient { /// Get publish job status pub async fn get_publish_status(&self, job_id: &str) -> CliResult { - let url = format!("{}/api/registry/publish/status/{}", self.base_url, job_id); + let url = format!( + "{}/api/v1/registry/publish/status/{}", + self.base_url, job_id + ); // Build request let mut request = self.client.get(&url); diff --git a/crates/fastskill-core/Cargo.toml b/crates/fastskill-core/Cargo.toml index ffee0c6..a1a4786 100644 --- a/crates/fastskill-core/Cargo.toml +++ b/crates/fastskill-core/Cargo.toml @@ -103,7 +103,7 @@ assert_cmd = "2.2" predicates = "3.0" [features] -default = ["filesystem-storage", "registry-publish"] +default = ["filesystem-storage"] # Storage backends filesystem-storage = [] diff --git a/crates/fastskill-core/README.md b/crates/fastskill-core/README.md index 7390439..0625f17 100644 --- a/crates/fastskill-core/README.md +++ b/crates/fastskill-core/README.md @@ -101,7 +101,7 @@ pub async fn retrieve_skills_for_turn( ## Feature flags - `filesystem-storage` (default): local storage backend. -- `registry-publish` (default): registry publishing support. +- `registry-publish`: opt-in registry publishing support. - `hot-reload`: filesystem watch support for skill changes. ## Core capabilities diff --git a/crates/fastskill-core/src/core/metadata.rs b/crates/fastskill-core/src/core/metadata.rs index bab122a..e72c8f2 100644 --- a/crates/fastskill-core/src/core/metadata.rs +++ b/crates/fastskill-core/src/core/metadata.rs @@ -295,9 +295,12 @@ mod tests { let result = parse_yaml_frontmatter(content); assert!( result.is_ok(), - "parse_yaml_frontmatter should succeed with legacy enabled key" + "parse_yaml_frontmatter should succeed with legacy enabled key: {:?}", + result.as_ref().err() ); - let fm = result.unwrap(); + let Ok(fm) = result else { + return; + }; assert!( fm.extra.contains_key("enabled"), "extra map should contain the enabled key" diff --git a/crates/fastskill-core/src/core/registry/index_manager.rs b/crates/fastskill-core/src/core/registry/index_manager.rs index f631b3f..cc4fa6b 100644 --- a/crates/fastskill-core/src/core/registry/index_manager.rs +++ b/crates/fastskill-core/src/core/registry/index_manager.rs @@ -105,15 +105,17 @@ impl IndexManager { } } - // Step 4: Acquire exclusive file lock with timeout + // Step 4: Acquire exclusive lock with timeout. Use a sidecar lock file + // because the index file is replaced by atomic rename during update. + let lock_path = index_path.with_extension("lock"); let lock_start = Instant::now(); let file = loop { let file = OpenOptions::new() .read(true) .write(true) .create(true) - .truncate(true) - .open(&index_path) + .truncate(false) + .open(&lock_path) .map_err(ServiceError::Io)?; // Try to acquire exclusive lock @@ -122,12 +124,12 @@ impl IndexManager { let elapsed = lock_start.elapsed(); if elapsed.as_millis() > 0 { info!( - "Acquired lock on index file: {:?} (waited {}ms)", + "Acquired lock for index file: {:?} (waited {}ms)", index_path, elapsed.as_millis() ); } else { - info!("Acquired lock on index file: {:?}", index_path); + info!("Acquired lock for index file: {:?}", index_path); } break file; } @@ -137,12 +139,12 @@ impl IndexManager { if elapsed >= self.lock_timeout { warn!( "Lock timeout exceeded for {:?} after {} seconds", - index_path, + lock_path, self.lock_timeout.as_secs() ); return Err(ServiceError::Custom(format!( "Timeout waiting for file lock on {:?} (exceeded {} seconds)", - index_path, + lock_path, self.lock_timeout.as_secs() ))); } @@ -150,7 +152,7 @@ impl IndexManager { if elapsed.as_secs() > 0 && elapsed.as_secs().is_multiple_of(5) { info!( "Waiting for lock on {:?} ({}s elapsed, timeout: {}s)", - index_path, + lock_path, elapsed.as_secs(), self.lock_timeout.as_secs() ); @@ -160,7 +162,7 @@ impl IndexManager { continue; } Err(e) => { - warn!("Failed to acquire lock on {:?}: {}", index_path, e); + warn!("Failed to acquire lock on {:?}: {}", lock_path, e); return Err(ServiceError::Io(e)); } } @@ -387,3 +389,83 @@ impl IndexManager { Ok(entries) } } + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use super::*; + use crate::core::registry_index::VersionEntry; + use std::collections::HashMap; + use std::sync::Arc; + use tempfile::TempDir; + + fn entry(version: &str) -> VersionEntry { + VersionEntry { + name: "testorg/test-skill".to_string(), + vers: version.to_string(), + deps: Vec::new(), + cksum: format!("checksum-{version}"), + features: HashMap::new(), + yanked: false, + links: None, + download_url: format!("https://example.com/test-skill-{version}.zip"), + published_at: "2024-01-01T00:00:00Z".to_string(), + metadata: None, + scoped_name: None, + } + } + + fn read_entries(registry_path: PathBuf) -> Vec { + let index_path = get_skill_index_path(®istry_path, "testorg/test-skill").unwrap(); + IndexManager::read_entries_from_path(&index_path).unwrap() + } + + #[test] + fn atomic_update_preserves_existing_entries() { + let temp_dir = TempDir::new().unwrap(); + let registry_path = temp_dir.path().to_path_buf(); + let manager = IndexManager::new(registry_path.clone()); + + for i in 0..25 { + let version = format!("1.0.{i}"); + let update = entry(&version); + let result = manager.atomic_update("testorg/test-skill", &version, &update); + assert!( + result.is_ok(), + "atomic_update failed for {version}: {result:?}" + ); + } + + let entries = read_entries(registry_path); + assert_eq!(entries.len(), 25); + } + + #[test] + fn atomic_update_serializes_concurrent_same_skill_writes() { + let temp_dir = TempDir::new().unwrap(); + let registry_path = temp_dir.path().to_path_buf(); + let manager = Arc::new(IndexManager::new(registry_path.clone())); + + let handles: Vec<_> = (0..10) + .map(|i| { + let manager = Arc::clone(&manager); + std::thread::spawn(move || { + let version = format!("1.0.{i}"); + let update = entry(&version); + manager.atomic_update("testorg/test-skill", &version, &update) + }) + }) + .collect(); + + for handle in handles { + let result = handle.join().unwrap(); + assert!( + result.is_ok(), + "concurrent atomic_update failed: {result:?}" + ); + } + + let entries = read_entries(registry_path); + assert_eq!(entries.len(), 10); + } +} diff --git a/crates/fastskill-core/src/core/repository/client.rs b/crates/fastskill-core/src/core/repository/client.rs index 0ae3afb..dc5a210 100644 --- a/crates/fastskill-core/src/core/repository/client.rs +++ b/crates/fastskill-core/src/core/repository/client.rs @@ -324,7 +324,7 @@ impl CratesRegistryClient { // Build the API endpoint URL let base_url = self.index_url.trim_end_matches('/'); - let mut url = format!("{}/api/registry/index/skills", base_url); + let mut url = format!("{}/api/v1/registry/index/skills", base_url); // Add query parameters let mut query_params = Vec::new(); diff --git a/crates/fastskill-core/src/http/handlers/claude_api.rs b/crates/fastskill-core/src/http/handlers/claude_api.rs deleted file mode 100644 index 7e77c04..0000000 --- a/crates/fastskill-core/src/http/handlers/claude_api.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! Claude Code v1 API endpoints implementation -//! -//! This module implements the Claude Code v1 API endpoints for skill management, -//! matching Anthropic's API specification for skill creation, versioning, and management. - -use crate::http::errors::{HttpError, HttpResult}; -use crate::http::handlers::{skill_storage::SkillStorage, AppState}; -use crate::http::models::*; -use axum::{ - extract::{Multipart, Path, Query, State}, - Json, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use validator::Validate; - -/// Create skill version request (multipart/form-data) - not used directly -/// We handle multipart parsing manually -/// Multipart file representation -#[derive(Debug)] -pub struct MultipartFile { - pub filename: String, - pub content: Vec, - pub content_type: Option, -} - -/// Create skill version response -#[derive(Debug, Serialize)] -pub struct CreateSkillVersionResponse { - pub created_at: String, - pub description: String, - pub directory: String, - pub id: String, - pub name: String, - pub skill_id: String, - pub r#type: String, // Always "skill_version" - pub version: String, -} - -/// List skills response -#[derive(Debug, Serialize)] -pub struct ListSkillsResponse { - pub skills: Vec, - pub count: usize, - pub total: usize, -} - -/// Skill information -#[derive(Debug, Serialize)] -pub struct SkillInfo { - pub id: String, - pub name: String, - pub description: String, - pub created_at: Option, - pub updated_at: Option, - pub latest_version: Option, - pub source: String, // "anthropic" or "custom" -} - -/// Get skill response -#[derive(Debug, Serialize)] -pub struct GetSkillResponse { - pub id: String, - pub name: String, - pub description: String, - pub created_at: Option, - pub updated_at: Option, - pub latest_version: Option, - pub source: String, -} - -/// Create skill response -#[derive(Debug, Serialize)] -pub struct CreateSkillResponse { - pub id: String, - pub name: String, - pub description: String, - pub created_at: String, - pub source: String, -} - -/// Query parameters for listing skills -#[derive(Debug, Deserialize)] -pub struct ListSkillsQuery { - pub source: Option, // Filter by source: "anthropic" or "custom" - pub limit: Option, - pub offset: Option, -} - -/// POST /v1/skills - Create a new skill -pub async fn create_skill( - State(state): State, - Json(request): Json, -) -> HttpResult>> { - // Validate request - request.validate().map_err(|e| { - HttpError::ValidationError( - e.field_errors() - .into_iter() - .map(|(field, errors)| { - ( - field.to_string(), - errors - .iter() - .map(|e| e.message.clone().unwrap_or_default().to_string()) - .collect(), - ) - }) - .collect(), - ) - })?; - - // Generate skill ID - let skill_id = SkillStorage::generate_skill_id(); - - // Create skill metadata - let skills_dir = state.service.config().skill_storage_path.clone(); - let _skill_storage = SkillStorage::new(state.service.clone(), skills_dir); - let created_at = chrono::Utc::now(); - - // For now, create a basic skill entry - // In a full implementation, this would create the skill in the registry - let response = CreateSkillResponse { - id: skill_id, - name: request.display_title.clone(), - description: request.description.unwrap_or_default(), - created_at: created_at.to_rfc3339(), - source: "custom".to_string(), - }; - - Ok(Json(ApiResponse::success(response))) -} - -/// GET /v1/skills - List all skills -pub async fn list_skills( - State(state): State, - Query(query): Query, -) -> HttpResult>> { - let skills_dir = state.service.config().skill_storage_path.clone(); - let skill_storage = SkillStorage::new(state.service.clone(), skills_dir); - - // Get skills from storage - let mut skills = skill_storage.list_skills().await?; - - // Apply source filter - if let Some(source_filter) = &query.source { - skills.retain(|skill| { - // Determine source based on skill ID format or metadata - // Anthropic skills have known IDs, custom skills have generated IDs - let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf"); - match source_filter.as_str() { - "anthropic" => is_anthropic, - "custom" => !is_anthropic, - _ => true, - } - }); - } - - // Apply pagination - let offset = query.offset.unwrap_or(0); - let limit = query.limit.unwrap_or(50); - let total = skills.len(); - let paginated_skills = skills - .into_iter() - .skip(offset) - .take(limit) - .collect::>(); - let count = paginated_skills.len(); - - // Convert to response format - let skill_infos: Vec = paginated_skills - .into_iter() - .map(|skill| { - let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf"); - SkillInfo { - id: skill.id, - name: skill.name, - description: skill.description, - created_at: Some(skill.created_at.to_rfc3339()), - updated_at: Some(skill.updated_at.to_rfc3339()), - latest_version: Some(skill.latest_version), - source: if is_anthropic { - "anthropic".to_string() - } else { - "custom".to_string() - }, - } - }) - .collect(); - - let response = ListSkillsResponse { - skills: skill_infos, - count, - total, - }; - - Ok(Json(ApiResponse::success(response))) -} - -/// GET /v1/skills/{skill_id} - Get skill details -pub async fn get_skill( - State(state): State, - Path(skill_id): Path, -) -> HttpResult>> { - let skills_dir = state.service.config().skill_storage_path.clone(); - let skill_storage = SkillStorage::new(state.service.clone(), skills_dir); - - let skill = skill_storage - .get_skill(&skill_id) - .await? - .ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?; - - let is_anthropic = matches!(skill.id.as_str(), "pptx" | "xlsx" | "docx" | "pdf"); - - let response = GetSkillResponse { - id: skill.id, - name: skill.name, - description: skill.description, - created_at: Some(skill.created_at.to_rfc3339()), - updated_at: Some(skill.updated_at.to_rfc3339()), - latest_version: Some(skill.latest_version), - source: if is_anthropic { - "anthropic".to_string() - } else { - "custom".to_string() - }, - }; - - Ok(Json(ApiResponse::success(response))) -} - -/// DELETE /v1/skills/{skill_id} - Delete a skill -pub async fn delete_skill( - State(state): State, - Path(skill_id): Path, -) -> HttpResult>> { - let skills_dir = state.service.config().skill_storage_path.clone(); - let skill_storage = SkillStorage::new(state.service.clone(), skills_dir); - - skill_storage.delete_skill(&skill_id).await?; - - Ok(Json(ApiResponse::success(serde_json::json!({ - "message": "Skill deleted successfully" - })))) -} - -/// POST /v1/skills/{skill_id}/versions - Create skill version -pub async fn create_skill_version( - State(state): State, - Path(skill_id): Path, - mut multipart: Multipart, -) -> HttpResult>> { - let skills_dir = state.service.config().skill_storage_path.clone(); - let skill_storage = SkillStorage::new(state.service.clone(), skills_dir); - - // Parse multipart form data - let mut files = HashMap::new(); - while let Some(field) = multipart - .next_field() - .await - .map_err(|e| HttpError::BadRequest(format!("Failed to read multipart field: {}", e)))? - { - let name = field.name().unwrap_or("unnamed").to_string(); - if name == "files" || name == "files[]" { - let filename = field.file_name().unwrap_or("unnamed").to_string(); - let content = field.bytes().await.map_err(|e| { - HttpError::BadRequest(format!("Failed to read file content: {}", e)) - })?; - files.insert(filename, content.to_vec()); - } - } - - if files.is_empty() { - return Err(HttpError::BadRequest("No files provided".to_string())); - } - - // Store the skill version - let version = skill_storage.store_skill_version(&skill_id, files).await?; - - let response = CreateSkillVersionResponse { - created_at: version.created_at.to_rfc3339(), - description: version.description, - directory: version.directory, - id: version.id, - name: version.name, - skill_id: version.skill_id, - r#type: "skill_version".to_string(), - version: version.version, - }; - - Ok(Json(ApiResponse::success(response))) -} - -/// GET /v1/skills/{skill_id}/versions - List skill versions -pub async fn list_skill_versions( - State(_state): State, - Path(_skill_id): Path, -) -> HttpResult>> { - // For now, return empty list - full implementation would track versions - let response = SkillVersionsListResponse { - versions: vec![], - count: 0, - total: 0, - }; - - Ok(Json(ApiResponse::success(response))) -} - -/// GET /v1/skills/{skill_id}/versions/{version} - Get skill version -pub async fn get_skill_version( - State(_state): State, - Path((_skill_id, _version)): Path<(String, String)>, -) -> HttpResult>> { - // This would need to be implemented to retrieve specific versions - Err(HttpError::NotFound(format!( - "Skill version not found: {}@{}", - _skill_id, _version - ))) -} - -/// DELETE /v1/skills/{skill_id}/versions/{version} - Delete skill version -pub async fn delete_skill_version( - State(_state): State, - Path((_skill_id, _version)): Path<(String, String)>, -) -> HttpResult>> { - // This would need to be implemented to delete specific versions - Err(HttpError::NotFound(format!( - "Skill version not found: {}@{}", - _skill_id, _version - ))) -} - -// Additional response types -#[derive(Debug, Serialize)] -pub struct SkillVersionsListResponse { - pub versions: Vec, - pub count: usize, - pub total: usize, -} - -#[derive(Debug, Serialize)] -pub struct SkillVersionInfo { - pub id: String, - pub version: String, - pub created_at: String, - pub name: String, - pub description: String, -} - -#[derive(Debug, Serialize)] -pub struct SkillVersionResponse { - pub id: String, - pub skill_id: String, - pub version: String, - pub created_at: String, - pub name: String, - pub description: String, - pub directory: String, - pub r#type: String, -} - -// Request models -#[derive(Debug, Deserialize, validator::Validate)] -pub struct CreateSkillRequest { - #[validate(length(min = 1, max = 64))] - pub display_title: String, - #[validate(length(max = 1024))] - pub description: Option, -} diff --git a/crates/fastskill-core/src/http/handlers/mod.rs b/crates/fastskill-core/src/http/handlers/mod.rs index b386f8f..385e7be 100644 --- a/crates/fastskill-core/src/http/handlers/mod.rs +++ b/crates/fastskill-core/src/http/handlers/mod.rs @@ -1,13 +1,12 @@ //! HTTP request handlers -pub mod claude_api; pub mod manifest; pub mod registry; +#[cfg(feature = "registry-publish")] pub mod registry_publish; pub mod reindex; pub mod resolve; pub mod search; -pub mod skill_storage; pub mod skills; pub mod status; @@ -15,4 +14,4 @@ pub mod status; pub use status::AppState; // Note: Handler functions are accessed via module paths (e.g., skills::list_skills) -// to avoid ambiguous re-exports between skills and claude_api modules +// to avoid ambiguous re-exports between modules. diff --git a/crates/fastskill-core/src/http/handlers/registry.rs b/crates/fastskill-core/src/http/handlers/registry.rs index 4abd7c4..ac40942 100644 --- a/crates/fastskill-core/src/http/handlers/registry.rs +++ b/crates/fastskill-core/src/http/handlers/registry.rs @@ -139,7 +139,7 @@ async fn get_sources_manager_from_repos( Ok(sources_manager) } -/// GET /api/registry/sources - List all configured sources/repositories +/// GET /api/v1/registry/sources - List all configured sources/repositories pub async fn list_sources( State(state): State, ) -> HttpResult>>> { @@ -185,7 +185,7 @@ pub async fn list_sources( Ok(Json(ApiResponse::success(source_responses))) } -/// GET /api/registry/skills - Get all skills grouped by source +/// GET /api/v1/registry/skills - Get all skills grouped by source pub async fn list_all_skills( State(state): State, ) -> HttpResult>> { @@ -271,7 +271,7 @@ pub async fn list_all_skills( }))) } -/// GET /api/registry/sources/:name/skills - Get skills from a specific source +/// GET /api/v1/registry/sources/:name/skills - Get skills from a specific source pub async fn list_source_skills( Path(source_name): Path, State(state): State, @@ -337,7 +337,7 @@ pub async fn list_source_skills( }))) } -/// GET /api/registry/sources/:name/marketplace - Get raw marketplace.json +/// GET /api/v1/registry/sources/:name/marketplace - Get raw marketplace.json pub async fn get_marketplace( Path(source_name): Path, State(state): State, @@ -372,7 +372,7 @@ pub async fn get_marketplace( Ok(Json(ApiResponse::success(marketplace))) } -/// POST /api/registry/refresh - Refresh sources cache +/// POST /api/v1/registry/refresh - Refresh sources cache pub async fn refresh_sources( State(state): State, ) -> HttpResult>> { @@ -399,7 +399,7 @@ impl SourceDefinition { } } -/// GET /api/registry/index/skills - List all skills from the registry index +/// GET /api/v1/registry/index/skills - List all skills from the registry index /// Query parameters: /// - scope: Filter by scope (optional) /// - all_versions: Include all versions (default: false) diff --git a/crates/fastskill-core/src/http/handlers/registry_publish.rs b/crates/fastskill-core/src/http/handlers/registry_publish.rs index 66eea16..3732974 100644 --- a/crates/fastskill-core/src/http/handlers/registry_publish.rs +++ b/crates/fastskill-core/src/http/handlers/registry_publish.rs @@ -42,7 +42,7 @@ pub struct PublishStatusResponse { pub blob_storage_url: Option, } -/// POST /api/registry/publish - Publish a skill package +/// POST /api/v1/registry/publish - Publish a skill package pub async fn publish_package( State(state): State, mut multipart: Multipart, @@ -185,7 +185,7 @@ pub async fn publish_package( Ok(Json(ApiResponse::success(response))) } -/// GET /api/registry/publish/status/:job_id - Get publish job status +/// GET /api/v1/registry/publish/status/:job_id - Get publish job status pub async fn get_publish_status( Path(job_id): Path, State(state): State, diff --git a/crates/fastskill-core/src/http/handlers/skill_storage.rs b/crates/fastskill-core/src/http/handlers/skill_storage.rs deleted file mode 100644 index 1bc432a..0000000 --- a/crates/fastskill-core/src/http/handlers/skill_storage.rs +++ /dev/null @@ -1,463 +0,0 @@ -//! Skill storage service for managing uploaded skills - -use crate::core::service::FastSkillService; -use crate::security::validate_path_component; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::fs; -use uuid::Uuid; - -/// Skill metadata stored locally -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SkillMetadata { - pub id: String, - pub name: String, - pub description: String, - pub directory: String, - pub created_at: DateTime, - pub updated_at: DateTime, - pub latest_version: String, -} - -/// Skill version information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SkillVersion { - pub id: String, - pub skill_id: String, - pub version: String, // epoch timestamp - pub created_at: DateTime, - pub name: String, - pub description: String, - pub directory: String, -} - -/// Skill storage service for managing uploaded skills -pub struct SkillStorage { - #[allow(dead_code)] // Reserved for future functionality - service: Arc, - skills_dir: PathBuf, -} - -impl SkillStorage { - /// Create a new skill storage service - pub fn new(service: Arc, skills_dir: PathBuf) -> Self { - let canonical_skills_dir = Self::canonicalize_path(skills_dir); - Self { - service, - skills_dir: canonical_skills_dir, - } - } - - /// Canonicalize a path if it exists, otherwise return as-is - fn canonicalize_path(path: PathBuf) -> PathBuf { - path.canonicalize().unwrap_or(path) - } - - /// Generate a skill ID following Anthropic format (skill_01...) - pub fn generate_skill_id() -> String { - format!("skill_{}", Uuid::new_v4().simple()) - } - - /// Generate a version ID (epoch timestamp) - pub fn generate_version_id() -> String { - chrono::Utc::now().timestamp_millis().to_string() - } - - /// Store a skill version from uploaded files - pub async fn store_skill_version( - &self, - skill_id: &str, - files: HashMap>, - ) -> Result { - // Find SKILL.md file - let skill_md_content = files - .get("SKILL.md") - .or_else(|| { - // Look for files with SKILL.md in the path - files - .keys() - .find(|k| k.ends_with("SKILL.md")) - .and_then(|k| files.get(k)) - }) - .ok_or_else(|| { - crate::http::errors::HttpError::BadRequest( - "SKILL.md file not found in upload".to_string(), - ) - })?; - - // Parse SKILL.md frontmatter - let metadata = self.parse_skill_metadata(skill_md_content)?; - - // Extract directory name from file paths - let directory_raw = self.extract_directory_name(&files)?; - - // Validate and get safe directory name for path construction - let directory = validate_path_component(&directory_raw).map_err(|e| { - crate::http::errors::HttpError::BadRequest(format!("Invalid directory name: {}", e)) - })?; - - // Create version - let version_id = Self::generate_version_id(); - let version = SkillVersion { - id: format!("skillver_{}", Uuid::new_v4().simple()), - skill_id: skill_id.to_string(), - version: version_id.clone(), - created_at: Utc::now(), - name: metadata.name, - description: metadata - .description - .unwrap_or_else(|| "No description provided".to_string()), - directory: directory.clone(), - }; - - // Store files - use validated directory string - let skill_path = self.skills_dir.join(&directory); - fs::create_dir_all(&skill_path).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to create skill directory: {}", - e - )) - })?; - - for (filename, content) in files { - // Validate each path component of filename to prevent path traversal - let filename_safe = validate_path_component(&filename).map_err(|e| { - crate::http::errors::HttpError::BadRequest(format!("Invalid filename: {}", e)) - })?; - - let file_path = skill_path.join(&filename_safe); - - // Ensure the file path is still under skill_path (prevent traversal) - let canonical_skill_path = skill_path.canonicalize().map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to resolve skill path: {}", - e - )) - })?; - - // Ensure parent directories exist - if let Some(parent) = file_path.parent() { - fs::create_dir_all(parent).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to create parent directory: {}", - e - )) - })?; - } - // Verify the resolved file path is under the canonical skill path - if file_path.starts_with(&canonical_skill_path) { - fs::write(file_path, content).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to write file: {}", - e - )) - })?; - } else { - return Err(crate::http::errors::HttpError::BadRequest(format!( - "File path escapes skill directory: {}", - filename_safe - ))); - } - } - - // Update skill metadata - self.update_skill_metadata(skill_id, &version).await?; - - Ok(version) - } - - /// Parse SKILL.md frontmatter to extract metadata - fn parse_skill_metadata( - &self, - content: &[u8], - ) -> Result { - let content_str = String::from_utf8(content.to_vec()).map_err(|e| { - crate::http::errors::HttpError::BadRequest(format!("Invalid UTF-8 content: {}", e)) - })?; - let lines: Vec<&str> = content_str.lines().collect(); - - // Find YAML frontmatter (between ---) - let mut in_frontmatter = false; - let mut frontmatter_lines = Vec::new(); - - for line in lines { - if line.trim() == "---" { - if in_frontmatter { - break; // End of frontmatter - } else { - in_frontmatter = true; - continue; - } - } - - if in_frontmatter { - frontmatter_lines.push(line); - } - } - - if frontmatter_lines.is_empty() { - return Err(crate::http::errors::HttpError::BadRequest( - "No YAML frontmatter found in SKILL.md".to_string(), - )); - } - - let yaml_content = frontmatter_lines.join("\n"); - let frontmatter: SkillFrontmatter = serde_yaml::from_str(&yaml_content).map_err(|e| { - crate::http::errors::HttpError::BadRequest(format!("Invalid YAML frontmatter: {}", e)) - })?; - - Ok(frontmatter) - } - - /// Extract directory name from uploaded file paths - fn extract_directory_name( - &self, - files: &HashMap>, - ) -> Result { - // Find the common directory prefix - let mut directories = Vec::new(); - - for filename in files.keys() { - if let Some(dir) = Path::new(filename).parent() { - if let Some(dir_str) = dir.to_str() { - directories.push(dir_str.to_string()); - } - } - } - - // Find the most common directory (should be the skill directory) - if directories.is_empty() { - return Err(crate::http::errors::HttpError::BadRequest( - "No directory structure found in uploaded files".to_string(), - )); - } - - // Use the first directory as the skill directory name - // This assumes all files are under a single top-level directory - let directory = directories.into_iter().next().ok_or_else(|| { - crate::http::errors::HttpError::InternalServerError( - "Unexpected empty directories list".to_string(), - ) - })?; - Ok(directory) - } - - /// Update or create skill metadata - async fn update_skill_metadata( - &self, - skill_id: &str, - version: &SkillVersion, - ) -> Result<(), crate::http::errors::HttpError> { - let metadata_path = self.skills_dir.join("metadata.json"); - - // Load existing metadata or create new - let mut metadata: HashMap = if metadata_path.exists() { - let content = fs::read_to_string(&metadata_path).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to read metadata: {}", - e - )) - })?; - serde_json::from_str(&content).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to parse metadata: {}", - e - )) - })? - } else { - HashMap::new() - }; - - // Update or create skill metadata - let skill_meta = metadata - .entry(skill_id.to_string()) - .or_insert(SkillMetadata { - id: skill_id.to_string(), - name: version.name.clone(), - description: version.description.clone(), - directory: version.directory.clone(), - created_at: version.created_at, - updated_at: version.created_at, - latest_version: version.version.clone(), - }); - - skill_meta.updated_at = version.created_at; - skill_meta.latest_version = version.version.clone(); - - // Save metadata - let content = serde_json::to_string_pretty(&metadata).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to serialize metadata: {}", - e - )) - })?; - fs::write(metadata_path, content).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to write metadata: {}", - e - )) - })?; - - Ok(()) - } - - /// Get skill metadata by ID - pub async fn get_skill( - &self, - skill_id: &str, - ) -> Result, crate::http::errors::HttpError> { - let metadata_path = self.skills_dir.join("metadata.json"); - - if !metadata_path.exists() { - return Ok(None); - } - - let content = fs::read_to_string(metadata_path).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to read metadata: {}", - e - )) - })?; - let metadata: HashMap = - serde_json::from_str(&content).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to parse metadata: {}", - e - )) - })?; - - Ok(metadata.get(skill_id).cloned()) - } - - /// List all skills - pub async fn list_skills(&self) -> Result, crate::http::errors::HttpError> { - let metadata_path = self.skills_dir.join("metadata.json"); - - if !metadata_path.exists() { - return Ok(Vec::new()); - } - - let content = fs::read_to_string(metadata_path).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to read metadata: {}", - e - )) - })?; - let metadata: HashMap = - serde_json::from_str(&content).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to parse metadata: {}", - e - )) - })?; - - Ok(metadata.values().cloned().collect()) - } - - /// Delete a skill and all its versions - pub async fn delete_skill(&self, skill_id: &str) -> Result<(), crate::http::errors::HttpError> { - // Get skill metadata - let skill_meta = match self.get_skill(skill_id).await? { - Some(meta) => meta, - None => return Ok(()), // Skill doesn't exist - }; - - // Validate directory name and get safe string for path construction - let directory_safe = validate_path_component(&skill_meta.directory).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Invalid skill directory: {}", - e - )) - })?; - - // Remove skill directory - use validated directory string - let skill_path = self.skills_dir.join(&directory_safe); - - // Ensure the skill path is under skills_dir (prevent traversal) - let canonical_skills_dir = self.skills_dir.canonicalize().map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to resolve skills directory: {}", - e - )) - })?; - - if skill_path.exists() { - let canonical_skill_path = skill_path.canonicalize().map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to resolve skill path: {}", - e - )) - })?; - - if !canonical_skill_path.starts_with(&canonical_skills_dir) { - return Err(crate::http::errors::HttpError::BadRequest( - "Skill path escapes skills directory".to_string(), - )); - } - - fs::remove_dir_all(canonical_skill_path) - .await - .map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to remove skill directory: {}", - e - )) - })?; - } - - // Update metadata file - let metadata_path = self.skills_dir.join("metadata.json"); - if metadata_path.exists() { - let content = fs::read_to_string(&metadata_path).await.map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to read metadata: {}", - e - )) - })?; - let mut metadata: HashMap = serde_json::from_str(&content) - .map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to parse metadata: {}", - e - )) - })?; - metadata.remove(skill_id); - - let updated_content = serde_json::to_string_pretty(&metadata).map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to serialize metadata: {}", - e - )) - })?; - fs::write(metadata_path, updated_content) - .await - .map_err(|e| { - crate::http::errors::HttpError::InternalServerError(format!( - "Failed to write metadata: {}", - e - )) - })?; - } - - Ok(()) - } -} - -/// Frontmatter structure from SKILL.md -#[derive(Debug, Deserialize)] -struct SkillFrontmatter { - name: String, - description: Option, -} - -impl Default for SkillFrontmatter { - fn default() -> Self { - Self { - name: "Unnamed Skill".to_string(), - description: Some("No description provided".to_string()), - } - } -} diff --git a/crates/fastskill-core/src/http/server.rs b/crates/fastskill-core/src/http/server.rs index 34e5d87..b637df1 100644 --- a/crates/fastskill-core/src/http/server.rs +++ b/crates/fastskill-core/src/http/server.rs @@ -1,10 +1,14 @@ //! Axum HTTP server implementation +#[cfg(feature = "registry-publish")] use crate::core::registry::{StagingManager, ValidationWorker, ValidationWorkerConfig}; -use crate::core::service::{FastSkillService, ServiceConfig}; +use crate::core::service::FastSkillService; +#[cfg(feature = "registry-publish")] +use crate::core::service::ServiceConfig; +#[cfg(feature = "registry-publish")] +use crate::http::handlers::registry_publish; use crate::http::handlers::{ - claude_api, manifest, registry, registry_publish, reindex, resolve, search, skills, status, - AppState, + manifest, registry, reindex, resolve, search, skills, status, AppState, }; use axum::{ body::Body, @@ -18,6 +22,7 @@ use cli_framework::api::{ApiServerBuilder, ApiVersion, ApiVersionName, DefaultVe use include_dir::{include_dir, Dir}; use std::env; use std::net::SocketAddr; +#[cfg(feature = "registry-publish")] use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -56,6 +61,7 @@ async fn serve_embedded_static(req: Request) -> Result { } /// Validate blob storage configuration fields +#[cfg(feature = "registry-publish")] fn validate_blob_storage_fields(blob_config: &crate::core::BlobStorageConfig) -> Vec<&'static str> { let mut missing = Vec::new(); @@ -79,6 +85,7 @@ fn validate_blob_storage_fields(blob_config: &crate::core::BlobStorageConfig) -> } /// Build error message for validation failures +#[cfg(feature = "registry-publish")] fn build_validation_error_message( missing: &[&str], incomplete_fields: &[(&str, Vec<&str>)], @@ -108,6 +115,7 @@ fn build_validation_error_message( } /// Validate registry configuration and return detailed error message if invalid +#[cfg(feature = "registry-publish")] fn validate_registry_config(config: &ServiceConfig) -> Result<(), String> { let mut missing = Vec::new(); let mut incomplete_fields = Vec::new(); @@ -332,7 +340,7 @@ impl FastSkillServer { /// Registry API routes under /api/v1/ (prefix stripped, axum 0.8 params) fn create_registry_api_routes_v1() -> Router { - Router::new() + let router = Router::new() .route("/registry/index/skills", get(registry::list_index_skills)) .route("/registry/sources", get(registry::list_sources)) .route("/registry/skills", get(registry::list_all_skills)) @@ -344,12 +352,17 @@ impl FastSkillServer { "/registry/sources/{name}/marketplace", get(registry::get_marketplace), ) - .route("/registry/refresh", post(registry::refresh_sources)) + .route("/registry/refresh", post(registry::refresh_sources)); + + #[cfg(feature = "registry-publish")] + let router = router .route("/registry/publish", post(registry_publish::publish_package)) .route( "/registry/publish/status/{job_id}", get(registry_publish::get_publish_status), - ) + ); + + router } /// Raw index routes for mount("/index") — paths relative to the mount point @@ -372,31 +385,6 @@ impl FastSkillServer { ) } - /// Claude Code v1 API routes for mount("/v1") — paths relative to the mount point - fn create_claude_api_routes_v1() -> Router { - Router::new() - .route("/skills", post(claude_api::create_skill)) - .route("/skills", get(claude_api::list_skills)) - .route("/skills/{skill_id}", get(claude_api::get_skill)) - .route("/skills/{skill_id}", delete(claude_api::delete_skill)) - .route( - "/skills/{skill_id}/versions", - post(claude_api::create_skill_version), - ) - .route( - "/skills/{skill_id}/versions", - get(claude_api::list_skill_versions), - ) - .route( - "/skills/{skill_id}/versions/{version}", - get(claude_api::get_skill_version), - ) - .route( - "/skills/{skill_id}/versions/{version}", - delete(claude_api::delete_skill_version), - ) - } - /// UI static file routes (unchanged paths, used as root_fallback) fn create_ui_routes() -> Router { info!("Serving UI at /"); @@ -442,9 +430,6 @@ impl FastSkillServer { .layer(CompressionLayer::new()) .with_state(state.clone()); - // Claude-API surface mounted at /v1 (unchanged URL contract) - let claude_router = Self::create_claude_api_routes_v1().with_state(state.clone()); - // Raw index surface mounted at /index (unchanged URL contract) let index_router = Self::create_registry_index_routes_v1().with_state(state.clone()); @@ -461,33 +446,36 @@ impl FastSkillServer { deprecation: None, }) .default_version(DefaultVersion::Pinned(ApiVersionName::parse("v1")?)) - .mount("/v1", claude_router) .mount("/index", index_router) .cors(cors_layer) .root_fallback(ui_router) .health_version(env!("CARGO_PKG_VERSION")) .build(); + #[cfg(feature = "registry-publish")] let shutdown = server.shutdown_token(); - // Start validation worker bound to the server shutdown token - let config = self.service.config(); - if validate_registry_config(config).is_ok() { - let staging_dir = config - .staging_dir - .clone() - .unwrap_or_else(|| PathBuf::from(".staging")); - - let staging_manager = StagingManager::new(staging_dir); - if staging_manager.initialize().is_ok() { - let worker_config = ValidationWorkerConfig { - poll_interval_secs: 5, - blob_storage_config: config.registry_blob_storage.clone(), - registry_index_path: config.registry_index_path.clone(), - blob_base_url: config.registry_blob_base_url.clone(), - }; - let worker = ValidationWorker::new(staging_manager, worker_config); - worker.start_with_shutdown(shutdown.clone()); + // Start validation worker bound to the server shutdown token. + #[cfg(feature = "registry-publish")] + { + let config = self.service.config(); + if validate_registry_config(config).is_ok() { + let staging_dir = config + .staging_dir + .clone() + .unwrap_or_else(|| PathBuf::from(".staging")); + + let staging_manager = StagingManager::new(staging_dir); + if staging_manager.initialize().is_ok() { + let worker_config = ValidationWorkerConfig { + poll_interval_secs: 5, + blob_storage_config: config.registry_blob_storage.clone(), + registry_index_path: config.registry_index_path.clone(), + blob_base_url: config.registry_blob_base_url.clone(), + }; + let worker = ValidationWorker::new(staging_manager, worker_config); + worker.start_with_shutdown(shutdown.clone()); + } } } diff --git a/docs/cli-framework-api-adoption.md b/docs/cli-framework-api-adoption.md index caa2d34..73cd62b 100644 --- a/docs/cli-framework-api-adoption.md +++ b/docs/cli-framework-api-adoption.md @@ -1,6 +1,6 @@ # fastskill → cli-framework `api-server` adoption spec -Status: draft (2026-05-25). Goal: replace fastskill's hand-rolled Axum host with cli-framework's shipped `api-server` (+ optional `api-swagger`) host, so fastskill follows the one enforced API structure (`/api/{version}/...`, fixed `/healthz`+`/readyz`, `X-API-Version`, graceful shutdown). cli-framework is consumed as a **library only** — fastskill keeps its clap CLI; only the serve path changes. +Status: implemented and updated for the public release (2026-06-16). FastSkill uses cli-framework's `api-server` host and one public API structure: `/api/{version}/...`, fixed `/healthz` and `/readyz`, `X-API-Version`, and graceful shutdown. Registry publish is now an opt-in `registry-publish` build feature. The old Claude-compatible `/v1/skills...` HTTP surface has been removed; use MCP integration for Claude Code. References (verified against source 2026-05-25): - cli-framework host API: `aroff/cli-framework` `src/api/mod.rs` (`ApiServerBuilder`, `ApiServer`), `skill/examples/with_api/src/main.rs`. Features `api-server`, `api-swagger`. @@ -15,7 +15,7 @@ cli-framework `api-server` pins **axum 0.8, tower 0.5, tower-http 0.6**, and `pu **Verified 2026-05-25 (post-pull):** - ✅ **axum 0.8** — done (`Cargo.toml:61` `axum = "0.8"`; bumped 0.7.9 → 0.8.9 in PR #138). - ❌ **tower-http still 0.5** (`Cargo.toml:63`) — **must bump to 0.6** so the `tower_http::cors::CorsLayer` passed to `.cors(...)` is the same major cli-framework links. Keep features `cors, trace, compression-gzip, fs`. -- ⚠️ **axum 0.8 path-param syntax.** axum 0.8 replaced `:param` / `*rest` with `{param}` / `{*rest}` and panics on the old form. fastskill route literals still use the 0.7 form (`/api/skills/:id`, `/v1/skills/:skill_id`, `/index/*skill_id`, …). Confirm whether #138 migrated these; regardless, when routes are re-registered under the version router / mounts they MUST use `{id}` / `{*skill_id}`. +- ✅ **axum 0.8 path-param syntax.** Routes use `{param}` / `{*rest}` syntax. - tower 0.5 — ok. Steps: @@ -46,7 +46,6 @@ let server = ApiServerBuilder::new() // openapi field exists ONLY with feature "api-swagger" (Phase 2) }) .default_version(DefaultVersion::Pinned(ApiVersionName::parse("v1")?)) - .mount("/v1", claude_api_router) // external Anthropic-compat surface (§4) .mount("/index", registry_index_router) // raw registry index files (§4) .cors(fastskill_cors_layer) // build_cors_layer(config) result .root_fallback(ui_router) // SPA / static console (§5) @@ -75,9 +74,9 @@ Current families (`server.rs:314-423`) and where each goes: | `/api/search`, `/api/resolve`, `/api/reindex*` | `create_search_routes` | → `version("v1")` → `/api/v1/...` | | `/api/status` | `create_status_routes` | → `version("v1")` → `/api/v1/status` (app status, NOT a health check — see §6) | | `/api/manifest/skills*` | `create_manifest_routes` | → `version("v1")` → `/api/v1/manifest/...` | -| `/api/registry/*`, `/api/registry/publish*` | `create_registry_routes` | → `version("v1")` → `/api/v1/registry/...` (multipart publish included — §8) | +| `/api/registry/*` | `create_registry_routes` | → `version("v1")` → `/api/v1/registry/...` | +| `/api/registry/publish*` | `create_registry_routes` | Optional under `registry-publish` → `/api/v1/registry/publish...` | | `/index/*skill_id` | `create_registry_routes` | **mount** `"/index"` (raw index files; external/CDN-like path, keep unversioned) | -| `/v1/skills*` (Claude-API) | `create_claude_api_routes` | **mount** `"/v1"` (external Anthropic-compat contract — DO NOT version-shift, §4) | | `/`, `/index.html`, `/app.js`, `/styles.css`, `/dashboard` | `create_ui_routes` | **root_fallback** (§5) | To build the `v1` router: merge skills+search+status+manifest+registry sub-routers with their paths rewritten to drop the leading `/api`, then `.with_state(state)`. The host prepends `/api/v1`. @@ -91,16 +90,15 @@ Every fastskill `/api/...` route literal (~30 across `server.rs:314-421`) loses - integration tests under `tests/` that hit `/api/...`; - any external clients of the internal API. -The Claude-API `/v1/...` surface and the `/index/*` surface are preserved verbatim via `mount(...)`, so external consumers of those are unaffected. +The raw `/index/*` surface is preserved verbatim via `mount(...)`, so external consumers of raw index files are unaffected. The old Claude-compatible `/v1/skills...` HTTP surface is removed. --- ## 4. Auxiliary mounts (not versioned) -- **Claude-API compatibility (`/v1/skills...`).** `crates/.../handlers/claude_api.rs` explicitly mirrors Anthropic's skill API ("matching Anthropic's API specification"). Forcing it under `/api/v1/...` would break that contract. Mount it verbatim: `.mount("/v1", claude_api_router.with_state(state))`. Note this coexists with fastskill's own `/api/v1/...` — two distinct surfaces, intentional; document it. - **Raw registry index (`/index/*skill_id`).** A CDN-like artifact path consumed by skill installers; keep its URL stable via `.mount("/index", index_router.with_state(state))` rather than versioning it. (Decision: preserve URL > consistency, because external installers may hardcode it. If no external consumer relies on it, folding into `/api/v1/registry/...` is the alternative.) -`mount()` paths participate in host collision checks; `/v1` and `/index` do not overlap `/api`, `/healthz`, `/readyz`, `/api/docs`, so they pass. +`mount()` paths participate in host collision checks; `/index` does not overlap `/api`, `/healthz`, `/readyz`, `/api/docs`, so it passes. --- @@ -131,7 +129,7 @@ Action: collect the UI routes (the `serve_embedded_static` handler + `/dashboard ## 8. Multipart upload -`POST /api/registry/publish` (`handlers/registry_publish.rs:45-186`) uses `axum::extract::Multipart`. It works unchanged inside the versioned router; the path becomes `/api/v1/registry/publish`. Requires axum's `multipart` feature (already enabled). No host-level body-limit is imposed; if a cap is wanted, add a per-route `RequestBodyLimitLayer` on fastskill's router (tower-http `limit`). +`POST /api/v1/registry/publish` (`handlers/registry_publish.rs`) uses `axum::extract::Multipart`. It exists only when FastSkill is built with the `registry-publish` feature. Requires axum's `multipart` feature (already enabled). No host-level body-limit is imposed; if a cap is wanted, add a per-route `RequestBodyLimitLayer` on FastSkill's router (tower-http `limit`). --- @@ -142,7 +140,7 @@ Action: collect the UI routes (the `serve_embedded_static` handler + `/dashboard fastskill ships no OpenAPI document today. With `api-server` only, omit the `openapi` field (it's `#[cfg(feature = "api-swagger")]`). To get runtime docs at `/api/docs` + `/api/v1/openapi.json`: 1. Enable feature `api-swagger` on the cli-framework dep (pulls `utoipa-swagger-ui`). 2. Produce an OpenAPI `serde_json::Value` for the v1 surface (hand-written or via utoipa) and set `openapi: Some(value)` on the `ApiVersion`. -3. Embedded Swagger UI works offline (no CDN). The Claude-API `/v1` mount is not auto-documented (it's a mount, not a version) — document it separately if needed. +3. Embedded Swagger UI works offline (no CDN). --- @@ -150,9 +148,9 @@ fastskill ships no OpenAPI document today. With `api-server` only, omit the `ope Documentation is in-scope for this migration, not a follow-up. Update everything that describes the API surface or how to run the server: -- **Console UI** (`crates/fastskill-core/src/http/static/app.js`, `index.html`): rewrite all fetch/XHR calls from `/api/...` to `/api/v1/...`. The Claude-API `/v1/...` and `/index/*` paths are unchanged. +- **Console UI** (`crates/fastskill-core/src/http/static/app.js`, `index.html`): rewrite all fetch/XHR calls from `/api/...` to `/api/v1/...`. The `/index/*` paths are unchanged. - **README / usage docs**: update any documented endpoints to `/api/v1/...`; document the new health endpoints `/healthz` + `/readyz`, the `X-API-Version` header, and the no-version → 308 redirect behaviour. -- **`webdocs/`** (the docs site): update the HTTP API reference/base path to `/api/v1/...`; note that `/v1/skills...` remains the Anthropic-compatible surface (a separate `mount`, not versioned). +- **`webdocs/`** (the docs site): update the HTTP API reference/base path to `/api/v1/...`; note that the old Claude-compatible `/v1/skills...` surface is removed. - **`docs/`**: keep this adoption spec as the record; add/adjust any API how-to docs. - **CHANGELOG**: entry noting the breaking move to `/api/v1/...`, the added `/healthz`/`/readyz`, graceful shutdown, and that `/v1/...` (Claude-compat) and `/index/*` are unchanged. - Migrate any `tests/` (integration) that hit `/api/...` to `/api/v1/...`. @@ -161,7 +159,7 @@ Documentation is in-scope for this migration, not a follow-up. Update everything 1. **Deps (§0):** axum 0.8 is done; **bump tower-http 0.5 → 0.6** and fix any axum-0.8 `{param}` route syntax; add `cli-framework` (`api-server`). 2. **Build the `v1` router:** copy `create_skill/search/status/manifest/registry` routers, strip the `/api` prefix from every literal, merge, `.with_state(state)`. -3. **Mounts:** Claude-API router → `mount("/v1")`; `/index/*` → `mount("/index")` (both `.with_state`). +3. **Mounts:** `/index/*` → `mount("/index")` (`.with_state`). 4. **UI:** UI routes → `root_fallback(...)`. 5. **Rewrite `FastSkillServer::serve()`** to build `ApiServerBuilder` (version + default_version + mounts + cors + root_fallback + health_version), `build()`, grab `shutdown_token()`, start the worker bound to it, `server.serve(&addr).await`. 6. **Delete** the old listener bind, `axum::serve`, manual router-merge/middleware-stack, and `/health`-style status-as-health assumptions. @@ -173,7 +171,7 @@ Documentation is in-scope for this migration, not a follow-up. Update everything ## 11. Acceptance criteria -1. `fastskill serve` brings up: `/api/v1/skills`, `/api/v1/search`, `/api/v1/status`, `/api/v1/manifest/...`, `/api/v1/registry/...` (incl. multipart publish); `/v1/skills...` (Claude-API, unchanged); `/index/*`; the console UI at `/`; and framework `/healthz` + `/readyz`. +1. `fastskill serve` brings up: `/api/v1/skills`, `/api/v1/search`, `/api/v1/status`, `/api/v1/manifest/...`, `/api/v1/registry/...`; optional multipart publish under `registry-publish`; `/index/*`; the console UI at `/`; and framework `/healthz` + `/readyz`. 2. `/healthz` returns `{status, version}` with **fastskill's** crate version; `/readyz` reflects the readiness check (or always-ready). 3. `X-API-Version: v1` present on `/api/v1/...` responses. 4. `GET /api/...` with no version → 308 redirect to `/api/v1/...` (default Pinned) — or, if `DefaultVersion::None` is chosen, a 404 listing versions. diff --git a/webdocs/cli-reference/auth-command.mdx b/webdocs/cli-reference/auth-command.mdx index 302643f..4c83c85 100644 --- a/webdocs/cli-reference/auth-command.mdx +++ b/webdocs/cli-reference/auth-command.mdx @@ -9,7 +9,7 @@ The `auth` command manages authentication for FastSkill registries. It supports Authentication is required for: -- Publishing skills to registries +- Publishing skills to registries in `registry-publish` builds - Accessing private repositories - Performing admin operations on registries @@ -70,7 +70,7 @@ fastskill auth login --registry https://registry.example.com # Token stored and ready to use # 2. Publish a skill -fastskill publish --artifacts ./artifacts/pptx-1.2.3.zip --registry https://registry.example.com +fastskill publish --artifacts ./artifacts/pptx-1.2.3.zip --api-url https://registry.example.com # 3. Token is used automatically ``` @@ -115,8 +115,8 @@ fastskill auth logout --registry https://registry.example.com # Output: Successfully logged out from https://registry.example.com # 3. Try to publish (will fail) -fastskill publish --artifacts ./artifacts/pptx-1.2.3.zip --registry https://registry.example.com -# Error: No authentication token found for registry: https://registry.example.com +fastskill publish --artifacts ./artifacts/pptx-1.2.3.zip --api-url https://registry.example.com +# Remote registry rejects the request if it requires authentication ``` ### auth whoami @@ -234,10 +234,10 @@ fastskill auth whoami --registry https://staging-registry.example.com ```bash # Publish to production -fastskill publish --registry https://prod-registry.example.com --artifacts ./artifacts/pptx-1.2.3.zip +fastskill publish --api-url https://prod-registry.example.com --artifacts ./artifacts/pptx-1.2.3.zip # Publish to staging -fastskill publish --registry https://staging-registry.example.com --artifacts ./artifacts/pptx-1.2.3.zip +fastskill publish --api-url https://staging-registry.example.com --artifacts ./artifacts/pptx-1.2.3.zip ``` ## Role-Based Access Control (RBAC) @@ -407,8 +407,8 @@ export FASTSKILL_API_URL=https://registry.example.com # 3. Package skill fastskill package --skills pptx --bump patch --output ./artifacts -# 4. Publish (token is used automatically) -fastskill publish --artifacts ./artifacts --registry https://registry.example.com +# 4. Publish (requires a registry-publish build; token is used automatically) +fastskill publish --artifacts ./artifacts --api-url https://registry.example.com # 5. Verify echo "Published successfully!" @@ -424,10 +424,10 @@ fastskill auth login --registry https://prod.example.com --role admin fastskill auth login --registry https://staging.example.com --role manager # 3. Publish to staging -fastskill publish --registry https://staging.example.com --artifacts ./artifacts/pptx-1.2.3.zip +fastskill publish --api-url https://staging.example.com --artifacts ./artifacts/pptx-1.2.3.zip # 4. Promote to production -fastskill publish --registry https://prod.example.com --artifacts ./artifacts/pptx-1.2.3.zip +fastskill publish --api-url https://prod.example.com --artifacts ./artifacts/pptx-1.2.3.zip ``` ## Troubleshooting @@ -492,9 +492,9 @@ fastskill publish --registry https://prod.example.com --artifacts ./artifacts/pp - **Specify registry explicitly**: Use `--registry` flag to avoid confusion. + **Specify registry explicitly**: Use `--api-url` for registry API URLs or `--registry` for configured repository names. ```bash - fastskill publish --registry https://prod.example.com --artifacts ./artifacts/pptx.zip + fastskill publish --api-url https://prod.example.com --artifacts ./artifacts/pptx.zip ``` @@ -562,5 +562,5 @@ Authentication tokens are valuable credentials. Treat them like passwords and fo ## See Also - [Registry Overview](/registry/overview) - Understanding registries -- [Publish Command](/cli-reference/publish-command) - Publishing skills +- [Publish Command](/cli-reference/publish-command) - Optional registry publishing - [repos Command](/cli-reference/repository-command) - Managing repositories diff --git a/webdocs/cli-reference/init-command.mdx b/webdocs/cli-reference/init-command.mdx index 032d340..97e0306 100644 --- a/webdocs/cli-reference/init-command.mdx +++ b/webdocs/cli-reference/init-command.mdx @@ -320,5 +320,5 @@ Use `fastskill init --yes` to: - [Install Command](/cli-reference/install-command) - Install skills from skill-project.toml - [Package Command](/cli-reference/package-command) - Package skills for distribution -- [Publish Command](/cli-reference/publish-command) - Publish skills to registries +- [Publish Command](/cli-reference/publish-command) - Optional registry publishing for `registry-publish` builds - [Installation Guide](/installation) - Environment setup and prerequisites diff --git a/webdocs/cli-reference/overview.mdx b/webdocs/cli-reference/overview.mdx index 525457b..5cd0287 100644 --- a/webdocs/cli-reference/overview.mdx +++ b/webdocs/cli-reference/overview.mdx @@ -1,11 +1,11 @@ --- title: "CLI Reference Overview" -description: "CLI for the FastSkill package manager: installs, manifests, validation, evals, search, package, publish, and serve." +description: "CLI for the FastSkill package manager: installs, manifests, validation, evals, search, package, and serve." --- ## Overview -The FastSkill CLI is how you **install and operate** skills: manifests and `skills.lock`, validation and `fastskill eval`, search, packaging and optional publish, plus `serve` when you want a local HTTP surface. +The FastSkill CLI is how you **install and operate** skills: manifests and `skills.lock`, validation and `fastskill eval`, search, packaging, plus `serve` when you want a local HTTP surface. The CLI supports both interactive and scripting use cases, with comprehensive help text and validation for all commands. @@ -107,14 +107,14 @@ fastskill list --help # Help for list command -### Packaging & publish +### Packaging Package skills into ZIP artifacts with change detection and version bumps. See [package Command](/cli-reference/package-command). - Publish packaged artifacts to blob storage with job tracking. See [publish Command](/cli-reference/publish-command). + Optional operator command for registry publishing. It is available only in builds compiled with `registry-publish`. See [publish Command](/cli-reference/publish-command). @@ -189,7 +189,7 @@ index_url = "https://api.fastskill.io/index" priority = 0 ``` -For publishing, create `.fastskill/publish.toml`: +For opt-in registry publishing builds, create `.fastskill/publish.toml`: ```toml .fastskill/publish.toml [blob_storage] diff --git a/webdocs/cli-reference/package-command.mdx b/webdocs/cli-reference/package-command.mdx index 66f7ff6..b2953ef 100644 --- a/webdocs/cli-reference/package-command.mdx +++ b/webdocs/cli-reference/package-command.mdx @@ -277,6 +277,5 @@ The package command is designed for CI/CD workflows: ## See Also -- [Publish Command](/cli-reference/publish-command) +- [Publish Command](/cli-reference/publish-command) - optional `registry-publish` builds only - [CI/CD Integration Guide](/integration/cicd-pipelines) - diff --git a/webdocs/cli-reference/publish-command.mdx b/webdocs/cli-reference/publish-command.mdx index bdd6452..d67a753 100644 --- a/webdocs/cli-reference/publish-command.mdx +++ b/webdocs/cli-reference/publish-command.mdx @@ -1,5 +1,5 @@ --- -title: publish +title: "publish (optional)" api: "publish" --- @@ -7,6 +7,10 @@ api: "publish" Publish skill packages to a registry API or local folder. + +`fastskill publish` is an optional operator command. Public/default builds do not include it. Build or install FastSkill with the `registry-publish` feature when you need this command. + + ## Usage ```bash @@ -18,11 +22,14 @@ fastskill publish [OPTIONS] | Option | Description | Default | |--------|-------------|---------| | `--artifacts ` | Package file or directory containing ZIP artifacts | `./artifacts` | -| `--target ` | API URL (for example `https://registry.example.com`) or local folder path | `FASTSKILL_API_URL` or `http://localhost:8080` | -| `--wait` | Wait for validation to complete | `true` | -| `--no-wait` | Do not wait for validation (overrides `--wait`) | `false` | +| `--registry ` | Repository name from `repositories.toml` | none | +| `--api-url ` | Registry API base URL, for example `https://registry.example.com` | `FASTSKILL_API_URL` if no target flag is set | +| `--local-dir ` | Local directory to copy ZIP artifacts into | none | +| `--wait ` | Wait for validation to complete in API mode | `true` | | `--max-wait ` | Maximum wait time in seconds | `300` | +`--registry`, `--api-url`, and `--local-dir` are mutually exclusive. + ## Examples ### Publish to Local `fastskill serve` @@ -30,7 +37,7 @@ fastskill publish [OPTIONS] ```bash fastskill publish \ --artifacts ./artifacts \ - --target http://localhost:8080 + --api-url http://localhost:8080 ``` Local `fastskill serve` publish endpoints do not require auth headers. @@ -41,17 +48,25 @@ Local `fastskill serve` publish endpoints do not require auth headers. fastskill auth login --registry https://registry.example.com fastskill publish \ --artifacts ./artifacts \ - --target https://registry.example.com + --api-url https://registry.example.com ``` Remote registries may require authentication depending on server configuration. +### Publish Through a Configured Repository + +```bash +fastskill publish \ + --artifacts ./artifacts \ + --registry official +``` + ### Publish to Local Folder ```bash fastskill publish \ --artifacts ./artifacts \ - --target ./local-registry + --local-dir ./local-registry ``` ## Local Publish Identity @@ -66,14 +81,14 @@ When publishing to local `fastskill serve`, identity is deterministic and anonym 1. Upload package using multipart form data 2. Package is staged for validation -3. Check status with `/api/registry/publish/status/:job_id` +3. Check status with `/api/v1/registry/publish/status/:job_id` 4. Accepted packages are written to configured outputs ## Troubleshooting - **"No artifacts found"**: Verify `--artifacts` path points to a ZIP file or directory containing ZIP files -- **"Publish timeout"**: Increase `--max-wait` or rerun with `--no-wait` -- **"Registry unavailable"**: Check `--target` URL and network connectivity +- **"Publish timeout"**: Increase `--max-wait` or set `--wait false` +- **"Registry unavailable"**: Check `--api-url` URL and network connectivity ## See Also diff --git a/webdocs/cli-reference/serve-command.mdx b/webdocs/cli-reference/serve-command.mdx index 13ac6d0..5707930 100644 --- a/webdocs/cli-reference/serve-command.mdx +++ b/webdocs/cli-reference/serve-command.mdx @@ -40,7 +40,7 @@ fastskill serve --host 0.0.0.0 --port 3000 - No token endpoint is exposed - API routes do not require `Authorization` or `x-api-key` headers -- Registry publish routes use anonymous identity (`anonymous/`) +- Registry publish routes, when compiled with the `registry-publish` feature, use anonymous identity (`anonymous/`) If you expose the server on a shared or untrusted network, use network controls or a reverse proxy. @@ -73,7 +73,7 @@ Sending `SIGINT` (Ctrl-C) or `SIGTERM` causes the server to: 1. Flip `/readyz` to HTTP 503 so load balancers stop routing new traffic. 2. Drain in-flight requests to completion. -3. Stop the background `ValidationWorker` (if registry is configured). +3. Stop the background `ValidationWorker` when the server is built with `registry-publish` and registry publishing is configured. 4. Exit cleanly with exit code 0. ## Core Endpoints @@ -89,15 +89,21 @@ Sending `SIGINT` (Ctrl-C) or `SIGTERM` causes the server to: | `/api/v1/search` | POST | Search skills | | `/api/v1/reindex` | POST | Reindex all skills | | `/api/v1/reindex/{id}` | POST | Reindex a specific skill | -| `/api/v1/registry/publish` | POST | Publish package (anonymous scope) | -| `/api/v1/registry/publish/status/{job_id}` | GET | Publish job status | | `/api/v1/registry/sources` | GET | List registry sources | | `/api/v1/manifest/skills` | GET/POST | Manifest skill management | -| `/v1/skills` | GET/POST/DELETE | Claude-API compatible skills surface (unchanged) | | `/index/{*skill_id}` | GET | Raw skill index (unchanged) | | `/healthz` | GET | Liveness probe | | `/readyz` | GET | Readiness probe | +## Optional Registry Publish Endpoints + +These endpoints exist only when FastSkill is built with the `registry-publish` feature: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/registry/publish` | POST | Publish package with anonymous local identity | +| `/api/v1/registry/publish/status/{job_id}` | GET | Publish job status | + ## Redirect Behavior `GET /api/skills` → HTTP 308 → `GET /api/v1/skills` (and similarly for all `/api/…` paths). diff --git a/webdocs/cli-reference/update-command.mdx b/webdocs/cli-reference/update-command.mdx index 3a9f1ae..133e9b6 100644 --- a/webdocs/cli-reference/update-command.mdx +++ b/webdocs/cli-reference/update-command.mdx @@ -459,7 +459,7 @@ $ fastskill update web-scraper --version "99.9.9" error: Version '99.9.9' not found for skill 'web-scraper' ``` -**Solution**: Check available versions with `fastskill registry show-skill web-scraper`. +**Solution**: Check available versions with `fastskill repos versions web-scraper`. ### No Updates Available diff --git a/webdocs/index.mdx b/webdocs/index.mdx index 27f3046..05c58ab 100644 --- a/webdocs/index.mdx +++ b/webdocs/index.mdx @@ -65,10 +65,10 @@ Then optional: `fastskill reindex`, `fastskill search "…" --local`, `fastskill ## Use cases (summary) -- **Authors**: init, validate, eval, package, publish when ready. +- **Authors**: init, validate, eval, and package when ready; publish requires an operator build with `registry-publish`. - **Developers**: install, list, read, search for what agents load. - **Teams**: shared manifest + lock; groups for optional stacks. -- **Automation**: `install --lock`, package, publish in CI (patterns in [CI/CD](/integration/cicd-pipelines)). +- **Automation**: `install --lock`, package, and optional publish in CI (patterns in [CI/CD](/integration/cicd-pipelines)). ## Next steps diff --git a/webdocs/integration/blob-storage.mdx b/webdocs/integration/blob-storage.mdx index 33ff8dd..979b17f 100644 --- a/webdocs/integration/blob-storage.mdx +++ b/webdocs/integration/blob-storage.mdx @@ -1,351 +1,82 @@ --- title: "Blob Storage Integration" -description: "Guide to configuring and using blob storage backends for skill artifact distribution" +description: "Configure registry artifact storage for opt-in publish builds." --- # Blob Storage Integration -FastSkill supports multiple blob storage backends for distributing skill artifacts. This guide covers configuration, authentication, and best practices for each storage type. +Blob storage is an operator-side concern for registry servers. Public/default FastSkill builds do not include registry publishing, and `fastskill publish` does not expose direct S3 flags. -## Supported Storage Backends +Use this page only when running a FastSkill build compiled with the `registry-publish` feature. -### Local Filesystem +## Model -Store artifacts on the local filesystem. Useful for: -- Development and testing -- Local deployments -- Backup storage +FastSkill separates packaging from publishing: -### S3 Storage +1. `fastskill package` creates ZIP artifacts. +2. `fastskill publish` sends those ZIPs to a registry API or copies them to a local directory. +3. A registry server built with `registry-publish` validates accepted packages, writes artifacts to configured blob storage, and updates the registry index. -Support for AWS S3 and S3-compatible services (via endpoint configuration): -- **AWS S3**: Amazon Web Services Simple Storage Service -- **S3-compatible services**: Any service that implements the S3 API (configure via endpoint) - -## Configuration - -### Command Line Options - -Configure storage via command line: - -```bash -fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --bucket skills-registry \ - --region us-east-1 \ - --endpoint https://s3.amazonaws.com -``` - -### Configuration File - -Use `.fastskill/publish.toml` for persistent configuration: - -```toml -[blob_storage] -type = "s3" # or "local" -endpoint = "https://s3.amazonaws.com" # Optional: set for S3-compatible services -bucket = "skills-registry" -region = "us-east-1" -access_key_id = "${AWS_ACCESS_KEY_ID}" -secret_access_key = "${AWS_SECRET_ACCESS_KEY}" - -[registry] -git_url = "https://github.com/GoFastSkill/skill-registry.git" -branch = "main" -blob_base_url = "https://skills-registry.s3.amazonaws.com" -``` - -### Environment Variables - -Set credentials via environment variables: - -```bash -export AWS_ACCESS_KEY_ID=your-access-key -export AWS_SECRET_ACCESS_KEY=your-secret-key - -# For S3-compatible services, use the same AWS environment variables -# and configure the endpoint in your service configuration -``` - -## Storage Backend Details - -### Local Filesystem - -**Configuration**: - -```bash -fastskill publish --artifacts ./artifacts --blob-storage local -``` - -**Use Cases**: -- Local development -- Testing workflows -- Backup storage -- Air-gapped environments - -**Storage Path**: Artifacts are stored in the specified base path (default: `./artifacts`). - -### AWS S3 - -**Configuration**: +## CLI Targets ```bash -fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --bucket my-bucket \ - --region us-east-1 -``` - -**Authentication**: -- AWS Access Key ID and Secret Access Key -- IAM roles (when running on AWS) -- AWS credentials file (`~/.aws/credentials`) +# Publish to a registry API +fastskill publish --artifacts ./artifacts --api-url https://registry.example.com -**IAM Permissions Required**: -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:PutObject", - "s3:GetObject", - "s3:DeleteObject", - "s3:ListBucket" - ], - "Resource": [ - "arn:aws:s3:::my-bucket/*", - "arn:aws:s3:::my-bucket" - ] - } - ] -} -``` - -**Best Practices**: -- Use IAM roles instead of access keys when possible -- Enable versioning on S3 buckets -- Use bucket policies for access control -- Enable server-side encryption - -### S3-Compatible Services +# Publish through a configured repository name +fastskill publish --artifacts ./artifacts --registry official -**Configuration**: - -For S3-compatible services (e.g., self-hosted storage), use the same S3 configuration but set the `endpoint` parameter: - -```bash -fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --endpoint https://your-s3-compatible-service.com \ - --bucket my-bucket \ - --region us-east-1 +# Copy artifacts to a local directory +fastskill publish --artifacts ./artifacts --local-dir ./upload ``` -**Use Cases**: -- Self-hosted storage solutions -- Development environments -- Private cloud deployments -- Any service implementing the S3 API +## Server Storage -**Note**: S3-compatible services use the same AWS-style credentials and API, so you can use `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. +Configure blob storage in the registry server's service configuration. S3 and S3-compatible storage require the `registry-publish` feature. -## Authentication - -### AWS S3 - -**Method 1: Environment Variables** - -```bash -export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE -export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -fastskill publish --artifacts ./artifacts --blob-storage s3 --bucket my-bucket -``` - -**Method 2: Configuration File** - -```toml -[blob_storage] -type = "s3" -access_key_id = "${AWS_ACCESS_KEY_ID}" -secret_access_key = "${AWS_SECRET_ACCESS_KEY}" -``` - -**Method 3: AWS Credentials File** - -```ini -# ~/.aws/credentials -[default] -aws_access_key_id = AKIAIOSFODNN7EXAMPLE -aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -``` +Typical fields are: -### S3-Compatible Services +- storage type +- bucket +- region +- endpoint for S3-compatible services +- access key and secret key, usually sourced from deployment secrets +- public blob base URL for generated download links -**Environment Variables**: +Keep credentials out of repository files. Inject them through your deployment platform or secret manager. -For S3-compatible services, use the same AWS environment variables: - -```bash -export AWS_ACCESS_KEY_ID=your-access-key -export AWS_SECRET_ACCESS_KEY=your-secret-key -fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --endpoint https://your-s3-compatible-service.com \ - --bucket my-bucket \ - --region us-east-1 -``` - -## Upload Workflow - -### Basic Upload +## Workflow ```bash # 1. Package skills fastskill package --detect-changes --auto-bump --output ./artifacts -# 2. Upload to blob storage -fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --bucket skills-registry \ - --region us-east-1 -``` - -### With Registry Index - -```bash -# 1. Package skills -fastskill package --detect-changes --auto-bump --output ./artifacts +# 2. Publish to the registry API +fastskill publish --artifacts ./artifacts --api-url https://registry.example.com -# 2. Publish to registry (automatically uploads to blob storage and updates index) -fastskill publish \ - --artifacts ./artifacts \ - --registry official +# 3. Poll the job status if needed +curl https://registry.example.com/api/v1/registry/publish/status/{job_id} ``` -## Download URLs - -### Public Access - -For public artifacts, the blob base URL is configured in your repository configuration and automatically used during publishing. - -This generates download URLs like: -``` -https://skills-registry.s3.amazonaws.com/skills/web-scraper-1.2.3.zip -``` - -### Private Access - -For private artifacts: - -1. Use signed URLs (future feature) -2. Configure access control via bucket policies -3. Use IAM roles for authenticated access - -## Best Practices - -### Security - -1. **Never commit credentials**: Use environment variables or secrets management -2. **Use IAM roles**: Prefer IAM roles over access keys when possible -3. **Enable encryption**: Use server-side encryption for sensitive artifacts -4. **Access control**: Use bucket policies to restrict access -5. **Rotate credentials**: Regularly rotate access keys - -### Performance - -1. **CDN Integration**: Use CloudFront or similar CDN for faster downloads -2. **Compression**: ZIP files are already compressed -3. **Parallel Uploads**: FastSkill uploads artifacts in parallel when possible -4. **Regional Storage**: Store artifacts close to users - -### Cost Optimization - -1. **Lifecycle Policies**: Set up S3 lifecycle policies to archive old artifacts -2. **Storage Classes**: Use appropriate S3 storage classes (Standard, IA, Glacier) -3. **Cleanup**: Regularly remove old or unused artifacts -4. **Monitoring**: Monitor storage usage and costs - -### Reliability - -1. **Versioning**: Enable versioning on storage buckets -2. **Replication**: Use cross-region replication for redundancy -3. **Backup**: Maintain backups of critical artifacts -4. **Monitoring**: Set up alerts for storage issues - -## Troubleshooting - -### Upload Failures - -**Issue**: Upload fails with authentication error - -**Solutions**: -- Verify credentials are correct -- Check IAM permissions -- Ensure bucket exists and is accessible -- Verify endpoint URL is correct - -### Access Denied - -**Issue**: Cannot access uploaded artifacts - -**Solutions**: -- Check bucket policies -- Verify IAM permissions -- Ensure public access is configured (if needed) -- Check CORS settings for web access - -### Slow Uploads - -**Issue**: Uploads are slow - -**Solutions**: -- Check network connectivity -- Use regional endpoints -- Enable multipart uploads (automatic for large files) -- Consider using faster storage class - -## CI/CD Integration - -### GitHub Actions +## CI Example ```yaml -- name: Upload to blob storage - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: | - fastskill publish \ - --artifacts ./artifacts \ - --blob-storage s3 \ - --bucket ${{ secrets.S3_BUCKET }} \ - --region us-east-1 -``` - -### GitLab CI - -```yaml -publish: - script: - - fastskill publish - --artifacts ./artifacts - --blob-storage s3 - --bucket $S3_BUCKET - --region us-east-1 - variables: - AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY +jobs: + publish-skills: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Package + run: fastskill package --git-diff HEAD~1 HEAD --auto-bump --output ./artifacts + - name: Publish + run: fastskill publish --artifacts ./artifacts --api-url "${FASTSKILL_API_URL}" + env: + FASTSKILL_API_URL: ${{ secrets.FASTSKILL_API_URL }} + FASTSKILL_API_TOKEN: ${{ secrets.FASTSKILL_API_TOKEN }} ``` ## See Also - [Publish Command](/cli-reference/publish-command) - [Registry Index System](/registry/index-system) -- [CI/CD Integration Guide](/integration/cicd-pipelines) - +- [CI/CD Pipelines](/integration/cicd-pipelines) diff --git a/webdocs/integration/cicd-pipelines.mdx b/webdocs/integration/cicd-pipelines.mdx index b4433e5..806e6c1 100644 --- a/webdocs/integration/cicd-pipelines.mdx +++ b/webdocs/integration/cicd-pipelines.mdx @@ -1,19 +1,19 @@ --- title: "CI/CD Pipeline Integration" -description: "Use FastSkill in automated pipelines to package and publish skills with reproducible installs." +description: "Use FastSkill in automated pipelines to package skills and optionally publish them with reproducible installs." --- # CI/CD Pipeline Integration -This page describes how teams typically wire `fastskill` into automation: detect changes, package ZIPs, and publish to a registry or object storage. It stays product-focused; adapt steps to GitHub Actions, GitLab CI, Jenkins, or any runner you already use. +This page describes how teams typically wire `fastskill` into automation: detect changes, package ZIPs, and optionally publish to a registry. Publishing requires a FastSkill build compiled with the `registry-publish` feature. ## What you can automate - **Automatic Change Detection**: Detect which skills have changed using git diff or file hashing - **Version Bumping**: Automatically increment semantic versions (major, minor, patch) - **Artifact Creation**: Package skills into ZIP files with metadata and checksums -- **Blob Storage Publishing**: Upload artifacts to S3 (supports S3-compatible services) -- **Registry Index Management**: Maintain a crates.io-like registry for skill distribution +- **Optional Registry Publishing**: Send packaged artifacts to a registry API or local handoff directory +- **Registry Index Management**: Maintain a registry index for skill distribution in operator builds ## Change Detection Strategies @@ -85,13 +85,13 @@ Future versions may analyze commit messages or change types to determine appropr ```bash fastskill package --detect-changes --auto-bump --output ./artifacts -fastskill publish --registry official # or --api-url / --local-dir; see publish docs +fastskill publish --registry official # requires registry-publish; or use --api-url / --local-dir ``` ### Example: hosted runner (outline) 1. Check out your repository with enough history for `fastskill package --git-diff `. -2. Install the `fastskill` binary using the same method you use on laptops (release archive, package feed, or internal installer). Avoid hard-coding monorepo paths from the FastSkill source tree. +2. Install the `fastskill` binary using the same method you use on laptops. Use an operator build with `registry-publish` if the job publishes artifacts. 3. Restore secrets for registries (`FASTSKILL_API_URL`, tokens, cloud credentials) as environment variables. 4. Run `fastskill package …` then `fastskill publish …` with the flags from [package](/cli-reference/package-command) and [publish](/cli-reference/publish-command). @@ -118,7 +118,7 @@ jobs: ### Other runners -GitLab CI, Jenkins, Buildkite, and others follow the same outline: put `fastskill` on `PATH`, check out sources, run `package` then `publish`. Copy the CLI arguments from `fastskill package --help` and `fastskill publish --help` rather than relying on historical workflow snippets. +GitLab CI, Jenkins, Buildkite, and others follow the same outline: put `fastskill` on `PATH`, check out sources, run `package`, then run `publish` only in jobs that use a `registry-publish` build. Copy the CLI arguments from `fastskill package --help` and `fastskill publish --help` rather than relying on historical workflow snippets. ```groovy // Jenkins-style pseudocode — replace install and credentials with yours @@ -231,6 +231,5 @@ If registry index operations fail: ## See Also - [Package Command](/cli-reference/package-command) -- [Publish Command](/cli-reference/publish-command) +- [Publish Command](/cli-reference/publish-command) - optional `registry-publish` builds only - [Blob Storage Guide](/integration/blob-storage) - diff --git a/webdocs/integration/claude-code-integration.mdx b/webdocs/integration/claude-code-integration.mdx index 5274b23..7400747 100644 --- a/webdocs/integration/claude-code-integration.mdx +++ b/webdocs/integration/claude-code-integration.mdx @@ -91,13 +91,6 @@ After running the command, reload Claude Code. The tools exposed by `fastskill m ## HTTP API -When using `fastskill serve`, the Claude-compatible skills API surface is available at: +`fastskill serve` exposes FastSkill's versioned application API under `/api/v1/…`, for example `GET /api/v1/status` and `GET /api/v1/skills`. -- `GET /v1/skills` — list skills (Anthropic-compatible endpoint, URL unchanged) -- `POST /v1/skills` — create/install a skill -- `GET /v1/skills/{skill_id}` — get a specific skill -- `DELETE /v1/skills/{skill_id}` — remove a skill -- `GET /v1/skills/{skill_id}/versions` — list versions - -The internal application API has moved to `/api/v1/…` (e.g. `GET /api/v1/status`). -The Claude-compatible `/v1/skills…` surface and the raw index `/index/…` URLs are unchanged. +The old Claude-compatible `/v1/skills…` HTTP surface is no longer part of the public server. Use the MCP integration above for Claude Code and use `/api/v1/…` for HTTP clients. diff --git a/webdocs/registry/index-system.mdx b/webdocs/registry/index-system.mdx index d58ecb9..c9c2ef6 100644 --- a/webdocs/registry/index-system.mdx +++ b/webdocs/registry/index-system.mdx @@ -94,6 +94,8 @@ Each skill has a single file at `{scope}/{id}` containing newline-delimited JSON The registry index is maintained automatically during the publishing process. No manual index management is required. +Publishing requires a FastSkill build compiled with the `registry-publish` feature. + ### Publishing Workflow ```bash @@ -231,7 +233,6 @@ Published skills are automatically uploaded to configured blob storage: ## See Also - [Package Command](/cli-reference/package-command) -- [Publish Command](/cli-reference/publish-command) +- [Publish Command](/cli-reference/publish-command) - optional `registry-publish` builds only - [Blob Storage Guide](/integration/blob-storage) - [Registry Overview](/registry/overview) - diff --git a/webdocs/registry/marketplace-json.mdx b/webdocs/registry/marketplace-json.mdx index 2422215..f243cc9 100644 --- a/webdocs/registry/marketplace-json.mdx +++ b/webdocs/registry/marketplace-json.mdx @@ -108,10 +108,10 @@ The conversion preserves all available metadata and enables FastSkill to work wi ## Generating marketplace.json -Use the `fastskill registry create` command to generate `marketplace.json` in Claude Code format from a directory of skills: +Use the `fastskill marketplace create` command to generate `marketplace.json` in Claude Code format from a directory of skills: ```bash -fastskill registry create --path ./skills --name my-repo +fastskill marketplace create ./skills --name my-repo ``` **Options:** @@ -213,4 +213,3 @@ Marketplace.json files are cached for 5 minutes to reduce network requests. To f - [Repository Command](/cli-reference/repository-command) - [Sources Configuration](/registry/sources) - [Registry Overview](/registry/overview) - diff --git a/webdocs/registry/overview.mdx b/webdocs/registry/overview.mdx index c174cd7..5fefb03 100644 --- a/webdocs/registry/overview.mdx +++ b/webdocs/registry/overview.mdx @@ -23,7 +23,7 @@ Start the server (UI and registry HTTP routes are available whenever `serve` is fastskill serve ``` -Then open `http://localhost:8080/registry` in your browser. +Then open `http://localhost:8080/` or `http://localhost:8080/dashboard` in your browser. ## Architecture @@ -46,9 +46,9 @@ Only sources that provide `marketplace.json` files are displayed in the registry ## Workflow 1. **Configure repositories**: Add repositories to `[tool.fastskill.repositories]` section in `skill-project.toml` -2. **Generate marketplace.json**: Use `fastskill registry create` to generate marketplace files +2. **Generate marketplace.json**: Use `fastskill marketplace create` to generate marketplace files 3. **Start server**: Run `fastskill serve` -4. **Browse skills**: Open `/registry` in your browser or use `fastskill registry list-skills` to list skills from HTTP registries +4. **Browse skills**: Open `/` or `/dashboard` in your browser or use `fastskill repos skills` to list skills from configured repositories 5. **List installed skills**: Use `fastskill list` to see locally installed skills with reconciliation status 6. **Install skills**: Click "Install" in the web UI or use `fastskill add` to add skills to your local installation @@ -58,4 +58,3 @@ Only sources that provide `marketplace.json` files are displayed in the registry - [Marketplace.json Format](/registry/marketplace-json) - [Web UI Guide](/registry/web-ui) - [Repository Command](/cli-reference/repository-command) - diff --git a/webdocs/registry/repository-architecture.mdx b/webdocs/registry/repository-architecture.mdx index bc889e9..b4bc6d8 100644 --- a/webdocs/registry/repository-architecture.mdx +++ b/webdocs/registry/repository-architecture.mdx @@ -370,52 +370,53 @@ priority = 0 ## CLI Commands for Repository Management -### fastskill sources +### fastskill repos Primary command for repository management: ```bash # List all repositories -fastskill sources list +fastskill repos list # Add new repository -fastskill sources add team-tools --type git-marketplace \ - --url https://github.com/team/skills.git \ +fastskill repos add team-tools --repo-type git-marketplace \ + https://github.com/team/skills.git \ --priority 1 # Remove repository -fastskill sources remove team-tools +fastskill repos remove team-tools # Show repository details -fastskill sources show team-tools +fastskill repos info team-tools # Update repository -fastskill sources update team-tools --priority 0 +fastskill repos update team-tools --priority 0 # Test repository connectivity -fastskill sources test team-tools +fastskill repos test team-tools # Refresh repository cache -fastskill sources refresh +fastskill repos refresh # Create marketplace.json from directory -fastskill sources create ./skills --output marketplace.json +fastskill marketplace create ./skills --output marketplace.json ``` -### fastskill registry - -Legacy alias for repository management (same functionality): +### Catalog browsing ```bash -# Add repository (legacy) -fastskill sources add team-tools --repo-type git-marketplace \ +# Add repository +fastskill repos add team-tools --repo-type git-marketplace \ https://github.com/team/skills.git # List skills from repository -fastskill registry list-skills +fastskill repos skills # Show skill from repository -fastskill registry show-skill pptx +fastskill repos show pptx + +# List available versions +fastskill repos versions pptx ``` ## Repository Authentication Examples @@ -424,33 +425,33 @@ fastskill registry show-skill pptx ```bash # Set up GitHub PAT repository -fastskill sources add github-skills \ - --type git-marketplace \ - --url https://github.com/user/skills.git \ +fastskill repos add github-skills \ + --repo-type git-marketplace \ + https://github.com/user/skills.git \ --auth-type pat \ - --auth-value ghp_xxxxxxxxxxxxxxxxxxxx + --auth-env GITHUB_TOKEN ``` ### Private Registry with API Key ```bash # Configure private registry -fastskill sources add private-registry \ - --type http-registry \ - --url https://private.example.com/index \ +fastskill repos add private-registry \ + --repo-type http-registry \ + https://private.example.com \ --auth-type api-key \ - --auth-value sk_live_xxxxxxxxxxxxxxxxxxxx + --auth-env PRIVATE_REGISTRY_TOKEN ``` ### Git with SSH ```bash # Set up SSH key repository -fastskill sources add gitlab-skills \ - --type git-marketplace \ - --url git@gitlab.com:team/skills.git \ +fastskill repos add gitlab-skills \ + --repo-type git-marketplace \ + git@gitlab.com:team/skills.git \ --auth-type ssh-key \ - --auth-value /home/user/.ssh/id_ed25519 + --auth-key-path /home/user/.ssh/id_ed25519 ``` ## Skill Discovery and Installation @@ -553,7 +554,7 @@ Use environment-specific repositories or branch parameters to control which skil - **Resolution**: Use `fastskill sources list` to see all repositories and `fastskill sources test ` to verify connectivity. + **Resolution**: Use `fastskill repos list` to see all repositories and `fastskill repos test ` to verify connectivity. @@ -571,7 +572,7 @@ Use environment-specific repositories or branch parameters to control which skil - **Resolution**: Test authentication with `fastskill sources test `. + **Resolution**: Test authentication with `fastskill repos test `. @@ -581,7 +582,7 @@ Use environment-specific repositories or branch parameters to control which skil - **Resolution**: Use `fastskill sources show ` to verify repository priority. + **Resolution**: Use `fastskill repos info ` to verify repository priority. @@ -602,7 +603,7 @@ Use environment-specific repositories or branch parameters to control which skil - Use `fastskill sources test ` to verify repository connectivity and authentication before relying on it. + Use `fastskill repos test ` to verify repository connectivity and authentication before relying on it. @@ -618,7 +619,7 @@ Use environment-specific repositories or branch parameters to control which skil - Use `fastskill sources refresh` to update repository caches after remote changes. + Use `fastskill repos refresh` to update repository caches after remote changes. diff --git a/webdocs/registry/sources.mdx b/webdocs/registry/sources.mdx index b781ebf..7ea8c35 100644 --- a/webdocs/registry/sources.mdx +++ b/webdocs/registry/sources.mdx @@ -101,7 +101,7 @@ EOF **3. Generate Marketplace.json** ```bash # Use fastskill to generate marketplace.json -fastskill registry create --path ./skills --name my-skills-repo +fastskill marketplace create ./skills --name my-skills-repo # This creates .claude-plugin/marketplace.json ``` @@ -121,13 +121,13 @@ priority = 0 **5. Test Repository** ```bash # Add the repository to your project -fastskill sources add my-skills-repo \ +fastskill repos add my-skills-repo \ --repo-type git-marketplace \ - --url https://github.com/your-org/my-skills-repo.git \ + https://github.com/your-org/my-skills-repo.git \ --priority 0 # List skills from the repository -fastskill registry list-skills --repository my-skills-repo +fastskill repos skills --repository my-skills-repo # Install a skill fastskill add my-skills-repo/web-scraper @@ -232,7 +232,7 @@ export FASTSKILL_API_TOKEN="your-jwt-token" [[tool.fastskill.repositories]] name = "company-registry" type = "http-registry" -index_url = "https://registry.yourcompany.com/api/registry/index" +index_url = "https://registry.yourcompany.com" priority = 0 auth = { type = "pat", env_var = "FASTSKILL_API_TOKEN" } ``` @@ -240,19 +240,19 @@ auth = { type = "pat", env_var = "FASTSKILL_API_TOKEN" } **4. Browse and Install Skills** ```bash # List all skills in registry -fastskill registry list-skills --repository company-registry +fastskill repos skills --repository company-registry # List skills by scope -fastskill registry list-skills --repository company-registry --scope engineering +fastskill repos skills --repository company-registry --scope engineering # Search for skills fastskill search "data analysis" --repo company-registry # Get skill details -fastskill registry show-skill engineering/data-analyzer --repository company-registry +fastskill repos show engineering/data-analyzer --repository company-registry # List skill versions -fastskill registry versions engineering/data-analyzer --repository company-registry +fastskill repos versions engineering/data-analyzer --repository company-registry # Install skill fastskill add engineering/data-analyzer @@ -270,6 +270,8 @@ fastskill package --skills data-analyzer web-scraper --output ./artifacts ``` **Publish to Registry:** +Requires a FastSkill build compiled with the `registry-publish` feature. + ```bash # Authenticate fastskill auth login @@ -277,12 +279,12 @@ fastskill auth login # Publish packages fastskill publish \ --artifacts ./artifacts \ - --target https://registry.yourcompany.com \ - --wait + --api-url https://registry.yourcompany.com \ + --wait true # Check publish status curl -H "Authorization: Bearer $FASTSKILL_API_TOKEN" \ - https://registry.yourcompany.com/api/registry/publish/status/{job_id} + https://registry.yourcompany.com/api/v1/registry/publish/status/{job_id} ``` #### Configuration Options @@ -327,21 +329,21 @@ auth = { type = "ssh-key", auth_key_path = "~/.ssh/fastskill_key" } [[tool.fastskill.repositories]] name = "dev-registry" type = "http-registry" -index_url = "https://dev-registry.company.com/api/registry/index" +index_url = "https://dev-registry.company.com" priority = 1 # Staging registry [[tool.fastskill.repositories]] name = "staging-registry" type = "http-registry" -index_url = "https://staging-registry.company.com/api/registry/index" +index_url = "https://staging-registry.company.com" priority = 2 # Production registry [[tool.fastskill.repositories]] name = "prod-registry" type = "http-registry" -index_url = "https://registry.company.com/api/registry/index" +index_url = "https://registry.company.com" priority = 0 ``` @@ -358,12 +360,12 @@ priority = 2 [[tool.fastskill.repositories]] name = "company-private" type = "http-registry" -index_url = "https://registry.company.com/api/registry/index" +index_url = "https://registry.company.com" priority = 0 auth = { type = "pat", env_var = "COMPANY_TOKEN" } ``` -Use `fastskill registry list-skills` to list skills from HTTP registries. The command calls `GET /api/registry/index/skills` on the configured registry's `index_url`. +Use `fastskill repos skills` to list skills from HTTP registries. The command calls `GET /api/v1/registry/index/skills` on the configured registry's `index_url`. The registry server serves index files at `/index/{skill_id}` where `skill_id` follows the format `{scope}/{skill-name}` (e.g., `dev-user/web-scraper`). @@ -446,13 +448,13 @@ priority = 0 **5. Test Repository** ```bash # Add repository -fastskill sources add company-tools \ +fastskill repos add company-tools \ --repo-type zip-url \ - --url https://skills.company.com/ \ + https://skills.company.com/ \ --priority 0 # List available skills -fastskill registry list-skills --repository company-tools +fastskill repos skills --repository company-tools # Install skills fastskill add company-tools/web-scraper @@ -536,7 +538,7 @@ jobs: run: fastskill package --detect-changes --output ./packages - name: Generate marketplace.json - run: fastskill registry create --path ./skills --output ./marketplace.json + run: fastskill marketplace create ./skills --output ./marketplace.json - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 @@ -662,7 +664,7 @@ EOF **3. Generate Marketplace.json (Optional)** ```bash # Generate marketplace.json for the local repository -fastskill registry create --path ~/local-skills --name local-dev +fastskill marketplace create ~/local-skills --name local-dev ``` **4. Configure Repository** @@ -679,13 +681,13 @@ priority = 0 **5. Test Local Repository** ```bash # Add local repository -fastskill sources add local-dev \ +fastskill repos add local-dev \ --repo-type local \ - --url ~/local-skills \ + ~/local-skills \ --priority 0 # List local skills -fastskill registry list-skills --repository local-dev +fastskill repos skills --repository local-dev # Install local skills fastskill add local-dev/test-skill @@ -753,9 +755,9 @@ git add . git commit -m "Add experimental skills" # Or reference external git repos -fastskill sources add external-dev \ +fastskill repos add external-dev \ --repo-type git-marketplace \ - --url ~/projects/external-skills \ + ~/projects/external-skills \ --priority 1 ``` @@ -776,10 +778,10 @@ export FASTSKILL_HOT_RELOAD=true **Skill Validation:** ```bash # Validate local skills -fastskill validate ~/local-skills +fastskill eval validate ~/local-skills # Check for common issues -fastskill validate ~/local-skills --strict +fastskill eval validate ~/local-skills --strict ``` **Integration Testing:** @@ -820,11 +822,11 @@ priority = 1 # GitHub Actions example - name: Test local skills run: | - fastskill sources add test-repo \ + fastskill repos add test-repo \ --repo-type local \ - --url ./test-skills + ./test-skills - fastskill validate ./test-skills + fastskill eval validate ./test-skills fastskill add test-repo/test-skill fastskill read test-repo/test-skill ``` @@ -848,7 +850,7 @@ Sources without `marketplace.json` will not appear in the registry. Add a new repository using the CLI: ```bash -fastskill sources add new-source --type git-marketplace --url https://github.com/user/repo.git --priority 0 +fastskill repos add new-source --repo-type git-marketplace https://github.com/user/repo.git --priority 0 ``` Or edit `skill-project.toml` directly: @@ -864,7 +866,7 @@ priority = 0 ### Removing a Repository -Remove the repository using `fastskill registry remove ` or edit `skill-project.toml` directly. +Remove the repository using `fastskill repos remove ` or edit `skill-project.toml` directly. ## Best Practices @@ -895,4 +897,3 @@ Remove the repository using `fastskill registry remove ` or edit `skill-pr - [Marketplace.json Format](/registry/marketplace-json) - [Registry Command](/cli-reference/repository-command) - [Registry Overview](/registry/overview) - diff --git a/webdocs/registry/web-ui.mdx b/webdocs/registry/web-ui.mdx index cc692bd..ac68727 100644 --- a/webdocs/registry/web-ui.mdx +++ b/webdocs/registry/web-ui.mdx @@ -15,7 +15,7 @@ Start the server with the registry enabled: fastskill serve ``` -Then open `http://localhost:8080/registry` in your browser. +Then open `http://localhost:8080/` or `http://localhost:8080/dashboard` in your browser. ## Features @@ -55,7 +55,7 @@ For sources with hundreds of skills, the UI uses virtual scrolling to: ### Browse Skills -1. Open the registry at `http://localhost:8080/registry` +1. Open the registry at `http://localhost:8080/` or `http://localhost:8080/dashboard` 2. Skills are automatically loaded from all configured sources 3. Use the sidebar to filter by source 4. Click on a source name to show only skills from that source @@ -143,4 +143,3 @@ The UI works in modern browsers: - [Registry Overview](/registry/overview) - [Sources Configuration](/registry/sources) - [Serve Command](/cli-reference/serve-command) - diff --git a/webdocs/welcome.mdx b/webdocs/welcome.mdx index c730336..d9fc8c3 100644 --- a/webdocs/welcome.mdx +++ b/webdocs/welcome.mdx @@ -19,7 +19,7 @@ AI agent skills are scattered across repositories with no standard way to discov ## How FastSkill Solves These Problems -FastSkill extends the Claude Code skill standard with a **manifest-first workflow**: one `skill-project.toml`, reproducible installs via `skills.lock`, validation and `fastskill eval` for quality gates, then discovery and optional publish when you distribute outward. +FastSkill extends the Claude Code skill standard with a **manifest-first workflow**: one `skill-project.toml`, reproducible installs via `skills.lock`, validation and `fastskill eval` for quality gates, then discovery and optional operator publishing when you distribute outward. ### Semantic discovery @@ -240,7 +240,7 @@ fastskill search "charts and graphs" - Standard manifests and locks across projects - Auditing via list/show and validation - Environment-specific groups -- Optional private sources and publish flows when you distribute +- Optional private sources and operator publish flows when you distribute ### Skill Authors @@ -281,4 +281,3 @@ FastSkill focuses on **repeatable operations** for AI agent skills: --- **FastSkill**: Package manager and operational toolkit for Agent AI Skills—manageable, versioned, and reproducible. -