This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

restart will actually restart

Nathan Herald (Mar 10, 2026, 11:07 AM +0100) 8810a158 2d367920

+100 -13
+6 -1
completions/pty.zsh
··· 52 52 '--with-delay[Delay between --seq items (seconds)]:seconds:' \ 53 53 '*--seq[Send a sequence item]:value:' 54 54 ;; 55 - kill|restart) 55 + kill) 56 56 _arguments '1:session:_pty_sessions' 57 + ;; 58 + restart) 59 + _arguments \ 60 + '(-y --yes)'{-y,--yes}'[Skip confirmation for running sessions]' \ 61 + '1:session:_pty_sessions' 57 62 ;; 58 63 run) 59 64 # After --, fall back to normal (command + file) completion
+23 -12
src/cli.ts
··· 34 34 pty send <name> "text" Send text to a session 35 35 pty send <name> --seq "text" --seq key:return Send an ordered sequence 36 36 pty send <name> --with-delay 0.5 --seq ... Delay between each --seq item 37 - pty restart <name> Restart an exited session 37 + pty restart <name> Restart a session (prompts if running) 38 + pty restart -y <name> Restart without confirmation 38 39 pty list List active sessions 39 40 pty list --json List sessions as JSON 40 41 pty kill <name> Kill or remove a session ··· 214 215 } 215 216 216 217 case "restart": { 217 - if (args.length < 2) { 218 - console.error("Usage: pty restart <name>"); 218 + const forceRestart = args[1] === "-y" || args[1] === "--yes"; 219 + const restartName = forceRestart ? args[2] : args[1]; 220 + if (!restartName) { 221 + console.error("Usage: pty restart [-y] <name>"); 219 222 process.exit(1); 220 223 } 221 - await cmdRestart(args[1]); 224 + await cmdRestart(restartName, forceRestart); 222 225 break; 223 226 } 224 227 ··· 460 463 } 461 464 } 462 465 463 - async function cmdRestart(name: string): Promise<void> { 466 + async function cmdRestart(name: string, force = false): Promise<void> { 464 467 try { 465 468 validateName(name); 466 469 } catch (e: any) { ··· 475 478 process.exit(1); 476 479 } 477 480 478 - if (session.status === "running") { 479 - console.error( 480 - `Session "${name}" is still running. Kill it first with "pty kill ${name}".` 481 - ); 482 - process.exit(1); 483 - } 484 - 485 481 const meta = session.metadata; 486 482 if (!meta) { 487 483 console.error(`Session "${name}" has no metadata — cannot restart.`); 488 484 cleanupAll(name); 489 485 process.exit(1); 486 + } 487 + 488 + if (session.status === "running" && session.pid) { 489 + if (!force) { 490 + const answer = await ask(`Session "${name}" is running. Kill and restart? [Y/n] `); 491 + if (answer.toLowerCase() === "n") { 492 + process.exit(0); 493 + } 494 + } 495 + try { 496 + process.kill(session.pid, "SIGTERM"); 497 + } catch {} 498 + cleanupSocket(name); 499 + // Wait briefly for the process to exit 500 + await new Promise((r) => setTimeout(r, 200)); 490 501 } 491 502 492 503 cleanupAll(name);
+71
tests/integration.test.ts
··· 979 979 peeker.destroy(); 980 980 }); 981 981 982 + // ─── restart ─── 983 + 984 + describe("restart", () => { 985 + it("restart preserves command and cwd after killing a running session", async () => { 986 + const name = uniqueName(); 987 + await startServer(name, "sh", ["-c", "echo 'original'; sleep 60"]); 988 + await new Promise((r) => setTimeout(r, 200)); 989 + 990 + // Verify it's running and has metadata 991 + const meta1 = readMetadata(name); 992 + expect(meta1).not.toBeNull(); 993 + expect(meta1!.command).toBe("sh"); 994 + expect(meta1!.cwd).toBe(testCwd); 995 + 996 + // Verify we can connect 997 + const client = await connect(name); 998 + const reader = new PacketReader(); 999 + client.write(encodeAttach(24, 80)); 1000 + const screen = await waitForType(client, reader, MessageType.SCREEN); 1001 + expect(screen.payload.toString()).toContain("original"); 1002 + client.destroy(); 1003 + 1004 + // Kill the server (simulating what cmdRestart does) 1005 + await servers[servers.length - 1].close(); 1006 + servers.pop(); 1007 + 1008 + // Metadata should still exist (server.close() only cleans socket/pid) 1009 + const meta2 = readMetadata(name); 1010 + expect(meta2).not.toBeNull(); 1011 + expect(meta2!.command).toBe("sh"); 1012 + expect(meta2!.cwd).toBe(testCwd); 1013 + 1014 + // Restart with the same metadata 1015 + await startServer(name, meta2!.command, meta2!.args, { cwd: meta2!.cwd }); 1016 + await new Promise((r) => setTimeout(r, 200)); 1017 + 1018 + // Verify the restarted session works 1019 + const client2 = await connect(name); 1020 + const reader2 = new PacketReader(); 1021 + client2.write(encodeAttach(24, 80)); 1022 + const screen2 = await waitForType(client2, reader2, MessageType.SCREEN); 1023 + expect(screen2.payload.toString()).toContain("original"); 1024 + client2.destroy(); 1025 + }); 1026 + 1027 + it("metadata persists through kill for restart", async () => { 1028 + const name = uniqueName(); 1029 + const server = await startServer(name, "cat", ["-u"], { cwd: testCwd }); 1030 + 1031 + // Verify initial metadata 1032 + const meta = readMetadata(name); 1033 + expect(meta).not.toBeNull(); 1034 + expect(meta!.command).toBe("cat"); 1035 + expect(meta!.args).toEqual(["-u"]); 1036 + expect(meta!.cwd).toBe(testCwd); 1037 + 1038 + // Simulate what cmdRestart does for a running session: 1039 + // 1. Read metadata (already done above) 1040 + // 2. Kill process 1041 + await server.close(); 1042 + servers = servers.filter((s) => s !== server); 1043 + 1044 + // 3. Metadata should still be on disk (close() only removes socket/pid) 1045 + const metaAfterKill = readMetadata(name); 1046 + expect(metaAfterKill).not.toBeNull(); 1047 + expect(metaAfterKill!.command).toBe("cat"); 1048 + expect(metaAfterKill!.args).toEqual(["-u"]); 1049 + expect(metaAfterKill!.cwd).toBe(testCwd); 1050 + }); 1051 + }); 1052 + 982 1053 // Detach/reattach: the mode was active, TERMINAL_SANITIZE popped it on 983 1054 // detach, and the SCREEN replay on reattach doesn't restore it. The 984 1055 // process only sent the mode push once at startup (like Claude Code does