[cli] Add a new Interactive CLI to replace the original Aptos CLI#28
[cli] Add a new Interactive CLI to replace the original Aptos CLI#28gregnazario wants to merge 15 commits intomainfrom
Conversation
New workspace member crate (crates/aptos-cli/) providing CLI access to all SDK capabilities: - account: create, fund, balance, lookup, resources, modules, transfer - key: generate, from-mnemonic, show - move: view, run, publish, inspect - transaction: lookup, simulate - info: ledger, gas-price, block Supports --network (mainnet/testnet/devnet/local), --node-url, --api-key, and --json output mode on all commands. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a `dashboard` command that launches a full-screen terminal UI with: - Dashboard tab: live-updating ledger info (version, epoch, block height, timestamp) and gas price estimates, auto-refreshing every 3 seconds - Account tab: address input with balance, sequence number, auth key, and resources list - Move tab: view function caller with function ID, type args, and args inputs, displaying results inline Navigation via Tab/arrow keys, number keys (1-3), vim keys (j/k), with input mode (i/Enter) for editing fields and x to execute. Co-authored-by: Cursor <cursoragent@cursor.com>
- Add `move init` command to scaffold new Move packages with templates - Add `move compile` command to invoke the Move compiler via aptos CLI - Add `move test` command to run Move unit tests - Add `move build-publish` command for compile + publish in one step - Add Compile tab to TUI dashboard with live compilation/test output - Intelligently select newest aptos CLI version when multiple are installed - Use clean environment when spawning aptos CLI to avoid interference Co-authored-by: Cursor <cursoragent@cursor.com>
- Add `repl` command for interactive session with readline support - Encrypted credential storage using AES-256-GCM + Argon2id key derivation - Vault stored at ~/.aptos-sdk-cli/vault.json with 0600 permissions - Credentials decrypted once at session start, held in memory - Active account selection: `use <alias>` auto-injects --private-key - Credential management: init, unlock, lock, add, remove, list, change-password - Network switching within the session via `network <name>` - Command history persisted across sessions - Private keys zeroized on drop via zeroize crate Co-authored-by: Cursor <cursoragent@cursor.com>
- Colorized prompt: network in green/yellow/cyan/magenta, alias highlighted - Vault lock indicator emoji in prompt when credentials are unlocked - Styled banner with box-drawing characters on REPL launch - Color-coded help with section headers (Session, Blockchain, Other) - Colorized credential list with active account indicator - output.rs: colored headers (cyan+bold), success (green checkmark), warnings (yellow), errors (red), info (blue), dim hints (grey) - Key-value pairs: grey labels, white values - JSON output: cyan keys for readability - whoami: colored alias/address/key-type display Co-authored-by: Cursor <cursoragent@cursor.com>
The colorized REPL is the primary interactive interface. Remove the full-screen ratatui dashboard, its module files, and the ratatui dependency (~2000 lines). The REPL already provides all the same capabilities (info, account lookup, move compile/test, credential management) in a familiar command-line workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
- Encrypted vault: ~/.aptos/credentials/vault.json - REPL history: ~/.aptos/config/repl_history Co-authored-by: Cursor <cursoragent@cursor.com>
Lines containing --private-key, --secret, --mnemonic, or --seed are never written to the history file, preventing accidental persistence of sensitive material to disk. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Adds a new aptos-repl crate to provide an interactive Aptos CLI/REPL experience, including encrypted credential storage and passthrough execution of common Aptos SDK commands.
Changes:
- Introduces the
aptos-replbinary crate with REPL loop, command dispatch, and SDK-backed subcommands (account/key/move/tx/info). - Adds encrypted credential vault + legacy
~/.aptos/config.yamlimport and persistent CLI settings. - Adds CI and release automation for building and publishing multi-platform CLI artifacts.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
crates/aptos-repl/src/main.rs |
Defines the aptos-repl CLI entrypoint and subcommand wiring (defaults into REPL). |
crates/aptos-repl/src/repl.rs |
Implements the interactive REPL, config defaulting, credential management UX, and command passthrough/injection. |
crates/aptos-repl/src/credentials.rs |
Implements encrypted vault storage (AES-GCM + Argon2id), persistence, and key handling. |
crates/aptos-repl/src/config.rs |
Adds persistent settings file handling under ~/.aptos/config/settings.json. |
crates/aptos-repl/src/import.rs |
Imports legacy Aptos CLI profiles and parses legacy private key formats. |
crates/aptos-repl/src/output.rs |
Adds colorized output helpers and JSON printing. |
crates/aptos-repl/src/common.rs |
Shared CLI types/utilities (network/key enums, address/amount parsing, SDK client helpers). |
crates/aptos-repl/src/commands/* |
Implements account/key/move/transaction/info commands and Move compiler passthrough via external aptos CLI. |
crates/aptos-repl/Cargo.toml |
Declares the new crate and its dependencies. |
crates/aptos-repl/BUILD.md |
Documents build profiles and platform/runtime compatibility for release artifacts. |
Cargo.toml |
Adds aptos-repl to workspace members and adjusts release/cli profile settings. |
Cargo.lock |
Updates lockfile for new dependencies. |
.github/workflows/ci.yml |
Extends CI with aptos-repl lint/test/release-build jobs. |
.github/workflows/cli-release.yml |
Adds dedicated CLI release workflow for tagged builds and artifact packaging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Use a clean environment to prevent Rust/Cargo env vars from interfering | ||
| // with the Move compiler embedded in the aptos CLI. | ||
| cmd.env_clear(); | ||
|
|
||
| // Re-add essential env vars | ||
| if let Ok(path) = std::env::var("PATH") { | ||
| cmd.env("PATH", path); | ||
| } | ||
| if let Ok(home) = std::env::var("HOME") { | ||
| cmd.env("HOME", home); | ||
| } | ||
| if let Ok(term) = std::env::var("TERM") { | ||
| cmd.env("TERM", term); | ||
| } | ||
| // Support custom Move home directory | ||
| if let Ok(move_home) = std::env::var("MOVE_HOME") { | ||
| cmd.env("MOVE_HOME", move_home); | ||
| } |
There was a problem hiding this comment.
aptos_command clears the entire environment (env_clear) unconditionally. On Windows (and sometimes macOS/Linux), child processes can depend on variables like SystemRoot, TEMP, USERPROFILE, etc.; clearing them can cause the external aptos CLI invocation to fail in surprising ways. Consider either skipping env_clear on Windows, or whitelisting the required OS variables in addition to PATH/HOME/TERM.
| //! Colorized output formatting for CLI results. | ||
| //! | ||
| //! Uses ANSI escape codes via crossterm for colored terminal output. | ||
| //! All functions gracefully degrade to plain text if the terminal does | ||
| //! not support colors. |
There was a problem hiding this comment.
The module-level docs state output “gracefully degrade[s] to plain text if the terminal does not support colors”, but the implementation always emits colored output via crossterm::execute! / Stylize without checking stdout/stderr is a TTY. Either add a TTY/color-support check (and skip styling when not supported) or adjust the docs to match actual behavior.
| eprintln!("\nCompilation failed. Check the errors above."); | ||
| std::process::exit(1); |
There was a problem hiding this comment.
These command implementations call std::process::exit(1) on failure. When invoked from the REPL (in-process dispatch), this will terminate the entire REPL session rather than returning an error that can be displayed and allow the session to continue. Prefer returning an Err (with context) and let the top-level main decide whether to exit.
| eprintln!("\nCompilation failed. Check the errors above."); | |
| std::process::exit(1); | |
| anyhow::bail!("Compilation failed. Check the errors above."); |
| eprintln!("\nSome tests failed. Check the output above."); | ||
| std::process::exit(1); |
There was a problem hiding this comment.
These command implementations call std::process::exit(1) on failure. When invoked from the REPL (in-process dispatch), this will terminate the entire REPL session rather than returning an error that can be displayed and allow the session to continue. Prefer returning an Err (with context) and let the top-level main decide whether to exit.
| eprintln!("\nSome tests failed. Check the output above."); | |
| std::process::exit(1); | |
| anyhow::bail!("\nSome tests failed. Check the output above."); |
| eprintln!("Transaction failed: {vm_status}"); | ||
| std::process::exit(1); | ||
| } |
There was a problem hiding this comment.
These command implementations call std::process::exit(1) on failure. When invoked from the REPL (in-process dispatch), this will terminate the entire REPL session rather than returning an error that can be displayed and allow the session to continue. Prefer returning an Err (with context) and let the top-level main decide whether to exit.
| /// Apply saved config defaults to global opts (CLI flags take precedence). | ||
| fn apply_config_defaults(global: &mut GlobalOpts, config: &CliConfig) { | ||
| // Only apply network from config if the user didn't pass --network on the | ||
| // command line (clap default is "mainnet", so we can't easily distinguish; | ||
| // we apply if config has a value). | ||
| if let Some(ref net) = config.network | ||
| && let Some(parsed) = parse_network_str(net) | ||
| { | ||
| global.network = parsed; | ||
| } |
There was a problem hiding this comment.
apply_config_defaults claims CLI flags take precedence, but network is overridden whenever config.network is set. Because GlobalOpts.network has a clap default, this can override an explicitly provided --network mainnet with a saved config value (e.g., testnet). If precedence matters, consider representing network as an Option<NetworkArg> (or tracking whether the flag was set) so you only apply config when the user didn’t pass --network.
| // For simulation we need an account - create a dummy one from the sender address. | ||
| // We use an Ed25519Account since simulation doesn't verify signatures. | ||
| let sender = common::require_address(&args.sender)?; | ||
| let dummy_account = aptos_sdk::account::Ed25519Account::generate(); | ||
|
|
||
| // Build the raw transaction with the sender address and simulate | ||
| let sequence_number = aptos | ||
| .get_sequence_number(sender) | ||
| .await | ||
| .context("failed to get sequence number")?; | ||
|
|
||
| let raw_txn = aptos_sdk::transaction::TransactionBuilder::new() | ||
| .sender(sender) | ||
| .sequence_number(sequence_number) | ||
| .payload(aptos_sdk::transaction::TransactionPayload::EntryFunction( | ||
| payload, | ||
| )) | ||
| .max_gas_amount(200_000) | ||
| .gas_unit_price(100) | ||
| .chain_id(aptos.chain_id()) | ||
| .expiration_from_now(600) | ||
| .build() | ||
| .context("failed to build transaction")?; | ||
|
|
||
| let signed = aptos_sdk::transaction::builder::sign_transaction(&raw_txn, &dummy_account) | ||
| .context("failed to sign transaction for simulation")?; |
There was a problem hiding this comment.
cmd_simulate builds a transaction for sender but signs it with a newly-generated dummy key. This will not match the sender’s on-chain authentication key and is likely to fail simulation. Also, the Aptos SDK’s own e2e simulation test constructs a SignedTransaction with a zeroed signature (the endpoint can reject valid signatures), so using sign_transaction here may be incorrect. Consider requiring a real signer (e.g., inject the active account’s credential like other signing commands) and constructing the simulated transaction/authenticator in the way expected by transactions/simulate (e.g., zero-signature but correct public key for the sender).
| eprintln!("Publish failed: {vm_status}"); | ||
| std::process::exit(1); |
There was a problem hiding this comment.
These command implementations call std::process::exit(1) on failure. When invoked from the REPL (in-process dispatch), this will terminate the entire REPL session rather than returning an error that can be displayed and allow the session to continue. Prefer returning an Err (with context) and let the top-level main decide whether to exit.
| eprintln!("Publish failed: {vm_status}"); | |
| std::process::exit(1); | |
| anyhow::bail!("Publish failed: {vm_status}"); |
| std::fs::rename(&tmp, &self.path) | ||
| .context(format!("failed to rename to {}", self.path.display()))?; | ||
|
|
There was a problem hiding this comment.
Vault::save writes to a temp file and then uses std::fs::rename onto the final path. On Windows, renaming over an existing destination commonly fails, which can break subsequent add/remove operations after the vault file already exists. Consider using a replace/atomic-write approach that works cross-platform (e.g., remove the destination first on Windows, or write to a temp file and use a platform-specific replace).
| std::fs::rename(&tmp, &self.path) | |
| .context(format!("failed to rename to {}", self.path.display()))?; | |
| // On Windows, renaming over an existing destination often fails. | |
| // Remove the old file first, then rename the temp file into place. | |
| #[cfg(windows)] | |
| { | |
| if self.path.exists() { | |
| std::fs::remove_file(&self.path) | |
| .context(format!("failed to remove existing {}", self.path.display()))?; | |
| } | |
| std::fs::rename(&tmp, &self.path) | |
| .context(format!("failed to rename to {}", self.path.display()))?; | |
| } | |
| // On non-Windows platforms, a simple rename is sufficient and atomic. | |
| #[cfg(not(windows))] | |
| { | |
| std::fs::rename(&tmp, &self.path) | |
| .context(format!("failed to rename to {}", self.path.display()))?; | |
| } |
| session.config.network = Some(network.clone()); | ||
| output::print_dim(&format!( | ||
| " Set default network to {network} (from legacy config)" |
There was a problem hiding this comment.
apply_legacy_defaults stores network.clone() from the legacy config (often "Testnet"/"Mainnet" with capitalization), but parse_network_str is case-sensitive and only accepts lowercase strings. This means imported legacy network defaults may never be applied on next startup. Consider normalizing and persisting the canonical lowercase value (e.g., net_lower) when saving to session.config.network.
| session.config.network = Some(network.clone()); | |
| output::print_dim(&format!( | |
| " Set default network to {network} (from legacy config)" | |
| session.config.network = Some(net_lower.clone()); | |
| output::print_dim(&format!( | |
| " Set default network to {net_lower} (from legacy config)" |
|
Is there a design doc somewhere? There are lots of ideas and requirements for a new CLI. I'm wondering also if you want to vibe code this without reviews and design process, whether we should separate the Move tooling out of it. |
|
To given an example, any REPL or command line which calls into Move should use the Move language, complete with syntax type checking and inference. |
Fine, I'll just call it then aptos-cli again |
|
@cursoragent please rename aptos-repl to aptos-cli, and remove all references to "repl" |
|
I've successfully renamed Directory and Crate Rename:
Source Code Changes:
CI/Release Updates:
Documentation:
All 237 tests pass successfully. Changes have been committed and pushed to the |
- Rename crates/aptos-repl directory to crates/aptos-cli - Update crate name and binary name from aptos-repl to aptos-cli - Rename repl.rs to interactive.rs and run_repl() to run_interactive() - Replace Command::Repl with Command::Interactive - Update all REPL references in comments and documentation to CLI/interactive mode - Update CI workflow and release workflow to use aptos-cli - Update BUILD.md with new crate name - All 237 tests pass Co-authored-by: Greg Nazario <greg@gnazar.io>
Co-authored-by: Greg Nazario <greg@gnazar.io>


This doesn't include the compiler in it, but actually calls out to the other CLI, but this adds much requested features including a interactive CLI, and encrypted credentials.
TODO: