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:
618
src/bin/release.rs
Normal file
618
src/bin/release.rs
Normal 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
853
src/bin/service.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user