Coverage for src/workstack/cli/shell_utils.py: 95%

37 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-19 09:31 -0400

1"""Utilities for generating shell integration scripts.""" 

2 

3import os 

4import tempfile 

5import time 

6import uuid 

7from datetime import datetime 

8from pathlib import Path 

9 

10from workstack.cli.debug import debug_log 

11 

12STALE_SCRIPT_MAX_AGE_SECONDS = 3600 

13 

14 

15def render_cd_script(path: Path, *, comment: str, success_message: str) -> str: 

16 """Generate shell script to change directory with feedback. 

17 

18 Args: 

19 path: Target directory path to cd into. 

20 comment: Shell comment describing the operation. 

21 success_message: Message to echo after successful cd. 

22 

23 Returns: 

24 Shell script that changes directory and shows success message. 

25 """ 

26 path_str = str(path) 

27 quoted_path = "'" + path_str.replace("'", "'\\''") + "'" 

28 lines = [ 

29 f"# {comment}", 

30 f"cd {quoted_path}", 

31 f'echo "{success_message}"', 

32 ] 

33 return "\n".join(lines) + "\n" 

34 

35 

36def write_script_to_temp( 

37 script_content: str, 

38 *, 

39 command_name: str, 

40 comment: str | None = None, 

41) -> Path: 

42 """Write shell script to temp file with unique UUID. 

43 

44 Args: 

45 script_content: The shell script to write 

46 command_name: Command that generated this (e.g., 'sync', 'switch', 'create') 

47 comment: Optional comment to include in script header 

48 

49 Returns: 

50 Path to the temp file 

51 

52 Filename format: workstack-{command}-{uuid}.sh 

53 """ 

54 unique_id = uuid.uuid4().hex[:8] # 8 chars sufficient (4 billion combinations) 

55 temp_dir = Path(tempfile.gettempdir()) 

56 temp_file = temp_dir / f"workstack-{command_name}-{unique_id}.sh" 

57 

58 # Add metadata header 

59 header = [ 

60 "#!/bin/bash", 

61 f"# workstack {command_name}", 

62 f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", 

63 f"# UUID: {unique_id}", 

64 f"# User: {os.getenv('USER', 'unknown')}", 

65 f"# Working dir: {Path.cwd()}", 

66 ] 

67 

68 if comment: 

69 header.append(f"# {comment}") 

70 

71 header.append("") # Blank line before script 

72 

73 full_content = "\n".join(header) + "\n" + script_content 

74 temp_file.write_text(full_content, encoding="utf-8") 

75 

76 # Make executable for good measure 

77 temp_file.chmod(0o755) 

78 

79 debug_log(f"write_script_to_temp: Created {temp_file}") 

80 debug_log(f"write_script_to_temp: Content:\n{full_content}") 

81 

82 return temp_file 

83 

84 

85def cleanup_stale_scripts(*, max_age_seconds: int = STALE_SCRIPT_MAX_AGE_SECONDS) -> None: 

86 """Remove workstack temp scripts older than max_age_seconds. 

87 

88 Args: 

89 max_age_seconds: Maximum age before cleanup (default 1 hour) 

90 """ 

91 temp_dir = Path(tempfile.gettempdir()) 

92 cutoff = time.time() - max_age_seconds 

93 

94 for script_file in temp_dir.glob("workstack-*.sh"): 

95 if script_file.exists(): 

96 try: 

97 if script_file.stat().st_mtime < cutoff: 

98 script_file.unlink() 

99 except (FileNotFoundError, PermissionError): 

100 # Scripts may disappear between stat/unlink or be owned by another user. 

101 continue