Skip to content

Commit 9e17b6f

Browse files
committed
add system command
1 parent 71be5e0 commit 9e17b6f

5 files changed

Lines changed: 294 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dbtp metrics query revenue --group-by metric_time --grain MONTH
1717
curl -fsSL https://raw.githubusercontent.com/trouze/dbtp/main/install.sh | bash
1818
```
1919

20-
This downloads the latest prebuilt binary from [GitHub Releases](https://github.com/trouze/dbtp/releases) and installs it to `/usr/local/bin`. Set `DBTP_INSTALL_DIR` to customize the location.
20+
This downloads the latest prebuilt binary from [GitHub Releases](https://github.com/trouze/dbtp/releases) and installs it to `~/.local/bin`. Set `DBTP_INSTALL_DIR` to customize the location.
2121

2222
### From source via Git (requires Rust)
2323

install.sh

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ set -euo pipefail
33

44
REPO="trouze/dbtp"
55
BINARY="dbtp"
6-
INSTALL_DIR="${DBTP_INSTALL_DIR:-/usr/local/bin}"
6+
INSTALL_DIR="${DBTP_INSTALL_DIR:-$HOME/.local/bin}"
77

88
get_latest_version() {
99
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
@@ -45,15 +45,23 @@ main() {
4545

4646
curl -fsSL "${url}" | tar xz -C "${tmpdir}"
4747

48-
if [ -w "${INSTALL_DIR}" ]; then
49-
mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
50-
else
51-
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
52-
sudo mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
53-
fi
54-
48+
mkdir -p "${INSTALL_DIR}"
49+
mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
5550
chmod +x "${INSTALL_DIR}/${BINARY}"
51+
5652
echo "Installed ${BINARY} to ${INSTALL_DIR}/${BINARY}"
53+
54+
case ":${PATH}:" in
55+
*":${INSTALL_DIR}:"*) ;;
56+
*)
57+
echo ""
58+
echo "NOTE: ${INSTALL_DIR} is not in your PATH."
59+
echo "Add it by running:"
60+
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
61+
echo "Or add that line to your ~/.zshrc / ~/.bashrc."
62+
;;
63+
esac
64+
5765
"${INSTALL_DIR}/${BINARY}" --version
5866
}
5967

src/cli/commands/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod seeds;
1616
pub mod semantic_models;
1717
pub mod snapshots;
1818
pub mod sources;
19+
pub mod system;
1920
pub mod tests;
2021

2122
use crate::cli::output::{format_output, OutputFormat};
@@ -45,6 +46,11 @@ pub async fn exec(
4546
return Ok(());
4647
}
4748

49+
Commands::System(args) => {
50+
system::exec(args).await?;
51+
return Ok(());
52+
}
53+
4854
// Admin API commands
4955
Commands::Accounts(args) => {
5056
let val = accounts::exec(args, rest, config).await?;

src/cli/commands/system.rs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use std::env;
2+
use std::fs;
3+
use std::path::PathBuf;
4+
5+
use clap::{Args, Subcommand};
6+
use serde::Deserialize;
7+
8+
use crate::core::config;
9+
use crate::core::error::{DbtpError, Result};
10+
11+
const REPO: &str = "trouze/dbtp";
12+
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
13+
14+
#[derive(Debug, Args)]
15+
pub struct SystemArgs {
16+
#[command(subcommand)]
17+
pub command: SystemCommand,
18+
}
19+
20+
#[derive(Debug, Subcommand)]
21+
pub enum SystemCommand {
22+
/// Show dbtp version, install path, and config location
23+
Info,
24+
/// Update dbtp to the latest release from GitHub
25+
Update {
26+
/// Specific version to install (e.g. "v0.2.0"). Defaults to latest.
27+
#[arg(long)]
28+
version: Option<String>,
29+
},
30+
/// Uninstall dbtp binary and optionally remove config
31+
Uninstall {
32+
/// Also remove the config directory (~/.config/dbtp)
33+
#[arg(long)]
34+
purge: bool,
35+
},
36+
}
37+
38+
#[derive(Debug, Deserialize)]
39+
struct GithubRelease {
40+
tag_name: String,
41+
assets: Vec<GithubAsset>,
42+
}
43+
44+
#[derive(Debug, Deserialize)]
45+
struct GithubAsset {
46+
name: String,
47+
browser_download_url: String,
48+
}
49+
50+
fn detect_target() -> Result<&'static str> {
51+
let target = match (env::consts::OS, env::consts::ARCH) {
52+
("macos", "aarch64") => "aarch64-apple-darwin",
53+
("macos", "x86_64") => "x86_64-apple-darwin",
54+
("linux", "x86_64") => "x86_64-unknown-linux-gnu",
55+
("linux", "aarch64") => "aarch64-unknown-linux-gnu",
56+
(os, arch) => {
57+
return Err(DbtpError::config(format!(
58+
"Unsupported platform: {os}/{arch}"
59+
)));
60+
}
61+
};
62+
Ok(target)
63+
}
64+
65+
fn binary_path() -> Result<PathBuf> {
66+
env::current_exe().map_err(DbtpError::Io)
67+
}
68+
69+
pub async fn exec(args: &SystemArgs) -> Result<()> {
70+
match &args.command {
71+
SystemCommand::Info => info().await,
72+
SystemCommand::Update { version } => update(version.as_deref()).await,
73+
SystemCommand::Uninstall { purge } => uninstall(*purge),
74+
}
75+
}
76+
77+
async fn info() -> Result<()> {
78+
let exe = binary_path().ok();
79+
let config_dir = config::config_dir().ok();
80+
81+
eprintln!("dbtp {CURRENT_VERSION}");
82+
eprintln!();
83+
if let Some(path) = &exe {
84+
eprintln!("Binary: {}", path.display());
85+
}
86+
if let Some(dir) = &config_dir {
87+
let exists = dir.exists();
88+
eprintln!(
89+
"Config: {}{}",
90+
dir.display(),
91+
if exists { "" } else { " (not created yet)" }
92+
);
93+
}
94+
eprintln!("Target: {}", detect_target().unwrap_or("unknown"));
95+
eprintln!("Repo: https://github.com/{REPO}");
96+
97+
Ok(())
98+
}
99+
100+
async fn update(pin_version: Option<&str>) -> Result<()> {
101+
let target = detect_target()?;
102+
let exe_path = binary_path()?;
103+
104+
eprintln!("Current version: v{CURRENT_VERSION}");
105+
106+
let client = reqwest::Client::builder()
107+
.user_agent("dbtp-updater")
108+
.build()
109+
.map_err(DbtpError::Http)?;
110+
111+
let release: GithubRelease = if let Some(v) = pin_version {
112+
let tag = if v.starts_with('v') {
113+
v.to_string()
114+
} else {
115+
format!("v{v}")
116+
};
117+
let url = format!("https://api.github.com/repos/{REPO}/releases/tags/{tag}");
118+
client
119+
.get(&url)
120+
.send()
121+
.await
122+
.map_err(DbtpError::Http)?
123+
.json()
124+
.await
125+
.map_err(DbtpError::Http)?
126+
} else {
127+
let url = format!("https://api.github.com/repos/{REPO}/releases/latest");
128+
client
129+
.get(&url)
130+
.send()
131+
.await
132+
.map_err(DbtpError::Http)?
133+
.json()
134+
.await
135+
.map_err(DbtpError::Http)?
136+
};
137+
138+
let latest = &release.tag_name;
139+
let current_tag = format!("v{CURRENT_VERSION}");
140+
141+
if latest == &current_tag && pin_version.is_none() {
142+
eprintln!("Already up to date ({latest}).");
143+
return Ok(());
144+
}
145+
146+
eprintln!("Updating to {latest}...");
147+
148+
let expected_name = format!("dbtp-{latest}-{target}.tar.gz");
149+
let asset = release
150+
.assets
151+
.iter()
152+
.find(|a| a.name == expected_name)
153+
.ok_or_else(|| {
154+
DbtpError::config(format!(
155+
"No binary found for {target} in release {latest}. Available: {}",
156+
release
157+
.assets
158+
.iter()
159+
.map(|a| a.name.as_str())
160+
.collect::<Vec<_>>()
161+
.join(", ")
162+
))
163+
})?;
164+
165+
let bytes = client
166+
.get(&asset.browser_download_url)
167+
.send()
168+
.await
169+
.map_err(DbtpError::Http)?
170+
.bytes()
171+
.await
172+
.map_err(DbtpError::Http)?;
173+
174+
let tmpdir = tempdir(&exe_path)?;
175+
let tarball = tmpdir.join("dbtp.tar.gz");
176+
fs::write(&tarball, &bytes).map_err(DbtpError::Io)?;
177+
178+
let status = std::process::Command::new("tar")
179+
.args(["xzf", &tarball.to_string_lossy(), "-C", &tmpdir.to_string_lossy()])
180+
.status()
181+
.map_err(DbtpError::Io)?;
182+
183+
if !status.success() {
184+
return Err(DbtpError::config("Failed to extract update archive"));
185+
}
186+
187+
let new_binary = tmpdir.join("dbtp");
188+
if !new_binary.exists() {
189+
return Err(DbtpError::config("Extracted archive does not contain dbtp binary"));
190+
}
191+
192+
let backup = exe_path.with_extension("old");
193+
if backup.exists() {
194+
fs::remove_file(&backup).ok();
195+
}
196+
fs::rename(&exe_path, &backup).map_err(DbtpError::Io)?;
197+
198+
if let Err(e) = fs::rename(&new_binary, &exe_path) {
199+
fs::rename(&backup, &exe_path).ok();
200+
return Err(DbtpError::Io(e));
201+
}
202+
203+
#[cfg(unix)]
204+
{
205+
use std::os::unix::fs::PermissionsExt;
206+
fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))
207+
.map_err(DbtpError::Io)?;
208+
}
209+
210+
fs::remove_file(&backup).ok();
211+
fs::remove_dir_all(&tmpdir).ok();
212+
213+
eprintln!("Updated dbtp to {latest}.");
214+
Ok(())
215+
}
216+
217+
fn uninstall(purge: bool) -> Result<()> {
218+
let exe_path = binary_path()?;
219+
220+
if purge {
221+
if let Ok(dir) = config::config_dir() {
222+
if dir.exists() {
223+
fs::remove_dir_all(&dir).map_err(DbtpError::Io)?;
224+
eprintln!("Removed config directory: {}", dir.display());
225+
}
226+
}
227+
}
228+
229+
eprintln!("Removing binary: {}", exe_path.display());
230+
fs::remove_file(&exe_path).map_err(DbtpError::Io)?;
231+
eprintln!("dbtp has been uninstalled.");
232+
233+
if !purge {
234+
if let Ok(dir) = config::config_dir() {
235+
if dir.exists() {
236+
eprintln!(
237+
"\nConfig directory was kept at {}. Use --purge to remove it.",
238+
dir.display()
239+
);
240+
}
241+
}
242+
}
243+
244+
Ok(())
245+
}
246+
247+
fn tempdir(near: &PathBuf) -> Result<PathBuf> {
248+
let dir = near
249+
.parent()
250+
.unwrap_or(std::path::Path::new("/tmp"))
251+
.join(".dbtp-update");
252+
if dir.exists() {
253+
fs::remove_dir_all(&dir).ok();
254+
}
255+
fs::create_dir_all(&dir).map_err(DbtpError::Io)?;
256+
Ok(dir)
257+
}

src/cli/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use clap::{Parser, Subcommand};
66
use self::commands::{
77
accounts, artifacts, configure, dimension_values, environments, exposures, jobs, lineage,
88
macros, metrics, models, projects, runs, saved_queries, seeds, semantic_models, snapshots,
9-
sources, tests,
9+
sources, system, tests,
1010
};
1111

1212
#[derive(Debug, Parser)]
@@ -305,6 +305,19 @@ pub enum Commands {
305305
)]
306306
DimensionValues(dimension_values::DimensionValuesArgs),
307307

308+
/// Manage the dbtp installation
309+
#[command(
310+
long_about = "Manage the dbtp installation.\n\n\
311+
Check version info, update to the latest release, or uninstall cleanly.",
312+
after_long_help = "EXAMPLES:\n \
313+
dbtp system info\n \
314+
dbtp system update\n \
315+
dbtp system update --version v0.2.0\n \
316+
dbtp system uninstall\n \
317+
dbtp system uninstall --purge"
318+
)]
319+
System(system::SystemArgs),
320+
308321
/// Generate shell completions
309322
#[command(
310323
long_about = "Generate shell completions for dbtp.\n\n\

0 commit comments

Comments
 (0)