mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-21 12:12:22 +02:00
123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
"""Consented first-run local recon: app names and folder shapes, never contents.
|
|
|
|
Reads only basenames and counts (top-level scandir, hard caps), so the summary
|
|
is safe to show the user and to hand to their own configured model for prep.
|
|
Results are returned to the caller and never persisted or sent to analytics.
|
|
"""
|
|
|
|
import os
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.onboarding.models import FolderSummary, ScanResult
|
|
|
|
MAX_APPS = 120
|
|
MAX_ENTRIES_PER_FOLDER = 5000
|
|
MAX_REPO_SCAN_ENTRIES = 400
|
|
SCAN_FOLDERS = ("Downloads", "Desktop", "Documents")
|
|
REPO_PARENT_CANDIDATES = ("dev", "code", "projects", "src", "repos", "Documents/GitHub")
|
|
SCREENSHOT_PREFIXES = ("screenshot", "screen shot", "screen recording")
|
|
|
|
# The high-signal tools that actually tell prep who this person is (an IDE, a design app, a DAW) so the greeting can lead with a confident read instead of drowning in Calculator + System Settings. Matched by exact name or "name " prefix so "Arc" never trips on "Search".
|
|
NOTABLE_APP_KEYWORDS = (
|
|
"xcode", "visual studio code", "cursor", "zed", "sublime text", "intellij idea",
|
|
"pycharm", "webstorm", "android studio", "docker", "orbstack", "postman", "iterm",
|
|
"warp", "ghostty", "tableplus", "github desktop", "figma", "sketch",
|
|
"adobe photoshop", "adobe illustrator", "adobe xd", "affinity", "framer", "blender",
|
|
"cinema 4d", "final cut pro", "davinci resolve", "adobe premiere pro",
|
|
"adobe after effects", "screenflow", "capcut", "obs", "logic pro", "ableton live",
|
|
"fl studio", "garageband", "unity", "unreal engine", "godot", "notion", "obsidian",
|
|
"ulysses", "scrivener", "craft", "linear", "tableau", "rstudio", "ollama", "lm studio", "arc",
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def p_list_apps() -> List[str]:
|
|
apps_dir = Path("/Applications")
|
|
if not apps_dir.is_dir():
|
|
return []
|
|
names: List[str] = []
|
|
try:
|
|
with os.scandir(apps_dir) as entries:
|
|
for entry in entries:
|
|
if entry.name.endswith(".app"):
|
|
names.append(entry.name[: -len(".app")])
|
|
if len(names) >= MAX_APPS:
|
|
break
|
|
except OSError:
|
|
return []
|
|
return sorted(names)
|
|
|
|
|
|
@typechecked
|
|
def p_summarize_folder(folder: Path) -> FolderSummary:
|
|
summary = FolderSummary(name=folder.name)
|
|
if not folder.is_dir():
|
|
return summary
|
|
extensions: Counter = Counter()
|
|
try:
|
|
with os.scandir(folder) as entries:
|
|
for entry in entries:
|
|
if entry.name.startswith("."):
|
|
continue
|
|
summary.entry_count += 1
|
|
lower = entry.name.lower()
|
|
if lower.startswith(SCREENSHOT_PREFIXES):
|
|
summary.screenshot_count += 1
|
|
ext = os.path.splitext(entry.name)[1].lower().lstrip(".")
|
|
if ext and entry.is_file(follow_symlinks=False):
|
|
extensions[ext] += 1
|
|
if summary.entry_count >= MAX_ENTRIES_PER_FOLDER:
|
|
break
|
|
except OSError:
|
|
return summary
|
|
summary.top_extensions = [ext for ext, count in extensions.most_common(5)]
|
|
return summary
|
|
|
|
|
|
@typechecked
|
|
def detect_signal_apps(apps: List[str]) -> List[str]:
|
|
out: List[str] = []
|
|
for name in apps:
|
|
low = name.lower()
|
|
if any(low == kw or low.startswith(kw + " ") for kw in NOTABLE_APP_KEYWORDS):
|
|
out.append(name)
|
|
return out[:20]
|
|
|
|
|
|
@typechecked
|
|
def p_count_git_repos(home: Path) -> int:
|
|
count = 0
|
|
for candidate in REPO_PARENT_CANDIDATES:
|
|
parent = home / candidate
|
|
if not parent.is_dir():
|
|
continue
|
|
try:
|
|
with os.scandir(parent) as entries:
|
|
scanned = 0
|
|
for entry in entries:
|
|
scanned += 1
|
|
if scanned > MAX_REPO_SCAN_ENTRIES:
|
|
break
|
|
if entry.is_dir(follow_symlinks=False) and (parent / entry.name / ".git").is_dir():
|
|
count += 1
|
|
except OSError:
|
|
continue
|
|
return count
|
|
|
|
|
|
@typechecked
|
|
def run_local_scan(home: Path) -> ScanResult:
|
|
folders = [p_summarize_folder(home / name) for name in SCAN_FOLDERS]
|
|
apps = p_list_apps()
|
|
return ScanResult(
|
|
apps=apps,
|
|
signal_apps=detect_signal_apps(apps),
|
|
folders=folders,
|
|
git_repo_count=p_count_git_repos(home),
|
|
has_gitconfig=(home / ".gitconfig").is_file(),
|
|
)
|