Public/Get-RepoActivity.ps1

function Get-RepoActivity {
    <#
    .SYNOPSIS
        Checks all repos for a GitHub user/org for new activity since the last check.
    .DESCRIPTION
        Queries the GitHub API for each repository owned by the specified user or
        organization. Returns new issues, comments, pull requests, star changes,
        and fork changes since the specified cutoff time or since the last tracked
        check. Forked repos are excluded by default.
    .PARAMETER Owner
        GitHub username or organization name.
    .PARAMETER Token
        GitHub personal access token. Falls back to $env:GITHUB_TOKEN.
    .PARAMETER SinceHours
        Number of hours to look back for activity. Default 24. Range 1-720.
    .PARAMETER SinceDate
        Explicit cutoff datetime. Overrides SinceHours when specified.
    .PARAMETER RepoFilter
        Specific repository names to check. Default checks all public repos.
    .PARAMETER ExcludeRepos
        Repository names to skip.
    .PARAMETER IncludeForks
        Include forked repositories. By default forks are skipped.
    .PARAMETER StatePath
        Path to the state JSON file for delta tracking.
    .EXAMPLE
        Get-RepoActivity -Owner "larro1991" -SinceHours 48
    .EXAMPLE
        Get-RepoActivity -Owner "larro1991" -RepoFilter "AD-SecurityAudit","ToolBridge-MCP"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Owner,

        [Parameter()]
        [string]$Token,

        [Parameter()]
        [ValidateRange(1, 720)]
        [int]$SinceHours = 24,

        [Parameter()]
        [datetime]$SinceDate,

        [Parameter()]
        [string[]]$RepoFilter,

        [Parameter()]
        [string[]]$ExcludeRepos,

        [Parameter()]
        [switch]$IncludeForks,

        [Parameter()]
        [string]$StatePath = (Join-Path $env:USERPROFILE '.repowatch\state.json')
    )

    # Determine the cutoff time
    $sinceTime = if ($SinceDate) {
        $SinceDate.ToUniversalTime()
    }
    else {
        (Get-Date).AddHours(-$SinceHours).ToUniversalTime()
    }
    $sinceIso = $sinceTime.ToString('yyyy-MM-ddTHH:mm:ssZ')
    Write-Verbose "Checking activity since: $sinceIso"

    # Resolve token
    $resolvedToken = if ($Token) { $Token } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { $null }

    # Load previous state for delta comparison
    $state = Get-LastCheckTime -StatePath $StatePath -Owner $Owner

    # Get repositories list
    if ($RepoFilter -and $RepoFilter.Count -gt 0) {
        # Fetch specific repos
        $repos = foreach ($repoName in $RepoFilter) {
            $repoData = Get-GitHubAPI -Endpoint "/repos/$Owner/$repoName" -Token $resolvedToken
            if ($repoData) { $repoData }
        }
    }
    else {
        # Fetch all repos for the owner (handles pagination)
        $repos = Get-GitHubAPI -Endpoint "/users/$Owner/repos?type=owner&per_page=100&sort=updated" -Token $resolvedToken
    }

    if (-not $repos) {
        Write-Warning "No repositories found for owner '$Owner'."
        return @()
    }

    # Filter repos
    $filteredRepos = @($repos)

    if (-not $IncludeForks) {
        $filteredRepos = @($filteredRepos | Where-Object { -not $_.fork })
    }

    if ($ExcludeRepos -and $ExcludeRepos.Count -gt 0) {
        $filteredRepos = @($filteredRepos | Where-Object { $_.name -notin $ExcludeRepos })
    }

    Write-Verbose "Checking $($filteredRepos.Count) repositories for activity."

    $results = [System.Collections.Generic.List[object]]::new()

    foreach ($repo in $filteredRepos) {
        $repoName = $repo.name
        Write-Verbose "Processing repo: $repoName"

        # Current star/fork counts
        $currentStars = [int]$repo.stargazers_count
        $currentForks = [int]$repo.forks_count

        # Get previous counts from state
        $prevStars = 0
        $prevForks = 0
        if ($state.repos -and $state.repos.PSObject.Properties.Name -contains $repoName) {
            $prevData = $state.repos.$repoName
            $prevStars = if ($prevData.PSObject.Properties.Name -contains 'stars') { [int]$prevData.stars } else { $currentStars }
            $prevForks = if ($prevData.PSObject.Properties.Name -contains 'forks') { [int]$prevData.forks } else { $currentForks }
        }
        else {
            # First check for this repo; set previous to current so delta is 0
            $prevStars = $currentStars
            $prevForks = $currentForks
        }

        $starsChange = $currentStars - $prevStars
        $forksChange = $currentForks - $prevForks

        # Fetch new/updated issues (since cutoff)
        $issuesRaw = Get-GitHubAPI -Endpoint "/repos/$Owner/$repoName/issues?state=open&since=$sinceIso&per_page=100" -Token $resolvedToken
        $newIssues = [System.Collections.Generic.List[object]]::new()
        $updatedIssues = [System.Collections.Generic.List[object]]::new()

        if ($issuesRaw) {
            foreach ($issue in @($issuesRaw)) {
                # Skip pull requests (GitHub API returns PRs in issues endpoint)
                if ($issue.pull_request) { continue }

                $issueObj = [PSCustomObject]@{
                    number     = $issue.number
                    title      = $issue.title
                    user       = $issue.user.login
                    created_at = $issue.created_at
                    url        = $issue.html_url
                }

                $createdAt = [datetime]$issue.created_at
                if ($createdAt.ToUniversalTime() -ge $sinceTime) {
                    $newIssues.Add($issueObj)
                }
                else {
                    $updatedIssues.Add($issueObj)
                }
            }
        }

        # Fetch new comments on issues
        $commentsRaw = Get-GitHubAPI -Endpoint "/repos/$Owner/$repoName/issues/comments?since=$sinceIso&per_page=100" -Token $resolvedToken
        $newComments = [System.Collections.Generic.List[object]]::new()

        if ($commentsRaw) {
            foreach ($comment in @($commentsRaw)) {
                # Extract issue number from the issue_url
                $issueNumber = 0
                if ($comment.issue_url -match '/issues/(\d+)$') {
                    $issueNumber = [int]$Matches[1]
                }

                $bodyPreview = if ($comment.body.Length -gt 100) {
                    $comment.body.Substring(0, 100) + '...'
                }
                else {
                    $comment.body
                }
                # Collapse newlines in preview
                $bodyPreview = $bodyPreview -replace '[\r\n]+', ' '

                $newComments.Add([PSCustomObject]@{
                    issue_number = $issueNumber
                    user         = $comment.user.login
                    body_preview = $bodyPreview
                    created_at   = $comment.created_at
                    url          = $comment.html_url
                })
            }
        }

        # Fetch new PRs
        $prsRaw = Get-GitHubAPI -Endpoint "/repos/$Owner/$repoName/pulls?state=open&sort=created&direction=desc&per_page=100" -Token $resolvedToken
        $newPRs = [System.Collections.Generic.List[object]]::new()

        if ($prsRaw) {
            foreach ($pr in @($prsRaw)) {
                $prCreated = [datetime]$pr.created_at
                if ($prCreated.ToUniversalTime() -ge $sinceTime) {
                    $newPRs.Add([PSCustomObject]@{
                        number     = $pr.number
                        title      = $pr.title
                        user       = $pr.user.login
                        created_at = $pr.created_at
                        url        = $pr.html_url
                    })
                }
            }
        }

        # Note: Discussions require GraphQL API (not available via REST).
        # This is acknowledged and skipped for REST-only implementation.

        # Determine if there is any activity
        $hasActivity = (
            $newIssues.Count -gt 0 -or
            $newComments.Count -gt 0 -or
            $newPRs.Count -gt 0 -or
            $updatedIssues.Count -gt 0 -or
            $starsChange -ne 0 -or
            $forksChange -ne 0
        )

        $repoActivity = [PSCustomObject]@{
            RepoName      = $repoName
            RepoUrl       = $repo.html_url
            Stars         = $currentStars
            StarsChange   = $starsChange
            Forks         = $currentForks
            ForksChange   = $forksChange
            NewIssues     = $newIssues.ToArray()
            NewComments   = $newComments.ToArray()
            NewPRs        = $newPRs.ToArray()
            UpdatedIssues = $updatedIssues.ToArray()
            HasActivity   = $hasActivity
        }

        $results.Add($repoActivity)
    }

    return $results.ToArray()
}