Skip to content

Instantly share code, notes, and snippets.

@April-June-August
Created July 20, 2025 10:16
Show Gist options
  • Save April-June-August/7a1ef5d190d385ae7005c10d150a82cc to your computer and use it in GitHub Desktop.
Save April-June-August/7a1ef5d190d385ae7005c10d150a82cc to your computer and use it in GitHub Desktop.
Check for invalid Alfred Workflows and remove them
#!/usr/bin/env python3
"""
Script to find and move Alfred workflow folders that are missing info.plist files.
This helps clean up "dirty" workflow folders that cause alfred-workflow-packager to fail.
"""
import glob
import os
import os.path
import plistlib
import shutil
from datetime import datetime
def read_plist_from_path(plist_path):
"""Read a .plist file and return its contents as a dictionary."""
with open(plist_path, "rb") as plist_file:
return plistlib.load(plist_file)
def get_alfred_prefs_dir():
"""Get the Alfred preferences directory, accounting for sync folder settings."""
library_dir = os.path.join(os.path.expanduser("~"), "Library")
# Try Alfred 3 preferences first, then fallback to current version
try:
core_prefs = read_plist_from_path(
os.path.join(
library_dir,
"Preferences",
"com.runningwithcrayons.Alfred-Preferences-3.plist"
)
)
except IOError:
core_prefs = read_plist_from_path(
os.path.join(
library_dir,
"Preferences",
"com.runningwithcrayons.Alfred-Preferences.plist"
)
)
# Check if user is syncing preferences
if core_prefs.get("syncfolder"):
return os.path.expanduser(core_prefs["syncfolder"])
else:
return os.path.join(library_dir, "Application Support", "Alfred")
def move_invalid_workflows():
"""Find and move all workflow directories that are missing/corrupt info.plist files."""
try:
prefs_dir = get_alfred_prefs_dir()
workflows_dir = os.path.join(prefs_dir, "Alfred.alfredpreferences", "workflows")
# Create invalid workflows subfolder with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
invalid_folder = os.path.join(workflows_dir, f"_invalid_workflows_{timestamp}")
print(f"Checking workflows in: {workflows_dir}")
print("-" * 60)
# Get all workflow directories
workflow_dirs = glob.glob(os.path.join(workflows_dir, "*"))
workflow_dirs = [d for d in workflow_dirs if os.path.isdir(d) and not os.path.basename(d).startswith("_invalid_workflows")]
invalid_workflows = []
total_workflows = len(workflow_dirs)
for workflow_dir in workflow_dirs:
workflow_name = os.path.basename(workflow_dir)
info_plist_path = os.path.join(workflow_dir, "info.plist")
is_invalid = False
reason = ""
if not os.path.exists(info_plist_path):
is_invalid = True
reason = "MISSING info.plist"
print(f"❌ MISSING: {workflow_name}")
else:
# Try to read the plist to check if it's valid
try:
read_plist_from_path(info_plist_path)
print(f"✅ OK: {workflow_name}")
except Exception as e:
is_invalid = True
reason = f"CORRUPT info.plist: {str(e)}"
print(f"⚠️ CORRUPT: {workflow_name} (Error: {str(e)})")
if is_invalid:
invalid_workflows.append((workflow_dir, workflow_name, reason))
# Move invalid workflows if any found
moved_count = 0
if invalid_workflows:
# Create the invalid folder
os.makedirs(invalid_folder, exist_ok=True)
print(f"\nCreated folder for invalid workflows: {invalid_folder}")
print(f"\nMoving {len(invalid_workflows)} invalid workflow(s):")
for workflow_dir, workflow_name, reason in invalid_workflows:
try:
dest_path = os.path.join(invalid_folder, workflow_name)
shutil.move(workflow_dir, dest_path)
print(f" ✅ Moved: {workflow_name} ({reason})")
moved_count += 1
except Exception as e:
print(f" ❌ Failed to move {workflow_name}: {str(e)}")
print("-" * 60)
print(f"Summary:")
print(f" Total workflow folders checked: {total_workflows}")
print(f" Invalid workflows found: {len(invalid_workflows)}")
print(f" Invalid workflows moved: {moved_count}")
print(f" Valid workflows remaining: {total_workflows - len(invalid_workflows)}")
if moved_count > 0:
print(f"\nInvalid workflows moved to: {invalid_folder}")
return invalid_workflows
except Exception as e:
print(f"Error: {str(e)}")
return []
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--move":
print("🚨 MOVING INVALID WORKFLOWS TO SUBFOLDER")
print("=" * 60)
move_invalid_workflows()
else:
print("📋 CHECKING WORKFLOWS (READ-ONLY MODE)")
print("To move invalid workflows, run: python3 check_workflows.py --move")
print("=" * 60)
# Read-only version of the function
try:
prefs_dir = get_alfred_prefs_dir()
workflows_dir = os.path.join(prefs_dir, "Alfred.alfredpreferences", "workflows")
print(f"Checking workflows in: {workflows_dir}")
print("-" * 60)
# Get all workflow directories
workflow_dirs = glob.glob(os.path.join(workflows_dir, "*"))
workflow_dirs = [d for d in workflow_dirs if os.path.isdir(d) and not os.path.basename(d).startswith("_invalid_workflows")]
invalid_workflows = []
total_workflows = len(workflow_dirs)
for workflow_dir in workflow_dirs:
workflow_name = os.path.basename(workflow_dir)
info_plist_path = os.path.join(workflow_dir, "info.plist")
if not os.path.exists(info_plist_path):
invalid_workflows.append(workflow_dir)
print(f"❌ MISSING: {workflow_name}")
else:
# Try to read the plist to check if it's valid
try:
read_plist_from_path(info_plist_path)
print(f"✅ OK: {workflow_name}")
except Exception as e:
invalid_workflows.append(workflow_dir)
print(f"⚠️ CORRUPT: {workflow_name} (Error: {str(e)})")
print("-" * 60)
print(f"Summary:")
print(f" Total workflow folders: {total_workflows}")
print(f" Missing/corrupt info.plist: {len(invalid_workflows)}")
print(f" Valid workflows: {total_workflows - len(invalid_workflows)}")
if invalid_workflows:
print(f"\nWorkflow folders with missing/corrupt info.plist:")
for workflow_dir in invalid_workflows:
print(f" {workflow_dir}")
print(f"\nTo move these to a subfolder, run: python3 check_workflows.py --move")
except Exception as e:
print(f"Error: {str(e)}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment