diff --git a/.github/workflows/scripts/create-release-packages.ps1 b/.github/workflows/scripts/create-release-packages.ps1 index bd9c16160..b9ee54ca6 100644 --- a/.github/workflows/scripts/create-release-packages.ps1 +++ b/.github/workflows/scripts/create-release-packages.ps1 @@ -20,6 +20,10 @@ Comma or space separated subset of script types to build (default: both) Valid scripts: sh, ps +.PARAMETER GenReleasesDir + Output directory for build artifacts (default: .genreleases in current directory) + Can also be set via GENRELEASES_DIR environment variable + .EXAMPLE .\create-release-packages.ps1 -Version v0.2.0 @@ -28,6 +32,12 @@ .EXAMPLE .\create-release-packages.ps1 -Version v0.2.0 -Agents claude -Scripts ps + +.EXAMPLE + $env:GENRELEASES_DIR = "$env:TEMP/releases"; .\create-release-packages.ps1 -Version v0.2.0 + +.EXAMPLE + .\create-release-packages.ps1 -Version v0.2.0 -GenReleasesDir "$env:TEMP/releases" #> param( @@ -38,7 +48,10 @@ param( [string]$Agents = "", [Parameter(Mandatory=$false)] - [string]$Scripts = "" + [string]$Scripts = "", + + [Parameter(Mandatory=$false)] + [string]$GenReleasesDir = "" ) $ErrorActionPreference = "Stop" @@ -51,8 +64,49 @@ if ($Version -notmatch '^v\d+\.\d+\.\d+$') { Write-Host "Building release packages for $Version" -# Create and use .genreleases directory for all build artifacts -$GenReleasesDir = ".genreleases" +# Resolve output directory: parameter > env var > default +if ([string]::IsNullOrEmpty($GenReleasesDir)) { + $GenReleasesDir = if ($env:GENRELEASES_DIR) { $env:GENRELEASES_DIR } else { ".genreleases" } +} + +# Safety check: refuse empty output directory +if ([string]::IsNullOrWhiteSpace($GenReleasesDir)) { + Write-Error "Output directory must not be empty" + exit 1 +} + +# Safety check: refuse paths containing '..' segments (path traversal) +if ($GenReleasesDir -match '\.\.') { + Write-Error "Refusing to use output directory containing '..' path segments: $GenReleasesDir" + exit 1 +} + +# Convert to absolute path for safety checks +$GenReleasesDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($GenReleasesDir) + +# Safety check: refuse to delete critical paths +$repoRoot = (Resolve-Path ".").Path +$forbiddenPaths = @( + $repoRoot, + (Join-Path $repoRoot ".git"), + (Join-Path $repoRoot "scripts"), + (Join-Path $repoRoot "templates"), + (Join-Path $repoRoot "src"), + $HOME, + [System.IO.Path]::GetTempPath().TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar), + [System.IO.Path]::GetPathRoot($repoRoot) # Root directory (e.g., C:\ or /) +) + +foreach ($forbidden in $forbiddenPaths) { + if ($GenReleasesDir -eq $forbidden) { + Write-Error "Refusing to use '$GenReleasesDir' as output directory (safety check failed)" + exit 1 + } +} + +Write-Host "Output directory: $GenReleasesDir" + +# Create and clean output directory if (Test-Path $GenReleasesDir) { Remove-Item -Path $GenReleasesDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 63df006cf..a36cd95f7 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -461,9 +461,6 @@ function Update-AllExistingAgents { if (-not (Update-IfNew -FilePath $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false } if (-not (Update-IfNew -FilePath $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false } if (-not (Update-IfNew -FilePath $AGENTS_FILE -AgentName 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge')) { $ok = $false } - if (-not (Update-IfNew -FilePath $AMP_FILE -AgentName 'Amp')) { $ok = $false } - if (-not (Update-IfNew -FilePath $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false } if (-not (Update-IfNew -FilePath $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false } if (-not (Update-IfNew -FilePath $JUNIE_FILE -AgentName 'Junie')) { $ok = $false } if (-not (Update-IfNew -FilePath $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false } @@ -478,7 +475,6 @@ function Update-AllExistingAgents { if (-not (Update-IfNew -FilePath $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false } if (-not (Update-IfNew -FilePath $TRAE_FILE -AgentName 'Trae')) { $ok = $false } if (-not (Update-IfNew -FilePath $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $FORGE_FILE -AgentName 'Forge')) { $ok = $false } if (-not $found) { Write-Info 'No existing agent files found, creating default Claude file...' diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index d11664199..49a28dbff 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -67,63 +67,6 @@ def _github_auth_headers(cli_token: str | None = None) -> dict: token = _github_token(cli_token) return {"Authorization": f"Bearer {token}"} if token else {} -def _parse_rate_limit_headers(headers: httpx.Headers) -> dict: - """Extract and parse GitHub rate-limit headers.""" - info = {} - - # Standard GitHub rate-limit headers - if "X-RateLimit-Limit" in headers: - info["limit"] = headers.get("X-RateLimit-Limit") - if "X-RateLimit-Remaining" in headers: - info["remaining"] = headers.get("X-RateLimit-Remaining") - if "X-RateLimit-Reset" in headers: - reset_epoch = int(headers.get("X-RateLimit-Reset", "0")) - if reset_epoch: - reset_time = datetime.fromtimestamp(reset_epoch, tz=timezone.utc) - info["reset_epoch"] = reset_epoch - info["reset_time"] = reset_time - info["reset_local"] = reset_time.astimezone() - - # Retry-After header (seconds or HTTP-date) - if "Retry-After" in headers: - retry_after = headers.get("Retry-After") - try: - info["retry_after_seconds"] = int(retry_after) - except ValueError: - # HTTP-date format - not implemented, just store as string - info["retry_after"] = retry_after - - return info - -def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str: - """Format a user-friendly error message with rate-limit information.""" - rate_info = _parse_rate_limit_headers(headers) - - lines = [f"GitHub API returned status {status_code} for {url}"] - lines.append("") - - if rate_info: - lines.append("[bold]Rate Limit Information:[/bold]") - if "limit" in rate_info: - lines.append(f" • Rate Limit: {rate_info['limit']} requests/hour") - if "remaining" in rate_info: - lines.append(f" • Remaining: {rate_info['remaining']}") - if "reset_local" in rate_info: - reset_str = rate_info["reset_local"].strftime("%Y-%m-%d %H:%M:%S %Z") - lines.append(f" • Resets at: {reset_str}") - if "retry_after_seconds" in rate_info: - lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds") - lines.append("") - - # Add troubleshooting guidance - lines.append("[bold]Troubleshooting Tips:[/bold]") - lines.append(" • If you're on a shared CI or corporate environment, you may be rate-limited.") - lines.append(" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN") - lines.append(" environment variable to increase rate limits.") - lines.append(" • Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.") - - return "\n".join(lines) - def _build_agent_config() -> dict[str, dict[str, Any]]: """Derive AGENT_CONFIG from INTEGRATION_REGISTRY.""" from .integrations import INTEGRATION_REGISTRY diff --git a/tests/test_core_pack_scaffold.py b/tests/test_core_pack_scaffold.py index e7a10736d..2982ab604 100644 --- a/tests/test_core_pack_scaffold.py +++ b/tests/test_core_pack_scaffold.py @@ -15,7 +15,7 @@ Per-agent invariants verified • File count matches the number of source templates • Extension is correct: .toml (TOML agents), .agent.md (copilot), .md (rest) • No unresolved placeholders remain ({SCRIPT}, {ARGS}, __AGENT__) - • Argument token is correct: {{args}} for TOML agents, $ARGUMENTS for others + • Argument token is correct: {{args}} for TOML agents, {{parameters}} for Forge, $ARGUMENTS for other non-TOML agents • Path rewrites applied: scripts/ → .specify/scripts/ etc. • TOML files have "description" and "prompt" fields • Markdown files have parseable YAML frontmatter