Skip to content

Commit 329e7e3

Browse files
montfortclaude
andcommitted
feat: add crates.io distribution with smart self-update detection
Add crates.io as a complementary distribution channel for the CLI. The self-update mechanism now detects whether the CLI was installed via cargo or prebuilt binary and adapts accordingly, using `cargo install` for cargo-managed installations and GitHub Releases for binary installs. Users can override with `--method=github` or `--method=cargo`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 11bc529 commit 329e7e3

9 files changed

Lines changed: 179 additions & 11 deletions

File tree

.github/workflows/release-cli.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,20 @@ jobs:
168168
gh release delete "$tag" --yes --cleanup-tag
169169
fi
170170
done
171+
172+
publish-crate:
173+
name: Publish to crates.io
174+
needs: [resolve-version, upload-to-release]
175+
runs-on: ubuntu-latest
176+
steps:
177+
- uses: actions/checkout@v6
178+
with:
179+
ref: ${{ needs.resolve-version.outputs.tag }}
180+
181+
- name: Install Rust toolchain
182+
uses: dtolnay/rust-toolchain@stable
183+
184+
- name: Publish to crates.io
185+
env:
186+
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
187+
run: cargo publish --manifest-path cli/Cargo.toml

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**AI Governance Platform for Responsible Software Development**
66

77
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8+
[![Crates.io](https://img.shields.io/crates/v/devtrail-cli.svg)](https://crates.io/crates/devtrail-cli)
89
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
910
[![Handbook](https://img.shields.io/badge/docs-Handbook-orange.svg)](dist/.devtrail/QUICK-REFERENCE.md)
1011
[![Strange Days Tech](https://img.shields.io/badge/by-Strange_Days_Tech-purple.svg)](https://strangedays.tech)
@@ -131,6 +132,8 @@ Or install from source with Cargo:
131132
cargo install devtrail-cli
132133
```
133134

135+
> **Note:** `devtrail update-cli` automatically detects how you installed the CLI. Prebuilt binary installs update from GitHub Releases; cargo installs update via `cargo install`. You can override with `--method=github` or `--method=cargo`.
136+
134137
Then initialize in your project:
135138

136139
```bash

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ description = "CLI tool for DevTrail - Documentation Governance for AI-Assisted
66
license = "MIT"
77
repository = "https://github.com/StrangeDaysTech/devtrail"
88
homepage = "https://strangedays.tech"
9-
readme = "../README.md"
109
keywords = ["devtrail", "documentation", "governance", "ai", "cli"]
1110
categories = ["command-line-utilities", "development-tools"]
1211
authors = ["Strange Days Tech, S.A.S."]
12+
include = ["src/**/*", "Cargo.toml", "Cargo.lock"]
1313

1414
[[bin]]
1515
name = "devtrail"

cli/src/commands/about.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Result;
22
use colored::Colorize;
33

44
use crate::manifest::DistManifest;
5+
use crate::self_update::{self, InstallMethod};
56

67
pub fn run() -> Result<()> {
78
let version = env!("CARGO_PKG_VERSION");
@@ -30,6 +31,13 @@ pub fn run() -> Result<()> {
3031
}
3132
}
3233

34+
// Show install method
35+
let install_label = match self_update::detect_install_method() {
36+
InstallMethod::Cargo => "cargo (crates.io)",
37+
InstallMethod::GitHubBinary => "prebuilt binary (GitHub Releases)",
38+
};
39+
println!(" {} {}", "Install:".dimmed(), install_label.dimmed());
40+
3341
println!(" {}", description.dimmed());
3442
println!();
3543
println!(" {} {}", "Author:".cyan(), authors);

cli/src/commands/update.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use colored::Colorize;
33

44
use crate::utils;
55

6-
pub fn run() -> Result<()> {
6+
pub fn run(method: &str) -> Result<()> {
77
let target = std::env::current_dir()?;
88

99
// Update framework (skip if not initialized)
@@ -19,7 +19,7 @@ pub fn run() -> Result<()> {
1919
// Update CLI
2020
println!();
2121
println!("{}", "── CLI ──".bold());
22-
if let Err(e) = super::update_cli::run() {
22+
if let Err(e) = super::update_cli::run(method) {
2323
utils::warn(&format!("CLI update failed: {}", e));
2424
}
2525

cli/src/commands/update_cli.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use anyhow::Result;
22

3-
pub fn run() -> Result<()> {
4-
crate::self_update::perform_update()
3+
use crate::self_update;
4+
5+
pub fn run(method: &str) -> Result<()> {
6+
let method = self_update::parse_method(method);
7+
self_update::perform_update(method)
58
}

cli/src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,19 @@ enum Commands {
3737
path: String,
3838
},
3939
/// Update both framework and CLI to the latest version
40-
Update,
40+
Update {
41+
/// Update method for the CLI binary: auto, github, or cargo
42+
#[arg(long, default_value = "auto", value_parser = ["auto", "github", "cargo"])]
43+
method: String,
44+
},
4145
/// Update the DevTrail framework to the latest version
4246
UpdateFramework,
4347
/// Update the CLI binary to the latest version
44-
UpdateCli,
48+
UpdateCli {
49+
/// Update method: auto (detect), github (prebuilt binary), or cargo (compile from source)
50+
#[arg(long, default_value = "auto", value_parser = ["auto", "github", "cargo"])]
51+
method: String,
52+
},
4553
/// Remove DevTrail from the project
4654
Remove {
4755
/// Remove everything including user-generated documents (requires confirmation)
@@ -164,9 +172,9 @@ fn main() {
164172

165173
let result = match cli.command {
166174
Commands::Init { path } => commands::init::run(&path),
167-
Commands::Update => commands::update::run(),
175+
Commands::Update { method } => commands::update::run(&method),
168176
Commands::UpdateFramework => commands::update_framework::run(),
169-
Commands::UpdateCli => commands::update_cli::run(),
177+
Commands::UpdateCli { method } => commands::update_cli::run(&method),
170178
Commands::Remove { full } => commands::remove::run(full),
171179
Commands::Validate { path, fix, staged } => commands::validate::run(&path, fix, staged),
172180
Commands::Audit {

cli/src/self_update.rs

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,134 @@ use crate::download;
66
use crate::platform;
77
use crate::utils;
88

9-
/// Perform the CLI self-update
10-
pub fn perform_update() -> Result<()> {
9+
/// How the CLI was installed
10+
#[derive(Debug, Clone, Copy, PartialEq)]
11+
pub enum InstallMethod {
12+
/// Installed via `cargo install devtrail-cli`
13+
Cargo,
14+
/// Installed via prebuilt binary from GitHub Releases
15+
GitHubBinary,
16+
}
17+
18+
/// Check if a path indicates a cargo installation (contains `.cargo/bin/`)
19+
fn path_indicates_cargo(path: &Path) -> bool {
20+
path.components().any(|c| c.as_os_str() == ".cargo")
21+
&& path
22+
.parent()
23+
.and_then(|p| p.file_name())
24+
.map(|name| name == "bin")
25+
.unwrap_or(false)
26+
}
27+
28+
/// Detect the installation method based on the executable path
29+
pub fn detect_install_method() -> InstallMethod {
30+
std::env::current_exe()
31+
.ok()
32+
.and_then(|p| p.canonicalize().ok().or(Some(p)))
33+
.filter(|p| path_indicates_cargo(p))
34+
.map(|_| InstallMethod::Cargo)
35+
.unwrap_or(InstallMethod::GitHubBinary)
36+
}
37+
38+
/// Parse a method string from the CLI flag into an InstallMethod override
39+
pub fn parse_method(method: &str) -> Option<InstallMethod> {
40+
match method {
41+
"cargo" => Some(InstallMethod::Cargo),
42+
"github" => Some(InstallMethod::GitHubBinary),
43+
_ => None, // "auto" or anything else → auto-detect
44+
}
45+
}
46+
47+
/// Perform the CLI self-update, using the specified method or auto-detecting
48+
pub fn perform_update(method_override: Option<InstallMethod>) -> Result<()> {
49+
let method = method_override.unwrap_or_else(detect_install_method);
50+
51+
match method {
52+
InstallMethod::Cargo => perform_cargo_update(),
53+
InstallMethod::GitHubBinary => perform_github_update(),
54+
}
55+
}
56+
57+
/// Update via `cargo install --force devtrail-cli`
58+
fn perform_cargo_update() -> Result<()> {
59+
let current_version = env!("CARGO_PKG_VERSION");
60+
utils::info(&format!("Current version: cli-{}", current_version));
61+
println!(
62+
" {} {}",
63+
"Install method:".dimmed(),
64+
"cargo (crates.io)".cyan()
65+
);
66+
67+
// Check for newer version via GitHub API
68+
utils::info("Checking for updates...");
69+
let release = download::get_latest_release_full()?;
70+
let tag_version = download::strip_tag_prefix(&release.tag_name);
71+
72+
println!(
73+
" {} {}",
74+
"Latest version:".dimmed(),
75+
release.tag_name.green()
76+
);
77+
78+
let current =
79+
semver::Version::parse(current_version).context("Failed to parse current version")?;
80+
let latest =
81+
semver::Version::parse(tag_version).context("Failed to parse release version")?;
82+
83+
if latest <= current {
84+
utils::success(&format!(
85+
"CLI is already at the latest version (cli-{})",
86+
current_version
87+
));
88+
return Ok(());
89+
}
90+
91+
// Verify cargo is available
92+
let cargo_available = std::process::Command::new("cargo")
93+
.arg("--version")
94+
.output()
95+
.is_ok();
96+
97+
if !cargo_available {
98+
utils::warn("cargo not found in PATH. Run the following command manually:");
99+
println!(
100+
"\n {}\n",
101+
"cargo install --force devtrail-cli".yellow().bold()
102+
);
103+
bail!("cargo is not available in PATH");
104+
}
105+
106+
// Confirm with user
107+
let confirm = dialoguer::Confirm::new()
108+
.with_prompt(format!(
109+
"Update from cli-{current_version} to cli-{tag_version} via cargo?"
110+
))
111+
.default(true)
112+
.interact()?;
113+
114+
if !confirm {
115+
utils::info("Update cancelled.");
116+
return Ok(());
117+
}
118+
119+
utils::info("Compiling from source, this may take a few minutes...");
120+
121+
let status = std::process::Command::new("cargo")
122+
.args(["install", "--force", "devtrail-cli"])
123+
.status()
124+
.context("Failed to run cargo install")?;
125+
126+
if status.success() {
127+
utils::success(&format!("CLI updated to cli-{}!", tag_version));
128+
} else {
129+
bail!("cargo install failed with exit code: {}", status);
130+
}
131+
132+
Ok(())
133+
}
134+
135+
/// Update via prebuilt binary from GitHub Releases
136+
fn perform_github_update() -> Result<()> {
11137
let current_version = env!("CARGO_PKG_VERSION");
12138
utils::info(&format!("Current version: cli-{}", current_version));
13139

docs/i18n/es/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**Plataforma de Gobernanza de IA para Desarrollo de Software Responsable**
66

77
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../../LICENSE)
8+
[![Crates.io](https://img.shields.io/crates/v/devtrail-cli.svg)](https://crates.io/crates/devtrail-cli)
89
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
910
[![Handbook](https://img.shields.io/badge/docs-Handbook-orange.svg)](../../../dist/.devtrail/QUICK-REFERENCE.md)
1011
[![Strange Days Tech](https://img.shields.io/badge/by-Strange_Days_Tech-purple.svg)](https://strangedays.tech)
@@ -131,6 +132,8 @@ O instalar desde el código fuente con Cargo:
131132
cargo install devtrail-cli
132133
```
133134

135+
> **Nota:** `devtrail update-cli` detecta automáticamente cómo instalaste el CLI. Las instalaciones con binario precompilado se actualizan desde GitHub Releases; las instalaciones con cargo se actualizan via `cargo install`. Puedes forzar el método con `--method=github` o `--method=cargo`.
136+
134137
Luego inicializa en tu proyecto:
135138

136139
```bash

0 commit comments

Comments
 (0)