# Continuous Delivery Script for EasyStream # Automatically commits and pushes changes to GitHub param( [int]$IntervalSeconds = 300, # Default: 5 minutes [string]$Branch = "dev", [string]$CommitPrefix = "auto:", [switch]$WatchMode, [switch]$Verbose ) $ErrorActionPreference = "Continue" $RepoPath = $PSScriptRoot # Color output functions function Write-Success { param($msg) Write-Host "āœ“ $msg" -ForegroundColor Green } function Write-Info { param($msg) Write-Host "ℹ $msg" -ForegroundColor Cyan } function Write-Warning { param($msg) Write-Host "⚠ $msg" -ForegroundColor Yellow } function Write-Error { param($msg) Write-Host "āœ— $msg" -ForegroundColor Red } # Load configuration $configPath = Join-Path $RepoPath ".cd-config.json" if (Test-Path $configPath) { $config = Get-Content $configPath | ConvertFrom-Json if ($config.intervalSeconds) { $IntervalSeconds = $config.intervalSeconds } if ($config.branch) { $Branch = $config.branch } if ($config.commitPrefix) { $CommitPrefix = $config.commitPrefix } if ($config.excludePatterns) { $excludePatterns = $config.excludePatterns } } else { $excludePatterns = @( "f_data/data_sessions/*", "f_data/data_cache/_c_tpl/*", ".setup_complete", "*.log", "db_data/*", "node_modules/*", "vendor/*" ) } function Test-GitRepo { Push-Location $RepoPath try { $null = git rev-parse --git-dir 2>&1 return $? } finally { Pop-Location } } function Get-GitStatus { Push-Location $RepoPath try { $status = git status --porcelain 2>&1 return $status } finally { Pop-Location } } function Get-ChangeSummary { Push-Location $RepoPath try { $modified = @(git diff --name-only).Count $staged = @(git diff --cached --name-only).Count $untracked = @(git ls-files --others --exclude-standard).Count return @{ Modified = $modified Staged = $staged Untracked = $untracked Total = $modified + $staged + $untracked } } finally { Pop-Location } } function Update-GitIgnore { $gitignorePath = Join-Path $RepoPath ".gitignore" $ignoreContent = @" # Temporary session files f_data/data_sessions/sess_* # Cache files f_data/data_cache/_c_tpl/* # Setup marker .setup_complete # Database runtime files db_data/* # Logs *.log # Dependencies node_modules/ vendor/ # IDE .vscode/ .idea/ "@ if (-not (Test-Path $gitignorePath)) { $ignoreContent | Out-File -FilePath $gitignorePath -Encoding UTF8 Write-Success "Created .gitignore" } else { # Append if patterns are missing $existing = Get-Content $gitignorePath -Raw if ($existing -notmatch "f_data/data_sessions/sess_\*") { "`n# Auto-generated exclusions`n$ignoreContent" | Add-Content -Path $gitignorePath Write-Success "Updated .gitignore" } } } function Invoke-AutoCommit { param([string]$message) Push-Location $RepoPath try { Write-Info "Checking for changes..." $changes = Get-ChangeSummary if ($changes.Total -eq 0) { if ($Verbose) { Write-Info "No changes detected" } return $false } Write-Info "Found $($changes.Total) changed files (Modified: $($changes.Modified), Staged: $($changes.Staged), Untracked: $($changes.Untracked))" # Stage all changes Write-Info "Staging changes..." git add -A 2>&1 | Out-Null # Generate commit message if not provided if (-not $message) { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $message = "${CommitPrefix} Update at $timestamp" # Add file change summary $fileList = @(git diff --cached --name-only | Select-Object -First 5) if ($fileList.Count -gt 0) { $message += "`n`nChanged files:" foreach ($file in $fileList) { $message += "`n- $file" } if ($changes.Total -gt 5) { $message += "`n- ... and $($changes.Total - 5) more files" } } $message += "`n`nšŸ¤– Generated with Claude Code Continuous Delivery`n`nCo-Authored-By: Claude " } # Create commit Write-Info "Creating commit..." $commitResult = git commit -m $message 2>&1 if ($LASTEXITCODE -eq 0) { Write-Success "Committed changes" return $true } else { Write-Warning "Commit failed or nothing to commit" if ($Verbose) { Write-Host $commitResult } return $false } } finally { Pop-Location } } function Invoke-AutoPush { Push-Location $RepoPath try { Write-Info "Pushing to origin/$Branch..." # Check if we're ahead of remote $ahead = git rev-list --count "origin/$Branch..$Branch" 2>&1 if ($ahead -match "^\d+$" -and [int]$ahead -gt 0) { Write-Info "Local is $ahead commit(s) ahead of remote" # Push with retry logic $maxRetries = 3 $retryCount = 0 while ($retryCount -lt $maxRetries) { $pushResult = git push origin $Branch 2>&1 if ($LASTEXITCODE -eq 0) { Write-Success "Successfully pushed to GitHub" return $true } else { $retryCount++ Write-Warning "Push attempt $retryCount failed" if ($Verbose) { Write-Host $pushResult } if ($retryCount -lt $maxRetries) { Write-Info "Retrying in 5 seconds..." Start-Sleep -Seconds 5 } } } Write-Error "Failed to push after $maxRetries attempts" return $false } else { if ($Verbose) { Write-Info "Already up to date with remote" } return $false } } finally { Pop-Location } } function Start-ContinuousDelivery { Write-Info "=========================================" Write-Info "EasyStream Continuous Delivery Started" Write-Info "=========================================" Write-Info "Repository: $RepoPath" Write-Info "Branch: $Branch" Write-Info "Interval: $IntervalSeconds seconds" Write-Info "=========================================" Write-Info "" # Verify git repository if (-not (Test-GitRepo)) { Write-Error "Not a git repository: $RepoPath" exit 1 } # Update .gitignore Update-GitIgnore $iteration = 0 while ($true) { $iteration++ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "`n[$timestamp] Check #$iteration" -ForegroundColor Magenta try { # Commit changes $committed = Invoke-AutoCommit # Push if there was a commit if ($committed) { Start-Sleep -Seconds 2 Invoke-AutoPush } # Wait for next interval Write-Info "Next check in $IntervalSeconds seconds... (Press Ctrl+C to stop)" Start-Sleep -Seconds $IntervalSeconds } catch { Write-Error "Error during CD cycle: $_" Write-Info "Continuing in 30 seconds..." Start-Sleep -Seconds 30 } } } function Start-FileWatcher { Write-Info "=========================================" Write-Info "EasyStream File Watcher Started" Write-Info "=========================================" Write-Info "Watching: $RepoPath" Write-Info "Branch: $Branch" Write-Info "Debounce: 30 seconds after last change" Write-Info "=========================================" Write-Info "" # Verify git repository if (-not (Test-GitRepo)) { Write-Error "Not a git repository: $RepoPath" exit 1 } # Update .gitignore Update-GitIgnore # Create file system watcher $watcher = New-Object System.IO.FileSystemWatcher $watcher.Path = $RepoPath $watcher.IncludeSubdirectories = $true $watcher.EnableRaisingEvents = $true # Exclude patterns $watcher.Filter = "*.*" $watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::DirectoryName -bor [System.IO.NotifyFilters]::LastWrite $global:lastChangeTime = Get-Date $global:changedFiles = @{} $onChange = { param($sender, $e) # Skip excluded patterns $relativePath = $e.FullPath.Replace($RepoPath, "").TrimStart('\', '/') $exclude = $false foreach ($pattern in $excludePatterns) { if ($relativePath -like $pattern) { $exclude = $true break } } if (-not $exclude) { $global:lastChangeTime = Get-Date $global:changedFiles[$e.FullPath] = $e.ChangeType Write-Host "[$(Get-Date -Format 'HH:mm:ss')] " -NoNewline -ForegroundColor Gray Write-Host "$($e.ChangeType): " -NoNewline -ForegroundColor Yellow Write-Host $relativePath -ForegroundColor White } } # Register events Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Created -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $onChange | Out-Null Write-Success "File watcher active. Monitoring for changes..." Write-Info "Changes will auto-commit 30 seconds after last modification" Write-Info "" try { while ($true) { Start-Sleep -Seconds 5 # Check if enough time has passed since last change $timeSinceLastChange = (Get-Date) - $global:lastChangeTime if ($global:changedFiles.Count -gt 0 -and $timeSinceLastChange.TotalSeconds -ge 30) { Write-Info "`nDebounce period elapsed. Processing $($global:changedFiles.Count) changes..." $committed = Invoke-AutoCommit if ($committed) { Start-Sleep -Seconds 2 Invoke-AutoPush } # Reset $global:changedFiles = @{} Write-Info "`nContinuing to watch for changes...`n" } } } finally { $watcher.Dispose() Get-EventSubscriber | Unregister-Event } } # Main execution if ($WatchMode) { Start-FileWatcher } else { Start-ContinuousDelivery }