Skip to content

Conversation

@katspaugh
Copy link
Member

Summary

This PR implements Phase 2 of the non-interactive CLI enhancement, adding full argument support to all wallet commands. This enables automation and CI/CD scenarios.

Changes

wallet import

  • Add --private-key <key> option to import without prompting
  • Add --name <name> option to set wallet name
  • Support JSON output mode with --json flag

wallet create

  • Add --name <name> option to set wallet name
  • Add --skip-backup-warning flag to bypass security confirmation
  • Include private key in JSON output for automation scenarios
  • Skip backup verification in non-interactive mode

wallet use

  • Add --address <address> option to switch wallet by address
  • Add --name <name> option to switch wallet by name
  • Support JSON output mode with --json flag

wallet remove

  • Add --address <address> option to remove wallet by address
  • Add --name <name> option to remove wallet by name
  • Add --force flag to skip confirmation prompts
  • Support JSON output mode with --json flag

All commands maintain backward compatibility with interactive mode when no arguments are provided.

Examples

# Import wallet non-interactively
safe wallet import --private-key 0x123... --name my-wallet --password-file ./pw.txt --json

# Create wallet in CI
safe wallet create --name ci-wallet --skip-backup-warning --json

# Switch wallet by address
safe wallet use --address 0x742d35... --json

# Remove wallet without confirmation
safe wallet remove --name old-wallet --force --json

Test plan

  • Build passes
  • Type check passes
  • Lint passes
  • Manual testing of each command with and without arguments
  • Verify JSON output format
  • Test error handling with invalid arguments
  • Verify backward compatibility with interactive mode

Related

Part of the phased plan to add argument support to all CLI commands. This completes Phase 2 (wallet commands). Next phases will cover account and transaction commands.

🤖 Generated with Claude Code

katspaugh and others added 3 commits November 7, 2025 16:01
Add command-line argument support for wallet import to enable non-interactive usage:

Changes:
- Added --private-key and --name options to wallet import command
- Updated importWallet() to accept WalletImportOptions
- Integrated password-handler for secure password input (env > file > CLI > prompt)
- Added JSON output mode support via outputSuccess()
- Added proper exit codes for different failure scenarios
- Security warnings when password provided via CLI args

Usage examples:
  # Interactive (existing behavior)
  safe wallet import

  # Non-interactive with env password
  SAFE_WALLET_PASSWORD="pass" safe wallet import --private-key="0x..." --name="My Wallet"

  # JSON output for automation
  safe wallet import --json --private-key="0x..." --name="Bot" --password="pass"

Benefits:
- Enables automation and CI/CD workflows
- Maintains backward compatibility (all args optional)
- Secure password handling with priority: env > file > CLI > prompt
- Machine-readable JSON output when --json flag used

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add command-line argument support for wallet create:

Changes:
- Added --name and --skip-backup-warning options
- Integrated password-handler for secure password input
- Skip backup verification in non-interactive mode or with flag
- Include private key in JSON output for automation
- Added proper exit codes for failures

Usage:
  # Non-interactive with auto-generated wallet
  SAFE_WALLET_PASSWORD="pass" safe wallet create \
    --name="Bot Wallet" \
    --skip-backup-warning

  # JSON output for automation
  safe wallet create --json \
    --name="CI Wallet" \
    --skip-backup-warning \
    --password="pass"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add CLI argument support for wallet use and remove commands to enable
non-interactive automation:

- wallet use: Add --address and --name options to select wallet
- wallet remove: Add --address, --name, and --force options
- Both commands support JSON output mode with --json flag
- Graceful fallback to interactive mode when no arguments provided

This completes Phase 2 of the wallet commands with full argument support
for automation and CI/CD scenarios.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings November 7, 2025 15:10
@katspaugh katspaugh merged commit d7c78f4 into main Nov 7, 2025
8 checks passed
@katspaugh katspaugh deleted the feat/wallet-arguments branch November 7, 2025 15:12
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

This pull request adds non-interactive mode support to wallet management commands, enabling programmatic usage and CI/CD automation. The changes introduce CLI options for specifying wallet parameters directly (--address, --name, --private-key, etc.) and add support for JSON output mode.

Key changes:

  • Added option interfaces and parameters to wallet commands (import, create, use, remove) for non-interactive operation
  • Implemented conditional UI rendering based on interactive vs non-interactive mode
  • Updated error handling to use standardized outputError and outputSuccess helpers with exit codes

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/commands/wallet/use.ts Added options for selecting wallets by address/name, non-interactive mode support
src/commands/wallet/remove.ts Added options for wallet selection and force flag to skip confirmations
src/commands/wallet/import.ts Added options for providing private key and name via CLI, integrated password handling utilities
src/commands/wallet/create.ts Added options for wallet name and backup warning skip, non-interactive mode support
src/cli.ts Added CLI option flags for all wallet commands to support programmatic usage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +58
// Interactive mode - prompt user to select
const selected = await p.select({
message: 'Select wallet to remove:',
options: wallets.map((w) => ({
value: w.id,
label: `${w.name} (${shortenAddress(w.address)})`,
})),
})

if (p.isCancel(confirm) || !confirm) {
p.cancel('Operation cancelled')
return
if (p.isCancel(selected)) {
p.cancel('Operation cancelled')
return
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

In non-interactive mode (JSON or quiet mode), if neither --address nor --name is provided, the code will attempt to show an interactive select prompt (line 47). This will likely fail or hang in non-interactive environments (CI/CD, scripts, etc.).

Consider adding validation to require either --address or --name when in non-interactive mode, and show an error if neither is provided. For example:

if (isNonInteractiveMode() && !options.address && !options.name) {
  outputError('Either --address or --name must be provided in non-interactive mode', ExitCode.INVALID_ARGS)
}

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +37
const wallet = wallets.find((w) => {
if (options.address) return w.address.toLowerCase() === options.address.toLowerCase()
if (options.name) return w.name === options.name
return false
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

The wallet lookup logic gives priority to address over name when both are provided (lines 34-37). If a user provides both --address and --name, only the address will be checked, and the name will be silently ignored. This could lead to unexpected behavior if the user expects both to be validated.

Consider either:

  1. Validating that only one option is provided and showing an error if both are given
  2. Validating that both match the same wallet if both are provided
  3. Documenting this priority behavior clearly in the CLI help text

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +147
let name = options.name
if (!name) {
const nameInput = await p.text({
message: 'Give this wallet a name:',
placeholder: 'my-wallet',
validate: (value) => validator.validateRequired(value, 'Wallet name'),
})

if (p.isCancel(name)) {
p.cancel('Operation cancelled')
return
if (p.isCancel(nameInput)) {
p.cancel('Operation cancelled')
return
}
name = nameInput as string
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

In non-interactive mode (JSON or quiet mode), if --name is not provided, the code will attempt to show an interactive text prompt (line 137). This will likely fail or hang in non-interactive environments.

Consider adding validation to require --name when in non-interactive mode, and show an error if it's not provided.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +84
let name = options.name
if (!name) {
const nameInput = await p.text({
message: 'Give this wallet a name:',
placeholder: 'my-wallet',
validate: (value) => validator.validateRequired(value, 'Wallet name'),
})

if (p.isCancel(name)) {
p.cancel('Operation cancelled')
return
if (p.isCancel(nameInput)) {
p.cancel('Operation cancelled')
return
}
name = nameInput as string
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

In non-interactive mode (JSON or quiet mode), if --name is not provided, the code will attempt to show an interactive text prompt (line 74). This will likely fail or hang in non-interactive environments.

Consider adding validation to require --name when in non-interactive mode, and show an error if it's not provided.

Copilot uses AI. Check for mistakes.

if (!wallet) {
const criteria = options.address ? `address: ${options.address}` : `name: ${options.name}`
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

The wallet variable is accessed before it's guaranteed to be assigned. If wallet is not found (line 40-43), outputError is called which exits the process, but TypeScript doesn't know this function never returns. The code at line 45 will try to access wallet.id when wallet is still undefined.

To fix this, ensure that after calling outputError on line 42, the code does not continue. Since outputError has a return type of never, add an explicit return statement or restructure to avoid the access.

Suggested change
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
return

Copilot uses AI. Check for mistakes.
name: wallet.name,
address: wallet.address,
type: wallet.type,
privateKey: privateKey, // Include in automation mode for immediate use
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

Outputting the private key in non-interactive mode (line 200) poses a significant security risk. Private keys should never be exposed in command outputs, logs, or console messages, even in automation scenarios. This could lead to:

  • Private keys being logged to files
  • Keys appearing in CI/CD logs
  • Accidental exposure in shared terminals or screenshots

Consider removing the privateKey field from the success output. If the private key must be available for automation, use a separate secure mechanism (e.g., write to a secure file with restricted permissions) rather than stdout.

Suggested change
privateKey: privateKey, // Include in automation mode for immediate use

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +63
} else {
// Interactive mode - prompt user to select
const currentActive = walletStorage.getActiveWallet()

const selected = await p.select({
message: 'Select wallet to use:',
options: wallets.map((wallet) => ({
value: wallet.id,
label: `${wallet.name} (${shortenAddress(wallet.address)})${
currentActive?.id === wallet.id ? ' [current]' : ''
}`,
})),
})

if (p.isCancel(selected)) {
p.cancel('Operation cancelled')
return
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

In non-interactive mode (JSON or quiet mode), if neither --address nor --name is provided, the code will attempt to show an interactive select prompt (line 50). This will likely fail or hang in non-interactive environments (CI/CD, scripts, etc.).

Consider adding validation to require either --address or --name when in non-interactive mode, and show an error if neither is provided.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +62
let privateKey = options.privateKey
if (!privateKey) {
const privateKeyInput = await p.password({
message: 'Enter your private key:',
validate: (value) => validator.validatePrivateKey(value),
})

if (!checkCancelled(privateKey)) return
if (!checkCancelled(privateKeyInput)) return
privateKey = privateKeyInput as string
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

In non-interactive mode (JSON or quiet mode), if --private-key is not provided, the code will attempt to show an interactive password prompt (line 56). This will likely fail or hang in non-interactive environments.

Consider adding validation to require --private-key when in non-interactive mode, and show an error if it's not provided.

Copilot uses AI. Check for mistakes.

if (!wallet) {
const criteria = options.address ? `address: ${options.address}` : `name: ${options.name}`
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

The wallet variable is accessed before it's guaranteed to be assigned. If wallet is not found (line 39-42), outputError is called which exits the process, but the code at line 44 will still try to access wallet.id when wallet is undefined.

Although outputError has a return type of never and exits the process, TypeScript may not infer this correctly without an explicit return or assertion.

Suggested change
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
outputError(`Wallet not found with ${criteria}`, ExitCode.WALLET_ERROR)
return

Copilot uses AI. Check for mistakes.
// Validate provided name
const error = validator.validateRequired(name, 'Wallet name')
if (error) {
outputError(error, ExitCode.INVALID_ARGS)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

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

Similar validation pattern issue as in import.ts. On lines 150-152, if the name validation returns an error, outputError is called, which exits the process. However, the code structure makes it appear that execution could continue, which is confusing for both readers and potentially for TypeScript's control flow analysis.

Consider restructuring for clearer control flow.

Suggested change
outputError(error, ExitCode.INVALID_ARGS)
outputError(error, ExitCode.INVALID_ARGS)
return

Copilot uses AI. Check for mistakes.
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.

2 participants