From 733fbcba140ff23694cc845bfe4b0d87451fe513 Mon Sep 17 00:00:00 2001 From: Lorenzo Delgado Date: Fri, 19 Jun 2026 11:43:50 +0200 Subject: [PATCH] feat(ampup): add ampsql binary with best-effort install Treat ampd as the only required binary while installing ampctl and ampsql on a best-effort basis, so releases or source trees that omit them no longer fail the whole install. - Add `ampsql` to download, build-from-source, activation, and uninstall flows alongside `version_ampsql_path`/`active_ampsql_path` config helpers - Fetch release metadata once via `fetch_release_assets` and resolve each asset in-memory with `ReleaseAssets::resolve`, pairing tasks to assets without zipping parallel collections - Filter unavailable optional artifacts in `DownloadManager` with a warning; only `ampd` is required - Build `ampd`, `ampctl`, and `ampsql` separately so a missing optional crate is skipped rather than aborting the build - Symlink `ampctl`/`ampsql` only when their binaries exist and update docs and mock fixtures Signed-off-by: Lorenzo Delgado --- ampup/src/builder.rs | 126 ++++++++++++++++++++++------------ ampup/src/commands/install.rs | 6 +- ampup/src/config.rs | 10 +++ ampup/src/download_manager.rs | 88 +++++++++++++++++++++--- ampup/src/github.rs | 72 +++++++++++++------ ampup/src/install.rs | 16 ++++- ampup/src/tests/fixtures.rs | 21 +++++- ampup/src/tests/it_ampup.rs | 12 ++++ ampup/src/version_manager.rs | 35 +++++++--- docs/code/apps-cli.md | 8 +-- docs/features/app-ampup.md | 37 +++++----- 11 files changed, 317 insertions(+), 114 deletions(-) diff --git a/ampup/src/builder.rs b/ampup/src/builder.rs index d7872e9..ea82871 100644 --- a/ampup/src/builder.rs +++ b/ampup/src/builder.rs @@ -510,7 +510,7 @@ impl<'a> GitRepo<'a> { } } -/// Build and install the ampd and ampctl binaries +/// Build and install the ampd, ampctl, and ampsql binaries fn build_and_install( version_manager: &VersionManager, repo_path: &Path, @@ -519,30 +519,33 @@ fn build_and_install( ) -> Result<()> { check_command_exists("cargo")?; - ui::info!("Building ampd and ampctl"); + let jobs_str = jobs.map(|j| j.to_string()); - let mut args = vec!["build", "--release", "-p", "ampd", "-p", "ampctl"]; + let config = version_manager.config(); + + // Create version directory + let version_dir = config.versions_dir.join(version_label); + fs::create_dir_all(&version_dir).context("Failed to create version directory")?; + + // Build ampd (required) + ui::info!("Building ampd"); - let jobs_str; - if let Some(j) = jobs { - jobs_str = j.to_string(); - args.extend(["-j", &jobs_str]); + let mut ampd_args = vec!["build", "--release", "-p", "ampd"]; + if let Some(ref j) = jobs_str { + ampd_args.extend(["-j", j]); } - let status = Command::new("cargo") - .args(&args) + let ampd_status = Command::new("cargo") + .args(&d_args) .current_dir(repo_path) .status() .context("Failed to execute cargo build")?; - if !status.success() { + if !ampd_status.success() { return Err(BuildError::CargoBuildFailed.into()); } - // Find the built binaries let ampd_source = repo_path.join("target/release/ampd"); - let ampctl_source = repo_path.join("target/release/ampctl"); - if !ampd_source.exists() { return Err(BuildError::BinaryNotFound { path: ampd_source.clone(), @@ -550,19 +553,6 @@ fn build_and_install( .into()); } - if !ampctl_source.exists() { - return Err(BuildError::BinaryNotFound { - path: ampctl_source.clone(), - } - .into()); - } - - let config = version_manager.config(); - - // Create version directory - let version_dir = config.versions_dir.join(version_label); - fs::create_dir_all(&version_dir).context("Failed to create version directory")?; - // Copy ampd binary let ampd_dest = version_dir.join("ampd"); fs::copy(&d_source, &d_dest).context("Failed to copy ampd binary")?; @@ -578,29 +568,79 @@ fn build_and_install( .context("Failed to set executable permissions on ampd")?; } - // Copy ampctl binary - let ampctl_dest = version_dir.join("ampctl"); - fs::copy(&ctl_source, &ctl_dest).context("Failed to copy ampctl binary")?; + // Build ampctl + ui::info!("Building ampctl"); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&ctl_dest) - .context("Failed to get ampctl metadata")? - .permissions(); - perms.set_mode(0o755); - fs::set_permissions(&ctl_dest, perms) - .context("Failed to set executable permissions on ampctl")?; + let mut ampctl_args = vec!["build", "--release", "-p", "ampctl"]; + if let Some(ref j) = jobs_str { + ampctl_args.extend(["-j", j]); + } + + let ampctl_status = Command::new("cargo") + .args(&ctl_args) + .current_dir(repo_path) + .status() + .context("Failed to execute cargo build")?; + + let ampctl_source = repo_path.join("target/release/ampctl"); + if ampctl_status.success() && ampctl_source.exists() { + // Copy ampctl binary + let ampctl_dest = version_dir.join("ampctl"); + fs::copy(&ctl_source, &ctl_dest).context("Failed to copy ampctl binary")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&ctl_dest) + .context("Failed to get ampctl metadata")? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&ctl_dest, perms) + .context("Failed to set executable permissions on ampctl")?; + } + } else { + ui::warn!("Skipping ampctl: build did not produce a binary"); + } + + // Build ampsql + ui::info!("Building ampsql"); + + let mut ampsql_args = vec!["build", "--release", "-p", "ampsql"]; + if let Some(ref j) = jobs_str { + ampsql_args.extend(["-j", j]); + } + + let ampsql_status = Command::new("cargo") + .args(&sql_args) + .current_dir(repo_path) + .status() + .context("Failed to execute cargo build")?; + + let ampsql_source = repo_path.join("target/release/ampsql"); + if ampsql_status.success() && ampsql_source.exists() { + // Copy ampsql binary + let ampsql_dest = version_dir.join("ampsql"); + fs::copy(&sql_source, &sql_dest).context("Failed to copy ampsql binary")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&sql_dest) + .context("Failed to get ampsql metadata")? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&sql_dest, perms) + .context("Failed to set executable permissions on ampsql")?; + } + } else { + ui::warn!("Skipping ampsql: build did not produce a binary"); } // Activate this version version_manager.activate(version_label)?; - ui::success!( - "Built and installed ampd and ampctl {}", - ui::version(version_label) - ); - ui::detail!("Run 'ampd --version' and 'ampctl --version' to verify installation"); + ui::success!("Built and installed amp {}", ui::version(version_label)); + ui::detail!("Run 'ampd --version' to verify installation"); Ok(()) } diff --git a/ampup/src/commands/install.rs b/ampup/src/commands/install.rs index f4957f5..6d5ee29 100644 --- a/ampup/src/commands/install.rs +++ b/ampup/src/commands/install.rs @@ -51,7 +51,7 @@ pub async fn run( ui::info!("Switching to version {}", ui::version(&version)); crate::commands::use_version::switch_to_version(&version_manager, &version)?; ui::success!("Switched to version {}", ui::version(&version)); - ui::detail!("Run 'ampd --version' and 'ampctl --version' to verify installation"); + ui::detail!("Run 'ampd --version' to verify installation"); return Ok(()); } @@ -94,8 +94,8 @@ pub async fn run( .install_from_release(&version, platform, arch) .await?; - ui::success!("Installed ampd and ampctl {}", ui::version(&version)); - ui::detail!("Run 'ampd --version' and 'ampctl --version' to verify installation"); + ui::success!("Installed amp {}", ui::version(&version)); + ui::detail!("Run 'ampd --version' to verify installation"); Ok(()) } diff --git a/ampup/src/config.rs b/ampup/src/config.rs index b2cb06e..3b0be7d 100644 --- a/ampup/src/config.rs +++ b/ampup/src/config.rs @@ -93,6 +93,16 @@ impl Config { self.bin_dir.join("ampctl") } + /// Get the ampsql binary path for a specific version + pub fn version_ampsql_path(&self, version: &str) -> PathBuf { + self.versions_dir.join(version).join("ampsql") + } + + /// Get the active ampsql binary symlink path + pub fn active_ampsql_path(&self) -> PathBuf { + self.bin_dir.join("ampsql") + } + /// Ensure all required directories exist pub fn ensure_dirs(&self) -> Result<()> { fs::create_dir_all(&self.amp_dir).context("Failed to create amp directory")?; diff --git a/ampup/src/download_manager.rs b/ampup/src/download_manager.rs index d752afb..0a4c02b 100644 --- a/ampup/src/download_manager.rs +++ b/ampup/src/download_manager.rs @@ -10,6 +10,7 @@ use tokio::{sync::Semaphore, task::JoinSet}; use crate::{ github::{GitHubClient, ResolvedAsset}, progress::ProgressReporter, + ui, }; // --------------------------------------------------------------------------- @@ -22,6 +23,9 @@ pub struct DownloadTask { pub artifact_name: String, /// Destination filename inside the version directory (e.g., "ampd") pub dest_filename: String, + /// Whether the artifact may be absent from the release. Optional artifacts + /// missing from the release are skipped; required ones fail the install. + pub optional: bool, } /// Errors that occur during bounded-concurrent download operations. @@ -173,13 +177,24 @@ impl DownloadManager { version_dir: PathBuf, reporter: Arc, ) -> Result<()> { - // Resolve all asset metadata with a single API call so that each - // spawned task can download directly without re-fetching the release. - let asset_names: Vec<&str> = tasks.iter().map(|t| t.artifact_name.as_str()).collect(); - let resolved = self - .github - .resolve_release_assets(version, &asset_names) - .await?; + // Fetch release metadata once, then resolve each task's asset in the + // same iteration that owns the task. Pairing the task with its resolved + // asset structurally avoids zipping two parallel collections whose + // alignment would only be guaranteed by convention. + let release = self.github.fetch_release_assets(version).await?; + + // Drop optional artifacts the release does not provide; a missing + // required artifact fails fast via `resolve`. + let mut planned: Vec<(DownloadTask, ResolvedAsset)> = Vec::with_capacity(tasks.len()); + for task in tasks { + match release.resolve(&task.artifact_name, task.optional)? { + Some(asset) => planned.push((task, asset)), + None => ui::warn!( + "Skipping {}: not available in this release", + task.artifact_name + ), + } + } let parent = version_dir.parent().ok_or_else(|| { anyhow::anyhow!("version_dir has no parent: {}", version_dir.display()) @@ -189,13 +204,16 @@ impl DownloadManager { let staging_dir = tempfile::tempdir_in(parent).context("Failed to create staging directory")?; - let names: Vec = tasks.iter().map(|t| t.artifact_name.clone()).collect(); - reporter.set_total(tasks.len(), names); + let names: Vec = planned + .iter() + .map(|(task, _)| task.artifact_name.clone()) + .collect(); + reporter.set_total(planned.len(), names); let semaphore = Arc::new(Semaphore::new(self.max_concurrent)); let mut join_set: JoinSet> = JoinSet::new(); - for (task, asset) in tasks.into_iter().zip(resolved) { + for (task, asset) in planned { let github = self.github.clone(); let sem = semaphore.clone(); let staging_path = staging_dir.path().to_path_buf(); @@ -786,10 +804,12 @@ mod tests { DownloadTask { artifact_name: "ampd-linux-x86_64".to_string(), dest_filename: "ampd".to_string(), + optional: false, }, DownloadTask { artifact_name: "ampctl-linux-x86_64".to_string(), dest_filename: "ampctl".to_string(), + optional: false, }, ] } @@ -860,6 +880,52 @@ mod tests { ); } + /// A missing *optional* asset is skipped; required artifacts still install. + #[tokio::test] + async fn download_all_with_missing_optional_asset_skips_it() { + //* Given — release only contains ampd; ampctl is optional and absent + let fixture = TestFixture::new( + &["ampd-linux-x86_64"], + vec![Route::ok( + "download/ampd-linux-x86_64", + b"fake-ampd-binary".to_vec(), + )], + 4, + ) + .await; + + let tasks = vec![ + DownloadTask { + artifact_name: "ampd-linux-x86_64".to_string(), + dest_filename: "ampd".to_string(), + optional: false, + }, + DownloadTask { + artifact_name: "ampctl-linux-x86_64".to_string(), + dest_filename: "ampctl".to_string(), + optional: true, + }, + ]; + + //* When + let result = fixture.download(tasks).await; + + //* Then + assert!( + result.is_ok(), + "a missing optional asset should not fail the install: {:?}", + result.err() + ); + assert!( + fixture.version_dir.join("ampd").exists(), + "required ampd should be installed" + ); + assert!( + !fixture.version_dir.join("ampctl").exists(), + "absent optional ampctl should be skipped, not installed" + ); + } + /// `-j 1` (sequential) mode still produces a correct install. #[tokio::test] async fn download_all_with_sequential_mode_succeeds() { @@ -913,6 +979,7 @@ mod tests { let tasks = vec![DownloadTask { artifact_name: "ampd-linux-x86_64".to_string(), dest_filename: "ampd".to_string(), + optional: false, }]; //* When @@ -952,6 +1019,7 @@ mod tests { let tasks = vec![DownloadTask { artifact_name: "ampd-linux-x86_64".to_string(), dest_filename: "ampd".to_string(), + optional: false, }]; //* When diff --git a/ampup/src/github.rs b/ampup/src/github.rs index 97944f9..b31859c 100644 --- a/ampup/src/github.rs +++ b/ampup/src/github.rs @@ -172,7 +172,7 @@ impl std::error::Error for GitHubError {} /// A release asset resolved from GitHub metadata, ready to download. /// -/// Produced by [`GitHubClient::resolve_release_assets`] and consumed by +/// Produced by [`ReleaseAssets::resolve`] and consumed by /// [`GitHubClient::download_resolved_asset`]. This allows fetching release /// metadata once and then downloading multiple assets without redundant API /// calls. @@ -186,6 +186,45 @@ pub struct ResolvedAsset { pub url: String, } +/// The assets of a single fetched release. +/// +/// Produced by [`GitHubClient::fetch_release_assets`] so that release metadata +/// is fetched once and individual assets are then resolved in memory via +/// [`ReleaseAssets::resolve`] — no redundant API calls, and each caller pairs a +/// resolved asset with its own request directly instead of relying on the +/// positional alignment of two parallel collections. +pub struct ReleaseAssets { + repo: String, + version: String, + assets: Vec, +} + +impl ReleaseAssets { + /// Resolve a single asset by name against this release. + /// + /// Returns `Ok(Some(_))` when the asset is present, `Ok(None)` when an + /// *optional* asset is absent, and `Err(GitHubError::AssetNotFound)` when a + /// *required* asset (`optional == false`) is missing. + pub fn resolve(&self, name: &str, optional: bool) -> anyhow::Result> { + match self.assets.iter().find(|a| a.name == name) { + Some(asset) => Ok(Some(ResolvedAsset { + id: asset.id, + name: asset.name.clone(), + url: asset.url.clone(), + })), + // Optional artifacts may be missing from a release; skip them. + None if optional => Ok(None), + None => Err(GitHubError::AssetNotFound { + repo: self.repo.clone(), + asset_name: name.to_string(), + version: self.version.clone(), + available_assets: self.assets.iter().map(|a| a.name.clone()).collect(), + } + .into()), + } + } +} + #[derive(Debug, Deserialize)] struct Release { #[serde(rename = "tag_name")] @@ -320,29 +359,18 @@ impl GitHubClient { }) } - /// Resolve multiple asset names from a single release, fetching the release - /// metadata only once. + /// Fetch a release's asset metadata with a single API call. /// - /// Returns a `ResolvedAsset` for each requested name. Fails with - /// `GitHubError::AssetNotFound` on the first name that does not match any - /// asset in the release. - pub async fn resolve_release_assets( - &self, - version: &str, - asset_names: &[&str], - ) -> Result> { + /// The returned [`ReleaseAssets`] resolves individual assets in memory via + /// [`ReleaseAssets::resolve`], so callers can fetch once and resolve many + /// without re-hitting the API and without aligning parallel collections. + pub async fn fetch_release_assets(&self, version: &str) -> anyhow::Result { let release = self.get_tagged_release(version).await?; - - let mut resolved = Vec::with_capacity(asset_names.len()); - for &name in asset_names { - let asset = self.find_asset(&release, name, version)?; - resolved.push(ResolvedAsset { - id: asset.id, - name: asset.name.clone(), - url: asset.url.clone(), - }); - } - Ok(resolved) + Ok(ReleaseAssets { + repo: self.repo.clone(), + version: version.to_string(), + assets: release.assets, + }) } /// Download a previously resolved asset without re-fetching release diff --git a/ampup/src/install.rs b/ampup/src/install.rs index 0b4f48e..afdc438 100644 --- a/ampup/src/install.rs +++ b/ampup/src/install.rs @@ -20,7 +20,8 @@ impl Installer { } } - /// Install ampd and ampctl from a GitHub release. + /// Install ampd (required) plus the optional ampctl and ampsql binaries + /// from a GitHub release. pub async fn install_from_release( &self, version: &str, @@ -31,22 +32,31 @@ impl Installer { let ampd_artifact = format!("ampd-{}-{}", platform.as_str(), arch.as_str()); let ampctl_artifact = format!("ampctl-{}-{}", platform.as_str(), arch.as_str()); + let ampsql_artifact = format!("ampsql-{}-{}", platform.as_str(), arch.as_str()); ui::info!( - "Downloading {} ({}, {})", + "Downloading {} ({}, {}, {})", ui::version(version), ampd_artifact, - ampctl_artifact + ampctl_artifact, + ampsql_artifact ); let tasks = vec![ DownloadTask { artifact_name: ampd_artifact, dest_filename: "ampd".to_string(), + optional: false, }, DownloadTask { artifact_name: ampctl_artifact, dest_filename: "ampctl".to_string(), + optional: true, + }, + DownloadTask { + artifact_name: ampsql_artifact, + dest_filename: "ampsql".to_string(), + optional: true, }, ]; diff --git a/ampup/src/tests/fixtures.rs b/ampup/src/tests/fixtures.rs index fcc8dc5..137839c 100644 --- a/ampup/src/tests/fixtures.rs +++ b/ampup/src/tests/fixtures.rs @@ -73,11 +73,11 @@ impl TempInstallDir { } } -/// Helper for creating mock ampd and ampctl binaries for testing. +/// Helper for creating mock ampd, ampctl, and ampsql binaries for testing. pub struct MockBinary; impl MockBinary { - /// Create mock ampd and ampctl binaries for a specific version. + /// Create mock ampd, ampctl, and ampsql binaries for a specific version. pub fn create(temp: &TempInstallDir, version: &str) -> Result<()> { let version_dir = temp.version_dir(version); fs::create_dir_all(&version_dir) @@ -117,6 +117,23 @@ impl MockBinary { .context("Failed to set executable permissions on ampctl")?; } + // Create mock ampsql binary + let ampsql_path = version_dir.join("ampsql"); + let ampsql_script = format!("#!/bin/sh\necho 'ampsql {}'", version); + fs::write(&sql_path, ampsql_script) + .with_context(|| format!("Failed to write mock ampsql binary for {}", version))?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&sql_path) + .context("Failed to get ampsql metadata")? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&sql_path, perms) + .context("Failed to set executable permissions on ampsql")?; + } + Ok(()) } } diff --git a/ampup/src/tests/it_ampup.rs b/ampup/src/tests/it_ampup.rs index b08ffc4..df190c4 100644 --- a/ampup/src/tests/it_ampup.rs +++ b/ampup/src/tests/it_ampup.rs @@ -291,6 +291,18 @@ async fn build_from_local_path_with_custom_name() -> Result<()> { fs::set_permissions(&mock_ampctl, perms)?; } + // Create mock ampsql binary + let mock_ampsql = target_dir.join("ampsql"); + fs::write(&mock_ampsql, "#!/bin/sh\necho 'ampsql test-version'")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&mock_ampsql)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&mock_ampsql, perms)?; + } + // Mock cargo by creating a fake cargo script let mock_cargo_dir = TempDir::new()?; let mock_cargo = mock_cargo_dir.path().join("cargo"); diff --git a/ampup/src/version_manager.rs b/ampup/src/version_manager.rs index dfef6a2..db975aa 100644 --- a/ampup/src/version_manager.rs +++ b/ampup/src/version_manager.rs @@ -110,14 +110,6 @@ impl VersionManager { .into()); } - let ampctl_binary_path = self.config.version_ampctl_path(version); - if !ampctl_binary_path.exists() { - return Err(VersionError::BinaryNotFound { - version: version.to_string(), - } - .into()); - } - // Handle ampd symlink let ampd_active_path = self.config.active_binary_path(); if ampd_active_path.exists() || ampd_active_path.is_symlink() { @@ -125,14 +117,29 @@ impl VersionManager { } symlink(&d_binary_path, &d_active_path).context("Failed to create ampd symlink")?; - // Handle ampctl symlink + // Handle ampctl symlink, replacing any stale symlink from a previous version + let ampctl_binary_path = self.config.version_ampctl_path(version); let ampctl_active_path = self.config.active_ampctl_path(); if ampctl_active_path.exists() || ampctl_active_path.is_symlink() { fs::remove_file(&ctl_active_path) .context("Failed to remove existing ampctl symlink")?; } - symlink(&ctl_binary_path, &ctl_active_path) - .context("Failed to create ampctl symlink")?; + if ampctl_binary_path.exists() { + symlink(&ctl_binary_path, &ctl_active_path) + .context("Failed to create ampctl symlink")?; + } + + // Handle ampsql symlink, replacing any stale symlink from a previous version + let ampsql_binary_path = self.config.version_ampsql_path(version); + let ampsql_active_path = self.config.active_ampsql_path(); + if ampsql_active_path.exists() || ampsql_active_path.is_symlink() { + fs::remove_file(&sql_active_path) + .context("Failed to remove existing ampsql symlink")?; + } + if ampsql_binary_path.exists() { + symlink(&sql_binary_path, &sql_active_path) + .context("Failed to create ampsql symlink")?; + } // Update current version file self.config.set_current_version(version)?; @@ -175,6 +182,12 @@ impl VersionManager { if ampctl_active_path.exists() || ampctl_active_path.is_symlink() { fs::remove_file(&ctl_active_path).context("Failed to remove ampctl symlink")?; } + + // Remove the ampsql symlink + let ampsql_active_path = self.config.active_ampsql_path(); + if ampsql_active_path.exists() || ampsql_active_path.is_symlink() { + fs::remove_file(&sql_active_path).context("Failed to remove ampsql symlink")?; + } } Ok(()) diff --git a/docs/code/apps-cli.md b/docs/code/apps-cli.md index 724f8e5..89e1d1d 100644 --- a/docs/code/apps-cli.md +++ b/docs/code/apps-cli.md @@ -90,7 +90,7 @@ All macros are defined in `ampup/src/ui.rs`: use crate::ui; // Success message -ui::success!("Installed ampd and ampctl {}", ui::version(&version)); +ui::success!("Installed amp {}", ui::version(&version)); // Info message ui::info!("Fetching latest version"); @@ -226,7 +226,7 @@ pub async fn run( ui::info!("Switching to version {}", ui::version(&version)); switch_to_version(&version_manager, &version)?; ui::success!("Switched to version {}", ui::version(&version)); - ui::detail!("Run 'ampd --version' and 'ampctl --version' to verify installation"); + ui::detail!("Run 'ampd --version' to verify installation"); return Ok(()); } @@ -236,8 +236,8 @@ pub async fn run( // Install the binary installer.install_from_release(&version, platform, arch).await?; - ui::success!("Installed ampd and ampctl {}", ui::version(&version)); - ui::detail!("Run 'ampd --version' and 'ampctl --version' to verify installation"); + ui::success!("Installed amp {}", ui::version(&version)); + ui::detail!("Run 'ampd --version' to verify installation"); Ok(()) } diff --git a/docs/features/app-ampup.md b/docs/features/app-ampup.md index 634caa3..a477692 100644 --- a/docs/features/app-ampup.md +++ b/docs/features/app-ampup.md @@ -10,7 +10,7 @@ components: "app:ampup" ## Summary -ampup is the official version manager and installer for Amp binaries (ampd and ampctl), similar to rustup or nvm. It manages downloading pre-built binaries from GitHub releases, installing multiple versions side-by-side, switching between versions via symlinks, building from source (branch, commit, PR, or local path), and self-updating. +ampup is the official version manager and installer for Amp binaries (`ampd`, plus the optional `ampctl` and `ampsql`), similar to rustup or nvm. It manages downloading pre-built binaries from GitHub releases, installing multiple versions side-by-side, switching between versions via symlinks, building from source (branch, commit, PR, or local path), and self-updating. `ampd` is the required core binary; `ampctl` and `ampsql` are installed on a best-effort basis and skipped when a release (or source tree) does not provide them. ## Table of Contents @@ -21,9 +21,9 @@ ampup is the official version manager and installer for Amp binaries (ampd and a ## Key Concepts -- **Version Manager**: Manages multiple installed versions of ampd/ampctl with symlink-based activation +- **Version Manager**: Manages multiple installed versions of ampd/ampctl/ampsql with symlink-based activation - **Installer**: Downloads pre-built binaries from GitHub releases and extracts to versioned directories -- **Builder**: Compiles ampd/ampctl from source using cargo, supporting branch, commit, PR, or local path builds +- **Builder**: Compiles ampd/ampctl/ampsql from source using cargo, supporting branch, commit, PR, or local path builds - **Self-updater**: Atomic in-place binary replacement for updating ampup itself to the latest version - **Active Version**: The currently selected version, tracked via symlinks in `~/.amp/bin/` and `.version` file @@ -37,7 +37,7 @@ Install ampup using the shell installer: curl -sSf https://ampup.sh/install | sh ``` -The installer downloads the appropriate binary for your platform, runs `ampup init` to set up directories and PATH, and installs the latest ampd/ampctl version. +The installer downloads the appropriate binary for your platform, runs `ampup init` to set up directories and PATH, and installs the latest ampd version (along with ampctl and ampsql when available). ### Install a Specific Version @@ -116,7 +116,7 @@ Clones the repository (or uses local path), runs `cargo build --release`, and in ### Update to Latest ```bash -# Update ampd/ampctl to latest release +# Update ampd/ampctl/ampsql to latest release ampup update # Update with parallel downloads @@ -147,17 +147,21 @@ The self-update performs atomic in-place replacement of the running executable. ├── bin/ # Symlinks to active version │ ├── ampup # The ampup binary itself │ ├── ampd -> ../versions/v0.1.0/ampd # Symlink to active ampd -│ └── ampctl -> ../versions/v0.1.0/ampctl # Symlink to active ampctl +│ ├── ampctl -> ../versions/v0.1.0/ampctl # Symlink to active ampctl (when present) +│ └── ampsql -> ../versions/v0.1.0/ampsql # Symlink to active ampsql (when present) ├── versions/ # All installed versions │ ├── v0.1.0/ │ │ ├── ampd -│ │ └── ampctl +│ │ ├── ampctl +│ │ └── ampsql │ ├── v0.2.0/ │ │ ├── ampd -│ │ └── ampctl +│ │ ├── ampctl +│ │ └── ampsql │ └── my-dev-build/ │ ├── ampd -│ └── ampctl +│ ├── ampctl +│ └── ampsql └── .version # Tracks currently active version (e.g., "v0.1.0") ``` @@ -166,7 +170,7 @@ The self-update performs atomic in-place replacement of the running executable. 1. User runs `ampup use ` 2. Verify version exists in `~/.amp/versions//` 3. Remove existing symlinks in `~/.amp/bin/` -4. Create new symlinks pointing to `~/.amp/versions//{ampd,ampctl}` +4. Create new symlinks pointing to `~/.amp/versions//ampd` (required) and to `ampctl`/`ampsql` when those binaries are present 5. Write version string to `~/.amp/.version` ### Installation Flow @@ -175,20 +179,21 @@ The self-update performs atomic in-place replacement of the running executable. 2. Resolve GitHub token (explicit `--github-token` → `gh auth token` → unauthenticated) 3. Detect platform (Linux/Darwin) and architecture (x86_64/aarch64) 4. Query GitHub API for release (latest or specific tag) -5. Download artifacts concurrently (bounded by `-j`, default 4): `ampd-{platform}-{arch}`, `ampctl-{platform}-{arch}` +5. Resolve release metadata once and select artifacts to download: `ampd-{platform}-{arch}` (required), plus `ampctl-{platform}-{arch}` and `ampsql-{platform}-{arch}` when present (optional artifacts absent from the release are skipped with a warning) +6. Download the selected artifacts concurrently (bounded by `-j`, default 4) - Downloads write to a staging directory (sibling of version dir for atomic rename) - Each download is verified (non-empty) and retried once on failure - - If any download fails, in-flight downloads are cancelled and the staging directory is cleaned up -6. Atomically move staging directory to `~/.amp/versions//` -7. Activate version (create symlinks) — only after all downloads succeed + - If any selected download fails, in-flight downloads are cancelled and the staging directory is cleaned up +7. Atomically move staging directory to `~/.amp/versions//` +8. Activate version (create symlinks) — only after all downloads succeed ### Build Flow 1. User runs `ampup build` with source specifier 2. Clone repository (or use local path) -3. Run `cargo build --release` in workspace +3. Run `cargo build --release -p ampd` (required), then best-effort `-p ampctl` and `-p ampsql` 4. Extract version from `ampd --version` output -5. Copy `target/release/{ampd,ampctl}` to `~/.amp/versions//` +5. Copy `target/release/ampd` (required) plus `ampctl`/`ampsql` when produced to `~/.amp/versions//` 6. Activate version (create symlinks) ### Communication