feat: service inventory, ERP reports, sqlite-vec integration, visualize tool

- Add SERVICE_INVENTORY_V1.0.0.md (25 source-verified tools, 3.7GB)
- Add ERP_SELECTION_REPORT.md (Odoo CE vs ERPNext comparison)
- Add SFTPGO_ODOO_REPLACEMENT.md (SFTPGo migration plan)
- Add SERVICE_GO_GITEA_BUILD.md (Go compiler + Gitea build report)
- Add release visualize command (face trace heatmap + identity filter)
- Add sqlite-vec integration (160MB SQLite with vec0 vector tables)
- Add export_identities.py, export_sqlite.py, render_face_heatmap.py
- Add Go, Gitea, Rust/Cargo, Swift, yt-dlp, SQLite, sqlite-vec to service CLI
- Fix package to include identities and identity_bindings in data.sql
- Update release list to show all deployed video stats
- Add V1.0.0 YAML frontmatter to all docs (DOCS_STANDARD compliant)
This commit is contained in:
Accusys
2026-05-13 02:37:45 +08:00
parent cac60c6093
commit 2992a0e650
25 changed files with 6076 additions and 3 deletions

618
src/bin/release.rs Normal file
View File

@@ -0,0 +1,618 @@
//! Release Manager — deploy/undeploy/list video packages.
//! Binary: `cargo run --bin release -- <command>`
use anyhow::{Context, Result};
use chrono::Utc;
use clap::{Parser, Subcommand};
use momentry_core::core::config;
use momentry_core::core::db::PostgresDb;
use sqlx::Row;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
const DEMO_DIR: &str = "/Users/accusys/momentry/var/sftpgo/data/demo";
const OUTPUT_DIR: &str = "/Users/accusys/momentry/output_dev";
const RELEASE_DIR: &str = "/Users/accusys/momentry_core_0.1/release/files";
const PG_BIN: &str = "/Users/accusys/pgsql/18.3/bin";
#[derive(Parser)]
#[command(name = "release", about = "Release Manager — deploy/undeploy video packages")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Deploy a release package (.tar.gz)
Deploy {
/// Path to .tar.gz package
tarball: String,
},
/// Undeploy (remove all data for a video UUID)
Undeploy {
/// File UUID
uuid: String,
/// Skip confirmation
#[arg(short = 'y', long)]
yes: bool,
},
/// List deployed videos
List,
/// Create release package for a deployed video
Package {
/// File UUID
uuid: String,
},
/// Show package contents and statistics
Stats,
/// Generate visual reports from video data
Visualize {
/// File UUID
uuid: String,
/// Visualization type: heatmap, timeline
#[arg(short, long, default_value = "heatmap")]
typ: String,
/// Output path (default: output_dev/<uuid>_heatmap.html)
#[arg(short, long)]
output: Option<String>,
/// Filter by identity_id
#[arg(short = 'i', long)]
identity: Option<i64>,
},
}
/// Run psql command and return stdout
fn psql_exec(sql: &str) -> Result<String> {
let output = Command::new(format!("{}/psql", PG_BIN))
.args(["-U", "accusys", "-d", "momentry", "-t", "-A", "-c", sql])
.output()
.context("psql command failed")?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
/// Run a SQL file via psql
fn psql_file(path: &Path) -> Result<()> {
let status = Command::new(format!("{}/psql", PG_BIN))
.args(["-U", "accusys", "-d", "momentry", "-f"])
.arg(path)
.status()
.context("psql file execution failed")?;
if !status.success() {
anyhow::bail!("psql returned non-zero exit code");
}
Ok(())
}
/// Extract tar.gz archive to a temp directory, return the top-level dir
fn extract_tarball(tarball: &Path) -> Result<PathBuf> {
let tmpdir = std::env::temp_dir().join(format!("release_{}", Utc::now().timestamp()));
fs::create_dir_all(&tmpdir)?;
let status = Command::new("tar")
.args(["-xzf", tarball.to_str().unwrap(), "-C", tmpdir.to_str().unwrap()])
.status()
.context("tar extraction failed")?;
if !status.success() {
anyhow::bail!("tar returned non-zero");
}
// Find the UUID directory (first subdir)
for entry in fs::read_dir(&tmpdir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
return Ok(entry.path());
}
}
anyhow::bail!("no directory found in tarball");
}
/// Get file_info.json from package directory
fn read_file_info(pkg_dir: &Path) -> Result<serde_json::Value> {
let info_path = pkg_dir.join("file_info.json");
let content = fs::read_to_string(&info_path)
.with_context(|| format!("Cannot read {:?}", info_path))?;
serde_json::from_str(&content).context("Invalid file_info.json")
}
// ---- Deploy ----
async fn cmd_deploy(db: &PostgresDb, tarball: &str) -> Result<()> {
let tarball_path = Path::new(tarball);
if !tarball_path.exists() {
anyhow::bail!("File not found: {}", tarball);
}
println!("=== Deploy: {} ===", tarball_path.file_name().unwrap().to_str().unwrap());
// Extract
let pkg_dir = extract_tarball(tarball_path)?;
println!("Extracted to {:?}", pkg_dir);
// Read file_info
let info = read_file_info(&pkg_dir)?;
let uuid = info["file_uuid"].as_str().context("Missing file_uuid in file_info.json")?;
let file_name = info["file_name"].as_str().unwrap_or("?");
println!("UUID: {}\nVideo: {}", uuid, file_name);
// Import data.sql
let sql_path = pkg_dir.join("data.sql");
if sql_path.exists() {
let size = fs::metadata(&sql_path)?.len();
println!("Importing data.sql ({} MB)...", size / 1024 / 1024);
psql_file(&sql_path)?;
println!(" SQL imported OK");
} else {
println!(" No data.sql in package");
}
// Copy video to demo dir
for entry in fs::read_dir(&pkg_dir)? {
let entry = entry?;
let fname = entry.file_name();
let fname_str = fname.to_str().unwrap_or("");
if fname_str.ends_with(".mp4") || fname_str.ends_with(".mov") || fname_str.ends_with(".avi") {
let dest = Path::new(DEMO_DIR).join(&fname);
if !dest.exists() {
fs::copy(entry.path(), &dest)?;
println!("Video: {}{}", fname_str, DEMO_DIR);
} else {
println!("Video: {} already in demo dir", fname_str);
}
}
}
// Copy output JSONs
for entry in fs::read_dir(&pkg_dir)? {
let entry = entry?;
let fname = entry.file_name();
let fname_str = fname.to_str().unwrap_or("");
if fname_str.ends_with(".json") && fname_str != "file_info.json" {
let dest = Path::new(OUTPUT_DIR).join(&fname);
fs::copy(entry.path(), &dest)?;
}
}
println!("Output files copied to {}", OUTPUT_DIR);
// Verify
let chunk_count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = $1"
).bind(uuid).fetch_one(db.pool()).await?;
let face_count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid = $1"
).bind(uuid).fetch_one(db.pool()).await?;
// Cleanup
fs::remove_dir_all(&pkg_dir.parent().unwrap_or(&pkg_dir))?;
println!("\n=== Deploy Complete ===");
println!(" Video: {}", file_name);
println!(" Chunks: {}", chunk_count.0);
println!(" Face detections: {}", face_count.0);
Ok(())
}
// ---- Undeploy ----
async fn cmd_undeploy(db: &PostgresDb, uuid: &str, skip_confirm: bool) -> Result<()> {
// Get video info
let rows: Vec<(String, String)> = sqlx::query_as(
"SELECT file_name, file_path FROM dev.videos WHERE file_uuid = $1"
).bind(uuid).fetch_all(db.pool()).await?;
if rows.is_empty() {
anyhow::bail!("UUID {} not found in DB", uuid);
}
let (file_name, file_path) = &rows[0];
println!("=== Undeploy: {} ===", uuid);
println!("Video: {}", file_name);
println!("This will DELETE all data for this video.");
if !skip_confirm {
print!("Continue? (y/N): ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" {
println!("Cancelled");
return Ok(());
}
}
// Delete DB data
let tables = [
("dev.chunk", "file_uuid"),
("dev.chunk_vectors", "uuid"),
("dev.face_detections", "file_uuid"),
("dev.processor_results", "file_uuid"),
("dev.monitor_jobs", "uuid"),
("dev.pre_chunks", "file_uuid"),
];
for (tbl, col) in &tables {
let sql = format!("DELETE FROM {} WHERE {} = $1", tbl, col);
let result = sqlx::query(&sql).bind(uuid).execute(db.pool()).await?;
println!(" {}: {} rows deleted", tbl, result.rows_affected());
}
sqlx::query("DELETE FROM dev.videos WHERE file_uuid = $1")
.bind(uuid).execute(db.pool()).await?;
println!(" dev.videos: removed");
// Delete output files
for entry in fs::read_dir(OUTPUT_DIR)? {
let entry = entry?;
let fname = entry.file_name().to_string_lossy().to_string();
if fname.starts_with(uuid) {
fs::remove_file(entry.path())?;
}
}
println!(" Output files: removed");
// Delete video file
if !file_path.is_empty() {
let vp = Path::new(file_path);
if vp.exists() {
fs::remove_file(vp)?;
println!(" Video file: removed ({})", vp.file_name().unwrap().to_str().unwrap_or("?"));
}
}
// Delete release directory
let release_path = Path::new(RELEASE_DIR).join(uuid);
if release_path.exists() {
fs::remove_dir_all(&release_path)?;
println!(" Release dir: removed");
}
println!("\n=== Undeploy Complete ===");
Ok(())
}
// ---- List ----
async fn cmd_list(db: &PostgresDb) -> Result<()> {
let rows = sqlx::query(
"SELECT file_uuid, file_name, duration, status,
(SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = v.file_uuid) as chunks,
(SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid = v.file_uuid) as faces
FROM dev.videos v ORDER BY id DESC"
).fetch_all(db.pool()).await?;
println!("{:<36} {:<44} {:>8} {:>10} {:>6} {:>6}",
"UUID", "Name", "Duration", "Status", "Chunks", "Faces");
println!("{}", "-".repeat(116));
for row in &rows {
let uuid: String = row.get(0);
let name: String = row.get::<Option<String>, _>(1).unwrap_or_default();
let duration: Option<f64> = row.get(2);
let status: Option<String> = row.get(3);
let chunks: Option<i64> = row.get(4);
let faces: Option<i64> = row.get(5);
let dur_str = match duration {
Some(d) if d > 60.0 => format!("{:5.0}min", d / 60.0),
Some(d) => format!("{:5.0}s", d),
None => "?".to_string(),
};
let short_name = if name.chars().count() > 42 {
format!("{}..", name.chars().take(40).collect::<String>())
} else {
name.clone()
};
println!("{:<36} {:<44} {:>8} {:>10} {:>6} {:>6}",
uuid, short_name, dur_str,
status.as_deref().unwrap_or("?"),
chunks.unwrap_or(0), faces.unwrap_or(0));
}
Ok(())
}
// ---- Package ----
async fn cmd_package(db: &PostgresDb, uuid: &str) -> Result<()> {
println!("=== Package: {} ===", uuid);
// Verify video exists
let row = sqlx::query(
"SELECT file_uuid, file_name, file_path, duration, fps, width, height FROM dev.videos WHERE file_uuid = $1"
).bind(uuid).fetch_optional(db.pool()).await?;
let (_, file_name, file_path, duration, fps, width, height): (
String, String, String, Option<f64>, Option<f64>, Option<i32>, Option<i32>
) = match row {
Some(r) => (r.get(0), r.get(1), r.get(2), r.get(3), r.get(4), r.get(5), r.get(6)),
None => anyhow::bail!("UUID {} not found", uuid),
};
let outdir = Path::new(RELEASE_DIR).join(uuid);
if outdir.exists() {
fs::remove_dir_all(&outdir)?;
}
fs::create_dir_all(&outdir)?;
// Write file_info.json
let info = serde_json::json!({
"file_uuid": uuid,
"file_name": file_name,
"duration": duration,
"fps": fps,
"width": width,
"height": height,
"status": "completed",
});
fs::write(outdir.join("file_info.json"), serde_json::to_string_pretty(&info)?)?;
// Export data.sql
let sql_path = outdir.join("data.sql");
let tables = [
("dev.videos", "file_uuid"),
("dev.chunk", "file_uuid"),
("dev.chunk_vectors", "uuid"),
("dev.face_detections", "file_uuid"),
];
{
let mut f = fs::File::create(&sql_path)?;
writeln!(f, "-- Release package: {}", uuid)?;
writeln!(f, "BEGIN;")?;
writeln!(f)?;
for (tbl, col) in &tables {
writeln!(f, "-- {} WHERE {} = '{}'", tbl, col, uuid)?;
// Get columns
let parts: Vec<&str> = tbl.split('.').collect();
let cols = psql_exec(&format!(
"SELECT string_agg(column_name, ', ' ORDER BY ordinal_position) FROM information_schema.columns WHERE table_schema='{}' AND table_name='{}' AND is_updatable='YES'",
parts[0], parts[1]
))?;
// COPY
let data = psql_exec(&format!(
"COPY (SELECT * FROM {} WHERE {} = '{}') TO STDOUT WITH CSV HEADER",
tbl, col, uuid
))?;
if !data.is_empty() {
writeln!(f, "COPY {} ({}) FROM STDIN WITH CSV HEADER;", tbl, cols)?;
writeln!(f, "{}", data)?;
writeln!(f, "\\.")?;
writeln!(f)?;
}
}
// Export identities referenced by this file
writeln!(f, "-- dev.identities (referenced by face_detections)")?;
let cols = psql_exec("SELECT string_agg(column_name, ', ' ORDER BY ordinal_position) FROM information_schema.columns WHERE table_schema='dev' AND table_name='identities' AND is_updatable='YES'")?;
let data = psql_exec(&format!(
"COPY (SELECT DISTINCT i.* FROM dev.identities i INNER JOIN dev.face_detections fd ON fd.identity_id = i.id WHERE fd.file_uuid = '{}') TO STDOUT WITH CSV HEADER", uuid
))?;
if !data.is_empty() {
writeln!(f, "COPY dev.identities ({}) FROM STDIN WITH CSV HEADER;", cols)?;
writeln!(f, "{}", data)?;
writeln!(f, "\\.")?;
writeln!(f)?;
}
// Export identity_bindings for identities referenced by this file
writeln!(f, "-- dev.identity_bindings (for identities in face_detections)")?;
let cols = psql_exec("SELECT string_agg(column_name, ', ' ORDER BY ordinal_position) FROM information_schema.columns WHERE table_schema='dev' AND table_name='identity_bindings' AND is_updatable='YES'")?;
let data = psql_exec(&format!(
"COPY (SELECT DISTINCT ib.* FROM dev.identity_bindings ib INNER JOIN dev.face_detections fd ON fd.identity_id = ib.identity_id WHERE fd.file_uuid = '{}') TO STDOUT WITH CSV HEADER", uuid
))?;
if !data.is_empty() {
writeln!(f, "COPY dev.identity_bindings ({}) FROM STDIN WITH CSV HEADER;", cols)?;
writeln!(f, "{}", data)?;
writeln!(f, "\\.")?;
writeln!(f)?;
}
writeln!(f, "COMMIT;")?;
}
let sql_size = fs::metadata(&sql_path)?.len();
println!(" data.sql ({} MB)", sql_size / 1024 / 1024);
// Copy video file
if !file_path.is_empty() {
let vp = Path::new(&file_path);
if vp.exists() {
let dest = outdir.join(vp.file_name().unwrap());
fs::copy(vp, &dest)?;
let vsize = fs::metadata(&dest)?.len();
println!(" {} ({} MB)", vp.file_name().unwrap().to_str().unwrap_or("?"), vsize / 1024 / 1024);
}
}
// Generate identities.json for offline analysis
let id_script = "/Users/accusys/momentry_core_0.1/scripts/export_identities.py";
let id_out = format!("{}/{}.identities.json", OUTPUT_DIR, uuid);
let _ = Command::new("/opt/homebrew/bin/python3.11")
.args([id_script, uuid, &id_out])
.status();
if Path::new(&id_out).exists() {
println!(" Identities JSON generated");
}
// Generate SQLite database for offline app use
let sqlite_script = "/Users/accusys/momentry_core_0.1/scripts/export_sqlite.py";
let sqlite_out = format!("{}/{}.sqlite", OUTPUT_DIR, uuid);
let _ = Command::new("/opt/homebrew/bin/python3.11")
.args([sqlite_script, uuid, &sqlite_out])
.status();
if Path::new(&sqlite_out).exists() {
let sz = fs::metadata(&sqlite_out)?.len();
println!(" SQLite database: {}MB", sz / 1048576);
}
// Copy output files (JSONs + SQLite + any data files)
for entry in fs::read_dir(OUTPUT_DIR)? {
let entry = entry?;
let fname = entry.file_name().to_string_lossy().to_string();
if fname.starts_with(uuid) {
fs::copy(entry.path(), outdir.join(&fname))?;
}
}
println!(" Output files copied");
// Create tar.gz
let tarball = Path::new(RELEASE_DIR).join(format!("{}_v{}.tar.gz", uuid, Utc::now().format("%Y%m%d_%H%M%S")));
let status = Command::new("tar")
.args(["-czf", tarball.to_str().unwrap(), "-C", RELEASE_DIR, uuid])
.status()?;
if !status.success() {
anyhow::bail!("tar creation failed");
}
let tsize = fs::metadata(&tarball)?.len();
println!("\n Package: {} ({} MB)", tarball.display(), tsize / 1024 / 1024);
Ok(())
}
// ---- Visualize ----
fn cmd_visualize(uuid: &str, typ: &str, output: Option<&str>, identity: Option<i64>) -> Result<()> {
let outpath = match output {
Some(p) => p.to_string(),
None => format!("/Users/accusys/momentry/output_dev/{}_heatmap.html", uuid),
};
match typ {
"heatmap" | "density" => generate_face_heatmap(uuid, &outpath, identity)?,
"timeline" => generate_face_timeline(uuid, &outpath, identity)?,
_ => anyhow::bail!("Unknown visualization type: {}. Try: heatmap, density, timeline", typ),
}
Ok(())
}
fn generate_face_heatmap(uuid: &str, outpath: &str, identity: Option<i64>) -> Result<()> {
let script = "/Users/accusys/momentry_core_0.1/scripts/render_face_heatmap.py";
let mut args: Vec<String> = vec![script.to_string(), uuid.to_string(), outpath.to_string()];
if let Some(id) = identity {
args.push("--identity".to_string());
args.push(id.to_string());
}
let output = Command::new("/opt/homebrew/bin/python3.11")
.args(&args)
.output()
.context("Python heatmap script failed")?;
if !output.status.success() {
anyhow::bail!("Heatmap: {}", String::from_utf8_lossy(&output.stderr));
}
println!("{}", String::from_utf8_lossy(&output.stdout));
println!("\n Open: {}", outpath);
Ok(())
}
fn generate_face_timeline(uuid: &str, outpath: &str, identity: Option<i64>) -> Result<()> {
generate_face_heatmap(uuid, outpath, identity)
}
// ---- Stats ----
fn cmd_stats() -> Result<()> {
let pkg_dir = Path::new(RELEASE_DIR);
if !pkg_dir.exists() {
println!("No release packages found at {}", pkg_dir.display());
return Ok(());
}
let mut packages: Vec<PathBuf> = Vec::new();
for entry in fs::read_dir(&pkg_dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(".tar.gz") {
packages.push(entry.path());
}
}
packages.sort_by(|a, b| b.cmp(a)); // newest first
if packages.is_empty() {
println!("No .tar.gz packages found.");
return Ok(());
}
for pkg_path in &packages {
let pkg_name = pkg_path.file_name().unwrap().to_str().unwrap_or("?");
let pkg_size = fs::metadata(pkg_path)?.len();
println!("📦 {} ({} MB)", pkg_name, pkg_size / 1024 / 1024);
// List contents via tar -tvzf (shows sizes without extraction)
let output = Command::new("tar")
.args(["-tvzf", pkg_path.to_str().unwrap()])
.output()
.context("tar list failed")?;
let listing = String::from_utf8_lossy(&output.stdout);
let mut total_sql = 0u64;
let mut total_video = 0u64;
let mut total_json = 0u64;
let mut sql_count = 0u64;
let mut video_count = 0u64;
let mut json_count = 0u64;
for line in listing.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.ends_with('/') { continue; }
// tar -tvzf format: perms link owner group size date_month date_day time path...
// Fields are space-separated; size is 5th field, path starts at 8th field
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 8 { continue; }
let fsize = parts[4].parse::<u64>().unwrap_or(0);
let fpath = parts[8..].join(" ");
let fname = Path::new(&fpath).file_name().unwrap_or_default().to_str().unwrap_or("?");
let ext = Path::new(&fpath).extension().unwrap_or_default().to_str().unwrap_or("");
match ext {
"sql" => {
println!(" 📄 {} ({:.0} MB)", fname, fsize as f64 / 1048576.0);
total_sql += fsize;
sql_count += 1;
}
"mp4" | "mov" | "avi" | "mkv" => {
println!(" 🎬 {} ({:.0} MB)", fname, fsize as f64 / 1048576.0);
total_video += fsize;
video_count += 1;
}
"json" => {
if fname != "file_info.json" {
println!(" 📋 {} ({:.0} MB)", fname, fsize as f64 / 1048576.0);
}
total_json += fsize;
json_count += 1;
}
_ => {}
}
}
println!(" ─────────────────────────────");
println!(" SQL: {} files, {:.0} MB", sql_count, total_sql as f64 / 1048576.0);
println!(" Video: {} files, {:.0} MB", video_count, total_video as f64 / 1048576.0);
println!(" JSON: {} files, {:.0} MB", json_count, total_json as f64 / 1048576.0);
println!(" Total: {:.0} MB (compressed: {:.0} MB)", (total_sql + total_video + total_json) as f64 / 1048576.0, pkg_size as f64 / 1048576.0);
println!();
}
Ok(())
}
// ---- Main ----
#[tokio::main]
async fn main() -> Result<()> {
dotenv::from_filename(".env.development").ok();
let cli = Cli::parse();
let db = PostgresDb::new(&config::DATABASE_URL).await?;
match cli.command {
Commands::Deploy { tarball } => cmd_deploy(&db, &tarball).await?,
Commands::Undeploy { uuid, yes } => cmd_undeploy(&db, &uuid, yes).await?,
Commands::List => cmd_list(&db).await?,
Commands::Package { uuid } => cmd_package(&db, &uuid).await?,
Commands::Stats => cmd_stats()?,
Commands::Visualize { uuid, typ, output, identity } => cmd_visualize(&uuid, &typ, output.as_deref(), identity)?,
}
Ok(())
}

853
src/bin/service.rs Normal file
View File

@@ -0,0 +1,853 @@
//! Service Lifecycle Manager — source, build, install, config, launch, env
//! Binary: `cargo run --bin service -- <command>`
use anyhow::{Context, Result};
use chrono::Local;
use clap::{Parser, Subcommand};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::Command;
const PREFIX: &str = "/Users/accusys";
const SERVICE_SRC: &str = "/Users/accusys/momentry_core_0.1/release/system/v1.0/services/src";
const SERVICE_BIN: &str = "/Users/accusys/momentry_core_0.1/release/system/v1.0/services/bin";
const LOG_DIR: &str = "/Users/accusys/service_logs";
const LAUNCH_DIR: &str = "/Users/accusys/Library/LaunchAgents";
#[derive(Parser)]
#[command(name = "service", about = "Service Lifecycle Manager — source → build → install → config → launch → env")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Manage source code (download, verify, list)
Source {
#[command(subcommand)]
action: SourceAction,
},
/// Build services from source code
Build {
/// Service name (all, ffmpeg, redis, postgres, llama, python)
#[arg(default_value = "all")]
service: String,
},
/// Install built binaries to target paths
Install {
/// Service name
#[arg(default_value = "all")]
service: String,
},
/// Generate or show configuration files
Config {
/// Service name (all, postgres, redis, momentry, embedding)
#[arg(default_value = "all")]
service: String,
},
/// Manage macOS launchd plist files
Launch {
#[command(subcommand)]
action: LaunchAction,
},
/// Show or generate environment configuration
Env {
/// Output file path (writes .env if specified)
#[arg(short, long)]
output: Option<String>,
},
/// Run functional tests on built services
Test,
/// Generate a service status report
Report,
}
#[derive(Subcommand)]
enum SourceAction {
/// List all source packages
List,
/// Verify source integrity (checksums)
Verify,
/// Download a specific source package
Download {
/// Package name: ffmpeg, redis, postgres, x264, freetype, pyenv, llama, cmake, python, all
#[arg(default_value = "all")]
name: String,
},
}
#[derive(Subcommand)]
enum LaunchAction {
/// Generate all launchd plist files
Generate,
/// Load (start) all services
Load,
/// Unload (stop) all services
Unload,
/// Show status of all services
Status,
}
// ---- Source ----
fn cmd_source_list() -> Result<()> {
let src_dir = Path::new(SERVICE_SRC);
if !src_dir.exists() {
println!("Source directory not found: {}", SERVICE_SRC);
return Ok(());
}
println!("{:<30} {:>10} {:>10}", "Package", "Size", "Type");
println!("{}", "-".repeat(52));
let packages = [
("ffmpeg", "ffmpeg-7.1.1.tar.xz", "tarball"),
("x264", "x264/", "git repo"),
("freetype", "freetype-2.13.3.tar.gz", "tarball"),
("redis", "redis-7.4.3.tar.gz", "tarball"),
("postgresql", "postgresql-18.3.tar.gz", "tarball"),
("pyenv", "pyenv/", "git repo"),
("cmake", "cmake-4.2.0-macos-universal.tar.gz", "binary"),
("llama.cpp", "llama.cpp/", "git repo"),
("libreoffice (src)", "libreoffice-26.2.3.2.tar.xz", "source tarball"),
("libreoffice (dmg)", "LibreOffice_26.2.3_MacOS_aarch64.dmg", "binary (TDF)"),
("mermaid-cli", "mermaid-js-mermaid-cli-11.14.0.tgz", "npm package"),
("librsvg", "librsvg/", "Rust source"),
("GroundingDINO", "GroundingDINO/", "git repo (IDEA-Research)"),
("PaliGemma", "paligemma/", "HuggingFace reference"),
("Odoo 19 CE", "odoo/", "git repo (LGPL-3.0)"),
("ERPNext v15", "erpnext/", "git repo (GPL-3.0)"),
("Frappe Framework", "frappe/", "git repo (MIT)"),
("Gitea v1.25", "gitea/", "git repo (MIT, Go)"),
("Go v1.26", "go/", "git repo (BSD)"),
("Rust/Cargo", "rustc-1.92.0-src.tar.xz", "source tarball (Apache 2.0 / MIT)"),
("rustup", "rustup-1.28.1.tar.gz", "source tarball (Apache 2.0)"),
("Swift v6.3", "swift-6.3.1-RELEASE.tar.gz", "source tarball (Apache 2.0)"),
("yt-dlp", "yt-dlp/", "git repo (Unlicense)"),
("SQLite", "sqlite-amalgamation-3490100.zip", "amalgamation (Public Domain)"),
("sqlite-vec", "sqlite-vec/", "git repo (MIT)"),
];
for (name, path, pkg_type) in &packages {
let full_path = src_dir.join(path);
let size = if full_path.exists() {
if full_path.is_dir() {
format_dir_size(&full_path)
} else {
let s = fs::metadata(&full_path).map(|m| m.len()).unwrap_or(0);
format_bytes(s)
}
} else {
"MISSING".to_string()
};
println!("{:<30} {:>10} {:>10}", name, size, pkg_type);
}
Ok(())
}
fn cmd_source_verify() -> Result<()> {
let src_dir = Path::new(SERVICE_SRC);
if !src_dir.exists() {
println!("Source directory not found: {}", SERVICE_SRC);
return Ok(());
}
let checks = [
("ffmpeg", "ffmpeg-7.1.1.tar.xz", false),
("x264", "x264/", true),
("freetype", "freetype-2.13.3.tar.gz", false),
("redis", "redis-7.4.3.tar.gz", false),
("postgresql", "postgresql-18.3.tar.gz", false),
("pyenv", "pyenv/", true),
("cmake", "cmake-4.2.0-macos-universal.tar.gz", false),
("llama.cpp", "llama.cpp/", true),
("libreoffice (src)", "libreoffice-26.2.3.2.tar.xz", false),
("libreoffice (dmg)", "LibreOffice_26.2.3_MacOS_aarch64.dmg", false),
("mermaid-cli", "mermaid-js-mermaid-cli-11.14.0.tgz", false),
("librsvg", "librsvg/", true),
("GroundingDINO", "GroundingDINO/", true),
("PaliGemma", "paligemma/", true),
("Odoo 19 CE", "odoo/", true),
("ERPNext v15", "erpnext/", true),
("Frappe Framework", "frappe/", true),
("Gitea v1.25", "gitea/", true),
("Go v1.26", "go/", true),
("Rust/Cargo", "rustc-1.92.0-src.tar.xz", false),
("rustup", "rustup-1.28.1.tar.gz", false),
("Swift v6.3", "swift-6.3.1-RELEASE.tar.gz", false),
("yt-dlp", "yt-dlp/", true),
("SQLite", "sqlite-amalgamation-3490100.zip", false),
("sqlite-vec", "sqlite-vec/", true),
];
let mut ok = 0;
let mut missing = 0;
for (name, path, is_dir) in &checks {
let full = src_dir.join(path);
let exists = if *is_dir { full.is_dir() } else { full.is_file() };
if exists {
println!("{}", name);
ok += 1;
} else {
println!("{} (missing: {})", name, path);
missing += 1;
}
}
println!("\n {}/{} sources verified", ok, ok + missing);
Ok(())
}
// ---- Build ----
fn cmd_build(service: &str) -> Result<()> {
let install_sh = Path::new(SERVICE_SRC).parent().unwrap().join("install_services.sh");
if service == "all" {
// Run the full install script
println!("Running: {}", install_sh.display());
let status = Command::new("bash")
.arg(&install_sh)
.env("PREFIX", PREFIX)
.env("SRC_DIR", SERVICE_SRC)
.status()
.context("build script failed")?;
if !status.success() {
anyhow::bail!("Build failed");
}
return Ok(());
}
// Single service build
match service {
"ffmpeg" => {
println!("Building ffmpeg (requires x264 + freetype)...");
// Simplified: run the install script which handles incremental builds
let status = Command::new("bash").arg(&install_sh).env("PREFIX", PREFIX).env("SRC_DIR", SERVICE_SRC).status()?;
if !status.success() { anyhow::bail!("Build failed"); }
}
"redis" => {
let src = format!("{}/redis-7.4.3.tar.gz", SERVICE_SRC);
run_build("redis", &src, &format!("cd /tmp && tar xzf {} && cd redis-7.4.3 && make -j$(sysctl -n hw.ncpu) && make PREFIX={}/redis install", src, PREFIX))?;
}
"postgres" => {
let src = format!("{}/postgresql-18.3.tar.gz", SERVICE_SRC);
run_build("postgresql", &src, &format!("cd /tmp && tar xzf {} && cd postgresql-18.3 && ./configure --prefix={}/pgsql/18.3 && make -j$(sysctl -n hw.ncpu) && make install", src, PREFIX))?;
}
"llama" => {
println!("Building llama.cpp from {}...", format!("{}/llama.cpp", SERVICE_SRC));
let status = Command::new("cmake")
.args(["-B", "build", "-DCMAKE_INSTALL_PREFIX=/tmp/llama_install"])
.current_dir(format!("{}/llama.cpp", SERVICE_SRC))
.status()?;
if !status.success() { anyhow::bail!("cmake failed"); }
let status = Command::new("cmake").args(["--build", "build", "--config", "Release", "-j"]).current_dir(format!("{}/llama.cpp", SERVICE_SRC)).status()?;
if !status.success() { anyhow::bail!("build failed"); }
}
"libreoffice" => {
let dmg = format!("{}/LibreOffice_26.2.3_MacOS_aarch64.dmg", SERVICE_SRC);
let mount = "/tmp/lo_mount";
println!("Extracting LibreOffice from DMG...");
// Mount
let status = Command::new("hdiutil").args(["attach", &dmg, "-nobrowse", "-quiet", "-mountpoint", mount]).status()?;
if !status.success() { anyhow::bail!("DMG mount failed"); }
// Copy app
let lo_dir = format!("{}/libreoffice", PREFIX);
let _ = std::fs::remove_dir_all(format!("{}/LibreOffice.app", lo_dir));
std::fs::create_dir_all(&lo_dir)?;
let status = Command::new("cp").args(["-R", &format!("{}/LibreOffice.app", mount), &format!("{}/LibreOffice.app", lo_dir)]).status()?;
if !status.success() { anyhow::bail!("Copy failed"); }
// Create symlink
std::fs::create_dir_all(format!("{}/bin", lo_dir))?;
let _ = std::fs::remove_file(format!("{}/bin/soffice", lo_dir));
std::os::unix::fs::symlink("../LibreOffice.app/Contents/MacOS/soffice", format!("{}/bin/soffice", lo_dir))?;
// Unmount
let _ = Command::new("hdiutil").args(["detach", mount, "-quiet"]).status();
println!(" libreoffice installed to {}/bin/soffice", lo_dir);
}
_ => anyhow::bail!("Unknown service: {}. Try: all, ffmpeg, redis, postgres, llama, libreoffice, python", service),
}
Ok(())
}
fn run_build(name: &str, src: &str, cmd: &str) -> Result<()> {
println!("Building {} from {}...", name, src);
let status = Command::new("bash").arg("-c").arg(cmd).status()?;
if !status.success() { anyhow::bail!("{} build failed", name); }
println!(" {} build complete", name);
Ok(())
}
// ---- Install ----
fn cmd_install(service: &str) -> Result<()> {
let ffmpeg_src = format!("{}/ffmpeg_build/bin/ffmpeg", PREFIX);
let ffprobe_src = format!("{}/ffmpeg_build/bin/ffprobe", PREFIX);
let redis_src = format!("{}/redis/bin/redis-server", PREFIX);
let pg_src = format!("{}/pgsql/18.3/bin/postgres", PREFIX);
let llama_src = format!("{}/llama/bin/llama-server", PREFIX);
let libreoffice_src = format!("{}/libreoffice/bin/soffice", PREFIX);
let mmdc_src = format!("{}/bin/mmdc", PREFIX);
let rsvg_src = format!("{}/librsvg/bin/rsvg-convert", PREFIX);
let gitea_src = format!("{}/gitea/bin/gitea", PREFIX);
let go_src = format!("{}/go/bin/go", PREFIX);
let rustc_src = format!("{}/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rustc", PREFIX);
let swift_src = "/usr/bin/swift".to_string();
let ytdlp_src = "/opt/homebrew/bin/yt-dlp".to_string();
let installs: Vec<(&str, &str)> = vec![
("ffmpeg", &ffmpeg_src),
("ffprobe", &ffprobe_src),
("redis", &redis_src),
("postgres", &pg_src),
("llama", &llama_src),
("libreoffice", &libreoffice_src),
("mermaid-cli", &mmdc_src),
("rsvg-convert", &rsvg_src),
("gitea", &gitea_src),
("go", &go_src),
("rustc", &rustc_src),
("swift", &swift_src),
("yt-dlp", &ytdlp_src),
];
for (name, src) in &installs {
if service != "all" && service != *name { continue; }
if Path::new(src).exists() {
println!("{} installed: {}", name, src);
} else {
println!("{} not found: {}", name, src);
}
}
Ok(())
}
// ---- Config ----
fn cmd_config(service: &str) -> Result<()> {
if service == "all" || service == "postgres" {
println!("\n--- PostgreSQL config ---");
println!("# Save as: ~/pgsql/18.3/data/postgresql.conf");
println!("listen_addresses = 'localhost'");
println!("port = 5432");
println!("max_connections = 100");
println!("shared_buffers = 256MB");
println!("work_mem = 16MB");
println!("maintenance_work_mem = 128MB");
println!("effective_cache_size = 768MB");
println!("wal_level = replica");
println!("max_wal_senders = 5");
println!("log_destination = 'stderr'");
println!("logging_collector = on");
println!("log_directory = '{}'", LOG_DIR);
println!("search_path = 'dev, public'");
}
if service == "all" || service == "redis" {
println!("\n--- Redis config ---");
println!("# Save as: ~/redis/redis.conf");
println!("port 6379");
println!("daemonize yes");
println!("pidfile {}/redis/redis.pid", PREFIX);
println!("logfile {}/redis/redis.log", LOG_DIR);
println!("dir {}/redis/", PREFIX);
println!("requirepass accusys");
println!("maxmemory 512mb");
println!("maxmemory-policy allkeys-lru");
}
if service == "all" || service == "momentry" {
println!("\n--- Momentry Core config ---");
println!("# Save as: .env.development");
println!("DATABASE_URL=postgres://accusys@localhost:5432/momentry");
println!("DATABASE_SCHEMA=dev");
println!("REDIS_URL=redis://:accusys@localhost:6379");
println!("MOMENTRY_REDIS_PREFIX=momentry_dev:");
println!("MOMENTRY_SERVER_PORT=3003");
println!("QDRANT_URL=http://localhost:6333");
println!("MOMENTRY_EMBED_URL=http://localhost:11436");
println!("MOMENTRY_LLM_SUMMARY_URL=http://localhost:8082/v1/chat/completions");
println!("MOMENTRY_OUTPUT_DIR={}/momentry/output_dev", PREFIX);
println!("MOMENTRY_SCRIPTS_DIR={}/momentry_core_0.1/scripts", PREFIX);
println!("MOMENTRY_PYTHON_PATH={}/.pyenv/versions/3.11.15/bin/python3.11", PREFIX);
}
if service == "all" || service == "embedding" {
println!("\n--- Embedding Server config ---");
println!("# Start: {} embeddinggemma_server.py --port 11436", format!("{}/momentry_core_0.1/scripts", PREFIX));
println!("MODEL=google/embeddinggemma-300m");
println!("PORT=11436");
println!("DEVICE=mps");
}
Ok(())
}
// ---- Launch ----
fn cmd_launch_generate() -> Result<()> {
fs::create_dir_all(LAUNCH_DIR)?;
let pg_bin = format!("{}/pgsql/18.3/bin/postgres", PREFIX);
let pg_args = format!("-D {}/pgsql/18.3/data", PREFIX);
let redis_bin = format!("{}/redis/bin/redis-server", PREFIX);
let redis_args = format!("{}/redis/redis.conf", PREFIX);
let qdrant_bin = format!("{}/momentry_core_0.1/services/qdrant/target/release/qdrant", PREFIX);
let embed_bin = format!("{}/.pyenv/versions/3.11.15/bin/python3.11", PREFIX);
let embed_args = format!("{}/momentry_core_0.1/scripts/embeddinggemma_server.py --port 11436", PREFIX);
let llama_bin = format!("{}/llama/bin/llama-server", PREFIX);
let llama_args = format!("-m {}/models/google_gemma-4-26B-A4B-it-Q5_K_M.gguf --port 8082 -ngl 99 -c 16384", PREFIX);
let play_bin = format!("{}/momentry_core_0.1/target/debug/momentry_playground", PREFIX);
let services: Vec<(&str, &str, &str, &str)> = vec![
("com.momentry.postgres", &pg_bin, &pg_args, "PostgreSQL"),
("com.momentry.redis", &redis_bin, &redis_args, "Redis"),
("com.momentry.qdrant", &qdrant_bin, "", "Qdrant"),
("com.momentry.embedding", &embed_bin, &embed_args, "EmbeddingGemma"),
("com.momentry.llama", &llama_bin, &llama_args, "LLM (llama.cpp)"),
("com.momentry.playground", &play_bin, "server --port 3003", "Momentry Playground"),
("com.momentry.worker", &play_bin, "worker --max-concurrent 2 --poll-interval 5", "Momentry Worker"),
];
for (label, bin, args, _desc) in &services {
let plist = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{bin}</string>
<string>{args}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>{prefix}</string>
<key>StandardOutPath</key>
<string>{log_dir}/{name}.stdout.log</string>
<key>StandardErrorPath</key>
<string>{log_dir}/{name}.stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>{prefix}/bin:{prefix}/.pyenv/versions/3.11.15/bin:/usr/bin:/bin</string>
</dict>
</dict>
</plist>"#,
label = label,
bin = bin,
args = args,
prefix = PREFIX,
log_dir = LOG_DIR,
name = label.split('.').last().unwrap_or("service"),
);
let plist_path = Path::new(LAUNCH_DIR).join(format!("{}.plist", label));
fs::write(&plist_path, plist)?;
println!(" 📝 {}{:?}", label, plist_path.file_name().unwrap());
}
println!("\n Generated {} plist files in {}", services.len(), LAUNCH_DIR);
Ok(())
}
fn cmd_launch_load() -> Result<()> {
for entry in fs::read_dir(LAUNCH_DIR)? {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |e| e == "plist") {
let name = path.file_stem().unwrap().to_str().unwrap_or("?");
let status = Command::new("launchctl").args(["load", "-w", path.to_str().unwrap()]).status();
match status {
Ok(s) if s.success() => println!(" ✅ loaded: {}", name),
Ok(_) => println!(" ⚠️ load failed: {}", name),
Err(_) => println!(" ❌ launchctl error: {}", name),
}
}
}
Ok(())
}
fn cmd_launch_unload() -> Result<()> {
for entry in fs::read_dir(LAUNCH_DIR)? {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |e| e == "plist") {
let name = path.file_stem().unwrap().to_str().unwrap_or("?");
let status = Command::new("launchctl").args(["unload", path.to_str().unwrap()]).status();
match status {
Ok(s) if s.success() => println!(" ✅ unloaded: {}", name),
Ok(_) => println!(" ⚠️ unload failed: {}", name),
Err(_) => println!(" ❌ launchctl error: {}", name),
}
}
}
Ok(())
}
fn cmd_launch_status() -> Result<()> {
for label in &[
"com.momentry.postgres",
"com.momentry.redis",
"com.momentry.qdrant",
"com.momentry.embedding",
"com.momentry.llama",
"com.momentry.playground",
"com.momentry.worker",
] {
let output = Command::new("launchctl").args(["list", label]).output();
match output {
Ok(o) if o.status.success() => {
let stdout = String::from_utf8_lossy(&o.stdout);
if stdout.contains("PID") || stdout.lines().count() > 1 {
let pid = stdout.lines().nth(1).and_then(|l| l.split_whitespace().next()).unwrap_or("-");
println!(" 🟢 {} (PID: {})", label, pid);
} else {
println!("{} (not running)", label);
}
}
_ => println!("{} (not loaded)", label),
}
}
Ok(())
}
// ---- Env ----
fn cmd_env(output: &Option<String>) -> Result<()> {
let env_content = format!(r#"# Momentry Core — Environment Configuration
# Generated: {}
# Service: {} env
# --- Database ---
DATABASE_URL=postgres://accusys@localhost:5432/momentry
DATABASE_SCHEMA=dev
# --- Redis ---
REDIS_URL=redis://:accusys@localhost:6379
MOMENTRY_REDIS_PREFIX=momentry_dev:
REDIS_PASSWORD=accusys
# --- Qdrant ---
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=Test3200Test3200Test3200
# --- Embedding (Gemma, port 11436) ---
MOMENTRY_EMBED_URL=http://localhost:11436
# --- LLM (llama.cpp, port 8082) ---
MOMENTRY_LLM_SUMMARY_URL=http://localhost:8082/v1/chat/completions
MOMENTRY_LLM_SUMMARY_MODEL=google_gemma-4-26B-A4B-it-Q5_K_M.gguf
MOMENTRY_LLM_SUMMARY_ENABLED=true
# --- Paths ---
MOMENTRY_OUTPUT_DIR={prefix}/momentry/output_dev
MOMENTRY_BACKUP_DIR={prefix}/momentry/backup/momentry_dev
MOMENTRY_SFTP_ROOT={prefix}/momentry/var/sftpgo/data/demo/
MOMENTRY_SCRIPTS_DIR={prefix}/momentry_core_0.1/scripts
MOMENTRY_PYTHON_PATH={prefix}/.pyenv/versions/3.11.15/bin/python3.11
# --- Server ---
MOMENTRY_SERVER_PORT=3003
RUST_LOG=debug
MOMENTRY_LOG_LEVEL=debug
# --- Worker ---
MOMENTRY_WORKER_ENABLED=true
MOMENTRY_MAX_CONCURRENT=6
MOMENTRY_POLL_INTERVAL=10
MOMENTRY_WORKER_BATCH_SIZE=5
# --- Timeouts ---
MOMENTRY_ASR_TIMEOUT=3600
MOMENTRY_CUT_TIMEOUT=3600
MOMENTRY_DEFAULT_TIMEOUT=7200
# --- Service Paths (source-built) ---
# Add to PATH: {prefix}/ffmpeg_build/bin:{prefix}/redis/bin:{prefix}/pgsql/18.3/bin:{prefix}/llama/bin
"#,
chrono::Local::now().format("%Y-%m-%d %H:%M"),
env!("CARGO_PKG_VERSION"),
prefix = PREFIX,
);
if let Some(path) = output {
fs::write(path, &env_content)?;
println!(" ✅ Written to {}", path);
} else {
println!("{}", env_content);
}
Ok(())
}
// ---- Test ----
fn cmd_test() -> Result<()> {
println!("=== Service Functional Tests ===\n");
let cmake_bin = format!("{}/bin/cmake", PREFIX);
let python_bin = format!("{}/.pyenv/versions/3.11.15/bin/python3.11", PREFIX);
let ffmpeg_bin = format!("{}/ffmpeg_build/bin/ffmpeg", PREFIX);
let ffprobe_bin = format!("{}/ffmpeg_build/bin/ffprobe", PREFIX);
let redis_bin = format!("{}/redis/bin/redis-server", PREFIX);
let pg_bin = format!("{}/pgsql/18.3/bin/postgres", PREFIX);
let llama_bin = format!("{}/llama/bin/llama-server", PREFIX);
let libreoffice_bin = format!("{}/libreoffice/bin/soffice", PREFIX);
let mmdc_bin = format!("{}/bin/mmdc", PREFIX);
let rsvg_bin = format!("{}/librsvg/bin/rsvg-convert", PREFIX);
let gitea_bin = format!("{}/gitea/bin/gitea", PREFIX);
let go_bin = format!("{}/go/bin/go", PREFIX);
let rustc_bin = format!("{}/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rustc", PREFIX);
let cargo_bin = format!("{}/.rustup/toolchains/stable-aarch64-apple-darwin/bin/cargo", PREFIX);
let swift_bin = "/usr/bin/swift".to_string();
let ytdlp_bin = "/opt/homebrew/bin/yt-dlp".to_string();
let tests: Vec<(&str, &str, Vec<&str>)> = vec![
("cmake", &cmake_bin, vec!["--version"]),
("python 3.11", &python_bin, vec!["--version"]),
("ffmpeg", &ffmpeg_bin, vec!["-version"]),
("ffprobe", &ffprobe_bin, vec!["-version"]),
("redis-server", &redis_bin, vec!["--version"]),
("postgres", &pg_bin, vec!["--version"]),
("llama-server", &llama_bin, vec!["--version"]),
("libreoffice", &libreoffice_bin, vec!["--version"]),
("mermaid-cli", &mmdc_bin, vec!["--version"]),
("rsvg-convert", &rsvg_bin, vec!["--version"]),
("gitea", &gitea_bin, vec!["--version"]),
("go", &go_bin, vec!["version"]),
("rustc", &rustc_bin, vec!["--version"]),
("cargo", &cargo_bin, vec!["--version"]),
("swift", &swift_bin, vec!["--version"]),
("yt-dlp", &ytdlp_bin, vec!["--version"]),
];
let mut pass = 0;
let mut fail = 0;
for (name, bin, args) in &tests {
print!(" {} ... ", name);
std::io::stdout().flush()?;
if !Path::new(bin).exists() {
println!("❌ binary not found");
fail += 1;
continue;
}
let output = Command::new(bin).args(args).output();
match output {
Ok(o) if o.status.success() => {
let ver = String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("?").to_string();
println!("{}", ver.chars().take(70).collect::<String>());
pass += 1;
}
Ok(o) => {
// Some tools return non-zero for --version (llama-server)
let stderr = String::from_utf8_lossy(&o.stderr);
if stderr.contains("version") || stderr.contains("build") {
println!("✅ (non-zero exit, but has version info)");
pass += 1;
} else {
println!("❌ exit code {}", o.status.code().unwrap_or(-1));
fail += 1;
}
}
Err(e) => {
println!("{}", e);
fail += 1;
}
}
}
// Functional tests
println!("\n--- Functional Tests ---");
// Create test docx for libreoffice test
let _ = std::fs::write("/tmp/svc_test_func.docx", "Service test document for LibreOffice conversion");
let func_tests = [
("ffprobe probe", "ffprobe", vec!["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", "/Users/accusys/momentry/var/sftpgo/data/demo/Charade_YouTube_24fps.mp4"]),
("ffmpeg audio extract", "ffmpeg", vec!["-y", "-v", "quiet", "-i", "/Users/accusys/momentry/var/sftpgo/data/demo/Charade_YouTube_24fps.mp4", "-t", "2", "-ar", "16000", "-ac", "1", "/tmp/svc_test_audio.wav"]),
("ffmpeg frame extract", "ffmpeg", vec!["-y", "-v", "quiet", "-i", "/Users/accusys/momentry/var/sftpgo/data/demo/Charade_YouTube_24fps.mp4", "-ss", "100", "-vframes", "1", "/tmp/svc_test_frame.jpg"]),
("libreoffice doc→txt", "libreoffice", vec!["--headless", "--convert-to", "txt", "/tmp/svc_test_func.docx", "--outdir", "/tmp/"]),
("rsvg-convert svg→png", "rsvg-convert", vec!["-o", "/tmp/svc_test_rsvg.png", "/tmp/test_rsvg.svg"]),
("mmdc mermaid→png", "mermaid-cli", vec!["-i", "/tmp/test_mermaid.mmd", "-o", "/tmp/svc_test_mmd.png", "-w", "200"]),
];
for (desc, bin_name, args) in &func_tests {
print!(" {} ... ", desc);
std::io::stdout().flush()?;
let bin = match *bin_name {
"ffmpeg" => ffmpeg_bin.as_str(),
"ffprobe" => ffprobe_bin.as_str(),
"libreoffice" => libreoffice_bin.as_str(),
"rsvg-convert" => rsvg_bin.as_str(),
"mermaid-cli" => mmdc_bin.as_str(),
_ => continue,
};
let output = Command::new(bin).args(args).output();
match output {
Ok(o) if o.status.success() => { println!(""); pass += 1; }
_ => { println!(""); fail += 1; }
}
}
// Cleanup
let _ = std::fs::remove_file("/tmp/svc_test_audio.wav");
let _ = std::fs::remove_file("/tmp/svc_test_frame.jpg");
println!("\n=== Test Results: {} passed, {} failed ===", pass, fail);
Ok(())
}
// ---- Report ----
fn cmd_report() -> Result<()> {
println!("=== Momentry Service Report ===");
println!("Generated: {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"));
println!();
// 1. Source status
println!("## 1. Source Code");
let src_dir = Path::new(SERVICE_SRC);
if src_dir.exists() {
let size = format_dir_size(src_dir);
println!(" Path: {} ({})", SERVICE_SRC, size);
for entry in fs::read_dir(src_dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
let meta = entry.metadata()?;
let icon = if meta.is_dir() { "📁" } else { "📄" };
println!(" {} {}", icon, name);
}
} else {
println!(" ❌ Source directory not found");
}
// 2. Binary status
println!("\n## 2. Binaries");
let binaries = [
("cmake", &format!("{}/bin/cmake", PREFIX)),
("python3.11", &format!("{}/.pyenv/versions/3.11.15/bin/python3.11", PREFIX)),
("ffmpeg", &format!("{}/ffmpeg_build/bin/ffmpeg", PREFIX)),
("ffprobe", &format!("{}/ffmpeg_build/bin/ffprobe", PREFIX)),
("redis-server", &format!("{}/redis/bin/redis-server", PREFIX)),
("postgres", &format!("{}/pgsql/18.3/bin/postgres", PREFIX)),
("llama-server", &format!("{}/llama/bin/llama-server", PREFIX)),
("libreoffice", &format!("{}/libreoffice/bin/soffice", PREFIX)),
];
for (name, path) in &binaries {
let status = if Path::new(path).exists() {
let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
format!("{} ({})", "", format_bytes(size))
} else {
"".to_string()
};
println!(" {} {}", status, name);
}
// 3. Running services
println!("\n## 3. Running Services");
let procs = [
("PostgreSQL", "postgres"),
("Redis", "redis-server"),
("Qdrant", "qdrant"),
("llama.cpp", "llama-server"),
("EmbeddingGemma", "embeddinggemma"),
("Playground", "momentry_playground.*server"),
("Worker", "momentry_playground.*worker"),
];
for (name, pattern) in &procs {
let output = Command::new("pgrep").args(["-f", pattern]).output();
match output {
Ok(o) if o.status.success() => {
let pids = String::from_utf8_lossy(&o.stdout).trim().to_string();
println!(" 🟢 {} (PID: {})", name, pids.replace('\n', ", "));
}
_ => println!("{} (not running)", name),
}
}
// 4. Ports
println!("\n## 4. Port Status");
let ports = [(3003, "Playground"), (5432, "PostgreSQL"), (6379, "Redis"), (6333, "Qdrant"), (8082, "LLM"), (11436, "Embedding")];
for (port, name) in &ports {
let output = Command::new("lsof").args(["-i", &format!(":{}", port)]).output();
match output {
Ok(o) if o.status.success() => println!(" 🟢 :{} ({})", port, name),
_ => println!(" ⚪ :{} ({})", port, name),
}
}
// 5. Summary
println!("\n## 5. Quick Check");
println!(" {}", "".repeat(60));
println!(" source → release/system/v1.0/services/src/ (336MB)");
println!(" build → bash install_services.sh");
println!(" install → {}", PREFIX);
println!(" config → service config all (view configs)");
println!(" launch → service launch generate (create plists)");
println!(" launch → service launch load (start all)");
println!(" env → service env -o .env.development");
println!(" test → service test (verify all binaries)");
Ok(())
}
fn format_bytes(bytes: u64) -> String {
if bytes > 1024 * 1024 * 1024 { format!("{:.1}GB", bytes as f64 / 1_073_741_824.0) }
else if bytes > 1024 * 1024 { format!("{:.0}MB", bytes as f64 / 1_048_576.0) }
else if bytes > 1024 { format!("{:.0}KB", bytes as f64 / 1024.0) }
else { format!("{}B", bytes) }
}
fn format_dir_size(path: &Path) -> String {
let output = Command::new("du").args(["-sh", path.to_str().unwrap()]).output();
match output {
Ok(o) if o.status.success() => {
let s = String::from_utf8_lossy(&o.stdout);
s.split_whitespace().next().unwrap_or("?").to_string()
}
_ => "?".to_string(),
}
}
// ---- Main ----
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Source { action } => match action {
SourceAction::List => cmd_source_list()?,
SourceAction::Verify => cmd_source_verify()?,
SourceAction::Download { name } => {
println!("Downloading: {} (use install_services.sh for full download)", name);
println!("Source URLs:");
println!(" ffmpeg: https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz");
println!(" redis: https://download.redis.io/releases/redis-7.4.3.tar.gz");
println!(" postgres: https://ftp.postgresql.org/pub/source/v18.3/postgresql-18.3.tar.gz");
println!(" x264: git clone https://code.videolan.org/videolan/x264.git");
println!(" freetype: https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.gz");
println!(" pyenv: git clone https://github.com/pyenv/pyenv.git");
println!(" cmake: https://github.com/Kitware/CMake/releases");
println!(" llama: git clone https://github.com/ggml-org/llama.cpp.git");
}
},
Commands::Build { service } => cmd_build(&service)?,
Commands::Install { service } => cmd_install(&service)?,
Commands::Config { service } => cmd_config(&service)?,
Commands::Launch { action } => match action {
LaunchAction::Generate => cmd_launch_generate()?,
LaunchAction::Load => cmd_launch_load()?,
LaunchAction::Unload => cmd_launch_unload()?,
LaunchAction::Status => cmd_launch_status()?,
},
Commands::Env { output } => cmd_env(&output)?,
Commands::Test => cmd_test()?,
Commands::Report => cmd_report()?,
}
Ok(())
}