···11# Changelog
2233+## Unreleased
44+55+### pty exec
66+- Add `pty exec -- <command> [args...]` to replace the current session's command from inside the session
77+- Updates session metadata so the supervisor restarts the new command, not the original
88+- Errors if not inside a pty session (`PTY_SESSION` not set) or if the session is managed by a pty.toml
99+- Preserves existing tags and other metadata
1010+- Emits `session_exec` event with previous and new command
1111+- Interactive TUI shows "Create new session..." for spawn-enabled remote hosts
1212+313## 0.8.0
414515### Relay integration
···1828- `groupedSelectable` `renderHeader` callback now receives the full group object instead of `(title, count)`
1929- Empty groups are now rendered (header shown) instead of being silently skipped
20303131+## 0.7.2
3232+2133### launchd
2234- `pty supervisor launchd install` now compiles a small C wrapper binary (`pty-supervisor`) that validates Full Disk Access before exec'ing node — grant FDA to this binary, not to node itself
2335- Install flow checks FDA via a one-shot launchd job (no false positives from terminal's FDA)
···3143- Fix `events --wait` timeout not being cancelled when event is found (caused exit code 1 even on match)
3244- Fix `displayCommand` duplication in `pty list` for toml-spawned sessions
3345- `displayCommand` now includes full command + args for `pty run` sessions
4646+- Supervisor logs every skip reason in `doRestart` for debugging
4747+- Supervisor state directory moved to `~/.local/state/pty/supervisor/` (no longer pollutes session dir)
4848+4949+## 0.7.1
5050+5151+### Fixes
3452- `pty peek` now works on exited sessions by reading saved output from metadata
3553- `pty peek --wait` handles exited sessions: checks saved output, shows last lines and exit code if pattern not found
3654- `--wait` accepts multiple patterns (`--wait "passed" --wait "failed"`) — matches on any
···3856- Exit metadata saved twice: immediately in `onExit` (for status display) and again in `close()` (for complete output after all PTY data has flushed)
3957- Fix TUI race where session showed as "running" after exit (delay list refresh 200ms to let metadata flush)
4058- Fix SKILL.md examples to use multiple `--wait` flags instead of regex syntax
4141-- Supervisor logs every skip reason in `doRestart` for debugging
4242-- Supervisor state directory moved to `~/.local/state/pty/supervisor/` (no longer pollutes session dir)
4343-4444-### TUI framework
4545-- Export `SelectableGroup<T>` interface from `@myobie/pty/tui`
4646-- `groupedSelectable` `renderHeader` callback now receives the full group object instead of `(title, count)` — allows custom group rendering
4747-- Empty groups are now rendered (header shown) instead of being silently skipped
48594960## 0.7.0
5061
+1
README.md
···63636464pty attach myserver # reconnect to a session
6565pty attach -r myserver # reconnect, auto-restart if exited
6666+pty exec -- codex # replace this session's process (inside a session)
6667pty peek myserver # print current screen and exit
6768pty peek --plain myserver # print as plain text (no ANSI)
6869pty peek --full myserver # print full scrollback
+1-1
completions/pty.bash
···66 COMPREPLY=()
77 cur="${COMP_WORDS[COMP_CWORD]}"
88 prev="${COMP_WORDS[COMP_CWORD-1]}"
99- commands="run attach a peek send events list ls stats restart kill rm remove gc tag supervisor up down wrap unwrap test help"
99+ commands="run attach a exec peek send events list ls stats restart kill rm remove gc tag supervisor up down wrap unwrap test help"
10101111 # Complete subcommand
1212 if [[ ${COMP_CWORD} -eq 1 ]]; then
+1
completions/pty.fish
···4747complete -c pty -n __pty_needs_command -a run -d 'Create a session and attach'
4848complete -c pty -n __pty_needs_command -a attach -d 'Attach to an existing session'
4949complete -c pty -n __pty_needs_command -a a -d 'Attach to an existing session'
5050+complete -c pty -n __pty_needs_command -a exec -d 'Replace the current session command'
5051complete -c pty -n __pty_needs_command -a peek -d 'Print current screen or follow output'
5152complete -c pty -n __pty_needs_command -a send -d 'Send text or keys to a session'
5253complete -c pty -n __pty_needs_command -a events -d 'Follow terminal events from sessions'
+1
completions/pty.zsh
···2727 'run:Create a session and attach'
2828 'attach:Attach to an existing session'
2929 'a:Attach to an existing session'
3030+ 'exec:Replace the current session command'
3031 'peek:Print current screen or follow output'
3132 'send:Send text or keys to a session'
3233 'events:Follow terminal events from sessions'
+73-1
src/cli.ts
···1616 releaseLock,
1717 updateTags,
1818 readMetadata,
1919+ writeMetadata,
1920 getSessionDir,
2021 type SessionInfo,
2122} from "./sessions.ts";
2223import { spawnDaemon, resolveCommand } from "./spawn.ts";
2323-import { EventFollower, readRecentEvents, formatEvent } from "./events.ts";
2424+import { EventFollower, EventWriter, EventType, readRecentEvents, formatEvent } from "./events.ts";
2425import { readPtyFile, type PtySessionDef } from "./ptyfile.ts";
2526import { getSupervisorDir } from "./supervisor.ts";
2627···4142 pty run --tag key=value -- <command> Tag a session with metadata
4243 pty run --cwd /path -- <command> Run in a specific directory
4344 pty attach <name> Attach to an existing session
4545+ pty exec -- <command> [args...] Replace the current session's command
4446 pty attach -r <name> Attach, auto-restart if exited
4547 pty peek <name> Print current screen and exit
4648 pty peek --plain <name> Print current screen as plain text (no ANSI)
···251253 process.exit(1);
252254 }
253255 await cmdAttach(attachName, autoRestart);
256256+ break;
257257+ }
258258+259259+ case "exec": {
260260+ // pty exec -- <command> [args...]
261261+ const dashDash = args.indexOf("--", 1);
262262+ if (dashDash === -1 || dashDash + 1 >= args.length) {
263263+ console.error("Usage: pty exec -- <command> [args...]");
264264+ process.exit(1);
265265+ }
266266+ const execCmd = args[dashDash + 1];
267267+ const execArgs = args.slice(dashDash + 2);
268268+ await cmdExec(execCmd, execArgs);
254269 break;
255270 }
256271···821836 onDetach: () => process.exit(0),
822837 onExit: (code) => process.exit(code),
823838 });
839839+}
840840+841841+async function cmdExec(command: string, cmdArgs: string[]): Promise<void> {
842842+ const sessionName = process.env.PTY_SESSION;
843843+ if (!sessionName) {
844844+ console.error("pty exec: not inside a pty session (PTY_SESSION not set).");
845845+ process.exit(1);
846846+ }
847847+848848+ const meta = readMetadata(sessionName);
849849+ if (!meta) {
850850+ console.error(`pty exec: session "${sessionName}" metadata not found.`);
851851+ process.exit(1);
852852+ }
853853+854854+ if (meta.tags?.ptyfile) {
855855+ console.error(`pty exec: session "${sessionName}" is managed by ${meta.tags.ptyfile}`);
856856+ console.error("Edit the pty.toml to change the command instead.");
857857+ process.exit(1);
858858+ }
859859+860860+ // Resolve the command to an absolute path
861861+ let resolved: string;
862862+ try {
863863+ resolved = resolveCommand(command);
864864+ } catch (e: any) {
865865+ console.error(e.message);
866866+ process.exit(1);
867867+ }
868868+869869+ // Update metadata with the new command
870870+ const previousCommand = meta.displayCommand ?? [meta.command, ...(meta.args ?? [])].join(" ");
871871+ const displayCommand = [command, ...cmdArgs].join(" ");
872872+ writeMetadata(sessionName, {
873873+ ...meta,
874874+ command: resolved,
875875+ args: cmdArgs,
876876+ displayCommand,
877877+ });
878878+879879+ // Emit exec event
880880+ const writer = new EventWriter(sessionName);
881881+ writer.append({
882882+ session: sessionName,
883883+ type: EventType.SESSION_EXEC,
884884+ ts: new Date().toISOString(),
885885+ previousCommand,
886886+ command: displayCommand,
887887+ } as any);
888888+ await writer.flush();
889889+890890+ // Replace this process with the new command
891891+ const result = spawnSync(resolved, cmdArgs, {
892892+ stdio: "inherit",
893893+ env: process.env,
894894+ });
895895+ process.exit(result.status ?? 1);
824896}
825897826898async function cmdPeekWait(name: string, patterns: string[], timeoutSec: number, plain: boolean): Promise<void> {
+1-1
src/client-api.ts
···3333 type EventRecord, type EventBase,
3434 type BellEvent, type TitleChangeEvent, type NotificationEvent,
3535 type FocusRequestEvent, type CursorVisibleEvent,
3636- type SessionStartEvent, type SessionExitEvent,
3636+ type SessionStartEvent, type SessionExitEvent, type SessionExecEvent,
3737 type SessionRestartEvent, type SessionFailedEvent,
3838 type SupervisorStartEvent, type SupervisorStopEvent,
3939 type FollowerOptions,