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:
31
src/commands/edit.rs
Normal file
31
src/commands/edit.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::cli::EditArgs;
|
||||
use crate::commands::{edits_from_mapping, mapping_is_empty};
|
||||
use crate::error::{PortholeError, Result};
|
||||
use crate::{instance, profile, ui};
|
||||
|
||||
pub fn run(args: EditArgs) -> Result<()> {
|
||||
if mapping_is_empty(&args.mapping) {
|
||||
return Err(PortholeError::NothingToDo(
|
||||
"pass at least one of -l/-r/-d, --via, --user, --identity, --port, --reconnect, \
|
||||
--retry-interval, --backoff-max, --keepalive."
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let name = profile::normalize(&args.name);
|
||||
let mut p = profile::load(&name)?;
|
||||
|
||||
let edits = edits_from_mapping(&args.mapping);
|
||||
p.apply_edits(&edits)?;
|
||||
profile::save(&p)?;
|
||||
|
||||
// Spec §5.4: edit never restarts a running instance - just warn.
|
||||
if instance::running_pid(&name)?.is_some() {
|
||||
ui::warn(&format!(
|
||||
"'{name}' is currently open; this change won't take effect until the next open/close cycle."
|
||||
));
|
||||
}
|
||||
|
||||
ui::ok(&format!("Updated profile '{name}'."));
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user