Skip to content

Commit 3bb365a

Browse files
author
Michael Taylor
committed
Add global non-interactive mode flag for all CLI commands
- Add --non-interactive flag as global option for automation and CI/CD - Update CLI context to track non-interactive mode - Modify all interactive prompts to respect non-interactive flag: - Wallet initialization prompts - Repository overwrite confirmations - User configuration prompts - Update installation prompts - Staging area operations - Use sensible defaults in non-interactive mode - Enhance Windows file locking fix with better cleanup - Support automation and scripting workflows Usage: digstore --non-interactive <command> Suppresses all prompts and uses default values for automated environments.
1 parent 3cd213c commit 3bb365a

File tree

10 files changed

+165
-80
lines changed

10 files changed

+165
-80
lines changed

src/cli/commands/staged/list.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,14 @@ pub fn clear_staged(json: bool, force: bool) -> Result<()> {
228228
);
229229
println!();
230230

231-
let confirmed = Confirm::new()
232-
.with_prompt("Are you sure you want to clear all staged files?")
233-
.default(false)
234-
.interact()?;
231+
let confirmed = if crate::cli::context::CliContext::should_auto_accept() {
232+
true // Auto-confirm in non-interactive or yes mode
233+
} else {
234+
Confirm::new()
235+
.with_prompt("Are you sure you want to clear all staged files?")
236+
.default(false)
237+
.interact()?
238+
};
235239

236240
if !confirmed {
237241
if json {

src/cli/commands/update.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ fn execute_interactive_mode(check_only: bool, force: bool) -> Result<()> {
5757
println!();
5858
let should_update = if force {
5959
true
60+
} else if crate::cli::context::CliContext::is_non_interactive() {
61+
false // Don't auto-update in non-interactive mode unless forced
6062
} else {
6163
Confirm::new()
6264
.with_prompt("Would you like to download and install the update?")

src/cli/context.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct CliContext {
1515
pub verbose: bool,
1616
pub quiet: bool,
1717
pub yes: bool,
18+
pub non_interactive: bool,
1819
pub custom_encryption_key: Option<String>,
1920
pub custom_decryption_key: Option<String>,
2021
}
@@ -52,6 +53,16 @@ impl CliContext {
5253
Self::get().map(|ctx| ctx.yes).unwrap_or(false)
5354
}
5455

56+
/// Check if non-interactive mode is enabled
57+
pub fn is_non_interactive() -> bool {
58+
Self::get().map(|ctx| ctx.non_interactive).unwrap_or(false)
59+
}
60+
61+
/// Check if we should auto-accept prompts (yes flag OR non-interactive mode)
62+
pub fn should_auto_accept() -> bool {
63+
Self::is_yes() || Self::is_non_interactive()
64+
}
65+
5566
/// Get custom encryption key from the current context
5667
pub fn get_custom_encryption_key() -> Option<String> {
5768
Self::get().and_then(|ctx| ctx.custom_encryption_key)

src/cli/interactive.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ pub fn ask_recreate_store(archive_path: &Path, store_id_hex: &str, auto_yes: boo
2828
println!(" Store ID: {}", store_id_hex.cyan());
2929
println!();
3030

31-
let recreate = Confirm::new()
32-
.with_prompt("Would you like to recreate this store?")
33-
.default(true)
34-
.interact()?;
31+
let recreate = if crate::cli::context::CliContext::should_auto_accept() {
32+
true // Auto-accept in non-interactive or yes mode
33+
} else {
34+
Confirm::new()
35+
.with_prompt("Would you like to recreate this store?")
36+
.default(true)
37+
.interact()?
38+
};
3539

3640
Ok(recreate)
3741
}
@@ -60,10 +64,14 @@ pub fn ask_overwrite_digstore(digstore_path: &Path, auto_yes: bool) -> Result<bo
6064
);
6165
println!();
6266

63-
let overwrite = Confirm::new()
64-
.with_prompt("Are you sure you want to overwrite the existing repository?")
65-
.default(false)
66-
.interact()?;
67+
let overwrite = if crate::cli::context::CliContext::should_auto_accept() {
68+
true // Auto-accept in non-interactive or yes mode
69+
} else {
70+
Confirm::new()
71+
.with_prompt("Are you sure you want to overwrite the existing repository?")
72+
.default(false)
73+
.interact()?
74+
};
6775

6876
Ok(overwrite)
6977
}
@@ -79,10 +87,14 @@ pub fn interactive_store_recreation(project_path: &Path) -> Result<Store> {
7987
.and_then(|n| n.to_str())
8088
.unwrap_or("my-project");
8189

82-
let repo_name: String = Input::new()
83-
.with_prompt("Repository name")
84-
.default(default_name.to_string())
85-
.interact_text()?;
90+
let repo_name: String = if crate::cli::context::CliContext::is_non_interactive() {
91+
default_name.to_string() // Use default in non-interactive mode
92+
} else {
93+
Input::new()
94+
.with_prompt("Repository name")
95+
.default(default_name.to_string())
96+
.interact_text()?
97+
};
8698

8799
println!();
88100
println!("Creating repository '{}'...", repo_name.cyan());

src/cli/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ pub struct Cli {
3636
#[arg(short = 'y', long, global = true)]
3737
pub yes: bool,
3838

39+
/// Non-interactive mode - suppress all prompts and use defaults
40+
#[arg(long, global = true)]
41+
pub non_interactive: bool,
42+
3943
/// Path to store directory
4044
#[arg(long, global = true)]
4145
pub store: Option<PathBuf>,

src/config/global_config.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -399,25 +399,33 @@ impl GlobalConfig {
399399
.or_else(|_| std::env::var("USERNAME"))
400400
.unwrap_or_else(|_| "".to_string());
401401

402-
let name: String = Input::new()
403-
.with_prompt("Your name")
404-
.default(default_name)
405-
.interact_text()
406-
.map_err(|e| DigstoreError::ConfigurationError {
407-
reason: format!("Failed to get user input: {}", e),
408-
})?;
402+
let name: String = if crate::cli::context::CliContext::is_non_interactive() {
403+
default_name // Use default in non-interactive mode
404+
} else {
405+
Input::new()
406+
.with_prompt("Your name")
407+
.default(default_name)
408+
.interact_text()
409+
.map_err(|e| DigstoreError::ConfigurationError {
410+
reason: format!("Failed to get user input: {}", e),
411+
})?
412+
};
409413

410414
self.user.name = Some(name);
411415
}
412416

413417
// Get email if not set
414418
if self.user.email.is_none() {
415-
let email: String = Input::new()
416-
.with_prompt("Your email")
417-
.interact_text()
418-
.map_err(|e| DigstoreError::ConfigurationError {
419-
reason: format!("Failed to get user input: {}", e),
420-
})?;
419+
let email: String = if crate::cli::context::CliContext::is_non_interactive() {
420+
"user@example.com".to_string() // Use placeholder in non-interactive mode
421+
} else {
422+
Input::new()
423+
.with_prompt("Your email")
424+
.interact_text()
425+
.map_err(|e| DigstoreError::ConfigurationError {
426+
reason: format!("Failed to get user input: {}", e),
427+
})?
428+
};
421429

422430
self.user.email = Some(email);
423431
}

src/main.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fn main() -> Result<()> {
6262
verbose: cli.verbose,
6363
quiet: cli.quiet,
6464
yes: cli.yes,
65+
non_interactive: cli.non_interactive,
6566
custom_encryption_key,
6667
custom_decryption_key,
6768
});
@@ -85,8 +86,8 @@ fn main() -> Result<()> {
8586
wallet_manager.ensure_wallet_initialized()?;
8687
}
8788

88-
// Check for updates (unless running update command or in quiet mode)
89-
if !matches!(cli.command, Commands::Update { .. }) && !cli.quiet {
89+
// Check for updates (unless running update command, in quiet mode, or non-interactive)
90+
if !matches!(cli.command, Commands::Update { .. }) && !cli.quiet && !cli.non_interactive {
9091
check_and_prompt_for_updates()?;
9192
}
9293

@@ -387,11 +388,15 @@ fn check_and_prompt_for_updates() -> Result<()> {
387388
update_info.latest_version.bright_green()
388389
);
389390

390-
let should_update = Confirm::new()
391-
.with_prompt("Would you like to download and install the update now?")
392-
.default(false)
393-
.interact()
394-
.unwrap_or(false);
391+
let should_update = if crate::cli::context::CliContext::is_non_interactive() {
392+
false // Don't auto-update in non-interactive mode
393+
} else {
394+
Confirm::new()
395+
.with_prompt("Would you like to download and install the update now?")
396+
.default(false)
397+
.interact()
398+
.unwrap_or(false)
399+
};
395400

396401
if should_update {
397402
println!();

src/storage/store.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,29 @@ impl Store {
7575
if let Ok(existing_digstore) = crate::core::digstore_file::DigstoreFile::load(&digstore_path) {
7676
if let Ok(existing_store_id) = existing_digstore.get_store_id() {
7777
let archive_path = get_archive_path(&existing_store_id)?;
78-
// Force close any memory maps by attempting to truncate and sync
79-
if archive_path.exists() {
80-
let _ = std::fs::OpenOptions::new()
81-
.write(true)
82-
.open(&archive_path)
83-
.and_then(|file| file.sync_all());
78+
let staging_path = archive_path.with_extension("staging.bin");
79+
80+
// Force close any memory maps and file handles
81+
for path in [&archive_path, &staging_path] {
82+
if path.exists() {
83+
// Try multiple approaches to release file locks
84+
let _ = std::fs::OpenOptions::new()
85+
.read(true)
86+
.open(path)
87+
.and_then(|file| file.sync_all());
88+
89+
// Force garbage collection to release any Rust file handles
90+
drop(std::fs::File::open(path));
91+
}
8492
}
8593
}
8694
}
8795

88-
// Small delay to ensure file handles are released on Windows
89-
std::thread::sleep(std::time::Duration::from_millis(100));
96+
// Longer delay to ensure all file handles are released on Windows
97+
std::thread::sleep(std::time::Duration::from_millis(500));
98+
99+
// Try to force garbage collection
100+
std::hint::black_box(());
90101

91102
// Remove existing .digstore file to proceed with new initialization
92103
std::fs::remove_file(&digstore_path)?;

src/wallet/wallet_manager.rs

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,18 @@ impl WalletManager {
319319
"Cancel",
320320
];
321321

322-
let selection = Select::new()
323-
.with_prompt("What would you like to do?")
324-
.items(&options)
325-
.default(0)
326-
.interact()
327-
.map_err(|e| DigstoreError::ConfigurationError {
328-
reason: format!("Failed to get user input: {}", e),
329-
})?;
322+
let selection = if crate::cli::context::CliContext::is_non_interactive() {
323+
0 // Default to "Generate new mnemonic" in non-interactive mode
324+
} else {
325+
Select::new()
326+
.with_prompt("What would you like to do?")
327+
.items(&options)
328+
.default(0)
329+
.interact()
330+
.map_err(|e| DigstoreError::ConfigurationError {
331+
reason: format!("Failed to get user input: {}", e),
332+
})?
333+
};
330334

331335
match selection {
332336
0 => self.generate_new_wallet_with_context(is_first_time),
@@ -413,13 +417,17 @@ impl WalletManager {
413417
println!();
414418

415419
// Confirm user has written down the mnemonic
416-
let confirmed = Confirm::new()
417-
.with_prompt("Have you securely written down your mnemonic phrase?")
418-
.default(false)
419-
.interact()
420-
.map_err(|e| DigstoreError::ConfigurationError {
421-
reason: format!("Failed to get user confirmation: {}", e),
422-
})?;
420+
let confirmed = if crate::cli::context::CliContext::is_non_interactive() {
421+
true // Auto-confirm in non-interactive mode
422+
} else {
423+
Confirm::new()
424+
.with_prompt("Have you securely written down your mnemonic phrase?")
425+
.default(false)
426+
.interact()
427+
.map_err(|e| DigstoreError::ConfigurationError {
428+
reason: format!("Failed to get user confirmation: {}", e),
429+
})?
430+
};
423431

424432
if !confirmed {
425433
println!();
@@ -483,12 +491,18 @@ impl WalletManager {
483491
println!("Please enter your mnemonic phrase:");
484492
println!();
485493

486-
let mnemonic: String = Input::new()
487-
.with_prompt("Mnemonic phrase")
488-
.interact_text()
489-
.map_err(|e| DigstoreError::ConfigurationError {
490-
reason: format!("Failed to get mnemonic input: {}", e),
491-
})?;
494+
let mnemonic: String = if crate::cli::context::CliContext::is_non_interactive() {
495+
return Err(DigstoreError::ConfigurationError {
496+
reason: "Cannot import wallet in non-interactive mode. Use --auto-import-mnemonic flag instead.".to_string(),
497+
});
498+
} else {
499+
Input::new()
500+
.with_prompt("Mnemonic phrase")
501+
.interact_text()
502+
.map_err(|e| DigstoreError::ConfigurationError {
503+
reason: format!("Failed to get mnemonic input: {}", e),
504+
})?
505+
};
492506

493507
let rt = tokio::runtime::Runtime::new().map_err(|e| DigstoreError::ConfigurationError {
494508
reason: format!("Failed to create tokio runtime: {}", e),
@@ -517,14 +531,18 @@ impl WalletManager {
517531
"Cancel",
518532
];
519533

520-
let selection = Select::new()
521-
.with_prompt("What would you like to do?")
522-
.items(&options)
523-
.default(0)
524-
.interact()
525-
.map_err(|e| DigstoreError::ConfigurationError {
526-
reason: format!("Failed to get user input: {}", e),
527-
})?;
534+
let selection = if crate::cli::context::CliContext::is_non_interactive() {
535+
1 // Default to "Delete and create new wallet" in non-interactive mode
536+
} else {
537+
Select::new()
538+
.with_prompt("What would you like to do?")
539+
.items(&options)
540+
.default(0)
541+
.interact()
542+
.map_err(|e| DigstoreError::ConfigurationError {
543+
reason: format!("Failed to get user input: {}", e),
544+
})?
545+
};
528546

529547
match selection {
530548
0 => {
@@ -533,13 +551,17 @@ impl WalletManager {
533551
},
534552
1 => {
535553
// Confirm deletion
536-
let confirmed = Confirm::new()
537-
.with_prompt("This will permanently delete your current wallet. Are you sure?")
538-
.default(false)
539-
.interact()
540-
.map_err(|e| DigstoreError::ConfigurationError {
541-
reason: format!("Failed to get user confirmation: {}", e),
542-
})?;
554+
let confirmed = if crate::cli::context::CliContext::should_auto_accept() {
555+
true // Auto-confirm in non-interactive or yes mode
556+
} else {
557+
Confirm::new()
558+
.with_prompt("This will permanently delete your current wallet. Are you sure?")
559+
.default(false)
560+
.interact()
561+
.map_err(|e| DigstoreError::ConfigurationError {
562+
reason: format!("Failed to get user confirmation: {}", e),
563+
})?
564+
};
543565

544566
if confirmed {
545567
// Try to delete the wallet using dig-wallet API

test_noninteractive/.digstore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version = "1.0.0"
2+
store_id = "08402bf6a9c4e3d7b75ea30022cf892f0f3293498ca0c1623a6766cf9f8caf6f"
3+
encrypted = false
4+
created_at = "2025-09-08T18:47:51.159139200+00:00"
5+
last_accessed = "2025-09-08T18:47:51.159139200+00:00"
6+
repository_name = "test_noninteractive"

0 commit comments

Comments
 (0)