Skip to content

Commit daf7138

Browse files
authored
[move-flow] Add update subcommand (#20170)
* [move-flow] bump version to 1.1.0 and add self_update dependency Same git pin and features as crates/aptos — already in Cargo.lock. * [move-flow] add update subcommand move-flow update [--check] --check Print whether a newer version is available, no download. (default) Fetch latest move-flow-v* release from aptos-labs/aptos-ai, verify version, download matching platform zip, and atomically replace the running binary. Implementation notes: - Uses the self_update crate (same banool fork + pin as the aptos CLI). - Offloads the blocking reqwest call to tokio::task::spawn_blocking to avoid panicking inside the async runtime. - Strips .beta/.rc suffixes before bump_is_greater (x.y.z only). - Hidden --repo-owner / --repo-name flags match the aptos CLI pattern and allow pointing at alternative repos for testing. --------- Co-authored-by: zwxxb <zwxxb@users.noreply.github.com>
1 parent 1dbcc78 commit daf7138

7 files changed

Lines changed: 184 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aptos-move/flow/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.1.0] - 2026-06-30
11+
12+
### Added
13+
- `move-flow update [--check]` subcommand for self-updating from
14+
`aptos-labs/aptos-ai` GitHub releases.
15+
1016
## [1.0.4] - 2026-06-10
1117

1218
### Added
@@ -16,5 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1622
`x86_64-apple-darwin`, `aarch64-apple-darwin`, and
1723
`x86_64-pc-windows-msvc`.
1824

19-
[Unreleased]: https://github.com/aptos-labs/aptos-core/compare/move-flow-v1.0.4...HEAD
25+
[Unreleased]: https://github.com/aptos-labs/aptos-core/compare/move-flow-v1.1.0...HEAD
26+
[1.1.0]: https://github.com/aptos-labs/aptos-core/compare/move-flow-v1.0.4...move-flow-v1.1.0
2027
[1.0.4]: https://github.com/aptos-labs/aptos-core/releases/tag/move-flow-v1.0.4

aptos-move/flow/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "aptos-move-flow"
33
description = "MoveFlow: agent support for Move"
4-
version = "1.0.4"
4+
version = "1.1.0"
55

66
authors = { workspace = true }
77
edition = { workspace = true }
@@ -56,6 +56,10 @@ move-vm-runtime = { workspace = true }
5656
notify = { workspace = true }
5757
pathsearch = { workspace = true }
5858
rmcp = { workspace = true }
59+
self_update = { git = "https://github.com/banool/self_update.git", rev = "8306158ad0fd5b9d4766a3c6bf967e7ef0ea5c4b", features = [
60+
"archive-zip",
61+
"compression-zip-deflate",
62+
] }
5963
serde = { workspace = true, features = ["derive"] }
6064
serde_json = { workspace = true }
6165
tempfile = { workspace = true }

aptos-move/flow/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
pub mod hooks;
55
pub mod mcp;
66
pub mod plugin;
7+
pub mod update;
78

8-
use anyhow::Result;
9+
use anyhow::{Context, Result};
910
use clap::{Args, Parser, Subcommand, ValueEnum};
1011
use mcp::supervisor::{run_supervised, RESTART_ENV_VAR};
1112
use std::path::PathBuf;
@@ -48,6 +49,8 @@ pub enum FlowCommand {
4849
/// Hook subcommands (called from AI platform hooks).
4950
#[command(subcommand)]
5051
Hook(hooks::HookCommand),
52+
/// Update move-flow to the latest release.
53+
Update(update::UpdateArgs),
5154
}
5255

5356
/// Supported AI platform targets.
@@ -76,6 +79,12 @@ impl FlowCli {
7679
Some(v) => mcp::run(args, &self.global, v == "1").await,
7780
},
7881
FlowCommand::Hook(cmd) => hooks::run(cmd),
82+
FlowCommand::Update(args) => {
83+
let args = args.clone();
84+
tokio::task::spawn_blocking(move || update::run(&args))
85+
.await
86+
.context("update task panicked")?
87+
},
7988
}
8089
}
8190
}

aptos-move/flow/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod move_package_status;
1717
mod move_package_test;
1818
mod move_package_verify;
1919
mod move_replay_transaction;
20+
mod update;
2021

2122
use super::*;
2223

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Aptos Foundation
2+
// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE
3+
4+
use crate::update::{run, strip_prerelease, UpdateArgs};
5+
use clap::Parser;
6+
use self_update::version::bump_is_greater;
7+
8+
#[derive(Parser)]
9+
struct Cli {
10+
#[command(flatten)]
11+
args: UpdateArgs,
12+
}
13+
14+
#[test]
15+
fn strip_prerelease_suffixes() {
16+
assert_eq!(strip_prerelease("1.0.5.beta"), "1.0.5");
17+
assert_eq!(strip_prerelease("1.0.5.rc"), "1.0.5");
18+
assert_eq!(strip_prerelease("1.0.5"), "1.0.5");
19+
assert_eq!(strip_prerelease("1.0.5.other"), "1.0.5.other");
20+
}
21+
22+
#[test]
23+
fn tag_version_extraction() {
24+
let tag = "move-flow-v1.0.5";
25+
assert_eq!(&tag["move-flow-v".len()..], "1.0.5");
26+
}
27+
28+
#[test]
29+
fn version_comparison() {
30+
assert!(bump_is_greater("1.0.4", "1.0.5").unwrap());
31+
assert!(!bump_is_greater("1.0.5", "1.0.4").unwrap());
32+
assert!(!bump_is_greater("1.0.5", "1.0.5").unwrap());
33+
assert!(bump_is_greater("1.0.4", strip_prerelease("1.0.5.beta")).unwrap());
34+
}
35+
36+
#[test]
37+
fn check_flag_parses() {
38+
let cli = Cli::try_parse_from(["cmd", "--check"]).unwrap();
39+
assert!(cli.args.check);
40+
}
41+
42+
#[test]
43+
fn check_flag_defaults_false() {
44+
let cli = Cli::try_parse_from(["cmd"]).unwrap();
45+
assert!(!cli.args.check);
46+
}
47+
48+
#[test]
49+
#[ignore]
50+
fn live_github_check() {
51+
let args = UpdateArgs {
52+
check: true,
53+
repo_owner: "aptos-labs".into(),
54+
repo_name: "aptos-ai".into(),
55+
};
56+
let result = run(&args);
57+
assert!(result.is_ok(), "live check failed: {result:?}");
58+
}

aptos-move/flow/src/update.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Aptos Foundation
2+
// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE
3+
4+
use anyhow::{anyhow, Context, Result};
5+
use clap::Args;
6+
use self_update::{
7+
backends::github::{ReleaseList, Update},
8+
version::bump_is_greater,
9+
};
10+
11+
#[derive(Args, Debug, Clone)]
12+
pub struct UpdateArgs {
13+
/// Check for updates without downloading.
14+
#[arg(long)]
15+
pub check: bool,
16+
17+
#[arg(long, default_value = "aptos-labs", hide = true)]
18+
pub repo_owner: String,
19+
20+
#[arg(long, default_value = "aptos-ai", hide = true)]
21+
pub repo_name: String,
22+
}
23+
24+
/// `bump_is_greater` only handles `x.y.z`; strip suffixes it can't parse.
25+
pub(crate) fn strip_prerelease(v: &str) -> &str {
26+
if let Some(stem) = v.strip_suffix(".beta").or_else(|| v.strip_suffix(".rc")) {
27+
stem
28+
} else {
29+
v
30+
}
31+
}
32+
33+
pub fn run(args: &UpdateArgs) -> Result<()> {
34+
let releases = ReleaseList::configure()
35+
.repo_owner(&args.repo_owner)
36+
.repo_name(&args.repo_name)
37+
.build()
38+
.context("failed to configure release list")?
39+
.fetch()
40+
.context("failed to fetch releases from GitHub")?;
41+
42+
let latest = releases
43+
.into_iter()
44+
.find(|r| r.version.starts_with("move-flow-v"))
45+
.ok_or_else(|| {
46+
anyhow!(
47+
"no move-flow release found in {}/{}",
48+
args.repo_owner,
49+
args.repo_name
50+
)
51+
})?;
52+
53+
let latest_version = &latest.version["move-flow-v".len()..];
54+
let current_version = env!("CARGO_PKG_VERSION");
55+
let needs_update = bump_is_greater(current_version, strip_prerelease(latest_version))
56+
.context("failed to compare versions")?;
57+
58+
if args.check {
59+
if needs_update {
60+
println!("Update available: v{latest_version}");
61+
} else {
62+
println!("Already up to date (v{current_version})");
63+
}
64+
return Ok(());
65+
}
66+
67+
if !needs_update {
68+
println!("Already up to date (v{current_version})");
69+
return Ok(());
70+
}
71+
72+
Update::configure()
73+
.repo_owner(&args.repo_owner)
74+
.repo_name(&args.repo_name)
75+
.bin_name("move-flow")
76+
.current_version(current_version)
77+
.target_version_tag(&latest.version)
78+
.no_confirm(true)
79+
.build()
80+
.context("failed to configure updater")?
81+
.update()
82+
.map_err(|e| {
83+
let msg = e.to_string();
84+
if msg.contains("permission denied") || msg.contains("Access is denied") {
85+
anyhow!("cannot replace binary: permission denied (try sudo)")
86+
} else if msg.contains("could not find binary") || msg.contains("no asset") {
87+
anyhow!(
88+
"no release asset for the current platform — download manually from \
89+
https://github.com/{}/{}/releases",
90+
args.repo_owner,
91+
args.repo_name
92+
)
93+
} else {
94+
anyhow!("update failed: {e:#}")
95+
}
96+
})?;
97+
98+
println!("Updated move-flow v{current_version} → v{latest_version}");
99+
Ok(())
100+
}

0 commit comments

Comments
 (0)