diff options
| author | Sebastian Korotkiewicz <skorotkiewicz@gmail.com> | 2026-08-10 03:01:52 +0200 |
|---|---|---|
| committer | Sebastian Korotkiewicz <skorotkiewicz@gmail.com> | 2026-08-10 03:01:52 +0200 |
| signature | Sebastian Korotkiewicz <skorotkiewicz@gmail.com> This commit was signed with a verified signature.
| |
| commit | 672fec0852fb45410b6d2e58bce17469768fa98a (patch) | |
| tree | fc992b4dd93bd0a728f54f8ddbbbc385381afe51 | |
| parent | d212af6e8f4236be97b5e768462d9ca3d7f5e904 (diff) | |
| download | aurcade-672fec0852fb45410b6d2e58bce17469768fa98a.tar.gz aurcade-672fec0852fb45410b6d2e58bce17469768fa98a.zip | |
Add interactive SSH lobby for repository browsing
When users connect via SSH without a Git command, display an
arcade-themed
lobby showing their accessible repositories ("cartridges") with clone
URLs.
Repositories are filtered by account permissions and marked as CO-OP for
shared sections or READY for personal ones.
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | src/main.rs | 88 |
2 files changed, 93 insertions, 1 deletions
@@ -76,6 +76,12 @@ paths = ["example", "team/tools"] # An exact path grants access to one repositor paths = ["alice/"] # Namespace; creates repositories on first push. ``` +Connect without a Git command to open the account's SSH lobby: + +```sh +ssh -p 2222 git@localhost +``` + ## Git ```sh diff --git a/src/main.rs b/src/main.rs index ee94426..cc99aee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -512,13 +512,64 @@ fn cleanup_failed_creation(path: &Path, created: bool, success: bool) -> Result< Ok(()) } +fn ssh_lobby(config: &Config, account: &Account, root: &Path) -> Result<String, Error> { + let mut repositories = BTreeSet::new(); + configured_repositories(config, root, root, &mut repositories)?; + repositories.retain(|repository| { + account + .paths + .iter() + .any(|rule| path_matches(rule, repository)) + }); + + let mut output = format!( + concat!( + "========================================\n", + " A U R C A D E\n", + " INSERT KEY TO CONTINUE\n", + "========================================\n", + "HOST: {}\n", + "PLAYER 1: {} [KEY ACCEPTED]\n\n", + "AVAILABLE CARTRIDGES\n" + ), + config.title, account.name + ); + if repositories.is_empty() { + output.push_str("-- NO CARTRIDGES LOADED\n"); + } + for (index, repository) in repositories.iter().enumerate() { + let state = if repository_section(config, repository) == Some("shared") { + "CO-OP" + } else { + "READY" + }; + output.push_str(&format!("{:02} {repository} [{state}]\n", index + 1)); + for prefix in config.clone_prefix.split_whitespace() { + output.push_str(&format!( + " {}/{repository}\n", + prefix.trim_end_matches('/') + )); + } + } + output.push_str("\nNO SHELL. ONLY GIT. GAME ON.\n"); + Ok(output) +} + fn serve(config: &Config, account_name: &str) -> Result<(), Error> { let account = config .accounts .iter() .find(|account| account.name == account_name) .ok_or("unknown account")?; - let original = env::var("SSH_ORIGINAL_COMMAND").map_err(|_| "Git command required")?; + let original = match env::var("SSH_ORIGINAL_COMMAND") { + Ok(command) if !command.trim().is_empty() => command, + Ok(_) | Err(env::VarError::NotPresent) => { + print!("{}", ssh_lobby(config, account, &repo_root())?); + io::stdout().flush()?; + return Ok(()); + } + Err(error) => return Err(error.into()), + }; let (action, repository) = parse_git_command(&original)?; let configured = config @@ -679,6 +730,41 @@ mod tests { } #[test] + fn renders_account_ssh_lobby() { + let root = test_directory("ssh-lobby"); + for repository in ["alice/game", "team/tools", "bob/private", "hidden"] { + let path = root.join(format!("{repository}.git")); + fs::create_dir_all(&path).unwrap(); + fs::write(path.join("HEAD"), "ref: refs/heads/main\n").unwrap(); + } + let config: Config = toml::from_str( + r#" + title = "Test Cabinet" + clone_prefix = "https://git.example ssh://git@git.example" + [[accounts]] + name = "alice" + ssh_keys = [] + paths = ["alice/", "team/tools"] + [[accounts]] + name = "bob" + ssh_keys = [] + paths = ["bob/", "team/tools"] + "#, + ) + .unwrap(); + + let output = ssh_lobby(&config, &config.accounts[0], &root).unwrap(); + assert!(output.contains("PLAYER 1: alice [KEY ACCEPTED]")); + assert!(output.contains("01 alice/game [READY]")); + assert!(output.contains("02 team/tools [CO-OP]")); + assert!(output.contains("https://git.example/alice/game")); + assert!(output.contains("ssh://git@git.example/team/tools")); + assert!(!output.contains("bob/private")); + assert!(!output.contains("hidden")); + fs::remove_dir_all(root).unwrap(); + } + + #[test] fn assigns_account_and_shared_sections() { let config: Config = toml::from_str( r#" |