Skip to content

[cli] Add a new Interactive CLI to replace the original Aptos CLI#28

Open
gregnazario wants to merge 15 commits intomainfrom
add-cli
Open

[cli] Add a new Interactive CLI to replace the original Aptos CLI#28
gregnazario wants to merge 15 commits intomainfrom
add-cli

Conversation

@gregnazario
Copy link
Contributor

@gregnazario gregnazario commented Feb 6, 2026

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:

  1. Add the move compiler natively in (still keep local testnet elsewhere?)
  2. Ensure name substitution
  3. Allow for deleting old credentials
  4. Add a recovery key to the encryption
  5. Add a useful TUI if it can be helpful for viewing.
  6. Release automation
  7. Automatic updating

gregnazario and others added 10 commits February 6, 2026 07:41
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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-repl binary crate with REPL loop, command dispatch, and SDK-backed subcommands (account/key/move/tx/info).
  • Adds encrypted credential vault + legacy ~/.aptos/config.yaml import 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.

Comment on lines +283 to +300
// 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);
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
//! 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.
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +548 to +549
eprintln!("\nCompilation failed. Check the errors above.");
std::process::exit(1);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
eprintln!("\nCompilation failed. Check the errors above.");
std::process::exit(1);
anyhow::bail!("Compilation failed. Check the errors above.");

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +607
eprintln!("\nSome tests failed. Check the output above.");
std::process::exit(1);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
eprintln!("\nSome tests failed. Check the output above.");
std::process::exit(1);
anyhow::bail!("\nSome tests failed. Check the output above.");

Copilot uses AI. Check for mistakes.
Comment on lines +698 to +700
eprintln!("Transaction failed: {vm_status}");
std::process::exit(1);
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +149
/// 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;
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +152
// 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")?;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +787 to +788
eprintln!("Publish failed: {vm_status}");
std::process::exit(1);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
eprintln!("Publish failed: {vm_status}");
std::process::exit(1);
anyhow::bail!("Publish failed: {vm_status}");

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +317
std::fs::rename(&tmp, &self.path)
.context(format!("failed to rename to {}", self.path.display()))?;

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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()))?;
}

Copilot uses AI. Check for mistakes.
Comment on lines +809 to +811
session.config.network = Some(network.clone());
output::print_dim(&format!(
" Set default network to {network} (from legacy config)"
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)"

Copilot uses AI. Check for mistakes.
@wrwg
Copy link

wrwg commented Feb 7, 2026

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.

@wrwg
Copy link

wrwg commented Feb 7, 2026

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.

@gregnazario
Copy link
Contributor Author

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

@gregnazario gregnazario changed the title [cli] Add a new REPL to replace the original Aptos CLI [cli] Add a new Interactive CLI to replace the original Aptos CLI Feb 7, 2026
@gregnazario
Copy link
Contributor Author

@cursoragent please rename aptos-repl to aptos-cli, and remove all references to "repl"

@cursor
Copy link

cursor bot commented Feb 7, 2026

I've successfully renamed aptos-repl to aptos-cli and removed all references to "repl". Here's what was changed:

Directory and Crate Rename:

  • Renamed crates/aptos-repl to crates/aptos-cli
  • Updated package name from aptos-repl to aptos-cli in Cargo.toml
  • Binary name changed from aptos-repl to aptos-cli

Source Code Changes:

  • Renamed repl.rs to interactive.rs
  • Renamed run_repl() function to run_interactive()
  • Changed Command::Repl enum variant to Command::Interactive
  • Updated the command line subcommand from repl to interactive
  • Updated CLI history file path from repl_history to cli_history
  • Updated banner text and all comments/documentation referencing "REPL"

CI/Release Updates:

  • Updated .github/workflows/ci.yml to reference aptos-cli
  • Updated .github/workflows/cli-release.yml to use aptos-cli and release name "Aptos CLI"

Documentation:

  • Updated BUILD.md with new crate/binary name in all examples

All 237 tests pass successfully. Changes have been committed and pushed to the add-cli branch.

Open in Cursor Open in Web

cursoragent and others added 4 commits February 7, 2026 03:23
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants