-
Notifications
You must be signed in to change notification settings - Fork 14
feat(cli): expose soar to frontends with JSON output and a plugin manifest #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
7989560
feat(events): emit the event stream as json lines
QaidVoid 9f59b04
feat(cli): report list, search, query and info as json
QaidVoid 881b633
feat(cli): describe how to drive soar with plugin-manifest
QaidVoid e8d0395
fix(cli): keep a json answer alone on stdout
QaidVoid ff79c00
feat(cli): name a package by its family in the manifest
QaidVoid 6b119f3
feat(cli): answer updates, repos, paths and diffs as json
QaidVoid 8db4ef2
feat(events): say what applying the configuration did
QaidVoid a5d0bb2
feat(cli): describe soar's settings in the manifest
QaidVoid 8b6c2f9
fix(db): wait for a busy database instead of failing
QaidVoid 35d916f
fix(cli): exit non-zero when a command fails
QaidVoid 6733153
feat(cli): report the stage each operation reaches
QaidVoid 46d9a5e
feat(cli): report the build date and richer package fields
QaidVoid d4b9502
feat(cli): report the package type in query output
QaidVoid 1a4e1ae
feat(cli): map the source url in the manifest
QaidVoid e82c983
refactor(cli): drop the unused pkg_id from json output
QaidVoid ef7ac1d
docs(registry): stop listing pkg_id as a required field
QaidVoid 916c3b3
feat(cli): describe how soar acts system wide
QaidVoid 9be1e21
feat(cli): report who maintains a package
QaidVoid a226f91
refactor(cli): tidy the json output pass
PitamberTiwari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| //! The shapes `--json` reports for the query commands. | ||
| //! | ||
| //! Written out rather than derived from the internal models: what a caller can | ||
| //! read is a contract, and the models carry fields meant for the installer. | ||
| //! Adding a field here is safe; a model gaining one should not change output. | ||
|
|
||
| use serde::Serialize; | ||
| use soar_config::repository::Repository; | ||
| use soar_core::{ | ||
| database::models::{InstalledPackage, Package}, | ||
| package::install::InstallTarget, | ||
| }; | ||
| use soar_operations::{ApplyDiff, InstalledEntry, PackageListEntry, SearchEntry, UpdateInfo}; | ||
|
|
||
| /// A package as published by a repository. | ||
| #[derive(Serialize)] | ||
| pub struct PackageJson { | ||
| pub name: String, | ||
| pub family: Option<String>, | ||
| pub repo: String, | ||
| pub version: String, | ||
| pub description: String, | ||
| pub pkg_type: Option<String>, | ||
| pub size: Option<u64>, | ||
| pub installed: bool, | ||
| /// Other versions the repository publishes, newest first. | ||
| pub other_versions: Vec<String>, | ||
| } | ||
|
|
||
| impl PackageJson { | ||
| fn new(package: &Package, installed: bool, other_versions: Vec<String>) -> Self { | ||
| Self { | ||
| name: package.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: package.repo_name.clone(), | ||
| version: package.version.clone(), | ||
| description: package.description.clone(), | ||
| pkg_type: package.pkg_type.clone(), | ||
| size: package.ghcr_size.or(package.size), | ||
| installed, | ||
| other_versions, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<&PackageListEntry> for PackageJson { | ||
| fn from(entry: &PackageListEntry) -> Self { | ||
| Self::new( | ||
| &entry.package, | ||
| entry.installed, | ||
| entry.other_versions.clone(), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| impl From<&SearchEntry> for PackageJson { | ||
| fn from(entry: &SearchEntry) -> Self { | ||
| Self::new( | ||
| &entry.package, | ||
| entry.installed, | ||
| entry.other_versions.clone(), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /// A package installed on this system. | ||
| #[derive(Serialize)] | ||
| pub struct InstalledJson { | ||
| pub name: String, | ||
| pub family: Option<String>, | ||
| pub repo: String, | ||
| pub version: String, | ||
| pub pkg_type: Option<String>, | ||
| pub installed_path: String, | ||
| pub installed_date: String, | ||
| /// Size on disk, which is not the download size. | ||
| pub disk_size: u64, | ||
| pub pinned: bool, | ||
| /// False when the install did not finish. | ||
| pub healthy: bool, | ||
| } | ||
|
|
||
| impl From<&InstalledEntry> for InstalledJson { | ||
| fn from(entry: &InstalledEntry) -> Self { | ||
| let package: &InstalledPackage = &entry.package; | ||
| Self { | ||
| name: package.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: package.repo_name.clone(), | ||
| version: package.version.clone(), | ||
| pkg_type: package.pkg_type.clone(), | ||
| installed_path: package.installed_path.clone(), | ||
| installed_date: package.installed_date.clone(), | ||
| disk_size: entry.disk_size, | ||
| pinned: package.pinned, | ||
| healthy: entry.is_healthy, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Everything known about one package, as `query` reports it. | ||
| #[derive(Serialize)] | ||
| pub struct PackageDetailJson { | ||
| pub name: String, | ||
| pub family: Option<String>, | ||
| pub repo: String, | ||
| pub version: String, | ||
| pub description: String, | ||
| pub pkg_type: Option<String>, | ||
| pub size: Option<u64>, | ||
| /// blake3, as a download is verified against. | ||
| pub checksum: Option<String>, | ||
| pub homepages: Vec<String>, | ||
| pub source_urls: Vec<String>, | ||
| pub licenses: Vec<String>, | ||
| pub categories: Vec<String>, | ||
| pub notes: Vec<String>, | ||
| pub download_url: String, | ||
| pub build_date: Option<String>, | ||
| /// Formatted for display. | ||
| pub maintainers: Vec<String>, | ||
| } | ||
|
|
||
| impl From<&Package> for PackageDetailJson { | ||
| fn from(package: &Package) -> Self { | ||
| Self { | ||
| name: package.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: package.repo_name.clone(), | ||
| version: package.version.clone(), | ||
| description: package.description.clone(), | ||
| pkg_type: package.pkg_type.clone(), | ||
| size: package.ghcr_size.or(package.size), | ||
| checksum: package.bsum.clone(), | ||
| homepages: package.homepages.clone().unwrap_or_default(), | ||
| source_urls: package.source_urls.clone().unwrap_or_default(), | ||
| licenses: package.licenses.clone().unwrap_or_default(), | ||
| categories: package.categories.clone().unwrap_or_default(), | ||
| notes: package.notes.clone().unwrap_or_default(), | ||
| download_url: package.download_url.clone(), | ||
| build_date: package.build_date.clone(), | ||
| maintainers: package | ||
| .maintainers | ||
| .as_ref() | ||
| .map(|all| all.iter().map(ToString::to_string).collect()) | ||
| .unwrap_or_default(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// A package with a newer version waiting for it. | ||
| #[derive(Serialize)] | ||
| pub struct UpdateJson { | ||
| pub name: String, | ||
| pub family: Option<String>, | ||
| pub repo: String, | ||
| pub current_version: String, | ||
| pub new_version: String, | ||
| pub size: Option<u64>, | ||
| } | ||
|
|
||
| impl From<&UpdateInfo> for UpdateJson { | ||
| fn from(update: &UpdateInfo) -> Self { | ||
| let package = &update.target.package; | ||
| Self { | ||
| name: update.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: update.repo_name.clone(), | ||
| current_version: update.current_version.clone(), | ||
| new_version: update.new_version.clone(), | ||
| size: package.ghcr_size.or(package.size), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// A repository soar is configured to read. | ||
| #[derive(Serialize)] | ||
| pub struct RepositoryJson { | ||
| pub name: String, | ||
| pub url: String, | ||
| pub enabled: bool, | ||
| pub signature_verification: bool, | ||
| pub desktop_integration: bool, | ||
| } | ||
|
|
||
| impl From<&Repository> for RepositoryJson { | ||
| fn from(repo: &Repository) -> Self { | ||
| Self { | ||
| name: repo.name.clone(), | ||
| url: repo.url.clone(), | ||
| enabled: repo.is_enabled(), | ||
| signature_verification: repo.signature_verification.unwrap_or(false), | ||
| desktop_integration: repo.desktop_integration.unwrap_or(false), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Where soar keeps its files, so a frontend can read and write the same ones. | ||
| #[derive(Serialize)] | ||
| pub struct EnvJson { | ||
| pub config: String, | ||
| pub packages_config: String, | ||
| pub bin: String, | ||
| pub db: String, | ||
| pub cache: String, | ||
| pub packages: String, | ||
| pub repositories: String, | ||
| } | ||
|
|
||
| /// One package the declarative configuration would change. | ||
| #[derive(Serialize)] | ||
| pub struct ApplyChangeJson { | ||
| pub name: String, | ||
| pub family: Option<String>, | ||
| pub repo: String, | ||
| pub version: String, | ||
| /// The version on disk now, for a package being replaced. | ||
| pub current_version: Option<String>, | ||
| } | ||
|
|
||
| /// What applying the declarative configuration would do. | ||
| #[derive(Serialize)] | ||
| pub struct ApplyDiffJson { | ||
| pub to_install: Vec<ApplyChangeJson>, | ||
| pub to_update: Vec<ApplyChangeJson>, | ||
| pub to_remove: Vec<ApplyChangeJson>, | ||
| pub in_sync: Vec<String>, | ||
| pub not_found: Vec<String>, | ||
| } | ||
|
|
||
| impl ApplyDiffJson { | ||
| pub fn new(diff: &ApplyDiff) -> Self { | ||
| let change = |target: &InstallTarget| { | ||
| let package = &target.package; | ||
| ApplyChangeJson { | ||
| name: package.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: package.repo_name.clone(), | ||
| version: package.version.clone(), | ||
| current_version: target.existing_install.as_ref().map(|e| e.version.clone()), | ||
| } | ||
| }; | ||
|
|
||
| Self { | ||
| to_install: diff.to_install.iter().map(|(_, t)| change(t)).collect(), | ||
| to_update: diff.to_update.iter().map(|(_, t)| change(t)).collect(), | ||
| to_remove: diff | ||
| .to_remove | ||
| .iter() | ||
| .map(|package| { | ||
| ApplyChangeJson { | ||
| name: package.pkg_name.clone(), | ||
| family: package.pkg_family.clone(), | ||
| repo: package.repo_name.clone(), | ||
| version: package.version.clone(), | ||
| current_version: Some(package.version.clone()), | ||
| } | ||
| }) | ||
| .collect(), | ||
| in_sync: diff.in_sync.clone(), | ||
| not_found: diff.not_found.clone(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Wraps a listing so fields can be added without changing the shape. | ||
| #[derive(Serialize)] | ||
| pub struct Listing<T: Serialize> { | ||
| pub items: Vec<T>, | ||
| pub total: usize, | ||
| } | ||
|
|
||
| impl<T: Serialize> Listing<T> { | ||
| pub fn new(items: Vec<T>, total: usize) -> Self { | ||
| Self { | ||
| items, | ||
| total, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Write a result to stdout as a single JSON document. | ||
| pub fn emit<T: Serialize>(value: &T) { | ||
| if let Ok(json) = serde_json::to_string(value) { | ||
| println!("{json}"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Include pending version updates in the JSON diff.
Line 41 emits
ApplyDiffJson, but that model omitspending_version_updates.execute_applychanges those declared versions before package operations. A frontend can therefore report a no-op when apply will modifypackages.toml.Add the pending version updates to
ApplyDiffJsonand the emitted schema.🤖 Prompt for AI Agents
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep stdout valid NDJSON during confirmation.
When JSON mode runs without
--yes, the normal apply path reaches the directprint!confirmation prompt. That plain-text prompt corrupts the event stream on stdout.Write interactive prompts to stderr, or reject interactive JSON mode and require
--yes.🤖 Prompt for AI Agents