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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Attach-only CLI policy

- `pty attach --no-restart <ref>` attaches only to a currently running
session. Missing, exited, or vanished sessions fail without prompting or
executing retained launch metadata. Existing interactive prompting and
`--auto-restart` behavior are unchanged.

### Generation-safe removal and immediate same-name reuse

- `pty rm` now returns success only after the removed session's daemon has
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pty list --filter-tag role=web # show only sessions with matching tag

pty attach myserver # reconnect to a session
pty attach -r myserver # reconnect, auto-restart if exited
pty attach --no-restart myserver # attach only; fail if not running
pty exec -- codex # replace this session's process (inside a session)
pty peek myserver # print current screen and exit
pty peek --plain myserver # print as plain text (no ANSI)
Expand Down
2 changes: 1 addition & 1 deletion completions/pty.bash
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ _pty() {
;;
attach|a)
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "-r --auto-restart --force --remote" -- "${cur}"))
COMPREPLY=($(compgen -W "-r --auto-restart --no-restart --force --remote" -- "${cur}"))
else
COMPREPLY=($(compgen -W "${names}" -- "${cur}"))
fi
Expand Down
1 change: 1 addition & 0 deletions completions/pty.fish
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ complete -c pty -n '__pty_using_command run' -l cwd -d 'Working directory'
complete -c pty -n '__pty_using_command run' -l isolate-env -d 'Scrub env to a safe allow-list'
complete -c pty -n '__pty_using_command run' -l force -d 'Create even from inside another pty'
complete -c pty -n '__pty_using_command attach a' -l auto-restart -s r -d 'Auto-restart if the session is exited'
complete -c pty -n '__pty_using_command attach a' -l no-restart -d 'Attach only; never prompt or restart an exited session'
complete -c pty -n '__pty_using_command attach a' -l force -d 'Attach even from inside another pty'
complete -c pty -n '__pty_using_command attach a' -l remote -d 'Attach a session on a fabric peer'
complete -c pty -n '__pty_using_command attach a' -a '(__pty_sessions)' -d 'Session'
Expand Down
1 change: 1 addition & 0 deletions completions/pty.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ _pty() {
attach|a)
_arguments \
'(r --auto-restart){r,--auto-restart}[Auto-restart if the session is exited]' \
'--no-restart[Attach only; never prompt or restart an exited session]' \
'--force[Attach even from inside another pty]' \
'--remote[Attach a session on a fabric peer]' \
'1:session:_pty_sessions'
Expand Down
32 changes: 27 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,22 @@ Examples:
pty run -- node server.js
pty run -d --name "API" --tag role=web --env PORT=3000 -- node server.js`,

attach: `Usage: pty attach [-r] [--force] [--remote <peer>] <ref>
attach: `Usage: pty attach [-r|--no-restart] [--force] [--remote <peer>] <ref>

Reconnect to a session (alias: pty a). Detach again with Ctrl+\\.

Flags:
-r, --auto-restart Auto-restart the session if it has exited
--no-restart Attach only while the session is running; never prompt
or execute its stored command
--force Attach even from inside another pty session (nested)
--remote <peer> Attach a session on a fabric peer (over fabric); <ref> is
the session's name/id ON THE REMOTE

Examples:
pty attach myserver
pty attach -r myserver
pty attach --no-restart myserver
pty attach --remote hetzner myshell`,

exec: `Usage: pty exec -- <command> [args...]
Expand Down Expand Up @@ -412,6 +415,7 @@ Attach & interact:
pty attach <ref> Attach to an existing session (alias: pty a)
pty attach --force <ref> Attach even from inside another pty session (nested)
pty attach -r <ref> Attach, auto-restart if the session is exited
pty attach --no-restart <ref> Attach only; fail if the session is not running
pty attach --remote <peer> <ref> Attach a session on a fabric peer (over fabric)
pty exec -- <command> [args...] Replace the current session's process (inside a session)
pty send <ref> "text" Send raw text (no implicit newline)
Expand Down Expand Up @@ -885,12 +889,14 @@ async function main(): Promise<void> {
case "attach":
case "a": {
let autoRestart = false;
let noRestart = false;
let force = false;
let attachName: string | null = null;
let attachRemotePeer: string | null = null;
for (let ai = 1; ai < args.length; ai++) {
const a = args[ai];
if (a === "--auto-restart" || a === "-r") autoRestart = true;
else if (a === "--no-restart") noRestart = true;
else if (a === "--force") force = true;
else if (a === "--remote" && ai + 1 < args.length) { attachRemotePeer = args[++ai]; }
else if (!attachName) attachName = a;
Expand All @@ -900,7 +906,11 @@ async function main(): Promise<void> {
}
}
if (!attachName) {
console.error("Usage: pty attach [-r|--auto-restart] [--force] [--remote <peer>] <name>");
console.error("Usage: pty attach [-r|--auto-restart|--no-restart] [--force] [--remote <peer>] <name>");
process.exit(1);
}
if (autoRestart && noRestart) {
console.error("pty attach: --auto-restart and --no-restart are mutually exclusive");
process.exit(1);
}
// Nesting guard runs BEFORE name validation / ref resolution. A nested
Expand All @@ -920,7 +930,9 @@ async function main(): Promise<void> {
await cmdAttachRemote(attachRemotePeer, attachName);
} else {
const resolvedAttachName = await resolveRef(attachName);
await cmdAttach(resolvedAttachName, autoRestart, force);
const restartPolicy: AttachRestartPolicy =
noRestart ? "never" : autoRestart ? "always" : "prompt";
await cmdAttach(resolvedAttachName, restartPolicy, force);
}
break;
}
Expand Down Expand Up @@ -1578,9 +1590,11 @@ async function cmdRun(
doAttach(name);
}

type AttachRestartPolicy = "prompt" | "always" | "never";

async function cmdAttach(
name: string,
autoRestart = false,
restartPolicy: AttachRestartPolicy = "prompt",
_force = false,
): Promise<void> {
// Nesting guard runs in the dispatcher (before name resolution) so the
Expand All @@ -1600,8 +1614,16 @@ async function cmdAttach(
return;
}

// Attach-only callers are relays/supervisors that must never turn future
// input into permission to execute retained launch metadata. Refuse before
// entering the dead-session presentation/restart path.
if (restartPolicy === "never") {
console.error(`Session "${name}" is not running (status: ${session.status}).`);
process.exit(1);
}

// Dead session — show last lines and offer to restart
await handleDeadSession(session, autoRestart);
await handleDeadSession(session, restartPolicy === "always");
}

async function handleDeadSession(
Expand Down
1 change: 1 addition & 0 deletions src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const COMMANDS: readonly CommandSpec[] = [
dynamic: "sessions",
flags: [
{ name: "auto-restart", short: "r", desc: "Auto-restart if the session is exited" },
{ name: "no-restart", desc: "Attach only; never prompt or restart an exited session" },
Comment thread
schickling-assistant marked this conversation as resolved.
{ name: "force", desc: "Attach even from inside another pty" },
{ name: "remote", desc: "Attach a session on a fabric peer" },
],
Expand Down
Loading
Loading