Implement porthole v0.1: profiles, supervised open/close, reconnect
Full CLI per spec v0.2 - add/open/close/edit/status/list/remove/wipe/ completions, plus a hidden `__supervise` subcommand that IS the supervisor process. - profile.rs: TOML-backed profiles at ~/.config/porthole/profiles/, validated -l/-r/-d mapping + --via grammar, atomic writes. - instance.rs: JSON runtime state at ~/.local/state/porthole/, an flock-based lock file that's the source of truth for "is this open" (survives a crash/kill -9 without stale-lock cleanup), pid liveness checked against /proc rather than trusted from disk. - supervisor.rs: the __supervise loop - spawns ssh, traps SIGTERM/SIGINT into a flag (rather than inferring intent from ssh's exit status), classifies failures as fatal/known-transient/unrecognized, backs off with a stability-reset, rotates its log. - ssh.rs: builds the ssh invocation, including splitting --via into a -J jump chain plus the mandatory positional target. - open.rs: the detach/re-exec dance (setsid via pre_exec) and a bounded wait for the supervisor to reach Up/Error before open returns, so an immediate failure surfaces as a non-zero exit instead of a false "opened" - this took a real bug fix during smoke testing, since the instance file's initial state (Reconnecting, meaning "attempt in flight") was indistinguishable from "already failed once" by state alone. - close.rs: SIGTERM+wait, or SIGKILL the whole process group with --force so ssh can't be left orphaned. Smoke-tested against invalid/unreachable hosts (no real infrastructure touched): CLI surface, validation errors, add/edit/list/status/remove, the reconnect/backoff loop with live state transitions, close mid-retry, edit-while-running's warning, open --all, and wipe. cargo test: 14/14. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
55
src/ui.rs
Normal file
55
src/ui.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Colorized status output (NO_COLOR-aware). Same conventions as vmic's
|
||||
//! `ui.rs` - kept identical on purpose so the two tools feel like one family.
|
||||
|
||||
use std::io::IsTerminal;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
fn color_enabled() -> bool {
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*ENABLED.get_or_init(|| {
|
||||
std::env::var_os("NO_COLOR").is_none()
|
||||
&& std::env::var("TERM").map(|t| t != "dumb").unwrap_or(true)
|
||||
&& std::io::stdout().is_terminal()
|
||||
&& std::io::stderr().is_terminal()
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(code: &str, s: &str) -> String {
|
||||
if color_enabled() {
|
||||
format!("\x1b[{code}m{s}\x1b[0m")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn red(s: &str) -> String {
|
||||
paint("31", s)
|
||||
}
|
||||
|
||||
pub fn yellow(s: &str) -> String {
|
||||
paint("33", s)
|
||||
}
|
||||
|
||||
pub fn green(s: &str) -> String {
|
||||
paint("32", s)
|
||||
}
|
||||
|
||||
pub fn blue(s: &str) -> String {
|
||||
paint("34", s)
|
||||
}
|
||||
|
||||
pub fn err(msg: &str) {
|
||||
eprintln!("{}", red(&format!("Error: {msg}")));
|
||||
}
|
||||
|
||||
pub fn warn(msg: &str) {
|
||||
eprintln!("{}", yellow(&format!("Warning: {msg}")));
|
||||
}
|
||||
|
||||
pub fn info(msg: &str) {
|
||||
println!("{}", blue(msg));
|
||||
}
|
||||
|
||||
pub fn ok(msg: &str) {
|
||||
println!("{}", green(msg));
|
||||
}
|
||||
Reference in New Issue
Block a user