Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f3fb8e1
build: require compak 0.2 for single-file compression
QaidVoid Jul 28, 2026
2798d45
feat(registry): read the declarative index format
QaidVoid Jul 28, 2026
1d3e9ac
feat(db): store binaries and side files
QaidVoid Jul 28, 2026
6a0ad6b
fix(dl): extract by content, not declared type
QaidVoid Jul 28, 2026
5d96b13
feat(core): install side files and fix extraction
QaidVoid Jul 28, 2026
fa89bd4
fix(operations): resolve binaries by path before name
QaidVoid Jul 28, 2026
04d44b1
fix(cli): report packages rejected during import
QaidVoid Jul 28, 2026
6557122
feat: select variants by family, deprecate pkg_id
QaidVoid Jul 28, 2026
38ef337
fix(cli): surface log events instead of dropping them
QaidVoid Jul 28, 2026
90f2981
feat: track package family for installed packages
QaidVoid Jul 29, 2026
2b589ee
feat: allow packages without an id
QaidVoid Jul 29, 2026
ef48f06
feat(db): stop inventing package ids on import
QaidVoid Jul 29, 2026
66c3529
fix(remove): restore write bits before deleting a package
QaidVoid Jul 29, 2026
fa73bdb
fix: match installed packages by name, not by id
QaidVoid Jul 29, 2026
450b8da
fix(update): order versions by segment, not by string
QaidVoid Jul 29, 2026
9bd5d5f
feat: declare an index format version
QaidVoid Jul 29, 2026
a1af52b
docs(install): explain why licences carry no hash
QaidVoid Jul 29, 2026
4715fb3
fmt
QaidVoid Jul 29, 2026
a72ee19
refactor(db): collapse the new migrations into one per database
QaidVoid Jul 29, 2026
b048a17
fix(url): keep the derived id when building a package
QaidVoid Jul 29, 2026
61bf815
fix: repair tests and lints left by the pkg_id removal
QaidVoid Jul 29, 2026
203b5c0
feat(config): deprecate install_patterns
QaidVoid Jul 29, 2026
1ad8b75
fix: scope package lookups by identity, not by a missing id
QaidVoid Jul 29, 2026
614cb70
fix(db): refuse a downgrade instead of deleting id-less rows
QaidVoid Jul 29, 2026
cd0513e
fix(db): let the metadata cache clear on downgrade
QaidVoid Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ blake3 = { version = "1.8.5", features = ["mmap"] }
clap = { version = "4.6.1", features = ["cargo", "derive"] }
clap_complete = "4.6.5"
chrono = "0.4"
compak = "0.1.2"
compak = "0.2.0"
diesel = { version = "2.3.10", features = [
"64-column-tables",
"returning_clauses_for_sqlite_3_35",
Expand Down
18 changes: 3 additions & 15 deletions crates/soar-cli/src/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,7 @@ fn display_diff(diff: &ApplyDiff, prune: bool) {
let pkg = &target.package;
builder.push_record([
format!("{}", Colored(Green, icon_or("+", "+"))),
format!(
"{}#{}",
Colored(Blue, &pkg.pkg_name),
Colored(Cyan, &pkg.pkg_id)
),
format!("{}", Colored(Blue, &pkg.pkg_name),),
format!("{}", Colored(Green, &pkg.version)),
format!("{}", Colored(Magenta, &pkg.repo_name)),
]);
Expand All @@ -105,11 +101,7 @@ fn display_diff(diff: &ApplyDiff, prune: bool) {
.map_or("?".to_string(), |e| e.version.clone());
builder.push_record([
format!("{}", Colored(Yellow, icon_or("~", "~"))),
format!(
"{}#{}",
Colored(Blue, &pkg.pkg_name),
Colored(Cyan, &pkg.pkg_id)
),
format!("{}", Colored(Blue, &pkg.pkg_name),),
format!(
"{} -> {}",
Colored(Red, &old_version),
Expand All @@ -123,11 +115,7 @@ fn display_diff(diff: &ApplyDiff, prune: bool) {
for pkg in &diff.to_remove {
builder.push_record([
format!("{}", Colored(Red, icon_or("-", "-"))),
format!(
"{}#{}",
Colored(Blue, &pkg.pkg_name),
Colored(Cyan, &pkg.pkg_id)
),
format!("{}", Colored(Blue, &pkg.pkg_name),),
format!("{}", Colored(Yellow, &pkg.version)),
format!("{}", Colored(Magenta, &pkg.repo_name)),
]);
Expand Down
7 changes: 3 additions & 4 deletions crates/soar-cli/src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ pub async fn handle_direct_downloads(
MetadataRepository::find_filtered(
conn,
query.name.as_deref(),
None,
query.pkg_id.as_deref(),
None,
None,
Expand All @@ -173,6 +174,7 @@ pub async fn handle_direct_downloads(
conn,
query.name.as_deref(),
query.pkg_id.as_deref(),
query.family.as_deref(),
None,
None,
None,
Expand Down Expand Up @@ -210,10 +212,7 @@ pub async fn handle_direct_downloads(

let package = package.resolve(query.version.as_deref());

info!(
"Downloading package: {}#{}",
package.pkg_name, package.pkg_id
);
info!("Downloading package: {}", package.pkg_name);
if let Some(ref url) = package.ghcr_blob {
let mut dl = OciDownload::new(url.as_str()).overwrite(ctx.get_overwrite_mode());

Expand Down
16 changes: 5 additions & 11 deletions crates/soar-cli/src/health.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nu_ansi_term::Color::{Blue, Cyan, Green, Red, Yellow};
use nu_ansi_term::Color::{Blue, Green, Red, Yellow};
use soar_core::SoarResult;
use soar_operations::{health, SoarContext};
use tabled::{
Expand Down Expand Up @@ -61,10 +61,9 @@ pub async fn display_health(ctx: &SoarContext) -> SoarResult<()> {
info!("\nBroken packages:");
for pkg in &report.broken_packages {
info!(
" {} {}#{}: {}",
" {} {}: {}",
Icons::ARROW,
Colored(Blue, &pkg.pkg_name),
Colored(Cyan, &pkg.pkg_id),
Colored(Yellow, &pkg.installed_path)
);
}
Expand Down Expand Up @@ -94,16 +93,11 @@ pub async fn remove_broken_packages(ctx: &SoarContext) -> SoarResult<()> {
}

for removed in &report.removed {
info!("Removed {}#{}", removed.pkg_name, removed.pkg_id);
info!("Removed {}", removed.pkg_name);
}

for failed in &report.failed {
tracing::error!(
"Failed to remove {}#{}: {}",
failed.pkg_name,
failed.pkg_id,
failed.error
);
tracing::error!("Failed to remove {}: {}", failed.pkg_name, failed.error);
}

if !report.removed.is_empty() && report.failed.is_empty() {
Expand All @@ -114,7 +108,7 @@ pub async fn remove_broken_packages(ctx: &SoarContext) -> SoarResult<()> {
report
.failed
.iter()
.map(|f| format!("{}#{}", f.pkg_name, f.pkg_id))
.map(|f| format!("{}", f.pkg_name))
.collect::<Vec<_>>()
.join(", ")
);
Expand Down
4 changes: 3 additions & 1 deletion crates/soar-cli/src/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn get_installed_path(
conn,
&package.repo_name,
&package.pkg_name,
&package.pkg_id,
package.pkg_id.as_deref(),
&package.version,
)
})?;
Expand All @@ -71,6 +71,7 @@ pub async fn inspect_log(package: &str, inspect_type: InspectType) -> SoarResult
conn,
query.name.as_deref(),
query.pkg_id.as_deref(),
query.family.as_deref(),
None,
None,
Some(SortDirection::Asc),
Expand All @@ -90,6 +91,7 @@ pub async fn inspect_log(package: &str, inspect_type: InspectType) -> SoarResult
conn,
query.name.as_deref(),
query.pkg_id.as_deref(),
query.family.as_deref(),
None,
None,
Some(SortDirection::Asc),
Expand Down
44 changes: 22 additions & 22 deletions crates/soar-cli/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ pub async fn install_packages(

if let Some(pkg) = pkg {
// Re-resolve with the specific selected package
let specific_query =
format!("{}#{}:{}", pkg.pkg_name, pkg.pkg_id, pkg.repo_name);
let specific_query = format!("{}:{}", pkg.pkg_name, pkg.repo_name);
let re_results =
install::resolve_packages(ctx, &[specific_query], &options).await?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
for r in re_results {
Expand All @@ -97,13 +96,13 @@ pub async fn install_packages(
}
ResolveResult::AlreadyInstalled {
pkg_name,
pkg_id,
repo_name,
version,
..
} => {
warn!(
"{}#{}:{} ({}) is already installed - skipping",
pkg_name, pkg_id, repo_name, version,
"{}:{} ({}) is already installed - skipping",
pkg_name, repo_name, version,
);
if !force {
info!("Hint: Use --force to reinstall, or --show to see other variants");
Expand Down Expand Up @@ -161,13 +160,13 @@ async fn install_with_show(
ResolveResult::Resolved(targets) => install_targets.extend(targets),
ResolveResult::AlreadyInstalled {
pkg_name,
pkg_id,
repo_name,
version,
..
} => {
warn!(
"{}#{}:{} ({}) is already installed - skipping",
pkg_name, pkg_id, repo_name, version,
"{}:{} ({}) is already installed - skipping",
pkg_name, repo_name, version,
);
if !force {
info!("Hint: Use --force to reinstall");
Expand Down Expand Up @@ -200,8 +199,7 @@ async fn install_with_show(
};

if let Some(pkg) = pkg {
let specific_query =
format!("{}#{}:{}", pkg.pkg_name, pkg.pkg_id, pkg.repo_name);
let specific_query = format!("{}:{}", pkg.pkg_name, pkg.repo_name);
let re_results =
install::resolve_packages(ctx, &[specific_query], options).await?;
for r in re_results {
Expand All @@ -221,13 +219,13 @@ async fn install_with_show(
}
ResolveResult::AlreadyInstalled {
pkg_name,
pkg_id,
repo_name,
version,
..
} => {
warn!(
"{}#{}:{} ({}) is already installed - skipping",
pkg_name, pkg_id, repo_name, version,
"{}:{} ({}) is already installed - skipping",
pkg_name, repo_name, version,
);
if !force {
info!(
Expand All @@ -249,6 +247,7 @@ async fn install_with_show(
None,
None,
None,
None,
Some(SortDirection::Asc),
)
})?
Expand All @@ -268,6 +267,7 @@ async fn install_with_show(
None,
None,
None,
None,
Some(SortDirection::Asc),
)?;
Ok(pkgs
Expand Down Expand Up @@ -317,7 +317,7 @@ async fn install_with_show(
)
})?
.into_iter()
.map(|p| (p.pkg_id, p.repo_name, p.version))
.map(|p| (p.pkg_id.unwrap_or_default(), p.repo_name, p.version))
.collect();

let pkg = select_package_interactively_with_installed(
Expand All @@ -337,7 +337,7 @@ async fn install_with_show(
conn,
Some(&pkg.repo_name),
Some(&pkg.pkg_name),
Some(&pkg.pkg_id),
pkg.pkg_id.as_deref(),
None,
None,
None,
Expand All @@ -352,9 +352,8 @@ async fn install_with_show(
if let Some(ref existing) = existing_install {
if existing.is_installed {
warn!(
"{}#{}:{} ({}) is already installed - {}",
"{}:{} ({}) is already installed - {}",
existing.pkg_name,
existing.pkg_id,
existing.repo_name,
existing.version,
if force { "reinstalling" } else { "skipping" }
Expand Down Expand Up @@ -402,10 +401,9 @@ fn display_install_report(report: &InstallReport, no_notes: bool) {

for info in &report.installed {
info!(
"\n{} {}#{}:{} [{}]",
"\n{} {}:{} [{}]",
icon_or(Icons::CHECK, "*"),
Colored(Blue, &info.pkg_name),
Colored(Cyan, &info.pkg_id),
Colored(Green, &info.repo_name),
Colored(Magenta, info.install_dir.display())
);
Expand All @@ -424,7 +422,9 @@ fn display_install_report(report: &InstallReport, no_notes: bool) {
}

if !no_notes {
if let Some(ref notes) = info.notes {
// Most packages have nothing to say, and an empty list would
// otherwise print a heading with no content under it.
if let Some(notes) = info.notes.as_ref().filter(|n| !n.is_empty()) {
info!(
" {} Notes:\n {}",
icon_or("📝", "-"),
Expand All @@ -436,8 +436,8 @@ fn display_install_report(report: &InstallReport, no_notes: bool) {

for err_info in &report.failed {
error!(
"Failed to install {}#{}: {}",
err_info.pkg_name, err_info.pkg_id, err_info.error
"Failed to install {}: {}",
err_info.pkg_name, err_info.error
);
}

Expand Down
23 changes: 19 additions & 4 deletions crates/soar-cli/src/json2db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use soar_db::{
connection::DbConnection, migration::DbType, repository::metadata::MetadataRepository,
};
use soar_registry::RemotePackage;
use tracing::info;
use tracing::{info, warn};

/// Converts JSON metadata file to SQLite database.
pub fn json_to_db(input_path: &str, output_path: &str, repo_name: Option<&str>) -> SoarResult<()> {
Expand All @@ -23,7 +23,7 @@ pub fn json_to_db(input_path: &str, output_path: &str, repo_name: Option<&str>)
let json_content = fs::read_to_string(input_path)
.with_context(|| format!("reading JSON metadata from {}", input_path))?;

let packages: Vec<RemotePackage> = serde_json::from_str(&json_content)
let packages: Vec<RemotePackage> = soar_registry::parse_index(json_content.as_bytes())
.map_err(|e| SoarError::Custom(format!("parsing JSON from {}: {}", input_path, e)))?;

info!(count = packages.len(), "Parsed JSON metadata");
Expand All @@ -49,11 +49,26 @@ pub fn json_to_db(input_path: &str, output_path: &str, repo_name: Option<&str>)
let mut conn = DbConnection::open(output_path, DbType::Metadata)
.map_err(|e| SoarError::Custom(format!("opening database: {}", e)))?;

MetadataRepository::import_packages(conn.conn(), &packages, repo_name)
// Packages with an unsafe pkg_name/pkg_id are skipped during import.
// Reporting success while writing nothing hides that entirely, which is
// how an empty pkg_id silently produced an empty database.
let imported = MetadataRepository::import_packages(conn.conn(), &packages, repo_name)
.map_err(|e| SoarError::Custom(format!("importing packages: {}", e)))?;

let skipped = packages.len() - imported;
if imported == 0 {
return Err(SoarError::Custom(format!(
"imported 0 of {} packages; every entry was rejected, most likely \
an unsafe or empty pkg_name/pkg_id",
packages.len()
)));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
if skipped > 0 {
warn!(skipped, "some packages were rejected during import");
}

info!(
count = packages.len(),
count = imported,
output = %output_path.display(),
"Successfully converted JSON to SQLite database"
);
Expand Down
Loading
Loading