Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,9 @@ jobs:
- name: Run clippy
run: cargo clippy -- -D warnings

- name: Check for security vulnerabilities
run: |
if command -v cargo-audit &> /dev/null; then
cargo audit
else
echo "cargo-audit not installed, skipping security check"
fi

- name: Build
run: cargo build --verbose

- name: Run tests
run: cargo test --verbose --all-features

- name: Install cargo-tarpaulin
run: |
cargo install cargo-tarpaulin --locked
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `config` subcommand for interactive configuration management
- Modal TUI dashboard for adding, editing, and deleting repositories
- Strict alphanumeric validation for repository owner and name fields

### Changed

- Updated key bindings for configuration dashboard (`e` for edit, `Enter` for edit/save)
- Refactored configuration management to use state-based UI logic
## [0.2.0] - 2025-11-21

### Added
Expand Down
127 changes: 124 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "github-secrets"
version = "0.1.0"
version = "0.3.0"
edition = "2024"
authors = ["Sudhanshu Ranjan <perfectsudh@gmail.com>"]
description = "A CLI tool to update GitHub repository secrets interactively"
Expand Down Expand Up @@ -28,6 +28,7 @@ colored = "2.1"
dirs = "5.0"
regex = "1.10"
async-trait = "0.1"
clap = { version = "4.5.53", features = ["derive"] }

[dev-dependencies]
serial_test = "0.5"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,29 @@ cargo run
github-secrets
```

### Interactive Configuration

You can manage your repositories directly through the interactive configuration dashboard:

```bash
github-secrets config
```

**Dashboard Features:**
- **Recall**: View your list of configured repositories
- **Add**: Press `a` to add a new repository
- **Edit**: Press `e` or `Enter` on a selected repository to edit details
- **Delete**: Press `d` to remove a repository
- **Navigation**: Use Up/Down arrows to scroll
- **Validation**: Owner and Name fields strictly accept alphanumeric characters, underscores, and hyphens.

**Key Bindings:**
- `↑`/`↓`: Navigate list
- `a`: Add repository
- `e` / `Enter`: Edit repository
- `d`: Delete repository
- `q` / `Esc`: Save and Quit dashboard

### Workflow

1. **Select repositories**: Choose one or more repositories from the interactive menu, or select "Select All" to update all repositories
Expand Down
58 changes: 58 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,64 @@ impl App {
Self::run_with_deps(&factory, &prompt_impl, &mut rate_limiter, token, config).await
}

pub async fn config() -> Result<()> {
let _factory = RealGitHubApiFactory;
let prompt_impl = RealPrompt;
let _rate_limiter = RealRateLimiter::new();
// We don't need factory or rate limiter for config really, but we need prompt.

Self::config_with_deps(&prompt_impl).await
}

pub async fn config_with_deps<P>(prompt_impl: &P) -> Result<()>
where
P: PromptInterface,
{
// 1. Check if config exists or use creation path
let config_path = match paths::find_config_file() {
Ok(path) => path,
Err(_) => paths::get_config_creation_path(),
};

// 2. Load existing config or default to empty
let initial_config = if config_path.exists() {
match config::Config::from_file(config_path.to_str().unwrap()) {
Ok(cfg) => cfg,
Err(e) => {
println!("Warning: Failed to parse existing config: {}", e);
// Ask user if they want to overwrite? Or just show empty?
// For now, let's treat as empty/new to allow recovery via UI
config::Config {
repositories: vec![],
repository: None,
}
}
}
} else {
config::Config {
repositories: vec![],
repository: None,
}
};

// 3. Launch TUI Dashboard
if let Some(new_config) = prompt_impl.manage_config(initial_config)? {
// 4. Save if changed/requested
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)?;
}

println!("Saving configuration to {}...", config_path.display());
let toml_string = toml::to_string(&new_config)?;
std::fs::write(&config_path, toml_string)?;
println!("{}", "Configuration saved successfully.".green());
} else {
println!("Configuration unchanged.");
}

Ok(())
}

/// Same logic as `run` but with injectable dependencies to enable testing.
pub async fn run_with_deps<F, P, RL>(
factory: &F,
Expand Down
16 changes: 11 additions & 5 deletions src/app_deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,31 @@ pub trait PromptInterface: Send + Sync {
fn prompt_secrets(&self) -> Result<Vec<prompt::SecretPair>>;
fn confirm_secret_update(&self, key: &str, last_updated: Option<&str>) -> Result<bool>;
fn confirm_retry(&self) -> Result<bool>;

fn manage_config(&self, initial: config::Config) -> Result<Option<config::Config>>;
}

pub struct RealPrompt;

impl PromptInterface for RealPrompt {
fn select_repositories(&self, repositories: &[config::Repository]) -> Result<Vec<usize>> {
prompt::select_repositories(repositories)
crate::prompt::select_repositories(repositories)
}

fn prompt_secrets(&self) -> Result<Vec<prompt::SecretPair>> {
prompt::prompt_secrets()
crate::prompt::prompt_secrets()
}

fn confirm_secret_update(&self, key: &str, last_updated: Option<&str>) -> Result<bool> {
prompt::confirm_secret_update(key, last_updated)
fn confirm_secret_update(&self, name: &str, last_updated: Option<&str>) -> Result<bool> {
crate::prompt::confirm_secret_update(name, last_updated)
}

fn confirm_retry(&self) -> Result<bool> {
prompt::confirm_retry()
crate::prompt::confirm_retry()
}

fn manage_config(&self, initial: config::Config) -> Result<Option<config::Config>> {
crate::prompt::manage_config(initial)
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}

#[derive(Subcommand)]
pub enum Commands {
/// Configure the application (view or edit settings)
Config,
}
Loading