chore: remove unused crates and add documentation
- Delete workshop-agent (HTTP server stub) - Delete workshop-builder-native (empty stub) - Delete workshop-helper-mac (unused) - Delete workshop-llm-detector (moved to separate repo) - Add AGENTS.md to bake/, engine/, finalize/, prebake/, cert/, vault/ - Fix finalize plugin validation and logging improvements
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-04-04
|
||||
**Commit:** 2133f8b
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
**Branch:** master
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Generated
-2588
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.11.0", features = ["rustls-0_23"] }
|
||||
actix-web-httpauth = "0.8.2"
|
||||
anyhow = "1.0.100"
|
||||
config = "0.15.18"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
querystring = "1.1.0"
|
||||
rustls = "0.23.35"
|
||||
rustls-pemfile = "2.2.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
shellflip = "2.1.2"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
@@ -1,3 +0,0 @@
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub tls: Option<TLSConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TLSConfig {
|
||||
pub cert: PathBuf,
|
||||
pub key: PathBuf,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn init() -> anyhow::Result<Self> {
|
||||
let mut settings = config::Config::builder();
|
||||
|
||||
settings = settings
|
||||
.add_source(config::Environment::with_prefix("HBWAGENT"))
|
||||
.add_source(config::File::with_name("agent.toml").required(false));
|
||||
let config: ServerConfig = settings.build()?.try_deserialize()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn is_tls_enabled(&self) -> bool {
|
||||
self.tls.is_some()
|
||||
}
|
||||
|
||||
pub fn bind_address(&self) -> (String, u16) {
|
||||
// format!("{}:{}", self.host, self.port)
|
||||
(self.host.clone(), self.port)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentError {
|
||||
#[error("Failed when loading TLS certificate: {0}")]
|
||||
LoadTLSCertError(#[from] rustls::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("General error: {0}")]
|
||||
GeneralError(String),
|
||||
|
||||
#[error("Internal server error: {0}")]
|
||||
InternalServerError(String),
|
||||
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AgentError>;
|
||||
@@ -1,79 +0,0 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::state;
|
||||
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, web};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ResponseData {
|
||||
message: String,
|
||||
code: i32,
|
||||
}
|
||||
|
||||
/// Default endpoint
|
||||
#[get("/")]
|
||||
async fn index() -> impl Responder {
|
||||
HttpResponse::Ok().json(ResponseData {
|
||||
message: "HBW agent is running.".to_string(),
|
||||
code: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Endpoint for initializing.
|
||||
/// Initialize is divided into %d stages.
|
||||
#[post("/init")]
|
||||
async fn init(req: HttpRequest, info: web::Json<crate::init::InitData>, data: web::Data<state::AppState>) -> impl Responder {
|
||||
// 检查是否启用TLS阻止访问
|
||||
if data.use_tls {
|
||||
return HttpResponse::Forbidden().body("Access forbidden: This agent is already configured. If you'd like to reconfigure, remove TLS section in agent.toml");
|
||||
}
|
||||
|
||||
let mut token_verified = false;
|
||||
// 检查Bearer Token
|
||||
if let Some(auth_header) = req.headers().get("authorization") {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
if auth_str.starts_with("Bearer ") {
|
||||
let token = &auth_str[7..]; // 去掉 "Bearer " 前缀
|
||||
if token == data.token.as_ref().unwrap() {
|
||||
// Token验证成功
|
||||
token_verified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if token_verified {
|
||||
// Token验证成功,执行初始化操作
|
||||
//
|
||||
let query = querystring::querify(req.query_string());
|
||||
let stage = query.get(0).unwrap_or(&("0","0"));
|
||||
match stage.0 {
|
||||
"1" => {
|
||||
// 初始化阶段1
|
||||
// todo!("实现初始化阶段1");
|
||||
crate::init::init_stage1(info);
|
||||
}
|
||||
"2" => {
|
||||
// 初始化阶段2
|
||||
todo!("实现初始化阶段2");
|
||||
}
|
||||
_ => {
|
||||
// 未知的初始化阶段
|
||||
// HttpResponse::BadRequest().body("Unknown initialization stage")
|
||||
return HttpResponse::BadRequest().body("Unknown initialization stage");
|
||||
}
|
||||
}
|
||||
|
||||
todo!("实现初始化操作");
|
||||
HttpResponse::Ok().body("Initialization successful")
|
||||
} else {
|
||||
// Token验证失败
|
||||
HttpResponse::Unauthorized().body("Invalid or missing Bearer token")
|
||||
}
|
||||
}
|
||||
|
||||
/// Endpoint for pinging the server.
|
||||
#[get("/ping")]
|
||||
async fn ping() -> impl Responder {
|
||||
HttpResponse::Ok().json(ResponseData {
|
||||
message: "pong".to_string(),
|
||||
code: 0,
|
||||
})
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn generate_token() -> String {
|
||||
// 生成UUIDv4 token
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InitData {
|
||||
server_id: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn init_stage1(data: web::Json<InitData>) -> () {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use log::info;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
|
||||
mod config;
|
||||
mod tls;
|
||||
mod error;
|
||||
mod handler;
|
||||
mod init;
|
||||
mod state;
|
||||
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), ()> {
|
||||
env_logger::init();
|
||||
info!("Starting workshop-agent...");
|
||||
let config = config::ServerConfig::init().unwrap();
|
||||
let token = if config.is_tls_enabled() {
|
||||
None
|
||||
} else {
|
||||
let _token = init::generate_token();
|
||||
println!("Generated token: {}", _token);
|
||||
Some(_token)
|
||||
};
|
||||
let appstate = web::Data::new(state::AppState {
|
||||
use_tls: config.is_tls_enabled(),
|
||||
token,
|
||||
});
|
||||
let mut server = HttpServer::new(move || App::new()
|
||||
.app_data(appstate.clone())
|
||||
.service(handler::index)
|
||||
.service(handler::ping)
|
||||
.service(handler::init));
|
||||
if config.is_tls_enabled() {
|
||||
let tls_certs_file = config.tls.clone().unwrap().cert;
|
||||
let tls_key_file = config.tls.clone().unwrap().key;
|
||||
let tls_config = tls::load_rustls_config(&tls_certs_file, &tls_key_file).unwrap();
|
||||
info!("Configuring TLS...");
|
||||
server = server.bind_rustls_0_23(config.bind_address(), tls_config).unwrap();
|
||||
}
|
||||
else {
|
||||
server = server.bind(config.bind_address()).unwrap();
|
||||
}
|
||||
let _ = server.run().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub token: Option<String>,
|
||||
pub use_tls: bool,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use crate::error::AgentError;
|
||||
use anyhow::Result;
|
||||
use rustls::ServerConfig;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn load_rustls_config(
|
||||
cert_path: &Path,
|
||||
key_path: &Path,
|
||||
) -> Result<rustls::ServerConfig, AgentError> {
|
||||
// 初始化 TLS 配置
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth();
|
||||
|
||||
// 加载 TLS 证书
|
||||
let mut cert_file = &mut BufReader::new(File::open(cert_path)?);
|
||||
let cert_chain = rustls_pemfile::certs(&mut cert_file)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// 加载私钥
|
||||
let mut key_file = &mut BufReader::new(File::open(key_path)?);
|
||||
let key = rustls_pemfile::pkcs8_private_keys(&mut key_file)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
if let Ok(key) = key {
|
||||
Ok(config.with_single_cert(cert_chain, rustls::pki_types::PrivateKeyDer::Pkcs8(key))?)
|
||||
} else {
|
||||
// TODO: How to implement better?
|
||||
Err(AgentError::GeneralError("Failed to load TLS key.".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
# workshop-baker/src
|
||||
|
||||
**Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.**
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# workshop-baker/src/bake
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Script parsing and build orchestration with decorator DSL.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
bake/
|
||||
├── parser.rs # @decorator-aware shell script parser
|
||||
├── decorator.rs # Decorator enum + 100+ unit tests
|
||||
├── builder.rs # Script template assembly
|
||||
└── schedule.rs # Function ordering via @after dependencies
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Parse script | `parser.rs:29` - `parse_script()` |
|
||||
| Decorator types | `decorator.rs` - `Decorator` enum variants |
|
||||
| Build script | `builder.rs` - template builder |
|
||||
| Schedule deps | `schedule.rs` - @after resolution |
|
||||
|
||||
## DECORATORS
|
||||
|
||||
| Decorator | Purpose |
|
||||
|-----------|---------|
|
||||
| `@pipeline` | Marks main build function |
|
||||
| `@timeout(N)` | Execution timeout seconds |
|
||||
| `@retry(N, M)` | Retry N times, M sec delay |
|
||||
| `@parallel` | Run concurrently |
|
||||
| `@fallible` | Failure doesn't fail pipeline |
|
||||
| `@after(func)` | Dependency ordering |
|
||||
| `@export` | Export vars to env |
|
||||
| `@loop(N)` | Repeat N times |
|
||||
| `@if(COND)` | Conditional execution |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Decorator Syntax**: `# @name(args)` comment above function
|
||||
- **Parsing**: Shell grammar with decorator annotation extraction
|
||||
- **Scheduling**: DAG resolution for @after dependencies
|
||||
- **Tests**: `decorator.rs:175-829` has 100+ inline unit tests
|
||||
@@ -0,0 +1,46 @@
|
||||
# workshop-baker/src/engine
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Execution runtime with resource isolation and package management.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
engine/
|
||||
├── executor.rs # Script execution with cgroups, pipes, exit codes
|
||||
├── cgroups.rs # Linux cgroup resource limits (memory, CPU, IO)
|
||||
├── types.rs # ResourceLimits, ExecutionResult, ProcessConfig
|
||||
├── pm.rs # Package manager detection (apt, pacman, etc.)
|
||||
├── pm/ # PM implementations
|
||||
│ ├── apt.rs # Debian/Ubuntu (WIP - todo!() stubs)
|
||||
│ └── pacman.rs # Arch Linux
|
||||
├── upm.rs # User package managers (Nix/Guix)
|
||||
├── upm/custom.rs # Custom PM support (unimplemented)
|
||||
└── repology.rs # Repology API for package name resolution
|
||||
└── local.rs # Python-based local repology (non-standard)
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Execute script with limits | `executor.rs:Executor::execute_script()` |
|
||||
| Resource limits | `types.rs:56` - `ResourceLimits` struct |
|
||||
| Cgroup management | `cgroups.rs` - CgroupManager v1/v2 |
|
||||
| Package detection | `pm.rs:detection()` |
|
||||
| Repology lookup | `repology.rs` - RepologyEndpoint |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Executor**: Lightweight wrapper around CgroupManager for isolation
|
||||
- **ResourceLimits**: Memory, CPU, IO bounds passed to cgroups
|
||||
- **Package Manager Detection**: Auto-detects from `/etc/os-release`
|
||||
- **WIP**: apt.rs has `todo!()` stubs (lines 22, 45, 50)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
1. **Python in Rust** — `repology/local.rs` embeds Python repology client
|
||||
2. **Unsafe libc** — Multiple `unsafe { libc::geteuid() }` in security.rs (parent)
|
||||
3. **Unwrap heavy** — cgroups.rs has many `.unwrap()` calls
|
||||
@@ -0,0 +1,56 @@
|
||||
# workshop-baker/src/finalize
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Artifact packaging, distribution, deployment, and notifications.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
finalize/
|
||||
├── config.rs # FinalizeConfig YAML (artifacts, deploy, notify)
|
||||
├── stage.rs # FinalizeStage enum
|
||||
├── stage/hook.rs # Post-build hooks
|
||||
├── event.rs # Event propagation
|
||||
├── template.rs # Variable substitution {{VAR}}
|
||||
├── plugin.rs # Plugin trait + registry
|
||||
├── plugin/
|
||||
│ ├── metadata.rs # Plugin manifest parsing
|
||||
│ ├── dylib.rs # Dynamic library plugins
|
||||
│ ├── rhai.rs # Rhai scripting (todo!())
|
||||
│ ├── shell.rs # Shell command plugins
|
||||
│ └── internal/ # Built-in plugins
|
||||
│ ├── mail/ # Email notifications (SMTP)
|
||||
│ ├── webhook.rs # HTTP webhooks
|
||||
│ ├── satori.rs # Satori integration
|
||||
│ └── insitenotify.rs # In-site notifications
|
||||
│ └── fetch/ # Artifact fetching
|
||||
│ ├── git.rs # Git repository fetch
|
||||
│ ├── http.rs # HTTP download
|
||||
│ ├── extract.rs # Archive extraction
|
||||
│ └── checksum.rs# SHA256 verification (SHA1 deprecated)
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Config loading | `config.rs` - FinalizeConfig |
|
||||
| Email notify | `plugin/internal/mail/` - SMTP templates |
|
||||
| Webhooks | `plugin/internal/webhook.rs` |
|
||||
| Artifact fetch | `plugin/fetch/` - git/http/extract |
|
||||
| Checksums | `plugin/fetch/checksum.rs:13` - SHA1 deprecated |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Plugin System**: Dynamic + internal plugins via Plugin trait
|
||||
- **Template Syntax**: `{{VARIABLE}}` handlebars-style
|
||||
- **Stages**: Hook → Fetch → Build → Publish → Notify → Cleanup
|
||||
- **Fetch**: Supports git, http, with extraction and checksum verify
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
1. **SHA1 deprecated** — `checksum.rs:13` warns SHA1 is deprecated
|
||||
2. **Unimplemented** — `rhai.rs:17` - "todo!()" for Rhai scripting
|
||||
3. **TODO markers** — Multiple TODOs in config.rs (lines 305-306)
|
||||
@@ -290,6 +290,18 @@ impl FinalizeConfig {
|
||||
Err(e) => errors.push(format!("Invalid version format: {}", e)),
|
||||
}
|
||||
|
||||
// Validate plugin definitions: each plugin must have exactly one URL
|
||||
for (plugin_name, urls) in &self.plugin {
|
||||
match urls.len() {
|
||||
0 => errors.push(format!("Plugin '{}' has no URL defined", plugin_name)),
|
||||
1 => {}
|
||||
n => errors.push(format!(
|
||||
"Plugin '{}' has {} URL definitions, expected exactly 1",
|
||||
plugin_name, n
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Schema-based validation for plugin configs
|
||||
// TODO: Validate that publish targets reference existing plugins
|
||||
|
||||
@@ -371,4 +383,60 @@ hooks:
|
||||
let early = hooks.early.unwrap();
|
||||
assert_eq!(early[0].timeout_ms, 30000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_zero_urls() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list: {}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| e.contains("show_file_list") && e.contains("no URL")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_one_url_ok() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list:
|
||||
"https://example.com/plugin.tar.gz":
|
||||
fallible: false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_plugin_multiple_urls() {
|
||||
let config: FinalizeConfig = serde_yaml::from_str(
|
||||
r#"
|
||||
version: "0.0.1"
|
||||
plugin:
|
||||
show_file_list:
|
||||
"https://example.com/plugin1.tar.gz":
|
||||
fallible: false
|
||||
"https://example.com/plugin2.tar.gz":
|
||||
fallible: false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = config.validate();
|
||||
assert!(result.is_err());
|
||||
let errors = result.unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,13 +205,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
|
||||
|
||||
// Register internal plugin
|
||||
for i in internal::get_internal_plugin() {
|
||||
log::info!("Registering internal plugin {}", i.name);
|
||||
log::debug!("Registering internal plugin {}", i.name);
|
||||
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
|
||||
}
|
||||
|
||||
// Register external plugin
|
||||
for (name, data) in external_plugins.into_iter() {
|
||||
log::info!("Registering external plugin {}", name);
|
||||
log::debug!("Registering external plugin {}", name);
|
||||
let (_source, config) = data.into_iter().next().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Internal Error: Plugin {} has no source configured, this should have been caught by validation!",
|
||||
|
||||
@@ -49,15 +49,18 @@ pub async fn fetch_plugin(
|
||||
// NOTE: as a practical compromise, we use git command for git clone
|
||||
// gix will be supported later, with ONLY https scheme
|
||||
// TODO: Implement gix
|
||||
log::info!("Fetching git plugin: {}", url);
|
||||
git::fetch_git_plugin(&path, name, basedir, argument.shallow).await
|
||||
}
|
||||
URLScheme::Http | URLScheme::Https => {
|
||||
log::info!("Fetching http plugin: {}", url);
|
||||
let checksum = argument.get_checksum();
|
||||
http::fetch_http_plugin(url, name, basedir, checksum).await
|
||||
}
|
||||
URLScheme::File => {
|
||||
// TODO: Support client mode
|
||||
// NOTE: In standalone mode, we just assume path is well-defined and utilize
|
||||
log::info!("Fetching file plugin: {}", url);
|
||||
return Ok(());
|
||||
}
|
||||
URLScheme::Unknown => {
|
||||
|
||||
@@ -92,6 +92,7 @@ pub async fn fetch_git_plugin(
|
||||
})
|
||||
}
|
||||
GitVersion::Single(version) => {
|
||||
log::debug!("Fetching git plugin: {}@{}", name, version);
|
||||
let shallow_flag = shallow.unwrap_or(false);
|
||||
git_clone_and_checkout(
|
||||
&baseurl,
|
||||
@@ -103,6 +104,7 @@ pub async fn fetch_git_plugin(
|
||||
.await
|
||||
}
|
||||
GitVersion::Mixed(tag, commit) => {
|
||||
log::debug!("Fetching git plugin: {}@{}/{}", name, tag, commit);
|
||||
let shallow_flag = shallow.unwrap_or(false);
|
||||
let result = git_clone_and_checkout(
|
||||
&baseurl,
|
||||
|
||||
@@ -48,6 +48,21 @@ pub async fn download_to_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_archive_extension(url: &str) -> &str {
|
||||
let path_part = url.split('?').next().unwrap_or(url).split('#').next().unwrap_or(url);
|
||||
let file_part = path_part.rsplit('/').next().unwrap_or("");
|
||||
let lower = file_part.to_lowercase();
|
||||
if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
|
||||
".tar.gz"
|
||||
} else if lower.ends_with(".tar.zst") || lower.ends_with(".tar.zstd") || lower.ends_with(".tzst") {
|
||||
".tar.zst"
|
||||
} else if lower.ends_with(".tar") {
|
||||
".tar"
|
||||
} else {
|
||||
".tar"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_http_plugin(
|
||||
url: &str,
|
||||
name: &str,
|
||||
@@ -56,7 +71,8 @@ pub async fn fetch_http_plugin(
|
||||
) -> Result<(), PluginError> {
|
||||
let temp_dir = std::env::temp_dir().join("workshop-plugins");
|
||||
tokio::fs::create_dir_all(&temp_dir).await.ok();
|
||||
let temp_file = temp_dir.join(format!("{}-{}.tar.tmp", name, uuid::Uuid::new_v4()));
|
||||
let ext = extract_archive_extension(url);
|
||||
let temp_file = temp_dir.join(format!("{}-{}{}", name, uuid::Uuid::new_v4(), ext));
|
||||
|
||||
download_to_file(url, &temp_file, name).await?;
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@ impl AsyncPluginFn for Mail {
|
||||
};
|
||||
|
||||
let subject = render_template(&subject_template, &template_data)
|
||||
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
|
||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render subject: {}", e)))?;
|
||||
let body_html = render_template(&body_template_html, &template_data)
|
||||
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
|
||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_html: {}", e)))?;
|
||||
let body_text = render_template(&body_template_text, &template_data)
|
||||
.map_err(|e| PluginError::GeneralError(format!("Template render error: {}", e)))?;
|
||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_text(?): {}", e)))?;
|
||||
|
||||
let email = build_email(&config, subject, body_html, body_text)?;
|
||||
|
||||
@@ -142,14 +142,17 @@ async fn send_email(
|
||||
|
||||
let mut transport_builder = match config.smtp.encryption {
|
||||
Encryption::Tls => {
|
||||
log::debug!("Using TLS encryption");
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&config.smtp.host)
|
||||
.map_err(|e| PluginError::GeneralError(format!("Invalid SMTP host: {}", e)))?
|
||||
}
|
||||
Encryption::Starttls => {
|
||||
log::debug!("Using StartTLS encryption");
|
||||
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.smtp.host)
|
||||
.map_err(|e| PluginError::GeneralError(format!("Invalid SMTP host: {}", e)))?
|
||||
}
|
||||
Encryption::None => {
|
||||
log::debug!("Using NO encryption");
|
||||
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.smtp.host)
|
||||
}
|
||||
};
|
||||
@@ -158,12 +161,15 @@ async fn send_email(
|
||||
|
||||
match config.smtp.auth.auth_type {
|
||||
AuthType::Password => {
|
||||
log::debug!("Using password auth");
|
||||
transport_builder = transport_builder.credentials((
|
||||
config.smtp.auth.username.clone(),
|
||||
config.smtp.auth.password.clone()
|
||||
).into());
|
||||
}
|
||||
AuthType::None => {}
|
||||
AuthType::None => {
|
||||
log::debug!("Using no auth");
|
||||
}
|
||||
}
|
||||
|
||||
let transport = transport_builder.build();
|
||||
|
||||
@@ -165,7 +165,6 @@ impl DefaultTemplates {
|
||||
<tr><td><strong>Build ID:</strong></td><td>{{ build.id }}</td></tr>
|
||||
<tr><td><strong>Status:</strong></td><td>{{ build.status }}</td></tr>
|
||||
<tr><td><strong>Duration:</strong></td><td>{{ build.duration }}</td></tr>
|
||||
<tr><td><strong>Commit:</strong></td><td>{{ commit.hash | slice(0, 7) }}</td></tr>
|
||||
<tr><td><strong>Author:</strong></td><td>{{ commit.author }}</td></tr>
|
||||
<tr><td><strong>Message:</strong></td><td>{{ commit.message }}</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -91,7 +91,7 @@ fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool
|
||||
envvars.insert(prefix.to_string(), value.to_string());
|
||||
}
|
||||
serde_yaml::Value::String(value) => {
|
||||
envvars.insert(prefix.to_string(), format!("\"{}\"", value));
|
||||
envvars.insert(prefix.to_string(), format!("{}", value));
|
||||
}
|
||||
serde_yaml::Value::Number(value) => {
|
||||
envvars.insert(prefix.to_string(), value.to_string());
|
||||
@@ -199,8 +199,8 @@ mod tests {
|
||||
let result = convert_argument("PLUGIN", yaml, false);
|
||||
|
||||
// Keys are uppercased in actual call() function
|
||||
assert_eq!(result.get("PLUGIN_HOST"), Some(&"\"docker.io\"".to_string()));
|
||||
assert_eq!(result.get("PLUGIN_USER"), Some(&"\"admin\"".to_string()));
|
||||
assert_eq!(result.get("PLUGIN_HOST"), Some(&"docker.io".to_string()));
|
||||
assert_eq!(result.get("PLUGIN_USER"), Some(&"admin".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -213,7 +213,7 @@ from:
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Nested keys use uppercase format: CFG_FROM_HOST
|
||||
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"\"smtp.example.com\"".to_string()));
|
||||
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"smtp.example.com".to_string()));
|
||||
assert_eq!(result.get("CFG_FROM_PORT"), Some(&"587".to_string()));
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ empty: ~
|
||||
assert_eq!(result.get("VAR_ENABLED"), Some(&"true".to_string()));
|
||||
assert_eq!(result.get("VAR_COUNT"), Some(&"42".to_string()));
|
||||
assert_eq!(result.get("VAR_RATIO"), Some(&"3.14".to_string()));
|
||||
assert_eq!(result.get("VAR_NAME"), Some(&"\"test\"".to_string()));
|
||||
assert_eq!(result.get("VAR_NAME"), Some(&"test".to_string()));
|
||||
assert!(!result.contains_key("VAR_EMPTY"));
|
||||
}
|
||||
|
||||
@@ -240,8 +240,7 @@ empty: ~
|
||||
let yaml = serde_yaml::from_str("tags:\n - latest\n - v1.0\n - stable").unwrap();
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Strings in arrays are quoted by convert_argument
|
||||
assert_eq!(result.get("CFG_TAGS"), Some(&"(\"latest\" \"v1.0\" \"stable\")".to_string()));
|
||||
assert_eq!(result.get("CFG_TAGS"), Some(&"(latest v1.0 stable)".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -255,10 +254,10 @@ env:
|
||||
").unwrap();
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"\"KEY1\"".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_0_VALUE"), Some(&"\"val1\"".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_1_NAME"), Some(&"\"KEY2\"".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_1_VALUE"), Some(&"\"val2\"".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"KEY1".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_0_VALUE"), Some(&"val1".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_1_NAME"), Some(&"KEY2".to_string()));
|
||||
assert_eq!(result.get("CFG_ENV_1_VALUE"), Some(&"val2".to_string()));
|
||||
assert!(!result.contains_key("CFG_ENV"));
|
||||
}
|
||||
|
||||
@@ -305,7 +304,7 @@ token: ~
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Keys are uppercased: CFG_HOST
|
||||
assert_eq!(result.get("CFG_HOST"), Some(&"\"docker.io\"".to_string()));
|
||||
assert_eq!(result.get("CFG_HOST"), Some(&"docker.io".to_string()));
|
||||
// Null value is skipped (not set)
|
||||
assert!(!result.contains_key("CFG_TOKEN"));
|
||||
}
|
||||
@@ -319,8 +318,8 @@ token: ~
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Number keys are preserved but prefixed: CFG_0, CFG_1
|
||||
assert_eq!(result.get("CFG_0"), Some(&"\"first\"".to_string()));
|
||||
assert_eq!(result.get("CFG_1"), Some(&"\"second\"".to_string()));
|
||||
assert_eq!(result.get("CFG_0"), Some(&"first".to_string()));
|
||||
assert_eq!(result.get("CFG_1"), Some(&"second".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -340,12 +339,11 @@ plugin:
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// All keys uppercased for shell environment variables
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"\"docker.io\"".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"\"user/app:tag\"".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"docker.io".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"user/app:tag".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_FORCE"), Some(&"true".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_RETRIES"), Some(&"3".to_string()));
|
||||
// Strings in sequences are quoted
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(\"latest\" \"v1.0\")".to_string()));
|
||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(latest v1.0)".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -358,9 +356,9 @@ token: 'Bearer abc123'
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Keys uppercased: CFG_URL, CFG_PATH, CFG_TOKEN
|
||||
assert_eq!(result.get("CFG_URL"), Some(&"\"https://example.com/path?query=value\"".to_string()));
|
||||
assert_eq!(result.get("CFG_PATH"), Some(&"\"/workspace/output.tar.gz\"".to_string()));
|
||||
assert_eq!(result.get("CFG_TOKEN"), Some(&"\"Bearer abc123\"".to_string()));
|
||||
assert_eq!(result.get("CFG_URL"), Some(&"https://example.com/path?query=value".to_string()));
|
||||
assert_eq!(result.get("CFG_PATH"), Some(&"/workspace/output.tar.gz".to_string()));
|
||||
assert_eq!(result.get("CFG_TOKEN"), Some(&"Bearer abc123".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -372,7 +370,7 @@ cmd: \"echo 'test'\"
|
||||
let result = convert_argument("CFG", yaml, false);
|
||||
|
||||
// Keys uppercased: CFG_MSG, CFG_CMD
|
||||
assert_eq!(result.get("CFG_MSG"), Some(&"\"Hello World\"".to_string()));
|
||||
assert_eq!(result.get("CFG_CMD"), Some(&"\"echo 'test'\"".to_string()));
|
||||
assert_eq!(result.get("CFG_MSG"), Some(&"Hello World".to_string()));
|
||||
assert_eq!(result.get("CFG_CMD"), Some(&"echo 'test'".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ pub async fn hook(
|
||||
log::error!("Plugin hook {} failed: {:?}", index, e);
|
||||
ExecutionError::ExecutionFailed(e.to_string())
|
||||
})?;
|
||||
return Ok(());
|
||||
continue;
|
||||
}
|
||||
let deltactx = DeltaExecutionContext {
|
||||
working_dir: hook_cmd.working_dir.as_ref().map(PathBuf::from),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# workshop-baker/src/prebake
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Build environment setup: bootstrap, dependencies, security, hooks.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
prebake/
|
||||
├── config.rs # PrebakeConfig YAML schema (environment, deps, cache)
|
||||
├── stage.rs # PrebakeStage enum (Bootstrap→Ready ordering)
|
||||
├── stage/
|
||||
│ ├── bootstrap.rs # User creation (vulcan), sudo/doas setup
|
||||
│ ├── environment.rs # Env var injection
|
||||
│ ├── depssystem.rs # System package installation
|
||||
│ ├── depsuser.rs # User package installation (cargo, npm, etc.)
|
||||
│ └── hook.rs # Hook execution (pre/post stage)
|
||||
├── env/ # Environment providers
|
||||
│ ├── mod.rs # EnvProvider trait
|
||||
│ ├── container.rs # Docker/container support
|
||||
│ ├── firecracker.rs # Firecracker microVM
|
||||
│ ├── baremetal.rs # Direct host execution
|
||||
│ └── custom.rs # Custom env (todo!())
|
||||
├── security.rs # Privilege dropping (unsafe libc calls)
|
||||
└── event.rs # Event logging
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Config loading | `config.rs:PrebakeConfig::from_yaml()` |
|
||||
| Stage ordering | `stage.rs:PrebakeStage` enum |
|
||||
| User setup | `stage/bootstrap.rs` - vulcan user creation |
|
||||
| Security | `security.rs:72` - privilege dropping |
|
||||
| Hooks | `stage/hook.rs` - pre/post stage hooks |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **PrebakeStage**: Ordered enum (Bootstrap→DepsSystem→DepsUser→Environment→Hooks→Ready)
|
||||
- **ExecutionContext**: Carries task_id, username, env_vars through all stages
|
||||
- **Target User**: "vulcan" (hardcoded in bootstrap.rs:6-24)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
1. **Hardcoded user** — "vulcan" username hardcoded throughout
|
||||
2. **Unsafe blocks** — `security.rs` has 5+ unsafe libc calls
|
||||
3. **Incomplete** — custom.rs and apt.rs have `todo!()` stubs
|
||||
4. **doas.conf** — bootstrap.rs:183: "We do not know how to write doas.conf lol"
|
||||
Generated
-7
@@ -1,7 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "workshop-builder-native"
|
||||
version = "0.1.0"
|
||||
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-builder-native"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,36 +0,0 @@
|
||||
// src/executors/error.rs
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExecutionError {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(String),
|
||||
|
||||
#[error("Command execution error: {0}")]
|
||||
CommandExecutionError(String),
|
||||
|
||||
#[error("Unsupported platform: {0}")]
|
||||
UnsupportedPlatform(String),
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
TemplateError(#[from] crate::services::template_engine::TemplateError),
|
||||
|
||||
#[error("Environment error: {0}")]
|
||||
EnvironmentError(#[from] crate::services::environment_manager::EnvironmentError),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ExecutionError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
ExecutionError::IoError(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_yaml::Error> for ExecutionError {
|
||||
fn from(error: serde_yaml::Error) -> Self {
|
||||
ExecutionError::YamlParseError(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# workshop-cert/src
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
TLS certificate generator (CA, server, client certificates).
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # CLI entry (gen-ca, gen-server, gen-client, gen-all)
|
||||
├── lib.rs # Library exports
|
||||
├── generator.rs # Certificate generation logic
|
||||
└── config.rs # Certificate config from cert.toml
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| CLI commands | `main.rs:144` - 4 subcommands |
|
||||
| Cert generation | `generator.rs` - rcgen-based |
|
||||
| Config loading | `config.rs` - cert.toml parser |
|
||||
|
||||
## BINARY
|
||||
|
||||
```bash
|
||||
workshop_certgen gen-ca # Generate CA cert/key
|
||||
workshop_certgen gen-server # Generate server cert
|
||||
workshop_certgen gen-client # Generate client cert
|
||||
workshop_certgen gen-all # Generate all
|
||||
```
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
**CRITICAL**: `certs/` directory contains private keys in source:
|
||||
- `ca-key.pem` - CA private key
|
||||
- `server-key.pem` - Server private key
|
||||
|
||||
These should be gitignored and generated at deploy time.
|
||||
|
||||
## CONFIG
|
||||
|
||||
`cert.toml` defines:
|
||||
- CA validity: 10 years
|
||||
- Server/client validity: 1 year
|
||||
- Subject fields (CN, O, OU)
|
||||
- Key algorithm (Ed25519)
|
||||
Generated
-169
@@ -1,169 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "if-addrs"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "macaddr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "route-rs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66fa49dc79c685a59fa33339e126a794a67cf4ed865c2c9785268e23ec51409"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "workshop-helper-mac"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"if-addrs",
|
||||
"macaddr",
|
||||
"route-rs",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-helper-mac"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
if-addrs = "0.14.0"
|
||||
macaddr = "1.0.1"
|
||||
route-rs = "0.1.0"
|
||||
thiserror = "2.0.17"
|
||||
@@ -1,69 +0,0 @@
|
||||
use if_addrs::IfAddr;
|
||||
use macaddr::MacAddr;
|
||||
use std::net::IpAddr;
|
||||
use thiserror::Error;
|
||||
|
||||
// 定义一个自定义的错误类型,让错误处理更优雅
|
||||
#[derive(Error, Debug)]
|
||||
enum MainMacError {
|
||||
#[error("无法获取网络接口列表: {0}")]
|
||||
IfAddrsError(#[from] if_addrs::Error),
|
||||
|
||||
#[error("无法获取路由信息: {0}")]
|
||||
RouteError(#[from] route_rs::Error),
|
||||
|
||||
#[error("未找到默认路由")]
|
||||
NoDefaultRoute,
|
||||
|
||||
#[error("默认路由对应的接口 '{0}' 未在接口列表中找到")]
|
||||
InterfaceNotFound(String),
|
||||
|
||||
#[error("接口 '{0}' 没有 MAC 地址")]
|
||||
NoMacAddress(String),
|
||||
|
||||
#[error("找到的主网卡 '{0}' 是虚拟接口,已跳过")]
|
||||
VirtualInterface(String),
|
||||
}
|
||||
|
||||
/// 检查接口名称是否暗示它是一个虚拟接口
|
||||
fn is_virtual_interface(name: &str) -> bool {
|
||||
let virtual_patterns = ["tun", "tap", "virbr", "veth", "lo"];
|
||||
virtual_patterns.iter().any(|&pat| name.contains(pat))
|
||||
}
|
||||
|
||||
/// 寻找并返回主网卡的 MAC 地址
|
||||
fn find_main_mac_address() -> Result<MacAddr, MainMacError> {
|
||||
// 步骤 1: 获取默认路由,以确定主网卡的名称
|
||||
let default_route = route_rs::get_default_route()?;
|
||||
let iface_name = default_route.interface_name().ok_or(MainMacError::NoDefaultRoute)?;
|
||||
println!("默认路由所在的接口名称: '{}'", iface_name);
|
||||
|
||||
// 步骤 2: 检查是否为虚拟接口
|
||||
if is_virtual_interface(iface_name) {
|
||||
return Err(MainMacError::VirtualInterface(iface_name.to_string()));
|
||||
}
|
||||
|
||||
// 步骤 3: 获取所有网络接口的详细信息
|
||||
let if_addrs = if_addrs::get_if_addrs()?;
|
||||
|
||||
// 步骤 4: 遍历接口列表,找到与默认路由接口名匹配的接口
|
||||
for if_addr in if_addrs {
|
||||
if if_addr.name == iface_name {
|
||||
// 找到了匹配的接口,提取其 MAC 地址
|
||||
return if_addr.mac_addr.ok_or_else(|| MainMacError::NoMacAddress(iface_name.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果循环结束都没有找到,说明出了问题
|
||||
Err(MainMacError::InterfaceNotFound(iface_name.to_string()))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("正在寻找主网卡的 MAC 地址...");
|
||||
|
||||
match find_main_mac_address() {
|
||||
Ok(mac) => println!("成功找到主网卡 MAC 地址: {}", mac),
|
||||
Err(e) => eprintln!("失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
target
|
||||
Generated
-2045
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-llm-detector"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "workshop-llm-detector"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "workshop_llm_detector"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
async-trait = "0.1.89"
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
log = "0.4.28"
|
||||
ollama-rs = "0.3.2"
|
||||
openai_api_rust = "0.1.9"
|
||||
reqwest = { version = "0.12.24", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
@@ -1,29 +0,0 @@
|
||||
name: "Rust"
|
||||
project_types:
|
||||
- "rust"
|
||||
- "cargo"
|
||||
install_path: "/install"
|
||||
artifacts:
|
||||
- "/install/bin/*"
|
||||
- "/install/lib/*.so"
|
||||
- "/install/lib/*.a"
|
||||
- "/target/release/*"
|
||||
dependencies:
|
||||
deb:
|
||||
- "build-essential"
|
||||
- "curl"
|
||||
- "pkg-config"
|
||||
- "libssl-dev"
|
||||
rpm:
|
||||
- "gcc"
|
||||
- "gcc-c++"
|
||||
- "curl"
|
||||
- "openssl-devel"
|
||||
pacman:
|
||||
- "base-devel"
|
||||
- "curl"
|
||||
- "openssl"
|
||||
environment:
|
||||
RUSTUP_DIST_SERVER: "https://static.rust-lang.org"
|
||||
CARGO_HOME: "/usr/local/cargo"
|
||||
RUSTUP_HOME: "/usr/local/rustup"
|
||||
@@ -1,24 +0,0 @@
|
||||
name: "make"
|
||||
project_types:
|
||||
- "make"
|
||||
- "makefile"
|
||||
install_path: "/install"
|
||||
artifacts:
|
||||
- "/install/*"
|
||||
dependencies:
|
||||
deb:
|
||||
- "build-essential"
|
||||
- "curl"
|
||||
- "pkg-config"
|
||||
- "libssl-dev"
|
||||
rpm:
|
||||
- "gcc"
|
||||
- "gcc-c++"
|
||||
- "curl"
|
||||
- "openssl-devel"
|
||||
pacman:
|
||||
- "base-devel"
|
||||
- "curl"
|
||||
- "openssl"
|
||||
environment:
|
||||
NONE: "none"
|
||||
@@ -1,40 +0,0 @@
|
||||
# workshop-llm-detector
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
LLM-powered repository analyzer that generates pipeline configurations by combining AI analysis with language-specific cookbook templates.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
src/
|
||||
├── llm/ # LLM client abstraction
|
||||
│ ├── client.rs # LLMClient trait (async_trait)
|
||||
│ ├── ollama.rs # OllamaClient implementation
|
||||
│ └── openai.rs # OpenAIClient implementation
|
||||
├── cookbook/ # Language-specific build configs
|
||||
│ └── mod.rs # CookbookManager + Cookbook struct
|
||||
├── llm_output.rs # LLMOutput parsed response struct
|
||||
└── lib.rs # CICDLLMHelper entry point
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Symbol | Type | Location | Role |
|
||||
|--------|------|----------|------|
|
||||
| CICDLLMHelper | struct | lib.rs:16 | Main entry point; analyze_repository() orchestrates LLM + cookbook merge |
|
||||
| LLMClient | trait | llm/client.rs:5 | Async trait for generate(prompt) -> Result<String> |
|
||||
| OllamaClient | struct | llm/ollama.rs:8 | Ollama local LLM adapter |
|
||||
| OpenAIClient | struct | llm/openai.rs:8 | OpenAI API adapter |
|
||||
| CookbookManager | struct | cookbook/mod.rs:26 | Loads .cookbook YAML files; get_cookbook(project_type) |
|
||||
| Cookbook | struct | cookbook/mod.rs:7 | Language-specific deps, env, artifacts template |
|
||||
| LLMOutput | struct | llm_output.rs:5 | Parsed YAML from LLM (project_type, dependencies, build_steps, artifacts) |
|
||||
| AnalysisResult | struct | lib.rs:157 | Merged LLM + cookbook output |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Async trait**: Uses `async_trait::async_trait` macro (not native async fn in traits)
|
||||
- **Cookbook files**: `*.cookbook.yaml` in cookbook directory; maps project_type -> Cookbook
|
||||
- **LLM prompt**: Builds YAML requiring project_type, dependencies, build_steps, artifacts, environment fields
|
||||
- **Output templates**: Writes prebake.tmpl.yaml, finalize.tmpl.yaml, build.sh via serde_yaml
|
||||
- **Error handling**: thiserror for CookbookError, anyhow for runtime errors
|
||||
@@ -1,62 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cookbook {
|
||||
pub name: String,
|
||||
pub project_types: Vec<String>,
|
||||
pub install_path: String,
|
||||
pub artifacts: Vec<String>,
|
||||
pub dependencies: HashMap<String, Vec<String>>, // key: package_type (deb, rpm, etc.)
|
||||
pub environment: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CookbookError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("YAML error: {0}")]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
#[error("Cookbook not found for project type: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
pub struct CookbookManager {
|
||||
cookbooks: HashMap<String, Cookbook>,
|
||||
}
|
||||
|
||||
impl CookbookManager {
|
||||
pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, CookbookError> {
|
||||
let mut cookbooks = HashMap::new();
|
||||
|
||||
let entries = fs::read_dir(dir)?;
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "yaml" || ext == "yml") {
|
||||
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
if file_name.ends_with(".cookbook") {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let cookbook: Cookbook = serde_yaml::from_str(&content)?;
|
||||
|
||||
// Register all project types for this cookbook
|
||||
for project_type in &cookbook.project_types {
|
||||
cookbooks.insert(project_type.clone(), cookbook.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CookbookManager { cookbooks })
|
||||
}
|
||||
|
||||
pub fn get_cookbook(&self, project_type: &str) -> Result<&Cookbook, CookbookError> {
|
||||
self.cookbooks
|
||||
.get(project_type)
|
||||
.ok_or_else(|| CookbookError::NotFound(project_type.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
mod cookbook;
|
||||
mod llm;
|
||||
mod llm_output;
|
||||
|
||||
pub use cookbook::{Cookbook, CookbookManager, CookbookError};
|
||||
pub use llm::{LLMClient, ollama::OllamaClient, openai::OpenAIClient};
|
||||
pub use llm_output::LLMOutput;
|
||||
use serde::Serialize;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use serde_yaml;
|
||||
|
||||
pub struct CICDLLMHelper {
|
||||
cookbook_manager: CookbookManager,
|
||||
}
|
||||
|
||||
impl CICDLLMHelper {
|
||||
pub fn new(cookbook_dir: &str) -> Result<Self, CookbookError> {
|
||||
let cookbook_manager = CookbookManager::load_from_dir(cookbook_dir)?;
|
||||
Ok(Self { cookbook_manager })
|
||||
}
|
||||
|
||||
pub async fn analyze_repository(
|
||||
&self,
|
||||
repository_path: &str,
|
||||
additional_files: &[String],
|
||||
llm_client: &dyn LLMClient,
|
||||
package_type: &str,
|
||||
) -> Result<AnalysisResult> {
|
||||
// Read README.md
|
||||
let readme_path = Path::new(repository_path).join("README.md");
|
||||
let readme_content = fs::read_to_string(&readme_path)
|
||||
.unwrap_or_else(|_| "No README.md found".to_string());
|
||||
|
||||
// Read additional files
|
||||
let mut file_contents = Vec::new();
|
||||
for file in additional_files {
|
||||
let file_path = Path::new(repository_path).join(file);
|
||||
if let Ok(content) = fs::read_to_string(&file_path) {
|
||||
file_contents.push(format!("File: {}\n{}\n", file, content));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prompt for LLM
|
||||
let prompt = self.build_prompt(&readme_content, &file_contents, package_type);
|
||||
|
||||
// Get LLM response
|
||||
let llm_response = llm_client.generate(&prompt).await?;
|
||||
|
||||
println!("LLM Response: {}", llm_response);
|
||||
|
||||
// Parse LLM output
|
||||
let llm_output: LLMOutput = serde_yaml::from_str(&llm_response)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse LLM output: {}", e))?;
|
||||
|
||||
// Get cookbook for project type
|
||||
let cookbook = self.cookbook_manager.get_cookbook(&llm_output.project_type)?;
|
||||
|
||||
// Merge cookbook and LLM output
|
||||
let result = self.merge_outputs(&llm_output, cookbook, package_type);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn build_prompt(&self, readme_content: &str, file_contents: &[String], package_type: &str) -> String {
|
||||
format!(
|
||||
r#"Analyze the following repository files and determine the build configuration.
|
||||
|
||||
README.md:
|
||||
{}
|
||||
|
||||
Additional files:
|
||||
{}
|
||||
|
||||
Please provide the analysis in YAML format with the following structure:
|
||||
project_type: <string>
|
||||
dependencies: <list of package names for {package_type} packages>
|
||||
build_steps: <list of shell commands>
|
||||
artifacts: <list of artifact paths, use /install as base directory>
|
||||
environment: <key-value pairs for environment variables>
|
||||
other: <any other relevant information>
|
||||
|
||||
Focus on identifying:
|
||||
1. The project type (e.g., rust, python, nodejs, cmake, make, etc.)
|
||||
2. Dependencies needed for building (as {package_type} packages)
|
||||
3. Build steps as shell commands
|
||||
4. Artifact paths (output files, typically in /install)
|
||||
5. Environment variables needed for building
|
||||
|
||||
Output only valid YAML, no additional text and remove '```' in output(output as plain text)."#,
|
||||
readme_content,
|
||||
file_contents.join("\n"),
|
||||
package_type = package_type
|
||||
)
|
||||
}
|
||||
|
||||
fn merge_outputs(&self, llm_output: &LLMOutput, cookbook: &Cookbook, package_type: &str) -> AnalysisResult {
|
||||
// Merge dependencies
|
||||
let cookbook_deps = cookbook.dependencies.get(package_type)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut all_dependencies = cookbook_deps;
|
||||
all_dependencies.extend_from_slice(&llm_output.dependencies);
|
||||
all_dependencies.sort();
|
||||
all_dependencies.dedup();
|
||||
|
||||
// Merge environment variables
|
||||
let mut all_environment = cookbook.environment.clone();
|
||||
for (key, value) in &llm_output.environment {
|
||||
all_environment.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
// Merge artifacts
|
||||
let mut all_artifacts = cookbook.artifacts.clone();
|
||||
all_artifacts.extend_from_slice(&llm_output.artifacts);
|
||||
all_artifacts.sort();
|
||||
all_artifacts.dedup();
|
||||
|
||||
AnalysisResult {
|
||||
dependencies: all_dependencies,
|
||||
build_steps: llm_output.build_steps.clone(),
|
||||
artifacts: all_artifacts,
|
||||
environment: all_environment,
|
||||
project_type: llm_output.project_type.clone(),
|
||||
other: llm_output.other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_output_files(&self, result: &AnalysisResult, output_dir: &str) -> Result<()> {
|
||||
let output_path = Path::new(output_dir);
|
||||
|
||||
// Write prebake.tmpl.yaml
|
||||
let prebake_content = serde_yaml::to_string(&PrebakeTemplate {
|
||||
dependencies: &result.dependencies,
|
||||
environment: &result.environment,
|
||||
})?;
|
||||
fs::write(output_path.join("prebake.tmpl.yaml"), prebake_content)?;
|
||||
|
||||
// Write finalize.tmpl.yaml
|
||||
let finalize_content = serde_yaml::to_string(&FinalizeTemplate {
|
||||
artifacts: &result.artifacts,
|
||||
})?;
|
||||
fs::write(output_path.join("finalize.tmpl.yaml"), finalize_content)?;
|
||||
|
||||
// Write build.sh
|
||||
let build_script = result.build_steps.join("\n");
|
||||
fs::write(output_path.join("build.sh"), build_script)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnalysisResult {
|
||||
pub dependencies: Vec<String>,
|
||||
pub build_steps: Vec<String>,
|
||||
pub artifacts: Vec<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub project_type: String,
|
||||
pub other: HashMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PrebakeTemplate<'a> {
|
||||
dependencies: &'a Vec<String>,
|
||||
environment: &'a HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FinalizeTemplate<'a> {
|
||||
artifacts: &'a Vec<String>,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LLMClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
mod client;
|
||||
|
||||
pub use client::LLMClient;
|
||||
|
||||
pub mod ollama;
|
||||
pub mod openai;
|
||||
@@ -1,27 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
use ollama_rs::generation::completion::request::GenerationRequest;
|
||||
use ollama_rs::Ollama;
|
||||
|
||||
use super::LLMClient;
|
||||
|
||||
pub struct OllamaClient {
|
||||
client: Ollama,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl OllamaClient {
|
||||
pub fn new(url: String, model: String) -> Self {
|
||||
let client = Ollama::new(url, 11434);
|
||||
Self { client, model }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LLMClient for OllamaClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String> {
|
||||
let request = GenerationRequest::new(self.model.clone(), prompt.to_string());
|
||||
let response = self.client.generate(request).await?;
|
||||
Ok(response.response)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
use openai_api_rust::*;
|
||||
use openai_api_rust::chat::*;
|
||||
|
||||
use super::LLMClient;
|
||||
|
||||
pub struct OpenAIClient {
|
||||
client: OpenAI,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl OpenAIClient {
|
||||
pub fn new(api_key: String, base_url: Option<String>, model: String) -> Self {
|
||||
let auth = Auth::new(api_key.as_str());
|
||||
let client = if let Some(url) = base_url {
|
||||
OpenAI::new(auth, url.as_str())
|
||||
} else {
|
||||
OpenAI::new(auth, "https://api.openai.com/v1/")
|
||||
};
|
||||
Self { client, model }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LLMClient for OpenAIClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String> {
|
||||
let message = Message {
|
||||
role: Role::User,
|
||||
content: prompt.to_string(),
|
||||
};
|
||||
|
||||
let body = ChatBody {
|
||||
model: self.model.clone(),
|
||||
messages: vec![message],
|
||||
max_tokens: Some(2048),
|
||||
temperature: Some(0.1),
|
||||
top_p: Some(0.1),
|
||||
n: Some(1),
|
||||
stream: Some(false),
|
||||
stop: None,
|
||||
presence_penalty: None,
|
||||
frequency_penalty: None,
|
||||
logit_bias: None,
|
||||
user: None,
|
||||
};
|
||||
|
||||
let response = self.client.chat_completion_create(&body).unwrap();
|
||||
|
||||
if let Some(choice) = response.choices.first() {
|
||||
Ok(choice.message.as_ref().unwrap().content.clone())
|
||||
} else {
|
||||
anyhow::bail!("No response from OpenAI")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LLMOutput {
|
||||
pub project_type: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub build_steps: Vec<String>,
|
||||
pub artifacts: Vec<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, serde_yaml::Value>,
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use clap::Parser;
|
||||
use log::error;
|
||||
use workshop_llm_detector::{CICDLLMHelper, LLMClient, OllamaClient, OpenAIClient};
|
||||
// use std::path::Path;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "workshop-llm-detector")]
|
||||
#[command(about = "LLM helper for analyzing repositories")]
|
||||
struct Cli {
|
||||
/// Repository path to analyze
|
||||
repository_path: String,
|
||||
|
||||
/// Additional files to read (comma separated)
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
files: Vec<String>,
|
||||
|
||||
/// LLM Provider
|
||||
#[arg(long, default_value = "openai")]
|
||||
provider: String,
|
||||
|
||||
/// Model to use
|
||||
#[arg(long)]
|
||||
model: String,
|
||||
|
||||
/// Model API URL
|
||||
#[arg(long)]
|
||||
model_url: Option<String>,
|
||||
|
||||
/// Package type for dependencies
|
||||
#[arg(long, default_value = "deb")]
|
||||
package_type: String,
|
||||
|
||||
/// Cookbook directory
|
||||
#[arg(long, default_value = "cookbook")]
|
||||
cookbook_dir: String,
|
||||
|
||||
/// Output directory
|
||||
#[arg(short, long, default_value = ".")]
|
||||
output_dir: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
||||
// Initialize cookbook manager
|
||||
let helper = CICDLLMHelper::new(&args.cookbook_dir)?;
|
||||
|
||||
// Initialize LLM client
|
||||
let llm_client: Box<dyn LLMClient> = match args.provider.as_str() {
|
||||
"openai" => {
|
||||
let api_key = std::env::var("OPENAI_API_KEY")
|
||||
.expect("OPENAI_API_KEY environment variable must be set");
|
||||
let base_url = args.model_url.clone();
|
||||
Box::new(OpenAIClient::new(api_key, base_url, args.model))
|
||||
}
|
||||
"ollama" => {
|
||||
let url = args.model_url.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
Box::new(OllamaClient::new(url, args.model))
|
||||
}
|
||||
_ => {
|
||||
error!("Unsupported provider: {}", args.provider);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Analyze repository
|
||||
let result = helper.analyze_repository(
|
||||
&args.repository_path,
|
||||
&args.files,
|
||||
&*llm_client,
|
||||
&args.package_type,
|
||||
).await?;
|
||||
|
||||
println!("Analysis completed for project type: {}", result.project_type);
|
||||
println!("Found {} dependencies", result.dependencies.len());
|
||||
println!("Generated {} build steps", result.build_steps.len());
|
||||
println!("Identified {} artifacts", result.artifacts.len());
|
||||
|
||||
// Write output files
|
||||
helper.write_output_files(&result, &args.output_dir)?;
|
||||
|
||||
println!("Output files written to {}", args.output_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# workshop-vault/src
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Secret management client for HashiCorp Vault.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs # Library entry, re-exports
|
||||
├── client.rs # WorkshopSecretManager client
|
||||
├── rusty_vault_client.rs # RustyVault adapter
|
||||
├── models.rs # Secret, SecretEngine structs
|
||||
├── error.rs # VaultError enum
|
||||
└── examples/usage.rs # Usage examples
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Symbol | Type | Location | Purpose |
|
||||
|--------|------|----------|---------|
|
||||
| create_vault_client | fn | lib.rs:45 | Create client from config |
|
||||
| create_vault_client_from_env | fn | lib.rs:60 | Create from env vars |
|
||||
| WorkshopSecretManager | struct | client.rs:16 | Main client |
|
||||
| Secret | struct | models.rs:5 | Secret data |
|
||||
| VaultError | enum | error.rs:8 | Error types |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Library only**: No main.rs, only lib.rs
|
||||
- **Adapter pattern**: rusty_vault_client.rs wraps rusty_vault crate
|
||||
- **Error handling**: thiserror for VaultError
|
||||
- **Async**: All operations async via rusty_vault
|
||||
|
||||
## DEPENDENCIES
|
||||
|
||||
- `rusty_vault` - Vault client library
|
||||
- `async-trait` - Async trait support
|
||||
- `tokio` - Async runtime
|
||||
- `serde` - Serialization
|
||||
Reference in New Issue
Block a user