diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000..02e0262 --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,153 @@ +# Release Tool - Claude Code Project Context + +## Project Overview + +This is **release-tool**, a comprehensive Python CLI tool for managing releases using semantic versioning. It automates release note generation by consolidating commits, fetching ticket details from GitHub, and creating beautifully formatted release notes. + +## Key Capabilities + +- **Semantic Versioning**: Full support for versions, release candidates, betas, alphas +- **Ticket Consolidation**: Groups commits by parent tickets for cleaner release notes +- **GitHub Integration**: Syncs PRs, issues, releases via parallelized API calls +- **Local Git Analysis**: Analyzes commit history from local repositories +- **Template-based Output**: Jinja2 templates for customizable release notes +- **Performance Optimized**: 20 parallel workers, GitHub Search API, efficient caching + +## Technology Stack + +- **Python 3.10+** with type hints and Pydantic models +- **PyGithub** - GitHub API integration +- **GitPython** - Local Git operations +- **Click** - CLI framework +- **Rich** - Beautiful terminal output with progress bars +- **Jinja2** - Template rendering +- **SQLite** - Local caching database +- **pytest** - Testing framework + +## Architecture + +``` +src/release_tool/ +├── main.py # CLI entry point (sync, generate, list-releases commands) +├── models.py # Pydantic models (SemanticVersion, Commit, PR, Ticket, Release) +├── config.py # Configuration with validation +├── db.py # SQLite database operations +├── git_ops.py # Git operations (find commits, tags, version comparison) +├── github_utils.py # GitHub API client (search, fetch, create releases/PRs) +├── sync.py # Parallelized sync manager +├── policies.py # Ticket extraction, consolidation, categorization +└── media_utils.py # Media download utilities +``` + +## Common Workflows + +### 1. Initial Setup +```bash +release-tool init-config # Create release_tool.toml +export GITHUB_TOKEN="ghp_..." # Set GitHub token +# Edit release_tool.toml with your repo settings +``` + +### 2. Sync GitHub Data +```bash +release-tool sync # Sync tickets, PRs, releases +``` + +### 3. Generate Release Notes +```bash +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --output docs/releases/2.0.0.md +``` + +### 4. Create GitHub Release or PR +```bash +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --upload # Creates GitHub release + +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --create-pr # Creates PR with release notes +``` + +## Performance Requirements (CRITICAL) + +When working on this codebase, ALWAYS adhere to these performance principles: + +### 1. Parallelize All Network Operations +- Use `ThreadPoolExecutor` with 20 workers for GitHub API calls +- Batch size: 100-200 items for optimal throughput +- GitHub rate limit: 5000/hour = safe to parallelize aggressively + +### 2. Use GitHub Search API +- NEVER use PyGithub's lazy iteration (`for issue in repo.get_issues()`) +- ALWAYS use Search API (`gh.search_issues(query)`) +- Search API is 10-20x faster than iteration + +### 3. Progress Feedback ALWAYS +- NEVER leave user waiting >2 seconds without feedback +- Show "Searching...", "Found X items", "Filtering...", "Fetching X/Y..." +- Use Rich progress bars for parallel operations + +### 4. Example Pattern +```python +# BAD - Sequential iteration +for issue in repo.get_issues(): # Each iteration = network call + process(issue) + +# GOOD - Search API + parallel fetch +query = f"repo:{repo_name} is:issue" +issues = gh.search_issues(query) # Fast +console.print(f"Searching for issues...") +console.print(f"Found {len(issues)} issues") + +with ThreadPoolExecutor(max_workers=20) as executor: + futures = [executor.submit(fetch_details, num) for num in issues] + for future in as_completed(futures): + # Process with progress bar +``` + +## Testing + +Run tests with: +```bash +pytest # All tests +pytest tests/test_sync.py -v # Specific module +pytest --cov=release_tool # With coverage +``` + +Current coverage: 74 tests across all modules. + +## Slash Commands Available + +See `.claude/commands/` for custom slash commands: +- `/sync-fast` - Quick sync with monitoring +- `/generate-release` - Generate release notes workflow +- `/test-affected` - Run tests for modified modules +- `/lint-fix` - Auto-fix code quality issues +- `/perf-profile` - Profile sync performance +- `/debug-sync` - Debug sync with verbose output + +## Important Files + +- `release_tool.toml` - Configuration file (created by `init-config`) +- `release_tool.db` - SQLite cache (auto-created by `sync`) +- `.release_tool_cache/` - Cloned git repositories for offline access +- `docs/` - User documentation +- `tests/` - Comprehensive unit tests + +## Documentation + +- Main docs: `docs/` +- README: `README.md` +- Example configs: `examples/example_*.toml` +- Template docs: `examples/MASTER_TEMPLATE_FEATURE.md`, `examples/OUTPUT_TEMPLATE_EXAMPLES.md` + +## Key Design Patterns + +1. **Ticket Consolidation**: Commits grouped by parent ticket for cleaner notes +2. **Version Comparison**: RCs compare to previous RC, finals to previous final +3. **Incremental Sync**: Only fetch new items since last sync +4. **Parallel Everything**: All GitHub operations parallelized +5. **Template System**: Jinja2 with category-based grouping and custom sections diff --git a/.claude/architecture.md b/.claude/architecture.md new file mode 100644 index 0000000..a15e86b --- /dev/null +++ b/.claude/architecture.md @@ -0,0 +1,418 @@ +# Release Tool Architecture Guidelines + +## Command Separation of Concerns + +The release tool has a strict separation between online (GitHub API) and offline operations: + +### ✅ `sync` command - GitHub Fetching (Online) +- **Purpose**: Fetch data from GitHub API and store in local database +- **Internet**: REQUIRED +- **Operations**: + - Fetch ALL tickets from ticket repos (comprehensive initial sync) + - Fetch pull requests from code repo + - Clone/update git repository + - Store everything in local SQLite database +- **Rules**: + - ✅ CAN call GitHub API + - ✅ CAN write to database + - ✅ CAN clone/update git repos + - ❌ MUST NOT generate release notes + +### ✅ `generate` command - Release Note Generation (Offline) +- **Purpose**: Generate release notes from local database +- **Internet**: NOT REQUIRED (must work offline) +- **Operations**: + - Read commits from git repository + - Extract ticket references from branches/PRs/commits + - Query tickets from LOCAL database only + - Generate formatted release notes +- **Rules**: + - ✅ CAN read from database + - ✅ CAN read from local git repo + - ✅ CAN write release notes to files + - ❌ MUST NOT call GitHub API + - ❌ MUST NOT fetch tickets from GitHub + - ⚠️ If ticket not in DB: warn user to run `sync` first + +### ✅ `publish` command - GitHub Publishing (Online) +- **Purpose**: Upload release notes to GitHub +- **Internet**: REQUIRED +- **Operations**: + - Create GitHub releases + - Post comments on PRs + - Upload release assets +- **Rules**: + - ✅ CAN call GitHub API + - ✅ CAN read from database + - ❌ MUST NOT fetch additional data from GitHub + +### ✅ `tickets` command - Database Query (Offline) +- **Purpose**: Query and explore tickets in local database +- **Internet**: NOT REQUIRED (fully offline) +- **Operations**: + - Search tickets by key, repo, or fuzzy patterns + - Support smart TICKET_KEY formats (8624, #8624, meta#8624, meta#8624~, owner/repo#8624) + - Export ticket data to CSV + - Debug partial ticket matches + - Explore synced data +- **Rules**: + - ✅ CAN read from database + - ✅ CAN display data in table or CSV format + - ❌ MUST NOT call GitHub API + - ⚠️ Only shows synced tickets (remind user to sync first) + +### Use Cases for tickets: +- **Debugging partial matches**: Find why a ticket wasn't matched during release note generation +- **Exploring tickets**: See what tickets are in the database +- **Data export**: Export tickets to CSV for analysis +- **Number proximity**: Find tickets with similar numbers (useful for tracking down typos) +- **Pattern matching**: Use starts-with, ends-with for flexible searching + +## Database Design + +### Ticket Storage +- Tickets are stored with their source `repo_id` (e.g., sequentech/meta) +- Code repos and ticket repos have different repo_ids +- Use `db.get_ticket_by_key(key)` to search across all repos +- Use `db.get_ticket(repo_id, key)` only when repo is known + +## Sync Strategy + +### Initial Sync (First Time) +- Fetch ALL tickets from ticket repos (no cutoff date) +- Fetch ALL pull requests from code repo (no cutoff date) +- This ensures historical tickets are available + +### Incremental Sync (Subsequent Runs) +- Use `last_sync` timestamp as cutoff +- Only fetch items created/updated since last sync +- Much faster than initial sync + +## Common Pitfalls + +### ❌ DON'T: Add GitHub fetching to generate +```python +# BAD - This makes generate require internet +github_client = GitHubClient(config) +ticket = github_client.fetch_issue_by_key(repo, key, repo_id) +``` + +### ✅ DO: Query from database only +```python +# GOOD - Works offline +ticket = db.get_ticket_by_key(change.ticket_key) +if not ticket: + console.print("Ticket not found. Run 'sync' first.") +``` + +### ❌ DON'T: Query with wrong repo_id +```python +# BAD - code_repo_id won't find tickets from ticket repos +ticket = db.get_ticket(code_repo_id, "8624") +``` + +### ✅ DO: Search across all repos +```python +# GOOD - Finds ticket in any repo +ticket = db.get_ticket_by_key("8624") +``` + +## Testing Guidelines + +- Generate command tests should NOT mock GitHub API +- Generate command tests MUST work with database only +- Sync command tests CAN mock GitHub API +- Publish command tests CAN mock GitHub API + +## When Adding Features + +Before adding any code to `generate` command: +1. Ask: "Does this require internet?" +2. If YES: Move it to `sync` or `publish` +3. If NO: Ensure it only reads from database/git + +## User Workflow + +1. **Setup**: `release-tool sync` (fetch all data from GitHub) +2. **Generate**: `release-tool generate 9.3.0-rc.7` (offline, uses cached data) +3. **Publish**: `release-tool publish 9.3.0-rc.7` (upload to GitHub) + +Users should be able to: +- Run `generate` on airplane (offline) +- Run `generate` repeatedly without API rate limits +- Run `generate` on different machines after syncing once + +## Config Versioning and Migrations + +### Overview +The release tool uses semantic versioning for config files to handle breaking changes and new features gracefully. + +### Current Version +- **Latest**: 1.2 (defined in `src/release_tool/migrations/manager.py`) +- Stored in config file as `config_version = "1.2"` + +### Version History +- **1.0**: Initial config format +- **1.1**: Added template variables (ticket_url, pr_url) +- **1.2**: Added partial_ticket_action policy + +### When to Bump Config Version + +**ALWAYS bump config version when:** +- Adding new required fields +- Changing field meanings/behavior +- Changing default values that affect existing configs +- Adding new policy options + +**NO bump needed when:** +- Adding optional fields with sensible defaults +- Adding documentation/comments +- Fixing bugs that don't change behavior +- Adding new sections that are entirely optional + +### How to Bump Config Version + +When adding a new config field or changing config structure: + +1. **Update MigrationManager** (`src/release_tool/migrations/manager.py`): + ```python + CURRENT_VERSION = "1.3" # Bump version + ``` + +2. **Create Migration File** (`src/release_tool/migrations/vX_Y_to_vX_Z.py`): + ```python + """Migration from config version X.Y to X.Z. + + Changes in X.Z: + - Added field_name to section_name + - Changed behavior of existing_field + """ + import tomlkit + from typing import Dict, Any + + def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """Migrate config from X.Y to X.Z.""" + doc = tomlkit.document() + + # Preserve existing config + for key, value in config_dict.items(): + doc[key] = value + + # Update version + doc['config_version'] = 'X.Z' + + # Add new fields with defaults + if 'section_name' not in doc: + doc['section_name'] = {} + if 'field_name' not in doc['section_name']: + doc['section_name']['field_name'] = 'default_value' + + return doc + ``` + +3. **Add Migration Description** (in `manager.py`): + ```python + descriptions = { + # ... existing ... + ("X.Y", "X.Z"): ( + "Version X.Z adds:\n" + " • New field_name in section_name\n" + " • Changed behavior description" + ), + } + ``` + +4. **Update config_template.toml**: + - Update version number at top + - Add new fields with extensive comments + - Document all options and examples + +5. **Update Config Models** (`src/release_tool/config.py`): + - Add new fields to Pydantic models + - Set appropriate defaults + - Add validation if needed + +6. **Create Tests**: + - Test migration function in `tests/test_migrations.py` + - Test new config fields + - Verify backwards compatibility + +7. **Update Architecture Docs** (this file): + - Add version to history + - Document what changed + +### Migration System + +**Automatic Migration**: +- When user runs any command with old config +- Tool prompts: "Config is v1.1, upgrade to v1.2? [Y/n]" +- Auto-upgrades in `--auto` mode +- Preserves user's existing settings +- Saves upgraded config back to file + +**Manual Migration**: +```bash +release-tool update-config # Upgrades config to latest version +``` + +**Migration Chain**: +- Supports multi-step migrations (1.0 → 1.1 → 1.2) +- Each migration file handles one version jump +- Manager applies migrations in sequence + +### Testing Migrations + +**Requirements**: +- Every migration MUST have tests +- Test that migration adds expected fields +- Test that existing fields are preserved +- Test that defaults are sensible +- Test error handling for malformed configs + +**Example Test**: +```python +def test_migration_1_1_to_1_2(): + """Test migration from v1.1 to v1.2.""" + from release_tool.migrations.v1_1_to_v1_2 import migrate + + old_config = { + 'config_version': '1.1', + 'repository': {'code_repo': 'test/repo'}, + 'ticket_policy': {} + } + + new_config = migrate(old_config) + + assert new_config['config_version'] == '1.2' + assert new_config['ticket_policy']['partial_ticket_action'] == 'warn' + assert new_config['repository']['code_repo'] == 'test/repo' # Preserved +``` + +### Common Migration Patterns + +**Adding Field with Default**: +```python +if 'section' not in doc: + doc['section'] = {} +if 'new_field' not in doc['section']: + doc['section']['new_field'] = 'default' +``` + +**Adding Section**: +```python +if 'new_section' not in doc: + doc['new_section'] = { + 'field1': 'value1', + 'field2': 'value2' + } +``` + +**Renaming Field**: +```python +if 'old_name' in doc['section']: + doc['section']['new_name'] = doc['section']['old_name'] + del doc['section']['old_name'] +``` + +**Changing Default**: +```python +# Only change if user hasn't customized it +if doc['section'].get('field') == 'old_default': + doc['section']['field'] = 'new_default' +``` + +### TOML Preservation + +⚠️ **CRITICAL**: Use `tomlkit` for BOTH reading AND writing configs! + +❌ **BAD** - Loses comments: +```python +import tomli +with open(path, 'rb') as f: + data = tomli.load(f) # Returns plain dict, NO comments! +``` + +✅ **GOOD** - Preserves comments: +```python +import tomlkit +with open(path, 'r', encoding='utf-8') as f: + data = tomlkit.load(f) # Preserves comments and formatting +``` + +**Rules**: +- Use `tomlkit` for reading configs (NOT `tomli`) +- Use text mode `'r'` (NOT binary `'rb'`) +- Use `tomlkit.load()` and `tomlkit.dumps()` +- Comments and formatting are preserved automatically +- Only modify what's necessary + +### Error Handling + +**Migration Fails**: +- Display clear error message +- Don't save partial config +- Suggest manual fix or file issue + +**Malformed Config**: +- Validate before migration +- Provide helpful error messages +- Include line numbers if possible + +## CLI Patterns + +### Confirmation Prompts + +ALL commands with user confirmations MUST respect both `--auto` and `-y/--assume-yes` flags. + +**Flags**: +- `--auto`: Non-interactive mode (skip all prompts, use defaults) - for automation/CI +- `-y, --assume-yes`: Answer "yes" to confirmations - for interactive convenience + +**Pattern**: +```python +@click.pass_context +def my_command(ctx, ...): + # Get flags from context + auto = ctx.obj.get('auto', False) + assume_yes = ctx.obj.get('assume_yes', False) + + # Check before prompting + if not (auto or assume_yes): + if not click.confirm("Proceed?"): + console.print("[yellow]Cancelled[/yellow]") + return + + # Continue with operation... +``` + +**Requirements**: +- ✅ ALL future commands with confirmations MUST implement this pattern +- ✅ Check BOTH flags: `if not (auto or assume_yes):` +- ✅ Always print cancellation message if user says no +- ⚠️ Never prompt if either flag is set + +### Help Text Formatting + +Use `\b` marker to preserve formatting in command docstrings: + +```python +@cli.command() +def my_command(): + """\b + This is my command. + + Examples: + command --option1 + command --option2 + + The \b prevents Click from rewrapping text. + """ +``` + +Without `\b`, Click will collapse all text into paragraphs and break formatting. + +**Requirements**: +- ✅ ALL commands with examples or formatted lists MUST use `\b` +- ✅ Place `\b` on its own line right after the opening `"""` +- ✅ Maintain proper indentation in docstrings +- ⚠️ Test help output with `--help` to verify formatting diff --git a/.claude/commands/debug-sync.md b/.claude/commands/debug-sync.md new file mode 100644 index 0000000..3022a0c --- /dev/null +++ b/.claude/commands/debug-sync.md @@ -0,0 +1,68 @@ +--- +description: Debug sync issues with verbose output and diagnostics +--- + +Debug synchronization issues with detailed logging and GitHub API diagnostics. + +Steps: +1. Check configuration is valid: + ```bash + cat release_tool.toml | grep -E "(code_repo|ticket_repos|parallel_workers|cutoff_date)" + ``` + +2. Verify GitHub token is set and valid: + ```bash + echo "Token set: $([ -n "$GITHUB_TOKEN" ] && echo 'YES' || echo 'NO')" + gh api user # Test token validity + ``` + +3. Check GitHub rate limits before sync: + ```bash + gh api rate_limit | jq '.rate' + ``` + - Show remaining requests + - Show limit and reset time + +4. Run sync and capture all output: + ```bash + poetry run release-tool sync 2>&1 | tee debug_sync.log + ``` + +5. Analyze the log for issues: + - **Errors**: Search for "[red]" or "Error" or "Warning" + - **API failures**: GitHub exceptions, rate limiting + - **Silent periods**: Gaps >2 seconds without output (performance issue) + - **Progress**: Verify all phases show progress + +6. Check database after sync: + ```bash + sqlite3 release_tool.db << EOF + SELECT 'Repositories:', COUNT(*) FROM repositories; + SELECT 'Tickets:', COUNT(*) FROM tickets; + SELECT 'Pull Requests:', COUNT(*) FROM pull_requests; + SELECT 'Releases:', COUNT(*) FROM releases; + SELECT 'Sync Metadata:', * FROM sync_metadata; + EOF + ``` + +7. If sync fails or hangs: + - Check network connectivity: `ping api.github.com` + - Check GitHub status: `gh api https://www.githubstatus.com/api/v2/status.json` + - Check repository access: `gh repo view OWNER/REPO` + - Verify repository names in config match GitHub + +8. Common issues and fixes: + - **401 Unauthorized**: GITHUB_TOKEN invalid or expired + - **404 Not Found**: Repository name wrong or no access + - **403 Rate Limited**: Wait for rate limit reset or use different token + - **No progress**: Missing console.print in github_utils.py or sync.py + - **Slow search**: Not using Search API (check github_utils.py) + - **Slow fetch**: parallel_workers too low (check config) + +9. Report diagnostics: + - Configuration status + - Token validity + - Rate limit usage + - Database state + - Errors found + - Suggested fixes diff --git a/.claude/commands/generate-release.md b/.claude/commands/generate-release.md new file mode 100644 index 0000000..80cdc25 --- /dev/null +++ b/.claude/commands/generate-release.md @@ -0,0 +1,46 @@ +--- +description: Interactive release notes generation workflow +--- + +Generate release notes for a version with automatic comparison detection and preview. + +Steps: +1. Ask user for: + - Version number (e.g., "2.0.0", "1.5.0-rc.1") + - Path to local git repository (default: current directory) + - Output preference: console, file, GitHub release, or PR + +2. Validate version format using SemanticVersion model + +3. Check if database has necessary data: + ```bash + poetry run release-tool list-releases + ``` + +4. Run generate command: + ```bash + poetry run release-tool generate VERSION \ + --repo-path ~/path/to/repo \ + [--output docs/releases/VERSION.md] \ + [--upload] \ + [--create-pr] + ``` + +5. Show preview of generated notes: + - Title and version + - Number of changes per category + - Breaking changes (if any) + - Migration notes (if any) + +6. If creating PR or release, confirm with user before proceeding + +7. Report success with links to: + - Output file (if --output) + - GitHub release URL (if --upload) + - Pull request URL (if --create-pr) + +Notes: +- Comparison version is auto-detected based on semantic versioning rules +- RCs compare to previous RC of same version, or previous final +- Finals compare to previous final version +- Use `--from-version` to override automatic comparison diff --git a/.claude/commands/lint-fix.md b/.claude/commands/lint-fix.md new file mode 100644 index 0000000..21cace3 --- /dev/null +++ b/.claude/commands/lint-fix.md @@ -0,0 +1,49 @@ +--- +description: Run linters and auto-fix code quality issues +--- + +Automatically fix code formatting and style issues using black, isort, and check types with mypy. + +Steps: +1. Run black formatter on all Python files: + ```bash + poetry run black src/ tests/ + ``` + - Reports files reformatted + - Auto-fixes formatting issues + +2. Run isort to organize imports: + ```bash + poetry run isort src/ tests/ + ``` + - Sorts imports alphabetically + - Groups imports by standard lib, third-party, local + +3. Run mypy type checker: + ```bash + poetry run mypy src/ + ``` + - Reports type errors (does NOT auto-fix) + - Show any type violations that need manual fixing + +4. Summary report: + - Files formatted: X + - Imports organized: Y + - Type errors: Z (if any) + +5. If type errors exist: + - Show the specific errors + - Offer to explain how to fix common type issues + - Suggest adding type hints where missing + +6. If changes were made, offer to: + - Show git diff of changes + - Commit the formatting changes + - Run tests to ensure nothing broke + +Example output: +``` +✓ Formatted 5 files with black +✓ Organized imports in 3 files with isort +✓ Type checking passed with 0 errors +``` diff --git a/.claude/commands/perf-profile.md b/.claude/commands/perf-profile.md new file mode 100644 index 0000000..58b3609 --- /dev/null +++ b/.claude/commands/perf-profile.md @@ -0,0 +1,60 @@ +--- +description: Profile sync performance and identify bottlenecks +--- + +Profile the sync operation to identify performance bottlenecks and measure parallelization effectiveness. + +Steps: +1. Check current configuration: + ```bash + grep -A 5 "\[sync\]" release_tool.toml + ``` + - Show parallel_workers setting (should be 20) + - Show cutoff_date if set + +2. Run sync with detailed timing: + ```bash + time -v poetry run release-tool sync 2>&1 | tee sync_profile.log + ``` + +3. Analyze the output for: + - **Search phase timing**: How long "Searching for tickets/PRs" takes + - **Filtering phase**: Time spent filtering against existing DB + - **Parallel fetch timing**: Items/second throughput + - **Progress gaps**: Any delays >2 seconds without feedback + +4. Calculate metrics: + - Total items fetched + - Total time taken + - Throughput (items/second) + - Expected vs actual performance + +5. Check GitHub API rate limit usage: + ```bash + gh api rate_limit + ``` + - Show remaining requests + - Show reset time + - Calculate requests used during sync + +6. Analyze database size and query performance: + ```bash + ls -lh release_tool.db + sqlite3 release_tool.db "SELECT COUNT(*) FROM tickets;" + sqlite3 release_tool.db "SELECT COUNT(*) FROM pull_requests;" + ``` + +7. Report findings: + - **Performance**: X items in Y seconds = Z items/sec + - **Expected**: ~20 items/sec with 20 workers + - **Bottlenecks**: If <15 items/sec, identify why: + - Network latency? + - Rate limiting? + - Database write contention? + - Sequential operations that should be parallel? + +8. Suggestions for improvement: + - If slow search: Using Search API? (check github_utils.py) + - If slow fetch: Enough parallel workers? (check config) + - If slow filter: DB indexes needed? (check db.py) + - If no progress: Missing console.print statements? diff --git a/.claude/commands/sync-fast.md b/.claude/commands/sync-fast.md new file mode 100644 index 0000000..c464a37 --- /dev/null +++ b/.claude/commands/sync-fast.md @@ -0,0 +1,27 @@ +--- +description: Fast sync with progress monitoring and timing statistics +--- + +Run a fast, parallelized sync of GitHub data (tickets, PRs, releases) with detailed progress monitoring. + +Steps: +1. Check if `release_tool.toml` exists and has valid configuration +2. Verify GITHUB_TOKEN environment variable is set +3. Run sync command with timing: + ```bash + time poetry run release-tool sync + ``` +4. Monitor the output for: + - Progress indicators (searching, filtering, fetching) + - Parallel fetch progress bars + - Final statistics (tickets, PRs, releases synced) +5. Report total time taken +6. If sync takes >60 seconds, suggest checking: + - Network connectivity + - GitHub rate limits: `gh api rate_limit` + - Database size: `ls -lh release_tool.db` + +Expected performance: +- Search phase: <5 seconds per repository +- Parallel fetch: 20 items/second with 20 workers +- Total for 1000 items: ~50-60 seconds diff --git a/.claude/commands/test-affected.md b/.claude/commands/test-affected.md new file mode 100644 index 0000000..b65529f --- /dev/null +++ b/.claude/commands/test-affected.md @@ -0,0 +1,47 @@ +--- +description: Run tests for recently modified modules +--- + +Smart test execution that focuses on modules affected by recent changes. + +Steps: +1. Check git status to find modified files: + ```bash + git status --short + ``` + +2. Identify modified Python modules in `src/release_tool/` + +3. Map modules to their corresponding test files: + - `models.py` → `tests/test_models.py` + - `config.py` → `tests/test_config.py` + - `db.py` → `tests/test_db.py` + - `git_ops.py` → `tests/test_git_ops.py` + - `policies.py` → `tests/test_policies.py` + - `sync.py` → `tests/test_sync.py` + +4. Run affected tests with verbose output: + ```bash + poetry run pytest tests/test_MODULE.py -v --tb=short + ``` + +5. If multiple modules changed, run all affected tests together: + ```bash + poetry run pytest tests/test_file1.py tests/test_file2.py -v + ``` + +6. Show summary: + - Tests run: X + - Passed: Y + - Failed: Z + - Duration: T seconds + +7. If tests fail, offer to: + - Show detailed failure output + - Run just the failed test + - Run with debugger (--pdb) + +Optional: Run with coverage for changed modules: +```bash +poetry run pytest tests/test_MODULE.py --cov=release_tool.MODULE --cov-report=term-missing +``` diff --git a/.claude/ignore/.gitignore b/.claude/ignore/.gitignore new file mode 100644 index 0000000..603f3e4 --- /dev/null +++ b/.claude/ignore/.gitignore @@ -0,0 +1,35 @@ +# Python +*.pyc +__pycache__/ +*.pyo +*.pyd +.Python + +# Database +*.db +*.sqlite +*.sqlite3 + +# Release tool cache +.release_tool_cache/ + +# Logs +*.log +sync_profile.log +debug_sync.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +tmp/ +temp/ +*.tmp diff --git a/.claude/prompts/architecture.md b/.claude/prompts/architecture.md new file mode 100644 index 0000000..27fe862 --- /dev/null +++ b/.claude/prompts/architecture.md @@ -0,0 +1,267 @@ +# Release Tool - System Architecture + +## Module Breakdown + +### main.py (CLI Entry Point) +**Purpose**: Command-line interface using Click + +**Commands**: +- `sync` - Synchronize GitHub data (tickets, PRs, releases) to local database +- `generate` - Generate release notes for a version (saves to cache by default) +- `publish` - Publish release notes to GitHub (create release/PR) +- `list-releases` - List releases from database with filtering options +- `init-config` - Create example configuration file +- `update-config` - Upgrade configuration file to latest version + +**Key Functions**: +- `cli()` - Main CLI group with config loading +- `sync()` - Orchestrates SyncManager for data fetching +- `generate()` - Orchestrates release note generation with auto-version bumping +- `publish()` - Creates GitHub releases/PRs from generated markdown files +- `list_releases()` - Queries database with filters (version, type, date range) + +### models.py (Data Models) +**Purpose**: Pydantic models for type-safe data handling + +**Core Models**: +- `SemanticVersion` - Parse/compare versions with prerelease support +- `Repository` - GitHub repo metadata (owner, name, default_branch) +- `Author` - Contributor info (username, email, display_name, avatar) +- `Label` - GitHub label (name, color, description) +- `Ticket` - GitHub issue (number, title, body, labels, state) +- `PullRequest` - GitHub PR (number, title, merged_at, author, labels) +- `Commit` - Git commit (sha, message, author, timestamp) +- `Release` - GitHub release (version, tag, body, published_at) +- `ReleaseNote` - Generated note (title, description, category, authors, PRs) + +**Key Methods**: +- `SemanticVersion.parse()` - Parse version strings +- `SemanticVersion.compare()` - Version comparison logic +- `SemanticVersion.is_final()` - Check if not a prerelease + +### config.py (Configuration Management) +**Purpose**: Load and validate configuration from TOML files + +**Config Sections**: +- `RepositoryConfig` - code_repo, ticket_repos, default_branch +- `GitHubConfig` - token, api_url +- `DatabaseConfig` - SQLite database path +- `SyncConfig` - parallel_workers (20), cutoff_date, clone_code_repo +- `TicketPolicyConfig` - extraction patterns, consolidation rules +- `VersionPolicyConfig` - tag_prefix, gap_detection +- `ReleaseNoteConfig` - categories, templates, excluded_labels +- `OutputConfig` - output paths, GitHub release/PR creation + +**Key Methods**: +- `load_config()` - Load from file with defaults (auto-upgrades old versions) +- `Config.from_file()` - Load from TOML with version checking +- `Config.from_dict()` - Create from dictionary +- `get_ticket_repos()` - Get ticket repositories list +- `get_category_map()` - Label to category mapping + +### migrations/ (Config Migration System) +**Purpose**: Handle automatic upgrades of configuration files between versions + +**Structure**: +- `migrations/manager.py` - MigrationManager class +- `migrations/v1_0_to_v1_1.py` - Individual migration scripts (one per version transition) + +**MigrationManager Methods**: +- `compare_versions()` - Semantic version comparison using packaging library +- `needs_upgrade()` - Check if config version is outdated +- `get_migration_path()` - Find migration chain (e.g., 1.0 → 1.1 → 1.2) +- `_discover_migrations()` - Auto-discover migration files in migrations/ directory +- `load_migration()` - Dynamically load migration module +- `apply_migration()` - Execute single migration +- `upgrade_config()` - Apply full migration chain +- `get_changes_description()` - Human-readable change summary + +**Migration File Format**: +Each migration is a Python file with a `migrate()` function: +```python +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """Migrate from version X to version Y.""" + # Transform config_dict + # Update config_version + return updated_config +``` + +**Auto-Upgrade Flow**: +1. Config loads, checks `config_version` field +2. If outdated, shows changes to user +3. Prompts to upgrade (or auto-upgrades with `--auto`) +4. Applies migration chain +5. Saves upgraded config back to TOML file + +### db.py (Database Operations) +**Purpose**: SQLite database for local caching + +**Tables**: +- `repositories` - Repository metadata +- `tickets` - GitHub issues +- `pull_requests` - GitHub PRs +- `commits` - Git commits with ticket associations +- `releases` - GitHub releases +- `authors` - Contributor information +- `sync_metadata` - Last sync timestamps for incremental updates + +**Key Methods**: +- `connect()` / `close()` - Database lifecycle +- `init_db()` - Create schema +- `upsert_*()` - Insert or update records +- `get_existing_ticket_numbers()` - Fast filtering for incremental sync +- `get_last_sync()` - Get last sync timestamp +- `update_sync_metadata()` - Record sync completion + +### git_ops.py (Git Operations) +**Purpose**: Local Git repository analysis + +**Key Functions**: +- `get_release_commit_range()` - Get commits between two versions +- `find_comparison_version()` - Auto-detect version to compare against + - Finals compare to previous final + - RCs compare to previous RC of same version or previous final +- `get_all_tags()` - List all version tags +- `parse_version_from_tag()` - Extract version from tag name + +### github_utils.py (GitHub API Client) +**Purpose**: Parallelized GitHub API operations + +**GitHubClient Methods**: +- `get_repository_info()` - Fetch repository metadata +- `search_ticket_numbers()` - Fast search for tickets using Search API +- `search_pr_numbers()` - Fast search for PRs using Search API +- `fetch_issue()` - Get full ticket details +- `get_pull_request()` - Get full PR details +- `fetch_releases()` - Get all releases (parallelized) +- `create_release()` - Create GitHub release +- `create_pr_for_release_notes()` - Create PR with release notes + +**Performance**: +- Uses GitHub Search API (not lazy iteration) +- ThreadPoolExecutor with 20 workers +- Batch processing for efficiency +- Progress feedback at all stages + +### sync.py (Sync Manager) +**Purpose**: Orchestrate parallelized GitHub data synchronization + +**SyncManager Methods**: +- `sync_all()` - Sync all data (tickets, PRs, git repo) +- `_sync_tickets_for_repo()` - Incremental ticket sync for a repository +- `_sync_pull_requests_for_repo()` - Incremental PR sync +- `_fetch_tickets_streaming()` - Parallel ticket fetch with progress +- `_fetch_prs_streaming()` - Parallel PR fetch with progress +- `_sync_git_repository()` - Clone or update local git repo + +**Workflow**: +1. Get last sync timestamp +2. Search for new items (GitHub Search API) +3. Filter against existing DB items +4. Parallel fetch full details +5. Store incrementally +6. Update sync metadata + +### policies.py (Business Logic) +**Purpose**: Implement ticket extraction, consolidation, and categorization + +**Classes**: + +**TicketExtractor**: +- Extract ticket references from commits, PRs, branches +- Multiple strategies with priority ordering +- Regex patterns for JIRA, GitHub issues, custom formats + +**CommitConsolidator**: +- Group commits by parent ticket +- Consolidate multiple commits into single release note +- Use ticket title and description instead of commit messages + +**ReleaseNoteGenerator**: +- Create ReleaseNote objects from commits/tickets +- Categorize by labels (Features, Bug Fixes, Breaking Changes, etc.) +- Apply exclusion rules (skip-changelog, internal, etc.) +- Group and sort by category + +**VersionGapChecker**: +- Detect gaps in version sequence +- Policy actions: ignore, warn, error + +### media_utils.py (Media Handling) +**Purpose**: Download and process media from ticket descriptions + +**Functions**: +- `download_media_from_description()` - Extract and download images/videos +- `replace_media_urls()` - Update URLs to local paths in markdown + +## Data Flow + +### Sync Flow +``` +User runs: release-tool sync + ↓ +SyncManager.sync_all() + ↓ +For each ticket_repo: + ├─ GitHubClient.search_ticket_numbers() [GitHub Search API] + ├─ Database.get_existing_ticket_numbers() [Filter] + ├─ GitHubClient.fetch_issue() × N [Parallel, 20 workers] + └─ Database.upsert_ticket() [Store] + ↓ +For code_repo: + ├─ GitHubClient.search_pr_numbers() [GitHub Search API] + ├─ Database.get_existing_pr_numbers() [Filter] + ├─ GitHubClient.get_pull_request() × N [Parallel, 20 workers] + └─ Database.upsert_pull_request() [Store] + ↓ +GitHubClient.fetch_releases() [Parallel] + ↓ +git clone/pull [If enabled] +``` + +### Generate Flow +``` +User runs: release-tool generate 2.0.0 --repo-path ~/repo + ↓ +GitOps.find_comparison_version() [Auto-detect from version] + ↓ +GitOps.get_release_commit_range() [Extract commits from Git] + ↓ +TicketExtractor.extract_all() [Find ticket references] + ↓ +CommitConsolidator.consolidate() [Group by ticket] + ↓ +Database.fetch_ticket() [Get ticket metadata] + ↓ +ReleaseNoteGenerator.generate() [Create categorized notes] + ↓ +Jinja2 template rendering [Format output] + ↓ +Output to console / file / GitHub release / PR +``` + +## Design Patterns + +### 1. Repository Pattern +- `Database` class abstracts SQLite operations +- Models define data structure +- DB operations return model instances + +### 2. Strategy Pattern +- Multiple ticket extraction strategies +- Policy-based actions (warn, error, ignore) +- Template-based output rendering + +### 3. Builder Pattern +- `ReleaseNoteGenerator` builds notes incrementally +- `Config` built from TOML with defaults + +### 4. Parallel Processing Pattern +- ThreadPoolExecutor for I/O-bound operations +- `as_completed()` for progress tracking +- Batch processing for efficiency + +### 5. Incremental Sync Pattern +- Track last sync timestamp +- Only fetch new/updated items +- Filter against existing DB data diff --git a/.claude/prompts/performance-guidelines.md b/.claude/prompts/performance-guidelines.md new file mode 100644 index 0000000..dde08ac --- /dev/null +++ b/.claude/prompts/performance-guidelines.md @@ -0,0 +1,318 @@ +# Release Tool - Performance Guidelines + +## Critical Performance Requirements + +When working on this codebase, these performance principles are **MANDATORY**: + +## 1. Parallelize ALL Network Operations + +### Rule: GitHub API Calls Must Be Parallel + +**Bad** - Sequential (NEVER do this): +```python +for issue_number in issue_numbers: + issue = github.fetch_issue(repo, issue_number) # Each call blocks + process(issue) +``` + +**Good** - Parallel (ALWAYS do this): +```python +from concurrent.futures import ThreadPoolExecutor, as_completed + +with ThreadPoolExecutor(max_workers=20) as executor: + futures = { + executor.submit(github.fetch_issue, repo, num): num + for num in issue_numbers + } + for future in as_completed(futures): + issue = future.result() + process(issue) +``` + +### Configuration +- **Workers**: 20 parallel workers (default in config.py:279) +- **Batch size**: 100-200 items per batch (github_utils.py:65) +- **Rate limit**: GitHub allows 5000 requests/hour = 83/minute +- **Safe**: 20 parallel workers ≈ 20 requests/second = well within limits + +## 2. Use GitHub Search API (Not Lazy Iteration) + +### Rule: NEVER Use PyGithub's Lazy Iteration + +**Bad** - Lazy iteration (NEVER do this): +```python +# Each iteration makes a network call - EXTREMELY SLOW +for issue in repo.get_issues(state='all', since=cutoff_date): + if issue.pull_request is None: + issue_numbers.append(issue.number) +``` + +**Good** - Search API (ALWAYS do this): +```python +# Single fast API call with pagination handled by GitHub +query = f"repo:{repo_name} is:issue" +if cutoff_date: + query += f" created:>={cutoff_date.strftime('%Y-%m-%d')}" + +issues = gh.search_issues(query, sort='created', order='desc') +issue_numbers = [issue.number for issue in issues] +``` + +### Why Search API is Faster +- Lazy iteration: 1 network call per item = 1000 items = 1000 calls +- Search API: Paginated results = 1000 items = ~10 calls (100 per page) +- **Speed improvement: 10-100x faster** + +## 3. CRITICAL: Use PyGithub raw_data (No Lazy Loading) + +### Rule: NEVER Access PyGithub Object Attributes Directly in Bulk Operations + +**CRITICAL PERFORMANCE RULE**: During bulk sync operations (issues, PRs, commits, etc.), accessing PyGithub object attributes directly triggers **individual API calls** for each attribute not in the partial response. + +**Bad** - Direct attribute access (NEVER do this in bulk operations): +```python +def _issue_to_ticket(self, gh_issue, repo_id): + return Ticket( + number=gh_issue.number, + title=gh_issue.title, # ❌ LAZY LOAD - triggers API call! + body=gh_issue.body, # ❌ LAZY LOAD - triggers API call! + state=gh_issue.state, # ❌ LAZY LOAD - triggers API call! + url=gh_issue.html_url, # ❌ LAZY LOAD - triggers API call! + created_at=gh_issue.created_at # ❌ LAZY LOAD - triggers API call! + ) +# Result: Converting 100 issues = 500+ API calls = 2 MINUTES! +``` + +**Good** - Use raw_data dictionary (ALWAYS do this): +```python +def _issue_to_ticket(self, gh_issue, repo_id): + # Use raw_data to avoid lazy loading + raw = getattr(gh_issue, 'raw_data', {}) + + return Ticket( + number=gh_issue.number, # ✅ Safe - always in partial response + title=raw.get('title'), # ✅ No API call - from raw_data + body=raw.get('body'), # ✅ No API call - from raw_data + state=raw.get('state'), # ✅ No API call - from raw_data + url=raw.get('html_url'), # ✅ No API call - from raw_data + created_at=raw.get('created_at') # ✅ No API call - from raw_data + ) +# Result: Converting 100 issues = 0 API calls = INSTANT! +``` + +### Applies To ALL PyGithub Objects +- **Issues**: `title`, `body`, `state`, `html_url`, `created_at`, `closed_at`, `labels` +- **Pull Requests**: `title`, `body`, `state`, `merged_at`, `base`, `head`, `labels`, `user` +- **Commits**: `message`, `author`, `committer`, `parents` +- **Labels**: `name`, `color`, `description` +- **Milestones**: `title`, `description`, `state`, `due_on` +- **Users**: `name`, `email`, `company`, `location`, `bio`, `blog` + +### Extracting Nested Objects from raw_data +```python +# Labels - extract from raw_data list +raw = getattr(gh_pr, 'raw_data', {}) +labels = [] +for label_data in raw.get('labels', []): + labels.append(Label( + name=label_data.get('name', ''), + color=label_data.get('color', ''), + description=label_data.get('description') + )) + +# Nested objects (base/head for PRs) +base_data = raw.get('base', {}) +head_data = raw.get('head', {}) +base_branch = base_data.get('ref') +head_sha = head_data.get('sha') + +# User objects - create mock with raw_data +user_data = raw.get('user') +if user_data: + from types import SimpleNamespace + gh_user = SimpleNamespace( + login=user_data.get('login'), + id=user_data.get('id'), + raw_data=user_data + ) +``` + +### Performance Impact +- **Without raw_data**: 100 items = 500+ API calls = 2+ minutes +- **With raw_data**: 100 items = 0 extra API calls = <1 second +- **Speed improvement: 100-1000x faster** + +## 4. Progress Feedback ALWAYS + +### Rule: Never Leave User Waiting >2 Seconds Without Feedback + +**Required feedback points**: +1. **Before network call**: "Searching for tickets..." +2. **After search**: "Found 123 tickets" +3. **Before filtering**: "Filtering 123 tickets against existing 456 in database..." +4. **Before parallel fetch**: "Fetching 67 new tickets in parallel..." +5. **During fetch**: Progress bar with "Fetched 13/67 tickets (19%)" +6. **After fetch**: "✓ Synced 67 tickets" + +**Example implementation**: +```python +console.print("[cyan]Searching for tickets...[/cyan]") +query = f"repo:{repo_name} is:issue" +issues = gh.search_issues(query) +console.print(f"[green]✓[/green] Found {len(issues)} tickets") + +if all_numbers: + console.print(f"[dim]Filtering {len(all_numbers)} tickets...[/dim]") +new_numbers = [n for n in all_numbers if n not in existing] + +console.print(f"[cyan]Fetching {len(new_numbers)} new tickets in parallel...[/cyan]") + +with Progress(...) as progress: + task = progress.add_task("Fetching tickets...", total=len(new_numbers)) + for future in as_completed(futures): + result = future.result() + progress.update(task, advance=1, description=f"Fetched {completed}/{total}") +``` + +## 4. Batch Processing + +### Optimal Batch Sizes +- **GitHub Search**: No batching needed (API handles pagination) +- **Parallel fetch**: 100-200 items per batch +- **Database writes**: Immediate (upsert as results arrive) + +### Example +```python +batch_size = 100 # Optimal for GitHub API throughput + +pr_batch = [] +for pr in gh_prs: + pr_batch.append(pr) + + if len(pr_batch) >= batch_size: + batch_results = self._process_pr_batch(pr_batch) # Parallel processing + prs_data.extend(batch_results) + pr_batch = [] # Reset batch +``` + +## 5. Incremental Sync + +### Rule: Only Fetch New/Updated Items + +**Bad** - Fetch everything every time: +```python +all_tickets = fetch_all_tickets() # Wasteful +store_in_db(all_tickets) +``` + +**Good** - Incremental with cutoff date: +```python +last_sync = db.get_last_sync(repo, 'tickets') +cutoff_date = last_sync or config.sync.cutoff_date + +# Only fetch items since last sync +query = f"repo:{repo_name} is:issue created:>={cutoff_date}" +new_tickets = gh.search_issues(query) + +# Filter out any we already have +existing_numbers = db.get_existing_ticket_numbers(repo) +to_fetch = [n for n in new_tickets if n not in existing_numbers] +``` + +## Performance Metrics + +### Expected Performance +- **Search phase**: <5 seconds per repository +- **Filter phase**: <1 second (in-memory set operations) +- **Parallel fetch**: ~20 items/second with 20 workers +- **Total for 1000 items**: ~50-60 seconds + +### Actual Performance Measurements +```bash +# Profile a sync +time poetry run release-tool sync + +# Check throughput +# Expected: 15-25 items/second +# If <10 items/second: investigate bottlenecks +``` + +### Common Bottlenecks +1. **Slow search** → Not using Search API +2. **Slow fetch** → Not enough parallel workers +3. **Slow filter** → Database query needs index +4. **No progress** → Missing console.print statements +5. **Rate limiting** → Too many requests (unlikely with 20 workers) + +## Code Review Checklist + +Before committing network-related code, verify: + +- [ ] Using GitHub Search API (not `repo.get_issues()` iteration) +- [ ] Parallel processing with ThreadPoolExecutor (max_workers=20) +- [ ] Progress feedback before/during/after each operation +- [ ] No silent periods >2 seconds +- [ ] Incremental sync (not fetching everything) +- [ ] Batch size 100-200 for optimal throughput +- [ ] Error handling with try/except +- [ ] Progress updates even on errors + +## Anti-Patterns to Avoid + +### ❌ Sequential Network Calls +```python +for item in items: + result = api.fetch(item) # BAD +``` + +### ❌ Lazy Iteration +```python +for issue in repo.get_issues(): # BAD - each iteration is a network call + process(issue) +``` + +### ❌ No Progress Feedback +```python +# User sees nothing for 30 seconds - BAD +items = fetch_all_items() +``` + +### ❌ Fetching Everything Every Time +```python +all_data = fetch_all() # BAD - wasteful +``` + +## Recommended Patterns + +### ✅ Parallel Fetch with Progress +```python +console.print("[cyan]Fetching items...[/cyan]") + +with Progress() as progress: + task = progress.add_task("Fetching...", total=len(items)) + + with ThreadPoolExecutor(max_workers=20) as executor: + futures = {executor.submit(fetch, i): i for i in items} + + for future in as_completed(futures): + result = future.result() + progress.update(task, advance=1) +``` + +### ✅ Search API with Cutoff Date +```python +query = f"repo:{repo_name} is:issue" +if since: + query += f" created:>={since.strftime('%Y-%m-%d')}" + +console.print(f"[cyan]Searching...[/cyan]") +results = gh.search_issues(query) +console.print(f"[green]✓[/green] Found {len(results)} items") +``` + +### ✅ Incremental with Filtering +```python +existing = db.get_existing_numbers(repo) +new_only = [n for n in all_numbers if n not in existing] +console.print(f"[cyan]Fetching {len(new_only)} new items...[/cyan]") +``` diff --git a/.claude/prompts/project-context.md b/.claude/prompts/project-context.md new file mode 100644 index 0000000..5a50d14 --- /dev/null +++ b/.claude/prompts/project-context.md @@ -0,0 +1,488 @@ +# Release Tool - Core Project Context + +## What This Project Is + +**release-tool** is a Python CLI application for managing semantic versioned releases. It automates the generation of release notes by: +1. Analyzing Git commit history between versions +2. Consolidating commits by parent tickets +3. Fetching ticket metadata from GitHub Issues +4. Categorizing changes by labels +5. Rendering formatted release notes via Jinja2 templates + +## Technology Stack + +- **Python 3.10+** with full type hints +- **PyGithub** - GitHub API v3 client +- **GitPython** - Local Git repository operations +- **Pydantic** - Data validation and settings +- **Click** - CLI framework +- **Rich** - Terminal formatting and progress bars +- **Jinja2** - Template engine +- **SQLite** - Local caching database +- **pytest** - Testing framework + +## Architecture Overview + +``` +CLI (main.py) + ↓ +Database (db.py) ←→ GitHub API (github_utils.py) + ↓ ↓ +Git Ops (git_ops.py) Sync Manager (sync.py) + ↓ ↓ +Policies (policies.py) ←→ Models (models.py) + ↓ +Output (Jinja2 templates) +``` + +## Core Data Models (models.py) + +- `SemanticVersion` - Parse and compare versions (2.0.0, 1.5.0-rc.1) +- `Repository` - GitHub repository metadata +- `Ticket` - GitHub issue/ticket details +- `PullRequest` - GitHub PR with merge info +- `Commit` - Git commit with ticket association +- `Release` - GitHub release information +- `ReleaseNote` - Generated release note entry +- `Author` - Contributor information + +## Key Workflows + +### Sync Workflow (sync.py) +1. Get last sync timestamp from DB +2. Use GitHub Search API to find new tickets/PRs +3. Filter against existing items in database +4. Parallel fetch full details (20 workers) +5. Store incrementally with progress updates + +### Generate Workflow (policies.py) +1. Extract commits between versions from Git +2. Extract ticket references from commits +3. Consolidate commits by parent ticket +4. Fetch ticket metadata from DB/GitHub +5. Categorize by labels +6. Render via Jinja2 template +7. Output to console/file/GitHub + +## Performance Requirements (CRITICAL) + +### Network Operations +- **ALWAYS use parallel processing** (ThreadPoolExecutor, 20 workers) +- **ALWAYS use GitHub Search API** instead of lazy iteration +- **NEVER** use `for item in repo.get_issues()` (slow, sequential) +- **ALWAYS** use `gh.search_issues(query)` (fast, paginated) + +### Progress Feedback +- **NEVER** leave user waiting >2 seconds without output +- Show progress at every phase: searching, filtering, fetching +- Use Rich progress bars with percentage/count +- Example: "Searching for tickets..." → "Found 123 tickets" → "Fetching 45 new tickets in parallel..." + +### Batch Sizes +- Search: Single API call per page (GitHub handles pagination) +- Fetch: 100-200 items per batch +- Workers: 20 parallel workers (safe for GitHub's 5000/hour limit) + +## Code Patterns + +### Good Pattern - Parallel Fetch +```python +console.print("[cyan]Searching for tickets...[/cyan]") +query = f"repo:{repo_name} is:issue" +issues = gh.search_issues(query) +console.print(f"[green]✓[/green] Found {len(issues)} tickets") + +with ThreadPoolExecutor(max_workers=20) as executor: + futures = {executor.submit(fetch_ticket, num): num for num in numbers} + for future in as_completed(futures): + ticket = future.result() + progress.update(...) # Show progress +``` + +### Bad Pattern - Sequential Iteration +```python +# DON'T DO THIS - Each iteration is a network call +for issue in repo.get_issues(state='all'): + process(issue) +``` + +## Branch Management Strategy (CRITICAL) + +### Automatic Release Branching +Release branches are **automatically created** based on semantic versioning rules: + +#### Branching Rules: +1. **New Major (X.0.0)**: Branches from `main` (configurable via `branch_policy.default_branch`) +2. **New Minor (x.Y.0)**: Branches from previous release branch `release/{major}.{minor-1}` if it exists, otherwise from `main` +3. **Patch/RC (x.y.Z or x.y.z-rc.N)**: Uses existing release branch `release/{major}.{minor}` + +#### Configuration (`release_tool.toml`): +```toml +[branch_policy] +release_branch_template = "release/{major}.{minor}" # Branch naming pattern +default_branch = "main" # For new major versions +create_branches = true # Auto-create branches +branch_from_previous_release = true # Minor from previous release branch +``` + +#### Examples: +```bash +# 9.0.0 (new major) → Creates release/9.0 from main +release-tool generate 9.0.0 --dry-run + +# 9.1.0 (new minor) → Creates release/9.1 from release/9.0 +release-tool generate 9.1.0 --dry-run + +# 9.1.0-rc (first RC) → Uses existing release/9.1, creates 9.1.0-rc.0 +release-tool generate --new-rc --dry-run + +# 9.0.5 (hotfix) → Uses existing release/9.0 (not release/9.1!) +release-tool generate 9.0.5 --dry-run +``` + +#### Important Notes: +- Branch creation happens during `generate` command (unless `create_branches = false`) +- Dry-run shows branch strategy without creating anything +- Tool displays: branch name, source branch, whether it will create +- Release branches persist for hotfix workflow +- See `docs/branching-strategy.md` for full documentation + +## Release Generation Workflow (CRITICAL) + +### Two-Step Process +Release generation is split into two distinct commands for safety and flexibility: + +1. **`generate`** - Creates release notes (read-only, safe) +2. **`publish`** - Uploads to GitHub (writes to GitHub, needs confirmation) + +### Generate Command +Creates release notes and saves to cache directory for review. Use `--dry-run` to preview without creating files. + +#### Version Specification (pick ONE): +- **Explicit**: `release-tool generate 9.1.0` +- **Auto-bump major**: `release-tool generate --new-major` (1.2.3 → 2.0.0) +- **Auto-bump minor**: `release-tool generate --new-minor` (1.2.3 → 1.3.0) +- **Auto-bump patch**: `release-tool generate --new-patch` (1.2.3 → 1.2.4) +- **Create RC**: `release-tool generate --new-rc` (auto-increments: 1.2.3 → 1.2.3-rc.0, then 1.2.3-rc.1, etc.) + +#### Key Options: +- `--dry-run` - Preview output without creating files or branches (ALWAYS use first!) +- `--output, -o` - Save to custom file path (defaults to `.release_tool_cache/draft-releases/{repo}/{version}.md`) +- `--format` - Output format: `markdown` (default) or `json` +- `--from-version` - Compare from specific version (auto-detected if omitted) +- `--repo-path` - Path to git repo (defaults to synced repo from `sync` command) + +#### Default Output Behavior: +- **Without `--output`**: Saves to `.release_tool_cache/draft-releases/{repo}/{version}.md` +- **With `--dry-run`**: Only displays output to console, creates nothing +- **Path printed**: Full path is displayed so you can edit before publishing +- **Configurable**: Set `draft_output_path` in `release_tool.toml` to customize default path + +#### Examples: +```bash +# ALWAYS start with dry-run to preview (uses synced repo automatically) +release-tool generate --new-minor --dry-run + +# Generate and save to default cache path (recommended workflow) +release-tool generate --new-minor +# Output: ✓ Release notes written to: .release_tool_cache/draft-releases/owner-repo/9.1.0.md + +# Create RC for testing (auto-increments: rc.0, rc.1, rc.2, etc.) +release-tool generate --new-rc +# Output: .release_tool_cache/draft-releases/owner-repo/9.1.0-rc.0.md + +# Save to custom path if needed +release-tool generate --new-minor -o docs/releases/9.1.0.md + +# Explicit version with dry-run +release-tool generate 9.1.0 --dry-run + +# Use custom repo path if needed +release-tool generate --new-patch --repo-path /path/to/repo +``` + +### Publish Command +Uploads release notes to GitHub. Separated from generation for safety. + +#### Key Options: +- `--notes-file, -f` - Path to markdown file with release notes (required for PR) +- `--release/--no-release` - Create GitHub release (default: true) +- `--pr/--no-pr` - Create PR with release notes (default: false) +- `--draft` - Create as draft release +- `--prerelease` - Mark as prerelease (auto-detected from version) + +#### Examples: +```bash +# Publish release to GitHub +release-tool publish 9.1.0 -f docs/releases/9.1.0.md + +# Create draft release for review +release-tool publish 9.1.0-rc.0 -f docs/releases/9.1.0-rc.0.md --draft + +# Create PR without GitHub release +release-tool publish 9.1.0 -f docs/releases/9.1.0.md --pr --no-release + +# Just create PR (for review process) +release-tool publish 9.1.0 -f docs/releases/9.1.0.md --pr --no-release +``` + +### Recommended Release Workflow +```bash +# 1. Sync repository first (one-time setup or to update) +release-tool sync + +# 2. Generate with dry-run first (ALWAYS!) +release-tool generate --new-minor --dry-run + +# 3. Generate and save to default cache location +release-tool generate --new-minor +# ✓ Release notes written to: .release_tool_cache/draft-releases/owner-repo/9.1.0.md + +# 4. Review/edit the file if needed +vim .release_tool_cache/draft-releases/owner-repo/9.1.0.md + +# 5. Publish to GitHub using the generated file +release-tool publish 9.1.0 -f .release_tool_cache/draft-releases/owner-repo/9.1.0.md + +# For RC workflow (auto-increments RC numbers): +release-tool generate --new-rc # Creates 9.1.0-rc.0 +# Edit: .release_tool_cache/draft-releases/owner-repo/9.1.0-rc.0.md +release-tool publish 9.1.0-rc.0 -f .release_tool_cache/draft-releases/owner-repo/9.1.0-rc.0.md --draft + +# Next RC (auto-increments to rc.1): +release-tool generate --new-rc # Creates 9.1.0-rc.1 +release-tool publish 9.1.0-rc.1 -f .release_tool_cache/draft-releases/owner-repo/9.1.0-rc.1.md --draft +``` + +## Testing Requirements (MANDATORY) + +**CRITICAL**: All code changes MUST include unit tests and all tests MUST pass before committing. + +### Rules +1. **Every new feature requires tests** - No exceptions +2. **All tests must pass** - Run `poetry run pytest tests/` before committing +3. **Test both success and error paths** - Happy path AND edge cases +4. **Mock external dependencies** - Don't hit real GitHub API in tests +5. **Use pytest fixtures** - Reuse setup code across tests + +### Current Test Suite +- **Total tests**: 100 tests across 8 test files +- **Coverage requirement**: >80% for critical paths +- **Test execution**: All tests must complete in <1 second + +### Test Patterns +- Use `pytest` fixtures for database, config, mocks +- Test file naming: `test_.py` +- Test function naming: `test_` +- Assert specific values, not just truthiness + +### Before Committing +```bash +# ALWAYS run this before committing +poetry run pytest tests/ -v + +# All tests must pass - no exceptions +# If tests fail, fix them before committing +``` + +## Configuration (config.py) + +Loaded from `release_tool.toml`: +- Repository settings (code_repo, ticket_repos) +- Sync configuration (parallel_workers, cutoff_date) +- Ticket policies (extraction patterns, consolidation) +- Version policies (tag_prefix, gap_detection) +- Branch policy (release_branch_template, default_branch, create_branches, branch_from_previous_release) +- Release note categories and templates +- Output settings (output_path, draft_output_path, assets_path, GitHub integration) + +### Key Configuration Fields: +- **`output.draft_output_path`**: Default path for generated release notes (default: `.release_tool_cache/draft-releases/{repo}/{version}.md`) +- **`branch_policy.release_branch_template`**: Template for release branch names (default: `release/{major}.{minor}`) +- **`branch_policy.create_branches`**: Auto-create release branches (default: true) +- **`branch_policy.branch_from_previous_release`**: Branch new minors from previous release (default: true) + +## Config Versioning System (CRITICAL) + +**MANDATORY**: The configuration file (`release_tool.toml`) is versioned using semantic versioning. When making format changes, you MUST follow these rules strictly. + +### Config Version Field +Every config file has a `config_version` field (e.g., `config_version = "1.1"`). This is automatically checked when loading the config. + +### When to Increment Config Version + +You MUST increment the config version when making ANY of these changes: + +1. **Adding new required fields** to the config schema +2. **Removing fields** from the config schema +3. **Changing field types** or validation rules +4. **Modifying template variables** available in: + - `entry_template` + - `output_template` + - `title_template` + - `description_template` + - PR templates (branch_template, title_template, body_template) +5. **Changing default template structure** (e.g., output_template formatting) +6. **Renaming fields** or changing field semantics + +### When NOT to Increment + +You do NOT need to increment version for: +- Bug fixes that don't affect config structure +- Internal code refactoring +- Documentation updates +- Adding optional fields with backward-compatible defaults + +### Versioning Scheme + +Use semantic versioning for config versions: +- **Major (2.0)**: Breaking changes that require manual intervention +- **Minor (1.1)**: Backward-compatible additions or improvements +- **Patch (1.1.1)**: Bug fixes (rare for config, usually use minor) + +Current version: **1.1** + +### Migration System + +The migration system handles automatic upgrades of config files between versions. + +#### Creating a Migration + +When incrementing the config version, create a migration file in `src/release_tool/migrations/`: + +```bash +# File naming: v{from}_to_v{to}.py (underscores for dots) +# Example: v1_0_to_v1_1.py for 1.0 → 1.1 +``` + +Migration file structure: +```python +"""Migration from config version X.Y to X.Z. + +Changes in X.Z: +- Change 1 +- Change 2 + +This migration: +- What it does to the config +""" + +from typing import Dict, Any + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """Migrate config from version X.Y to X.Z.""" + updated_config = config_dict.copy() + + # Apply transformations + # Example: Add new field with default + if 'new_section' not in updated_config: + updated_config['new_section'] = {'new_field': 'default_value'} + + # Example: Update template if still using old default + if updated_config.get('template') == OLD_DEFAULT: + updated_config['template'] = NEW_DEFAULT + + # Update version + updated_config['config_version'] = 'X.Z' + + return updated_config +``` + +#### Migration Process + +1. **Auto-detection**: When loading config, system checks `config_version` +2. **User prompt**: If old version detected, shows changes and prompts to upgrade +3. **Auto-upgrade**: With `--auto` flag, upgrades without prompting +4. **Migration chain**: Supports sequential upgrades (1.0 → 1.1 → 1.2) +5. **File update**: Upgraded config is saved back to the TOML file + +#### Example Migration (v1.0 to v1.1) + +```python +# src/release_tool/migrations/v1_0_to_v1_1.py + +V1_0_DEFAULT_TEMPLATE = "..." # Old default +V1_1_DEFAULT_TEMPLATE = "..." # New default with improvements + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """Migrate from 1.0 to 1.1.""" + updated = config_dict.copy() + + # Only update template if user hasn't customized it + if updated.get('output_template') == V1_0_DEFAULT_TEMPLATE: + updated['output_template'] = V1_1_DEFAULT_TEMPLATE + + updated['config_version'] = '1.1' + return updated +``` + +### Commands + +#### Automatic Upgrade (on any command) +```bash +# If old config detected, prompts to upgrade +release-tool generate --new-minor + +# Auto-upgrade without prompt +release-tool --auto generate --new-minor +``` + +#### Manual Upgrade Command +```bash +# Check current version and available upgrades +release-tool update-config + +# Dry-run to preview changes +release-tool update-config --dry-run + +# Auto-upgrade without prompt +release-tool --auto update-config + +# Upgrade to specific version +release-tool update-config --target-version 1.1 +``` + +### Version History + +#### v1.1 (Current) +- Added `ticket_url` and `pr_url` template variables +- Made `url` a smart field (ticket_url if available, else pr_url) +- Improved `output_template` formatting with better spacing and blank lines +- Added `config_version` field for version tracking + +#### v1.0 (Initial) +- Original config format +- Single `url` field in templates +- Basic output_template structure + +## Documentation Maintenance (CRITICAL) + +**MANDATORY**: When modifying CLI commands, features, or workflows, you MUST update documentation: + +### What to Update: +1. **.claude/prompts/architecture.md** - Command list, module descriptions +2. **.claude/prompts/project-context.md** - Workflows, examples, configuration +3. **docs/*.md** - User-facing documentation (usage.md, configuration.md, etc.) +4. **Docstrings in code** - CLI help text, function descriptions + +### When to Update: +- Adding/removing CLI commands or options +- Changing command behavior or defaults +- Adding new configuration options +- Modifying workflows or best practices +- Fixing bugs that affect user behavior + +### Documentation Quality: +- Keep examples up-to-date with actual command names +- Include proper formatting (newlines after "Examples:") +- Test help text formatting: `release-tool --help` +- Ensure consistency across all documentation files + +## Common Issues to Avoid + +1. **Slow sync** - Not using Search API or not parallelizing +2. **No feedback** - Missing console.print statements +3. **Type errors** - Missing type hints or Pydantic validation +4. **Test failures** - Not updating tests after refactoring +5. **Rate limiting** - Too many sequential requests (parallelize!) +6. **Outdated docs** - Not updating documentation when changing commands diff --git a/.claude/prompts/testing-patterns.md b/.claude/prompts/testing-patterns.md new file mode 100644 index 0000000..37be223 --- /dev/null +++ b/.claude/prompts/testing-patterns.md @@ -0,0 +1,392 @@ +# Release Tool - Testing Patterns and Expectations + +## Testing Philosophy + +All modules must have comprehensive unit tests. Tests should: +- Cover both success and error paths +- Use fixtures for common setup +- Mock external dependencies (GitHub API, Git operations) +- Run fast (mock network calls) +- Be maintainable (clear naming, good assertions) + +## Test Structure + +### File Organization +``` +tests/ +├── test_models.py # Pydantic model tests +├── test_config.py # Configuration loading and validation +├── test_db.py # Database operations +├── test_git_ops.py # Git repository operations +├── test_github_utils.py # GitHub API client (if needed) +├── test_policies.py # Ticket extraction, consolidation, generation +├── test_sync.py # Sync manager tests +├── test_output_template.py # Template rendering +└── test_default_template.py # Default template behavior +``` + +### Current Coverage +- **Total tests**: 74 +- **Modules covered**: 8/10 +- **Target coverage**: >80% for critical paths + +## Common Test Patterns + +### 1. Pydantic Model Tests + +**Pattern**: Test parsing, validation, and methods +```python +def test_semantic_version_parse(): + """Test parsing various version formats.""" + v = SemanticVersion.parse("2.0.0") + assert v.major == 2 + assert v.minor == 0 + assert v.patch == 0 + assert v.is_final() is True + +def test_semantic_version_prerelease(): + """Test prerelease version parsing.""" + v = SemanticVersion.parse("2.0.0-rc.1") + assert v.prerelease == "rc.1" + assert v.is_final() is False + +def test_invalid_version_raises(): + """Test that invalid versions raise ValueError.""" + with pytest.raises(ValueError): + SemanticVersion.parse("invalid") +``` + +### 2. Configuration Tests + +**Pattern**: Test loading, validation, defaults +```python +def test_config_from_dict(): + """Test creating config from dictionary.""" + config_dict = { + "repository": {"code_repo": "owner/repo"}, + "sync": {"parallel_workers": 20} + } + config = Config.from_dict(config_dict) + + assert config.repository.code_repo == "owner/repo" + assert config.sync.parallel_workers == 20 + +def test_config_defaults(): + """Test that defaults are applied correctly.""" + config = Config.from_dict({"repository": {"code_repo": "test/repo"}}) + + assert config.sync.parallel_workers == 20 # Default + assert config.sync.show_progress is True +``` + +### 3. Database Tests + +**Pattern**: Use in-memory SQLite, test CRUD operations +```python +@pytest.fixture +def test_db(): + """Create in-memory test database.""" + db = Database(":memory:") + db.connect() + db.init_db() + yield db + db.close() + +def test_upsert_ticket(test_db): + """Test inserting and updating tickets.""" + ticket = Ticket( + repo_id=1, + number=123, + key="#123", + title="Test ticket", + body="Description", + state="open", + labels=[], + url="https://github.com/owner/repo/issues/123" + ) + + # Insert + test_db.upsert_ticket(ticket) + + # Verify + result = test_db.get_ticket_by_number(1, 123) + assert result.title == "Test ticket" + + # Update + ticket.title = "Updated title" + test_db.upsert_ticket(ticket) + + # Verify update + result = test_db.get_ticket_by_number(1, 123) + assert result.title == "Updated title" +``` + +### 4. Git Operations Tests + +**Pattern**: Mock Git repository, test commit extraction +```python +@pytest.fixture +def mock_git_repo(tmp_path): + """Create a mock Git repository for testing.""" + repo_path = tmp_path / "test_repo" + repo_path.mkdir() + + # Initialize repo with test data + # ... create commits, tags, etc. + + return repo_path + +def test_find_comparison_version(): + """Test automatic version comparison detection.""" + versions = [ + SemanticVersion.parse("1.0.0"), + SemanticVersion.parse("1.1.0"), + SemanticVersion.parse("2.0.0-rc.1"), + SemanticVersion.parse("2.0.0") + ] + + # Final version should compare to previous final + comparison = find_comparison_version( + SemanticVersion.parse("2.0.0"), + versions + ) + assert comparison.to_string() == "1.1.0" + + # RC should compare to previous RC or final + comparison = find_comparison_version( + SemanticVersion.parse("2.0.0-rc.1"), + versions + ) + assert comparison.to_string() == "1.1.0" +``` + +### 5. GitHub API Tests + +**Pattern**: Mock GitHub client, test parallelization +```python +@pytest.fixture +def mock_github(): + """Create mock GitHub client.""" + mock = Mock(spec=GitHubClient) + return mock + +def test_search_ticket_numbers(mock_github): + """Test ticket number search.""" + # Mock the search results + mock_github.search_ticket_numbers.return_value = [1, 2, 3, 4, 5] + + numbers = mock_github.search_ticket_numbers("owner/repo", since=None) + + assert len(numbers) == 5 + assert 1 in numbers + mock_github.search_ticket_numbers.assert_called_once() +``` + +### 6. Sync Manager Tests + +**Pattern**: Test incremental sync, filtering, parallel fetch +```python +def test_incremental_sync_filters_existing(test_db, mock_github): + """Test that incremental sync only fetches new items.""" + # Setup: Add existing tickets to DB + for num in [1, 2, 3]: + ticket = Ticket( + repo_id=1, number=num, key=f"#{num}", + title=f"Ticket {num}", body="", state="open", + labels=[], url=f"https://github.com/owner/repo/issues/{num}" + ) + test_db.upsert_ticket(ticket) + + # Mock GitHub to return all tickets (including existing) + mock_github.search_ticket_numbers.return_value = [1, 2, 3, 4, 5, 6] + + sync_manager = SyncManager(test_config, test_db, mock_github) + + # Get ticket numbers to fetch + to_fetch = sync_manager._get_ticket_numbers_to_fetch("owner/repo", None) + + # Should only fetch new tickets (4, 5, 6) + assert set(to_fetch) == {4, 5, 6} +``` + +### 7. Policy Tests + +**Pattern**: Test ticket extraction, consolidation, categorization +```python +class TestTicketExtractor: + """Test ticket reference extraction.""" + + def test_extract_from_branch_name(self): + """Test extracting ticket from branch name.""" + commit = Commit( + sha="abc123", + message="Fix bug", + author_name="Test", + author_email="test@example.com", + timestamp=datetime.now(), + branch_name="feat/meta-123/description" + ) + + extractor = TicketExtractor(test_config) + ticket_key = extractor.extract_from_commit(commit) + + assert ticket_key == "meta-123" + + def test_extract_from_pr_body(self): + """Test extracting parent issue from PR body.""" + pr = PullRequest( + repo_id=1, number=456, + title="Fix bug", + body="Parent issue: https://github.com/owner/repo/issues/123", + # ... other fields + ) + + extractor = TicketExtractor(test_config) + ticket_key = extractor.extract_from_pr(pr) + + assert ticket_key == "#123" +``` + +### 8. Template Rendering Tests + +**Pattern**: Test Jinja2 template output +```python +def test_output_template_with_categories(): + """Test template renders categories correctly.""" + notes = [ + ReleaseNote( + title="Add feature", + category="🚀 Features", + pr_numbers=[1], + # ... other fields + ), + ReleaseNote( + title="Fix bug", + category="🛠 Bug Fixes", + pr_numbers=[2], + # ... other fields + ) + ] + + generator = ReleaseNoteGenerator(test_config) + output = generator.render_template("2.0.0", notes) + + assert "🚀 Features" in output + assert "Add feature" in output + assert "🛠 Bug Fixes" in output + assert "Fix bug" in output +``` + +## Test Fixtures + +### Common Fixtures +```python +@pytest.fixture +def test_config(): + """Create test configuration.""" + return Config.from_dict({ + "repository": {"code_repo": "test/repo"}, + "sync": {"parallel_workers": 20} + }) + +@pytest.fixture +def test_db(): + """In-memory test database.""" + db = Database(":memory:") + db.connect() + db.init_db() + yield db + db.close() + +@pytest.fixture +def mock_github(): + """Mock GitHub client.""" + return Mock(spec=GitHubClient) +``` + +## Running Tests + +### Basic Test Commands +```bash +# Run all tests +pytest + +# Run specific module +pytest tests/test_sync.py -v + +# Run with coverage +pytest --cov=release_tool --cov-report=term-missing + +# Run specific test +pytest tests/test_models.py::TestSemanticVersion::test_parse_simple_version -v + +# Run with verbose output +pytest -vv + +# Run with print statements visible +pytest -s +``` + +### Test Naming Conventions +- Test files: `test_.py` +- Test classes: `Test` (e.g., `TestTicketExtractor`) +- Test functions: `test_` (e.g., `test_extract_from_branch_name`) + +## Assertions + +### Good Assertions (Specific and Clear) +```python +assert result.title == "Expected Title" +assert len(results) == 5 +assert "feature" in result.labels +assert result.is_final() is True +with pytest.raises(ValueError, match="Invalid version"): + SemanticVersion.parse("invalid") +``` + +### Bad Assertions (Vague) +```python +assert result # What are we checking? +assert True # Meaningless +``` + +## Mocking Best Practices + +### Mock External Dependencies +```python +# Mock GitHub API +mock_gh = Mock(spec=GitHubClient) +mock_gh.search_ticket_numbers.return_value = [1, 2, 3] + +# Mock file system +with patch('pathlib.Path.exists', return_value=True): + # Test code + +# Mock time +with patch('datetime.datetime.now', return_value=fixed_time): + # Test code +``` + +### Don't Mock What You Own +```python +# DON'T mock your own models +mock_ticket = Mock(spec=Ticket) # Bad + +# DO create real instances +ticket = Ticket(...) # Good +``` + +## Test Coverage Goals + +- **Critical paths**: 100% (sync, generate, version comparison) +- **Business logic**: >90% (policies, consolidation) +- **Utilities**: >80% (config, db, models) +- **CLI**: >70% (main.py - integration tests) + +## When Tests Fail + +1. **Read the error message carefully** +2. **Check if test expectations match new behavior** +3. **Update tests when refactoring** (don't just delete failing tests) +4. **Add tests for new features** +5. **Run affected tests before committing** diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..35fcfe9 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,48 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "main" ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml deleted file mode 100644 index fb1fdac..0000000 --- a/.github/workflows/ort.yml +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: 2014-2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only -name: ORT licensing - -on: - push: - branches: - - master - - '[0-9]+.[0-9]+.x' - tags: - - '**' - pull_request: - branches: - - master - - '[0-9]+.[0-9]+.x' - -jobs: - ort: - uses: sequentech/meta/.github/workflows/ort.yml@main - with: - ort-cli-analyze-args: '--package-managers PIP' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..61196c1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Run tests (excluding Docker tests) + run: poetry run pytest tests/ --ignore=tests/test_docker.py + + - name: Run Docker tests + run: poetry run pytest tests/test_docker.py -v diff --git a/.gitignore b/.gitignore index 42e051f..082da25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ notes.* +*.db +release_tool.toml +results.log + +.release_tool_cache/ __pycache__/ +node_modules +.docusaurus \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6046942 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-azuretools.vscode-docker", + "tamasfe.even-better-toml" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b4028f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/__pycache__": true, + "**/*.pyc": true + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..290e469 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,63 @@ +{ + "version": "2.0.0", + "tasks": [ + // --- Release Tool (Docker) --- + { + "label": "Tool: Build Docker Image", + "type": "shell", + "command": "docker build -t release-tool:local .", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + }, + // --- Release Tool (Python) --- + { + "label": "Tool: Run Tests", + "type": "shell", + "command": "poetry run pytest", + "group": "test", + "presentation": { + "reveal": "always" + } + }, + // --- Release Tool (Docusaurus) --- + { + "label": "Docs: Start Server", + "type": "shell", + "command": "cd docs && npm install && npm start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": "Compiling...", + "endsPattern": "Client compiled successfully" + } + }, + "presentation": { + "group": "docs", + "reveal": "always" + } + }, + { + "label": "Docs: Build Static Site", + "type": "shell", + "command": "cd docs && npm install && npm run build", + "group": "build" + }, + // --- CI/CD --- + { + "label": "CI: Run Tests (act)", + "type": "shell", + "command": "act -j test --container-architecture linux/amd64", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..933e080 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.10-slim + +WORKDIR /app + +# Install git (required for git operations) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY . /app + +# Install the package +RUN pip install --no-cache-dir . + +# Verify installation +RUN release-tool -h + +CMD ["release-tool"] diff --git a/README.md b/README.md index 16592f3..af000b1 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,253 @@ # release-tool -scripts used for creating a new release +A comprehensive tool to manage releases using semantic versioning for efficient release workflows. Written in Python with full type safety and designed to be flexible and configurable for different team workflows. -This repo contains the release.py script, which is used to update the version -number for all other Sequent Tech projects. In order to use the script, please -read the documentation in [https://sequent.github.io/documentation/docs/contribute/release-howto]. +## Features -# Setup +- **Semantic Versioning Support**: Full support for semantic versioning with release candidates, betas, and alphas +- **Intelligent Version Comparison**: Automatically determines the right version to compare against (RC to RC, final to final, etc.) +- **Ticket-based Consolidation**: Groups commits by parent tickets for cleaner release notes +- **Configurable Policies**: Flexible policies for ticket extraction, consolidation, version gaps, and more +- **GitHub Integration**: Syncs PRs, issues, and releases from GitHub; can create releases and PRs automatically +- **Local Git Analysis**: Analyzes commit history from local repositories +- **SQLite Database**: Efficient local caching of GitHub data to minimize API calls +- **Template-based Release Notes**: Jinja2 templates for customizable release note formatting +- **Category-based Grouping**: Organize release notes by configurable categories with label mapping -First, download `release-tool` repository and install the dependencies. It -uses Python 3 so you need it installed: +## Installation ```bash -git clone https://github.com/sequentech/release-tool.git -pip install -r requirements.txt -``` - -Some other setup steps: -- To execute the `release.py` command, please always do it within the directory -containing `release-tool`. Meaning, CWD needs to be that directory when -executing the command. -- You need to have your git's username and email configured, as this command -will create release commits. -- If you are releasing for example the `election-portal` repository, it -needs to be in the parent directory and with the origin remote having write -permissions and the ssh-agent active to be able to push automatically. -- You need to have the github cli [gh](https://github.com/cli/cli) installed, -configured and properly authenticated authorized. Follow [] -- If you are using the github release notes automatic generation by github with -the `--generate-release-notes` option, then you need to configure the -environment variables `GITHUB_USER` to be your github username and -`GITHUB_TOKEN` to be a [personal access token](https://github.com/settings/tokens). -This token needs to have general repository permissions. +# Install with poetry +poetry install + +# Or for development +poetry install --with dev +``` + +## Quick Start + +1. **Initialize configuration**: + ```bash + release-tool init-config + ``` + +2. **Edit** `release_tool.toml` and set your repository: + ```toml + [repository] + code_repo = "your-org/your-repo" + ``` + +3. **Set GitHub token**: + ```bash + export GITHUB_TOKEN="your_github_token" + ``` + +4. **Sync repository data**: + ```bash + release-tool sync + ``` + +5. **Generate release notes**: + ```bash + release-tool generate 1.0.0 --repo-path /path/to/local/repo + ``` + +## CLI Commands + +### `sync` +Sync repository data from GitHub to the local database: +```bash +release-tool sync [repository] [--repo-path PATH] +``` + +### `generate` +Generate release notes for a version: +```bash +release-tool generate VERSION \ + --repo-path /path/to/repo \ + [--from-version VERSION] \ + [--output FILE] \ + [--upload] \ + [--create-pr] +``` + +Options: +- `--from-version`: Compare from this version (auto-detected if not specified) +- `--output, -o`: Output file for release notes +- `--upload`: Upload release to GitHub +- `--create-pr`: Create PR with release notes + +### `list-releases` +List all releases in the database: +```bash +release-tool list-releases [repository] +``` + +### `init-config` +Create an example configuration file: +```bash +release-tool init-config +``` + +## Configuration + +The tool is configured via a TOML file (`release_tool.toml`). Key sections: + +### Repository Configuration +```toml +[repository] +code_repo = "owner/repo" # Required +ticket_repo = "owner/tickets" # Optional: separate repo for tickets +default_branch = "main" +``` + +### Ticket Policy +```toml +[ticket_policy] +patterns = ["([A-Z]+-\\d+)", "#(\\d+)"] # Regex patterns to find tickets +no_ticket_action = "warn" # ignore, warn, or error +unclosed_ticket_action = "warn" +consolidation_enabled = true +``` + +### Version Policy +```toml +[version_policy] +gap_detection = "warn" # ignore, warn, or error +tag_prefix = "v" +``` + +### Release Notes Categories +```toml +[[release_notes.categories]] +name = "Features" +labels = ["feature", "enhancement"] +order = 1 + +[[release_notes.categories]] +name = "Bug Fixes" +labels = ["bug", "fix"] +order = 2 +``` + +### Output Configuration +```toml +[release_notes] +excluded_labels = ["skip-changelog", "internal"] +title_template = "Release {{ version }}" +include_authors = true +include_pr_links = true + +[output] +output_file = "docs/releases/{major}.{minor}.{patch}.md" +create_github_release = false +create_pr = false +pr_branch_template = "release-notes-{version}" +``` + +## How It Works + +### Version Comparison Logic + +The tool implements intelligent version comparison: + +- **Final versions** (e.g., `2.0.0`) compare to the previous final version +- **Release candidates** (e.g., `2.0.0-rc.2`) compare to: + - Previous RC of the same version (`2.0.0-rc.1`) if it exists, OR + - Previous final version if no RCs exist +- **Consolidated final releases** incorporate all changes from RCs, betas, alphas of that version + +### Ticket Consolidation + +Commits are consolidated by their parent ticket: + +1. Extract ticket references from commits using configurable regex patterns +2. Try multiple strategies: commit message, PR body, branch name +3. Group commits with the same ticket key +4. Fetch ticket details from GitHub (title, labels, description) +5. Apply configurable policies for missing tickets + +### Release Note Generation + +1. **Extract commits** between two versions from Git history +2. **Consolidate by ticket** to group related changes +3. **Fetch ticket metadata** from GitHub Issues API +4. **Categorize** based on labels and configured category mappings +5. **Format** using Jinja2 templates +6. **Output** to console, file, GitHub release, or PR + +## Architecture + +``` +src/release_tool/ +├── main.py # CLI entry point with Click commands +├── models.py # Pydantic data models (SemanticVersion, Commit, PR, Ticket, etc.) +├── config.py # Configuration management with Pydantic validation +├── db.py # SQLite database operations +├── git_ops.py # Git operations using GitPython +├── github_utils.py # GitHub API client using PyGithub +└── policies.py # Policy implementations (extraction, consolidation, generation) +``` + +## Testing + +All modules have comprehensive unit tests: + +```bash +# Run all tests +poetry run pytest + +# Run with coverage +poetry run pytest --cov=release_tool + +# Run specific test file +poetry run pytest tests/test_models.py -v +``` + +Current test coverage: 44 tests covering models, database, Git operations, policies, and configuration. + +## Development + +Built with modern Python best practices: + +- **Python 3.10+**: Modern Python with type hints +- **Poetry**: Dependency management and packaging +- **Pydantic**: Data validation and settings management +- **Click**: Command-line interface +- **Rich**: Beautiful terminal output +- **PyGithub**: GitHub API integration +- **GitPython**: Local Git repository operations +- **Jinja2**: Template rendering +- **pytest**: Comprehensive testing + +## Example Workflow + +```bash +# 1. Initial setup +release-tool init-config +export GITHUB_TOKEN="ghp_your_token" + +# 2. Edit release_tool.toml with your repo settings + +# 3. Sync GitHub data once or periodically +release-tool sync + +# 4. Generate release notes for version 2.0.0 +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --output docs/releases/2.0.0.md + +# 5. Or generate and upload to GitHub +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --upload + +# 6. Or generate and create PR +release-tool generate 2.0.0 \ + --repo-path ~/projects/myrepo \ + --create-pr +``` + +## License + +Copyright (c) Sequent Tech Inc. All rights reserved. diff --git a/comprehensive_release_notes.py b/comprehensive_release_notes.py deleted file mode 100644 index 7cdf5b5..0000000 --- a/comprehensive_release_notes.py +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -import os -import re -import yaml -import argparse -from github import Github -from collections import defaultdict -from release_notes import ( - create_new_branch, - get_sem_release, - get_release_head, - get_release_notes, - create_release_notes_md, - parse_arguments, - verbose_print -) - -META_REPOSITORY = "sequentech/meta" - -REPOSITORIES = [ - "sequentech/common-ui", - "sequentech/admin-console", - "sequentech/election-portal", - "sequentech/voting-booth", - "sequentech/ballot-box", - "sequentech/deployment-tool", - "sequentech/tally-methods", - "sequentech/tally-pipes", - "sequentech/election-verifier", - "sequentech/frestq", - "sequentech/election-orchestra", - "sequentech/iam", - "sequentech/misc-tools", - "sequentech/mixnet", - "sequentech/documentation", - "sequentech/ballot-verifier", - #"sequentech/release-tool", -] - -def get_comprehensive_release_notes( - args, token, repos, prev_major_release, prev_release, new_release, config -): - """ - Generate comprehensive release notes for a list of repositories. - - Args: - token (str): GitHub access token. - repos (list): A list of repository paths, e.g., ["org/repo1", "org/repo2"]. - prev_major_release (str|None): The previous major release version (e.g. "1.0.0") or None if prev_release and new_release share their major version. - prev_release (str): The previous release version (e.g. "1.1.0"). - new_release (str): The new release version (e.g. "1.2.0"). - config (dict): the configuration for generating release notes. - - :return: dict, the release notes categorized by their labels. - """ - gh = Github(token) - release_notes = defaultdict(list) - - for repo_path in repos: - verbose_print(args, f"Generating release notes for repo {repo_path}..") - repo = gh.get_repo(repo_path) - - hidden_links = [] - # if we are going to do a new major release for example - # new_release="8.0.0", we need to obtain a list of all the changes made - # in the previous major release cycle (from 7.0.0 to - # previous_release="7.4.0") and mark them as hidden. - if prev_major_release: - verbose_print(args, f"Generating release notes for hidden links:") - (_, hidden_links) = get_release_notes( - gh, repo, prev_major_release, prev_release, config, hidden_links=[] - ) - - verbose_print(args, f"Generating release notes:") - (repo_notes, _) = get_release_notes( - gh, repo, prev_release, new_release, config, hidden_links=hidden_links - ) - verbose_print(args, f"..generated") - for category, notes in repo_notes.items(): - release_notes[category].extend(notes) - - # Deduplicate notes by removing duplicates based on links - deduplicated_release_notes = {} - links = set() - for category, notes in release_notes.items(): - deduplicated_notes = [] - for note in notes: - link = re.search(r'https://\S+', note) - if link and link.group(0) not in links: - deduplicated_notes.append(note) - links.add(link.group(0)) - deduplicated_release_notes[category] = deduplicated_notes - - return deduplicated_release_notes - -def parse_arguments(): - """ - Parse command-line arguments specific for the comprehensive release notes script. - - Returns: - argparse.Namespace: An object containing parsed arguments. - """ - parser = argparse.ArgumentParser( - description='Generate comprehensive release notes for multiple repositories.' - ) - parser.add_argument( - 'previous_release', - help='Previous release version in format `.`, i.e. `7.2`' - ) - parser.add_argument( - 'new_release', - help=( - 'New release version in format `.`, i.e. `7.2` ' - 'or full semver release if it already exists i.e. `7.3.0`' - ) - ) - parser.add_argument( - '--dry-run', - action='store_true', - help=( - 'Output the release notes but do not create any tag, release or ' - 'new branch.' - ) - ) - parser.add_argument( - '--silent', - action='store_true', - help='Disables verbose output' - ) - parser.add_argument( - '--draft', - action='store_true', - help='Mark the new release be as draft' - ) - parser.add_argument( - '--prerelease', - action='store_true', - help='Mark the new release be as a prerelease' - ) - return parser.parse_args() - - -def main(): - args = parse_arguments() - - previous_release = args.previous_release - new_release = args.new_release - dry_run = args.dry_run - github_token = os.getenv("GITHUB_TOKEN") - - g = Github(github_token) - meta_repo = g.get_repo(META_REPOSITORY) - - with open(".github/release.yml") as f: - config = yaml.safe_load(f) - - prev_major, prev_minor, prev_patch = get_sem_release(previous_release) - new_major, new_minor, new_patch = get_sem_release(new_release) - - prev_release_head = get_release_head(prev_major, prev_minor, prev_patch) - if new_patch or prev_major == new_major: - new_release_head = get_release_head(new_major, new_minor, new_patch) - else: - new_release_head = meta_repo.default_branch - - verbose_print(args, f"Input Parameters: {args}") - verbose_print(args, f"Previous Release Head: {prev_release_head}") - verbose_print(args, f"New Release Head: {new_release_head}") - - if prev_major != new_major: - # if we are going to do a new major release for example - # new_release="8.0.0", we need to obtain a list of all the changes made - # in the previous major release cycle (from 7.0.0 to - # previous_release="7.4.0") and mark them as hidden. - prev_major_release_head = get_release_head(prev_major, 0, "0") - else: - prev_major_release_head = None - verbose_print( - args, - f"Previous Major Release Head: {prev_major_release_head}" - ) - - release_notes = get_comprehensive_release_notes( - args, - github_token, - REPOSITORIES, - prev_major_release_head, - prev_release_head, - new_release_head, - config - ) - - if not new_patch: - latest_release = meta_repo.get_releases()[0] - latest_tag = latest_release.tag_name - major, minor, new_patch = map(int, latest_tag.split(".")) - if new_major == major and new_minor == minor: - new_patch += 1 - else: - new_patch = 0 - - new_tag = f"{new_major}.{new_minor}.{new_patch}" - new_title = f"{new_tag} release" - verbose_print(args, f"New Release Tag: {new_tag}") - - release_notes_md = create_release_notes_md(release_notes, new_tag) - - verbose_print(args, f"Generated Release Notes: {release_notes_md}") - - if not dry_run: - branch = None - try: - branch = meta_repo.get_branch(new_release_head) - except: - verbose_print(args, "Creating new branch") - create_new_branch(meta_repo, new_release_head) - branch = meta_repo.get_branch(new_release_head) - - verbose_print(args, "Creating new release") - meta_repo.create_git_tag_and_release( - tag=new_tag, - tag_message=new_title, - type='commit', - object=branch.commit.sha, - release_name=new_title, - release_message=release_notes_md, - prerelease=args.prerelease, - draft=args.draft - ) - verbose_print(args, f"Executed Actions: Branch created and new release created") - else: - verbose_print(args, "Dry Run: No actions executed") - -if __name__ == "__main__": - main() diff --git a/docs/branching-strategy.md b/docs/branching-strategy.md new file mode 100644 index 0000000..128a5b8 --- /dev/null +++ b/docs/branching-strategy.md @@ -0,0 +1,254 @@ +# Branching Strategy + +The release tool implements an automated branching strategy that creates and manages release branches according to semantic versioning best practices. + +## Overview + +Release branches follow the pattern `release/{major}.{minor}` (configurable) and are automatically created based on the version being released. This enables: + +- **Parallel development**: Work on multiple release lines simultaneously +- **Hotfix workflows**: Patch older releases without affecting newer ones +- **Clear release history**: Each major.minor gets its own dedicated branch + +## How It Works + +### Branch Creation Rules + +The tool determines the source branch based on the version being released: + +#### New Major Version (e.g., 9.0.0) +- **Source**: `main` (or configured `default_branch`) +- **Reason**: Major versions represent significant changes and start fresh from the main development line +- **Example**: `release/9.0` branches from `main` + +#### New Minor Version (e.g., 9.1.0) +- **Source**: Previous release branch (e.g., `release/9.0`) +- **Fallback**: `main` if no previous release branch exists +- **Reason**: Minor versions build upon the previous minor release +- **Example**: `release/9.1` branches from `release/9.0` +- **Config**: Controlled by `branch_from_previous_release` setting + +#### Patch or RC (e.g., 9.1.1 or 9.1.0-rc.1) +- **Source**: Existing release branch (e.g., `release/9.1`) +- **Reason**: Patches and RCs are built on the same release branch +- **No new branch created** if the release branch already exists + +## Configuration + +Configure the branching behavior in `release_tool.toml`: + +```toml +[branch_policy] +# Template for release branch names +# Use {major}, {minor}, {patch} as placeholders +release_branch_template = "release/{major}.{minor}" + +# Default branch for new major versions +default_branch = "main" + +# Automatically create release branches +create_branches = true + +# Branch new minor versions from previous release +branch_from_previous_release = true +``` + +### Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `release_branch_template` | `"release/{major}.{minor}"` | Template for branch names. Supports `{major}`, `{minor}`, `{patch}` placeholders | +| `default_branch` | `"main"` | Branch to use for new major versions | +| `create_branches` | `true` | Automatically create branches if they don't exist | +| `branch_from_previous_release` | `true` | Branch new minors from previous release branch | + +## Examples + +### Example 1: First Major Release + +```bash +# Current state: No release branches exist +# Creating first 9.0.0 release + +$ release-tool generate 9.0.0 --repo-path . + +# Output: +# Release branch: release/9.0 +# → Branch does not exist, will create from: main +# ✓ Created branch 'release/9.0' from 'main' +``` + +### Example 2: New Minor Release + +```bash +# Current state: release/9.0 exists +# Creating 9.1.0 release + +$ release-tool generate 9.1.0 --repo-path . + +# Output: +# Release branch: release/9.1 +# → Branch does not exist, will create from: release/9.0 +# ✓ Created branch 'release/9.1' from 'release/9.0' +``` + +### Example 3: RC on Existing Branch + +```bash +# Current state: release/9.1 exists with 9.1.0-rc.0 +# Creating 9.1.0-rc.1 + +$ release-tool generate 9.1.0-rc.1 --repo-path . + +# Output: +# Release branch: release/9.1 +# → Using existing branch (source: release/9.1) +# (No branch creation needed) +``` + +### Example 4: Hotfix Patch Release + +```bash +# Current state: release/9.0 and release/9.1 both exist +# Creating hotfix 9.0.5 for older release + +$ release-tool generate 9.0.5 --repo-path . + +# Output: +# Release branch: release/9.0 +# → Using existing branch (source: release/9.0) +# (Commits will be based on release/9.0, not latest release/9.1) +``` + +## Branch Workflow + +### Recommended Git Workflow + +1. **Generate Release** + ```bash + release-tool generate --new-minor --repo-path . -o notes.md + ``` + - Tool creates release branch automatically + - You remain on your current working branch + +2. **Review Branch** + ```bash + git branch -a | grep release/ + ``` + - Verify the release branch was created + +3. **Optional: Checkout and Test** + ```bash + git checkout release/9.1 + # Run tests, build, etc. + ``` + +4. **Publish Release** + ```bash + release-tool publish 9.1.0 -f notes.md + ``` + +## Custom Branch Templates + +You can customize the branch naming pattern: + +```toml +[branch_policy] +# Custom template examples: + +# Example 1: Include patch version +release_branch_template = "release/{major}.{minor}.{patch}" +# Results in: release/9.1.0 + +# Example 2: Different prefix +release_branch_template = "rel-{major}.{minor}.x" +# Results in: rel-9.1.x + +# Example 3: Version prefix +release_branch_template = "v{major}.{minor}" +# Results in: v9.1 +``` + +## Disabling Automatic Branch Creation + +If you prefer to manage branches manually: + +```toml +[branch_policy] +create_branches = false +``` + +With this setting, the tool will: +- ✅ Still determine which branch should be used +- ✅ Display the expected branch name +- ❌ **Not** automatically create the branch +- ⚠️ You must create the branch manually before generating + +## Branching from Main Instead of Previous Release + +To always branch from `main` instead of the previous release: + +```toml +[branch_policy] +branch_from_previous_release = false +``` + +This changes the behavior: +- New major (9.0.0): Still from `main` ✓ +- New minor (9.1.0): From `main` instead of `release/9.0` +- Use case: When releases are independent and don't build on each other + +## Troubleshooting + +### Branch Already Exists + +If you see "Branch already exists" warnings: +- This is **expected** for subsequent RCs or patches +- The tool will use the existing branch +- No action needed + +### Branch Not Found + +If generation fails with "branch not found": +- Enable `create_branches = true` in config +- Or manually create the branch: `git branch release/9.1 main` + +### Wrong Source Branch + +If a branch was created from the wrong source: +- Delete the branch: `git branch -D release/9.1` +- Verify configuration in `release_tool.toml` +- Re-run `generate` command + +## Integration with Version Gaps + +The branching strategy works with the version gap detection: + +```bash +# Attempting to skip from 9.0 to 9.2 +$ release-tool generate 9.2.0 --repo-path . + +# Output: +# Warning: Version gap detected: 9.0.0 → 9.2.0 (missing 9.1.x) +# Release branch: release/9.2 +# → Branch does not exist, will create from: release/9.0 +``` + +The tool will: +- Warn about the gap (based on `gap_detection` setting) +- Still create `release/9.2` from `release/9.0` +- Allow you to proceed or abort + +## Best Practices + +1. **Keep release branches**: Don't delete old release branches - they're needed for hotfixes +2. **Tag at release time**: Tag the commit when you publish the release +3. **Merge strategy**: Consider cherry-picking hotfixes to newer releases +4. **Dry-run first**: Always use `--dry-run` to preview branch creation +5. **Version consistency**: Ensure branch naming matches your tag naming convention + +## See Also + +- [Usage Guide](usage.md) - Basic command usage +- [Configuration](configuration.md) - Full configuration reference +- [Policies](policies.md) - Version policies and gap detection diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..23061c9 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 3 +--- + +# Configuration + +The tool is configured using a `release_tool.toml` file in the root of your project. + +## Setup in Repository + +1. Create a file named `release_tool.toml` in your repository root. +2. Add the following configuration structure: + +```toml +[release] +tag_prefix = "v" # Prefix for git tags +main_branch = "main" # Main branch name + +[github] +owner = "sequentech" # GitHub organization or user + +[database] +path = "release_tool.db" # Local database path + +[policies] +# Regex patterns to find tickets in PR titles/bodies +ticket_patterns = ["([A-Z]+-\\d+)"] + +# Categories for grouping release notes +categories = [ + "Features", + "Bug Fixes", + "Documentation", + "Maintenance" +] + +# Policy for handling version gaps (ignore, warn, error) +version_gap_policy = "warn" + +# Mapping labels to categories +[policies.label_map] +"enhancement" = "Features" +"feature" = "Features" +"bug" = "Bug Fixes" +"fix" = "Bug Fixes" +"docs" = "Documentation" +"chore" = "Maintenance" +``` + +## Environment Variables + +Sensitive information like tokens should be set via environment variables: + +- `GITHUB_TOKEN`: Your GitHub Personal Access Token. + +## Options Reference + +### `release` + +- `tag_prefix`: String to prepend to version numbers for git tags (e.g., "v" for "v1.0.0"). +- `main_branch`: The name of the default branch (e.g., "main", "master"). + +### `github` + +- `owner`: The GitHub account that owns the repositories. + +### `policies` + +This section controls the behavior of the release tool's logic. + +#### `ticket_patterns` +- **Type**: `List[str]` +- **Description**: A list of regular expressions used to identify ticket IDs in Pull Request titles and bodies. +- **Example**: `["([A-Z]+-\\d+)"]` matches tickets like `JIRA-123`. + +#### `categories` +- **Type**: `List[str]` +- **Description**: An ordered list of category names. Release notes will be grouped into these categories in the order specified. +- **Default**: `["Features", "Bug Fixes", "Other"]` + +#### `version_gap_policy` +- **Type**: `str` +- **Description**: Determines how the tool handles gaps between the previous version and the new version (e.g., skipping from 1.0.0 to 1.2.0). +- **Values**: + - `"ignore"`: Do nothing. + - `"warn"`: Print a warning message but proceed. + - `"error"`: Stop execution with an error. +- **Default**: `"warn"` + +#### `label_map` +- **Type**: `Dict[str, str]` +- **Description**: A key-value mapping where keys are GitHub labels and values are the corresponding categories defined in `categories`. +- **Example**: `"enhancement" = "Features"` means PRs with the `enhancement` label will be listed under the "Features" section. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..cee10c5 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,177 @@ +# Development Guide + +This guide covers development workflows, testing, Docker image management, and CI/CD for the `release-tool` project. + +## Table of Contents +- [Testing](#testing) +- [GitHub Actions](#github-actions) +- [Docker Image & Registry](#docker-image--registry) +- [Local Development](#local-development) + +## Testing + +### Running Tests + +The project uses pytest for testing. All tests can be run using Poetry: + +```bash +# Run all tests +poetry run pytest + +# Run with verbose output +poetry run pytest -v + +# Run specific test file +poetry run pytest tests/test_models.py -v + +# Run with coverage report +poetry run pytest --cov=release_tool --cov-report=html +``` + +### Test Categories + +#### Unit Tests +Standard unit tests covering: +- Models and data structures (`test_models.py`) +- Configuration management (`test_config.py`) +- Database operations (`test_db.py`) +- Git operations (`test_git_ops.py`) +- Policies and business logic (`test_policies.py`) +- Template rendering (`test_output_template.py`, `test_default_template.py`) +- Ticket management (`test_query_tickets.py`, `test_partial_tickets.py`) +- Publishing and syncing (`test_publish.py`, `test_sync.py`) + +```bash +# Run all unit tests except Docker tests +poetry run pytest tests/ --ignore=tests/test_docker.py +``` + +#### Docker Tests +Docker integration tests verify: +- Docker image builds successfully from the Dockerfile +- `release-tool` executable is available in the image +- `release-tool -h` returns a successful exit code +- Docker image runs `release-tool` as its default command + +```bash +# Run Docker tests (requires Docker to be running) +poetry run pytest tests/test_docker.py -v +``` + +**Note**: Docker tests take longer (~20-30 seconds) as they build the actual Docker image. + +## GitHub Actions + +The project uses GitHub Actions for continuous integration and delivery. All workflows are defined in `.github/workflows/`. + +### Test Workflow + +File: `.github/workflows/test.yml` + +**Triggers**: +- Push to `main` branch +- Pull requests to `main` branch + +**What it does**: +1. Sets up a test matrix for Python 3.10, 3.11, and 3.12 +2. Installs Poetry and project dependencies +3. Runs all unit tests (excluding Docker tests) +4. Runs Docker tests separately + +**Running locally with act**: + +[act](https://github.com/nektos/act) allows you to test GitHub Actions workflows locally: + +```bash +# Install act (macOS) +brew install act + +# List available workflows +act -l + +# Run the test workflow +act -j test --container-architecture linux/amd64 + +# Run with a specific runner image +act -j test --container-architecture linux/amd64 -P ubuntu-latest=catthehacker/ubuntu:act-latest + +# Run and show verbose output +act -j test --container-architecture linux/amd64 -v +``` + +**Note**: On Apple M-series chips, use `--container-architecture linux/amd64` to avoid compatibility issues. + +### Docker Publish Workflow + +File: `.github/workflows/docker-publish.yml` + +**Triggers**: +- Push to `main` branch → updates `latest` and `main` tags +- Push of version tags (`v*`) → creates versioned image tags (e.g., `v1.0.0`) +- Pull requests → builds image for validation only (doesn't push) + +**What it does**: +1. Builds the Docker image from the Dockerfile +2. Tags the image appropriately based on the trigger +3. Pushes to GitHub Container Registry (GHCR) + +**Testing locally**: + +```bash +# Test the Docker publish workflow locally (builds but doesn't push) +act -j build-and-push --container-architecture linux/amd64 +``` + +## Docker Image & Registry + +The `release-tool` is available as a Docker image stored in the GitHub Container Registry (GHCR). This allows other tools (like `release-bot`) or CI pipelines to use the tool without installing Python dependencies manually. + +## Public Registry Access + +The Docker image is published to: +`ghcr.io/sequentech/release-tool` + +### Enabling Public Access + +To ensure the image is publicly pullable (so `release-bot` or other users can use it without authentication): + +1. **Ensure the repository is public**: Go to the repository settings and verify that the repository visibility is set to "Public". +2. **Configure organization package settings**: In the organization settings (Settings -> Packages), ensure that packages are configured to inherit the repository's visibility. This allows packages from public repositories to be automatically public. +3. **Verify package visibility**: Navigate to the repository's **Packages** section (right sidebar on the main page), click on the `release-tool` package, and confirm it shows as "Public". + +### Configuration +The workflow uses the standard `GITHUB_TOKEN` to authenticate with GHCR. No additional secrets are required for the repository itself, provided "Read and write permissions" are enabled for workflows in the repository settings (Settings -> Actions -> General -> Workflow permissions). + +## Local Development + +### Building the Docker Image Locally +You can build the image locally for testing purposes: + +```bash +docker build -t release-tool:local . +``` + +### VS Code Task +For convenience, a VS Code task is included. Open the Command Palette (`Cmd+Shift+P`) and run **Tasks: Run Build Task** (or select "Docker: Build Image") to build the image directly from the editor. + +## Usage + +### Pulling the image +```bash +docker pull ghcr.io/sequentech/release-tool:latest +``` + +### Running the tool +```bash +docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/sequentech/release-tool release-tool --help +``` + +### Extending the image (e.g., for Release Bot) +```dockerfile +FROM ghcr.io/sequentech/release-tool:latest + +# Add your wrapper scripts +COPY main.py /app/ + +ENTRYPOINT ["python", "/app/main.py"] +``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js new file mode 100644 index 0000000..7df4f18 --- /dev/null +++ b/docs/docusaurus.config.js @@ -0,0 +1,84 @@ +// @ts-check +// Note: type annotations allow type checking and IDEs autocompletion + +const { themes } = require('prism-react-renderer'); +const lightCodeTheme = themes.github; +const darkCodeTheme = themes.dracula; + +/** @type {import('@docusaurus/types').Config} */ +const config = { + title: 'Release Tool', + tagline: 'Semantic Versioning & Release Management', + url: 'https://sequentech.github.io', + baseUrl: '/release-tool/', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + favicon: 'img/favicon.ico', + organizationName: 'sequentech', + projectName: 'release-tool', + + presets: [ + [ + 'classic', + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + path: '.', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: 'https://github.com/sequentech/release-tool/tree/main/docs/', + exclude: ['**/node_modules/**'], + }, + theme: { + customCss: require.resolve('./src/css/custom.css'), + }, + }), + ], + ], + + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + colorMode: { + defaultMode: 'light', + disableSwitch: false, + respectPrefersColorScheme: true, + }, + navbar: { + title: 'Release Tool', + items: [ + { + type: 'doc', + docId: 'intro', + position: 'left', + label: 'Docs', + }, + { + href: 'https://github.com/sequentech/release-tool', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { + label: 'Introduction', + to: '/docs/intro', + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} Sequent Tech Inc. Built with Docusaurus.`, + }, + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme, + }, + }), +}; + +module.exports = config; diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..5a91599 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +--- + +# Installation + +## Prerequisites + +- Python 3.10 or higher +- Poetry (for dependency management) + +## Setup + +1. Clone the repository: + ```bash + git clone https://github.com/sequentech/release-tool.git + cd release-tool + ``` + +2. Install dependencies: + ```bash + poetry install + ``` + +## Usage + +Run the tool using `poetry run`: + +```bash +poetry run release-tool --help +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..d95d9b2 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 1 +--- + +# Introduction + +The **Release Tool** is a CLI application designed to manage releases using semantic versioning. It automates the process of generating release notes, managing versions, and interacting with GitHub. + +## Features + +- **Semantic Versioning**: Automatically determines the next version based on changes. +- **Release Notes Generation**: Groups and formats release notes from Pull Requests and tickets. +- **Policy-Driven**: Highly configurable policies for versioning, ticket extraction, and grouping. +- **GitHub Integration**: Syncs data from GitHub and creates releases. +- **SQLite Storage**: Caches repository data locally for efficiency. + +## Goals + +- Efficient and flexible release management. +- Configurable to support different workflows. +- "Create once, run everywhere" philosophy with a single binary/script. diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..089bd8b --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,18083 @@ +{ + "name": "release-tool-website", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "release-tool-website", + "version": "0.0.0", + "dependencies": { + "@docusaurus/core": "latest", + "@docusaurus/preset-classic": "latest", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz", + "integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", + "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.106", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.106.tgz", + "integrity": "sha512-TU8ONNhm64GI7O60UDCcOz9CdyCp3emQwSYrSnq+QWBNgS8vDlRQ3ZwXyPNAJQdXyBTafVS2iyS0kvV+KXaPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.18", + "ai": "5.0.106", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.11.0.tgz", + "integrity": "sha512-a7oQ8dwiyoyVmzLY0FcuBqyqcNSq78qlcOtHmNBumRlHCSnXDcuoYGBGPN1F6n8JoGhviDDsIaF/oQrzTzs6Lg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.45.0.tgz", + "integrity": "sha512-WTW0VZA8xHMbzuQD5b3f41ovKZ0MNTIXkWfm0F2PU+XGcLxmxX15UqODzF2sWab0vSbi3URM1xLhJx+bXbd1eQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.45.0.tgz", + "integrity": "sha512-I3g7VtvG/QJOH3tQO7E7zWTwBfK/nIQXShFLR8RvPgWburZ626JNj332M3wHCYcaAMivN9WJG66S2JNXhm6+Xg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.45.0.tgz", + "integrity": "sha512-/nTqm1tLiPtbUr+8kHKyFiCOfhRfgC+JxLvOCq471gFZZOlsh6VtFRiKI60/zGmHTojFC6B0mD80PB7KeK94og==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.45.0.tgz", + "integrity": "sha512-suQTx/1bRL1g/K2hRtbK3ANmbzaZCi13487sxxmqok+alBDKKw0/TI73ZiHjjFXM2NV52inwwcmW4fUR45206Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.45.0.tgz", + "integrity": "sha512-CId/dbjpzI3eoUhPU6rt/z4GrRsDesqFISEMOwrqWNSrf4FJhiUIzN42Ac+Gzg69uC0RnzRYy60K1y4Na5VSMw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.45.0.tgz", + "integrity": "sha512-tjbBKfA8fjAiFtvl9g/MpIPiD6pf3fj7rirVfh1eMIUi8ybHP4ovDzIaE216vHuRXoePQVCkMd2CokKvYq1CLw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.45.0.tgz", + "integrity": "sha512-nxuCid+Nszs4xqwIMDw11pRJPes2c+Th1yup/+LtpjFH8QWXkr3SirNYSD3OXAeM060HgWWPLA8/Fxk+vwxQOA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.45.0.tgz", + "integrity": "sha512-t+1doBzhkQTeOOjLHMlm4slmXBhvgtEGQhOmNpMPTnIgWOyZyESWdm+XD984qM4Ej1i9FRh8VttOGrdGnAjAng==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.45.0.tgz", + "integrity": "sha512-IaX3ZX1A/0wlgWZue+1BNWlq5xtJgsRo7uUk/aSiYD7lPbJ7dFuZ+yTLFLKgbl4O0QcyHTj1/mSBj9ryF1Lizg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.45.0.tgz", + "integrity": "sha512-1jeMLoOhkgezCCPsOqkScwYzAAc1Jr5T2hisZl0s32D94ZV7d1OHozBukgOjf8Dw+6Hgi6j52jlAdUWTtkX9Mg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.45.0.tgz", + "integrity": "sha512-46FIoUkQ9N7wq4/YkHS5/W9Yjm4Ab+q5kfbahdyMpkBPJ7IBlwuNEGnWUZIQ6JfUZuJVojRujPRHMihX4awUMg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.45.0.tgz", + "integrity": "sha512-XFTSAtCwy4HdBhSReN2rhSyH/nZOM3q3qe5ERG2FLbYId62heIlJBGVyAPRbltRwNlotlydbvSJ+SQ0ruWC2cw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.45.0.tgz", + "integrity": "sha512-8mTg6lHx5i44raCU52APsu0EqMsdm4+7Hch/e4ZsYZw0hzwkuaMFh826ngnkYf9XOl58nHoou63aZ874m8AbpQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz", + "integrity": "sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz", + "integrity": "sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", + "license": "MIT", + "dependencies": { + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.3.1", + "@docsearch/css": "4.3.2", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", + "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/runtime-corejs3": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", + "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.9.2", + "@docusaurus/cssnano-preset": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", + "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.9.2", + "@docusaurus/bundler": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.6", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", + "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", + "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", + "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", + "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", + "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", + "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", + "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", + "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", + "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", + "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", + "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/gtag.js": "^0.0.12", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", + "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", + "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", + "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", + "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/plugin-css-cascade-layers": "3.9.2", + "@docusaurus/plugin-debug": "3.9.2", + "@docusaurus/plugin-google-analytics": "3.9.2", + "@docusaurus/plugin-google-gtag": "3.9.2", + "@docusaurus/plugin-google-tag-manager": "3.9.2", + "@docusaurus/plugin-sitemap": "3.9.2", + "@docusaurus/plugin-svgr": "3.9.2", + "@docusaurus/theme-classic": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-search-algolia": "3.9.2", + "@docusaurus/types": "3.9.2" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", + "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", + "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", + "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.9.0 || ^4.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", + "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/types": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", + "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", + "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", + "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", + "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ai": { + "version": "5.0.106", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.106.tgz", + "integrity": "sha512-M5obwavxSJJ3tGlAFqI6eltYNJB0D20X6gIBCFx/KVorb/X1fxVVfiZZpZb+Gslu4340droSOjT0aKQFCarNVg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.18", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.45.0.tgz", + "integrity": "sha512-wrj4FGr14heLOYkBKV3Fbq5ZBGuIFeDJkTilYq/G+hH1CSlQBtYvG2X1j67flwv0fUeQJwnWxxRIunSemAZirA==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.11.0", + "@algolia/client-abtesting": "5.45.0", + "@algolia/client-analytics": "5.45.0", + "@algolia/client-common": "5.45.0", + "@algolia/client-insights": "5.45.0", + "@algolia/client-personalization": "5.45.0", + "@algolia/client-query-suggestions": "5.45.0", + "@algolia/client-search": "5.45.0", + "@algolia/ingestion": "1.45.0", + "@algolia/monitoring": "1.45.0", + "@algolia/recommend": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz", + "integrity": "sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", + "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.3.tgz", + "integrity": "sha512-8aaDS5nVqMXmYjlmmJpqlDJosiqbl2NJkYuSFOXR6RTY14qNosMrqT4t7O+EUm+OdduQg3GNI2ZwC03No1Y58Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", + "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-forge": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz", + "integrity": "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.21", + "browserslist": "^4.26.0", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.4.2", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", + "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", + "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/swr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.7.tgz", + "integrity": "sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpackbar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", + "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/webpackbar/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/webpackbar/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpackbar/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpackbar/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..eee59c6 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,37 @@ +{ + "name": "release-tool-website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "latest", + "@docusaurus/preset-classic": "latest", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/docs/policies.md b/docs/policies.md new file mode 100644 index 0000000..a4f6346 --- /dev/null +++ b/docs/policies.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 4 +--- + +# Policies + +The Release Tool uses various policies to control its behavior. These policies can be configured to match your workflow. See the [Configuration](configuration.md) guide for details on how to set them up in `release_tool.toml`. + +## Version Policy + +Determines the next version number based on the changes since the last release. + +- **Semantic Versioning**: Adheres to SemVer 2.0.0. +- **Gap Handling**: Configurable behavior when version gaps are detected (Ignore, Warn, Error). + +## Ticket Policy + +Extracts ticket information from Pull Requests and commits. + +- **Extraction**: Finds ticket references (e.g., `JIRA-123`) in PR titles and bodies. +- **Info Retrieval**: Fetches ticket details like title, description, and type. +- **Consolidation**: Groups multiple commits belonging to the same parent ticket. + +## Release Note Policy + +Controls how release notes are generated and formatted. + +- **Grouping**: Groups notes by category (e.g., Features, Bug Fixes). +- **Ordering**: Defines the order of categories. +- **Exclusions**: Excludes specific labels or tickets from the notes. +- **Templates**: Uses Jinja2 templates for the release note output. + +## Output Policy + +Defines where the generated release notes are published. + +- **GitHub Release**: Updates the body of the GitHub release. +- **File Generation**: Creates a new markdown file in the repository (e.g., `CHANGELOG.md` or `releases/v1.0.0.md`). diff --git a/docs/scenarios.md b/docs/scenarios.md new file mode 100644 index 0000000..5c68437 --- /dev/null +++ b/docs/scenarios.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 5 +--- + +# Scenarios + +This section covers different scenarios you might encounter when using the Release Tool. + +## Scenario 1: Standard Release + +**Context**: You are releasing a regular update (e.g., `1.2.0`) that includes several feature PRs and bug fixes. + +**Steps**: +1. Sync the repo: `release-tool sync sequentech/app` +2. Generate notes: `release-tool generate-notes 1.2.0` +3. Release: `release-tool release 1.2.0` + +**Outcome**: A new release `v1.2.0` is created on GitHub with categorized notes. + +## Scenario 2: Hotfix Release + +**Context**: You need to release a critical bug fix (e.g., `1.2.1`) immediately. + +**Steps**: +1. Sync the repo: `release-tool sync sequentech/app` +2. Generate notes: `release-tool generate-notes 1.2.1 --from-version 1.2.0` + * *Note: Explicitly setting the previous version ensures only the hotfix changes are included.* +3. Release: `release-tool release 1.2.1` + +## Scenario 3: First Release + +**Context**: You are releasing the very first version of a project (e.g., `1.0.0`). + +**Steps**: +1. Sync the repo. +2. Generate notes: `release-tool generate-notes 1.0.0` + * *The tool will detect there are no previous versions and include all history up to this point.* +3. Release: `release-tool release 1.0.0` + +## Scenario 4: Release Candidate + +**Context**: You are preparing a release candidate (e.g., `2.0.0-rc.1`). + +**Steps**: +1. Sync the repo. +2. Generate notes: `release-tool generate-notes 2.0.0-rc.1` +3. Release: `release-tool release 2.0.0-rc.1` + +**Subsequent RC**: +When releasing `2.0.0-rc.2`, the tool will compare it against `2.0.0-rc.1`. + +**Final Release**: +When releasing `2.0.0` (final), the tool will consolidate changes from all RCs (`rc.1`, `rc.2`) and compare against the previous stable version (e.g., `1.5.0`), providing a comprehensive changelog. diff --git a/docs/sidebars.js b/docs/sidebars.js new file mode 100644 index 0000000..b26b7d6 --- /dev/null +++ b/docs/sidebars.js @@ -0,0 +1,11 @@ +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const sidebars = { + tutorialSidebar: [ + { + type: 'autogenerated', + dirName: '.', + }, + ], +}; + +module.exports = sidebars; diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 0000000..c3c3502 --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,20 @@ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability, you can also add your own custom CSS here. */ diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js new file mode 100644 index 0000000..5378c07 --- /dev/null +++ b/docs/src/pages/index.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { Redirect } from '@docusaurus/router'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function Home() { + return ; +} diff --git a/docs/tickets.md b/docs/tickets.md new file mode 100644 index 0000000..983c525 --- /dev/null +++ b/docs/tickets.md @@ -0,0 +1,479 @@ +--- +sidebar_position: 8 +--- + +# Tickets + +Query and explore tickets in your local database without needing an internet connection. + +## Overview + +The `tickets` command allows you to search and inspect tickets that have been synced from GitHub to your local database. This is particularly useful for: + +- **Debugging partial ticket matches** - Find out why a ticket wasn't found during release note generation +- **Exploring ticket data** - See what tickets are available in your database +- **Exporting data** - Generate CSV reports of tickets +- **Finding typos** - Use fuzzy matching to find tickets with similar numbers +- **Offline access** - Query tickets without internet connectivity + +:::caution Prerequisites +This command only searches tickets that have been synced. Make sure to run `release-tool sync` first to populate your local database. +::: + +## Smart TICKET_KEY Format + +The `tickets` command supports multiple smart formats for specifying ticket numbers, making it easier and faster to query tickets: + +### Supported Formats + +1. **Plain number**: `8624` + - Searches for ticket 8624 across all repositories + +2. **Hash prefix**: `#8624` + - Same as plain number, GitHub-style reference + +3. **Repo name + number**: `meta#8624` + - Searches for ticket 8624 specifically in the "meta" repository + - Automatically resolves short repo names (e.g., "meta" → "owner/meta") + +4. **Full repo path + number**: `sequentech/meta#8624` + - Searches for ticket 8624 in the specified full repository path + +5. **Proximity search with ~**: `8624~` or `meta#8624~` + - Finds tickets numerically close to 8624 (within ±20 by default) + - Can be combined with repo specifications + - Equivalent to using `--close-to 8624` + +### Examples + +```bash +# Find ticket 8624 in any repository +release-tool tickets 8624 + +# Find ticket 8624 with # prefix +release-tool tickets #8624 + +# Find ticket 8624 only in 'meta' repo (short name) +release-tool tickets meta#8624 + +# Find ticket 8624 in specific full repository +release-tool tickets sequentech/meta#8624 + +# Find tickets close to 8624 (±20) in any repo +release-tool tickets 8624~ + +# Find tickets close to 8624 in 'meta' repo +release-tool tickets meta#8624~ + +# Find tickets close to 8624 in full repo path +release-tool tickets sequentech/meta#8624~ +``` + +:::tip Combining with Options +The smart format can be combined with other options. For example, `--repo` flag will override the repository parsed from the TICKET_KEY: + +```bash +# The --repo flag takes precedence +release-tool tickets --repo sequentech/step meta#1024 +``` +::: + +## Basic Usage + +### Find a Specific Ticket + +Search for a ticket by its exact key or number: + +```bash +release-tool tickets 8624 +``` + +This will display the ticket in a formatted table if found in your local database. + +### Find Ticket in Specific Repository + +If the same ticket number exists in multiple repositories, filter by repository: + +```bash +release-tool tickets 8624 --repo sequentech/meta +``` + +### List All Tickets in a Repository + +View all tickets from a specific repository: + +```bash +release-tool tickets --repo sequentech/meta +``` + +By default, this shows 20 results. Use `--limit` to change this: + +```bash +release-tool tickets --repo sequentech/meta --limit 50 +``` + +## Fuzzy Matching + +### Find Tickets Starting With a Prefix + +Useful when you only remember the beginning of a ticket number: + +```bash +release-tool tickets --starts-with 86 +``` + +This finds all tickets starting with "86" (e.g., 8624, 8625, 8650, 8653, etc.) + +### Find Tickets Ending With a Suffix + +Useful when you remember the ending of a ticket number: + +```bash +release-tool tickets --ends-with 24 +``` + +This finds all tickets ending with "24" (e.g., 8624, 7124, 1024, etc.) + +### Find Tickets Numerically Close to a Number + +This is particularly useful for debugging partial matches or finding typos: + +```bash +# Find tickets within ±20 of 8624 (default range) +release-tool tickets --close-to 8624 + +# Find tickets within ±50 of 8624 +release-tool tickets --close-to 8624 --range 50 +``` + +For example, if you had a typo in your branch name like `feat/meta-8634/main` instead of `feat/meta-8624/main`, this would help you find the correct ticket: + +```bash +release-tool tickets --close-to 8634 --range 20 +# Shows: 8624, 8625, 8650, ... (all tickets in range 8614-8654) +``` + +## Output Formats + +### Table Format (Default) + +The default output is a formatted table with colors: + +```bash +release-tool tickets --repo sequentech/meta --limit 5 +``` + +Output: +``` +┏━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Key ┃ Repository ┃ Title ┃ State ┃ URL ┃ +┡━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━┩ +│ 8624 │ sequentech/meta │ Add dark mode to UI │ open │ https://github.com/... │ +│ 8625 │ sequentech/meta │ Fix login bug │ closed│ https://github.com/... │ +│ 8650 │ sequentech/meta │ Update documentation │ open │ https://github.com/... │ +└──────┴──────────────────┴────────────────────────┴───────┴─────────────────────────┘ + +Showing 1-3 tickets (all results) +``` + +Features: +- **Color coding**: Open tickets in green, closed tickets dimmed +- **Truncation**: Long titles and URLs are automatically truncated +- **Pagination info**: Shows which results you're viewing + +### CSV Format + +Export tickets to CSV for analysis in spreadsheet applications: + +```bash +release-tool tickets --repo sequentech/meta --format csv > tickets.csv +``` + +The CSV includes all fields: +- `id` - Database ID +- `repo_id` - Repository ID +- `number` - Ticket number +- `key` - Ticket key (may include prefix) +- `title` - Ticket title +- `body` - Ticket description (truncated to 500 chars) +- `state` - "open" or "closed" +- `labels` - JSON array of label names +- `url` - GitHub URL +- `created_at` - Creation timestamp (ISO format) +- `closed_at` - Closure timestamp (ISO format, empty if still open) +- `category` - Category (if assigned) +- `tags` - JSON object of tags +- `repo_full_name` - Repository full name (owner/repo) + +:::tip CSV Output +Pipe the output to a file using `> tickets.csv` or use it directly in shell scripts. +::: + +## Pagination + +Control how many results are shown and skip results for pagination: + +### Limit Results + +Show only the first N tickets: + +```bash +release-tool tickets --repo sequentech/meta --limit 10 +``` + +### Skip Results (Offset) + +Skip the first N results: + +```bash +release-tool tickets --repo sequentech/meta --offset 20 +``` + +### Combined Pagination + +Get results 21-30: + +```bash +release-tool tickets --repo sequentech/meta --limit 10 --offset 20 +``` + +## Debugging Partial Matches + +When `release-tool generate` shows partial ticket match warnings, use `query-tickets` to investigate. + +### Scenario 1: Ticket Not Found + +If you see: +``` +⚠️ Found 1 partial ticket match(es) + +Tickets not found in database (1): + • 8853 (from branch feat/meta-8853/main, PR #2122) + → Ticket may be older than sync cutoff date + → Ticket may not exist (typo in branch/PR) + → Sync may not have been run yet +``` + +**Investigation steps:** + +1. Check if ticket exists in any repo: + ```bash + release-tool tickets 8853 + ``` + +2. Search for similar ticket numbers: + ```bash + release-tool tickets --close-to 8853 --range 50 + ``` + +3. Check tickets ending with same digits: + ```bash + release-tool tickets --ends-with 53 + ``` + +4. If not found, verify on GitHub or re-run sync: + ```bash + release-tool sync + ``` + +### Scenario 2: Ticket in Different Repo + +If you see: +``` +⚠️ Found 1 partial ticket match(es) + +Tickets in different repository (1): + • 8624 (from branch feat/meta-8624/main, PR #2122) + Found in: sequentech/step + URL: https://github.com/sequentech/step/issues/8624 +``` + +**Investigation steps:** + +1. Verify ticket location: + ```bash + release-tool tickets 8624 + ``` + +2. Check all repos for this ticket: + ```bash + # Without --repo filter to see all matches + release-tool tickets 8624 + ``` + +3. Fix your branch name or config's `ticket_repos` setting + +### Scenario 3: Finding Typos in Branch Names + +If your branch has a typo like `feat/meta-8643/main` but the real ticket is `8624`: + +```bash +# Find tickets close to the typo +release-tool tickets --close-to 8643 --range 30 + +# This will show tickets like 8624, 8625, 8650, etc. +# helping you identify the correct ticket number +``` + +## Advanced Examples + +### Combine Filters + +Find tickets ending with "24" in a specific repo: + +```bash +release-tool tickets --ends-with 24 --repo sequentech/step +``` + +### Export Filtered Results + +Export all open tickets to CSV: + +```bash +# First, find them in table format +release-tool tickets --repo sequentech/meta --limit 100 + +# Then export to CSV +release-tool tickets --repo sequentech/meta --limit 100 --format csv > open_tickets.csv +``` + +### Check Database Contents + +See what's in your database: + +```bash +# List recent tickets across all repos +release-tool tickets --limit 50 + +# List tickets from each repo +release-tool tickets --repo sequentech/meta --limit 20 +release-tool tickets --repo sequentech/step --limit 20 +``` + +## Options Reference + +| Option | Short | Description | Default | +|--------|-------|-------------|---------| +| `ticket_key` | - | Exact ticket key to search (positional argument) | - | +| `--repo` | `-r` | Filter by repository (owner/repo format) | All repos | +| `--limit` | `-n` | Maximum number of results | 20 | +| `--offset` | - | Skip first N results (for pagination) | 0 | +| `--format` | `-f` | Output format: `table` or `csv` | table | +| `--starts-with` | - | Find tickets starting with prefix (fuzzy) | - | +| `--ends-with` | - | Find tickets ending with suffix (fuzzy) | - | +| `--close-to` | - | Find tickets numerically close to number | - | +| `--range` | - | Range for `--close-to` (±N) | 20 | + +## Validation Rules + +- `--range` must be >= 0 +- `--limit` must be > 0 +- `--offset` must be >= 0 +- Cannot combine `--close-to` with `--starts-with` or `--ends-with` +- `--range` is only valid with `--close-to` + +## Tips and Tricks + +### 1. Quick Ticket Lookup + +Add a shell alias for quick lookups: + +```bash +# In your ~/.bashrc or ~/.zshrc +alias qt='release-tool tickets' + +# Then use it: +qt 8624 +qt --repo sequentech/meta +``` + +### 2. Pipeline with grep + +Filter table output with grep: + +```bash +release-tool tickets --repo sequentech/meta | grep "bug" +``` + +### 3. Count Tickets + +Count tickets in a repo using CSV and wc: + +```bash +release-tool tickets --repo sequentech/meta --limit 1000 --format csv | wc -l +``` + +### 4. Find Recently Synced Tickets + +Since results are sorted by `created_at DESC`, the first results show the newest tickets: + +```bash +release-tool tickets --limit 10 +``` + +### 5. Verify Sync Coverage + +Check if tickets from a specific time period are synced: + +```bash +# Export all tickets with timestamps +release-tool tickets --limit 1000 --format csv > all_tickets.csv + +# Then inspect created_at dates in a spreadsheet +``` + +## Troubleshooting + +### "Database not found" + +**Error:** +``` +Error: Database not found. Please run 'release-tool sync' first. +``` + +**Solution:** +```bash +release-tool sync +``` + +The database is created by the `sync` command. If you've never synced, the database doesn't exist yet. + +### "Repository 'X' not found in database" + +**Error:** +``` +Error: Repository 'sequentech/unknown' not found in database. +Tip: Run 'release-tool sync' to fetch repository data. +``` + +**Solutions:** +1. Check the repository name spelling +2. Ensure the repository is configured in `release_tool.toml` +3. Run `release-tool sync` to fetch repository metadata + +### No Results Found + +If your query returns no results: + +1. Verify tickets exist on GitHub +2. Check your sync cutoff date (might be excluding old tickets) +3. Re-run sync: `release-tool sync` +4. Try fuzzy matching instead of exact match + +### CSV Output Looks Wrong in Terminal + +CSV is designed for file output, not terminal display: + +```bash +# Instead of this: +release-tool tickets --format csv + +# Do this: +release-tool tickets --format csv > tickets.csv +``` + +## See Also + +- [Usage Guide](usage.md) - General workflow including sync +- [Troubleshooting](troubleshooting.md) - Common issues +- [Configuration](configuration.md) - Config file reference diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..af5338f --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 6 +--- + +# Troubleshooting + +Common issues and how to resolve them. + +## GitHub API Rate Limit + +**Issue**: The tool fails with a 403 or 429 error when syncing. + +**Cause**: You have exceeded the GitHub API rate limit. + +**Solution**: +- Ensure you have configured a `GITHUB_TOKEN`. Authenticated requests have a much higher rate limit (5000/hour) than unauthenticated ones (60/hour). +- Check your token permissions. + +## Missing Tickets in Release Notes + +**Issue**: PRs are showing up in "Other" or not appearing at all. + +**Cause**: +- The PR title or body does not contain a valid ticket reference (e.g., `JIRA-123`). +- The `ticket_patterns` configuration does not match your ticket format. +- The PR is not closed/merged. + +**Solution**: +- Verify the PR has a ticket reference. +- Check your `release_tool.toml` configuration for `ticket_patterns`. +- Ensure the PR is merged. + +## Database Locked + +**Issue**: `sqlite3.OperationalError: database is locked`. + +**Cause**: Another process is accessing the `release_tool.db` file. + +**Solution**: +- Ensure no other instance of the tool is running. +- Check if an IDE or database viewer has the file open. + +## Version Gap Warning + +**Issue**: The tool warns about a version gap (e.g., releasing `1.3.0` when previous is `1.1.0`). + +**Cause**: You might have skipped a version number. + +**Solution**: +- Verify the version number is correct. +- If intentional, you can ignore the warning or configure the `version_gap_policy` to `ignore`. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..48140a4 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 2 +--- + +# Usage Guide + +This guide provides a step-by-step walkthrough of how to use the Release Tool in your daily workflow. + +## Prerequisites + +Ensure you have the tool installed and configured as described in the [Installation](installation.md) and [Configuration](configuration.md) guides. + +## Step-by-Step Workflow + +### 1. Sync Repository Data + +Before generating release notes, you need to sync the latest data from GitHub to your local database. + +```bash +release-tool sync +``` + +This command: +- Clones the repository (first time) or updates it (subsequent runs) +- Fetches the latest tickets, PRs, and releases from GitHub +- Stores data in `.release_tool_cache/release_tool.db` + +### 2. Generate Release Notes + +Generate release notes using the `generate` command. You can specify the version explicitly or use auto-bump options. + +#### Explicit Version +```bash +release-tool generate 9.1.0 --dry-run +``` + +#### Auto-Bump Options +```bash +# Bump major version (1.2.3 → 2.0.0) +release-tool generate --new-major --dry-run + +# Bump minor version (1.2.3 → 1.3.0) +release-tool generate --new-minor --dry-run + +# Bump patch version (finds latest final release, e.g., 9.2.0 → 9.2.1) +release-tool generate --new-patch --dry-run + +# Create release candidate (auto-increments: 1.2.3-rc.0, 1.2.3-rc.1, etc.) +release-tool generate --new-rc --dry-run +``` + +#### Partial Version Support +You can use partial versions with bump flags: +```bash +# Use 9.2 as base, bump patch → 9.2.1 +release-tool generate 9.2 --new-patch +``` + +### 3. Review and Edit + +By default, release notes are saved to `.release_tool_cache/draft-releases/{repo}/{version}.md`: + +```bash +# Generate saves to cache automatically +release-tool generate --new-minor +# Output: ✓ Release notes written to: .release_tool_cache/draft-releases/owner-repo/9.1.0.md + +# Edit the file +vim .release_tool_cache/draft-releases/owner-repo/9.1.0.md +``` + +### 4. Publish Release + +Once satisfied with the notes, publish to GitHub. The tool will automatically find your draft notes if you don't specify a file: + +```bash +# Auto-finds draft notes for the version +release-tool publish 9.1.0 + +# Or explicitly specify a notes file +release-tool publish 9.1.0 -f .release_tool_cache/draft-releases/owner-repo/9.1.0.md +``` + +This will: +- Auto-find draft release notes (or use specified file) +- Create a git tag `v9.1.0` +- Create a GitHub release with the release notes +- Optionally create a PR with release notes (use `--pr`) + +#### Testing Before Publishing + +Use `--dry-run` to preview what would be published without making any changes: + +```bash +# Preview the publish operation +release-tool publish 9.1.0 -f notes.md --dry-run + +# Preview with specific flags +release-tool publish 9.1.0 -f notes.md --dry-run --release --pr --draft +``` + +#### Debugging Issues + +Use `--debug` to see detailed information: + +```bash +# Show verbose debugging information +release-tool publish 9.1.0 -f notes.md --debug + +# Combine with dry-run for safe debugging +release-tool publish 9.1.0 -f notes.md --debug --dry-run +``` + +Debug mode shows: +- Configuration values being used +- Version parsing details +- Template substitution results +- File paths and content lengths +- Docusaurus file preview (if configured) + +#### Using Configuration Defaults + +Configure default behavior in `release_tool.toml`: + +```toml +[output] +create_github_release = true # Auto-create releases +create_pr = true # Auto-create PRs +draft_release = false # Publish immediately (not draft) +prerelease = "auto" # Auto-detect prerelease from version (or true/false) +``` + +Prerelease options: +- `"auto"`: Auto-detect from version (e.g., `1.0.0-rc.1` → prerelease, `1.0.0` → stable) +- `true`: Always mark as prerelease +- `false`: Always mark as stable + +Then simply run: + +```bash +# Uses config defaults (auto-finds notes file) +release-tool publish 9.1.0 + +# Explicitly specify notes file +release-tool publish 9.1.0 -f notes.md + +# Override config with CLI flags +release-tool publish 9.1.0 -f notes.md --no-release --pr --draft + +# Override prerelease setting +release-tool publish 9.1.0 --prerelease true # Force prerelease +release-tool publish 9.1.0 --prerelease false # Force stable +release-tool publish 9.1.0 --prerelease auto # Auto-detect +``` + +## Common Commands + +| Command | Description | +|---------|-------------| +| `sync` | Syncs repository, tickets, PRs, and releases from GitHub | +| `generate ` | Generates release notes for the specified version | +| `generate --new-major/minor/patch/rc` | Auto-bumps version and generates notes | +| `generate --dry-run` | Preview generated notes without creating files | +| `list-releases` | Lists releases from the database with filters | +| `publish ` | Creates a GitHub release (auto-finds draft notes) | +| `publish -f ` | Creates a GitHub release from a markdown file | +| `publish --list` or `publish -l` | List all available draft releases | +| `publish --dry-run` | Preview publish operation without making changes | +| `publish --debug` | Show detailed debugging information | +| `publish --prerelease auto\|true\|false` | Control prerelease status | +| `init-config` | Creates an example configuration file | + +## Advanced Usage + +### List Draft Releases + +View all available draft releases that are ready to be published: + +```bash +# List all draft releases +release-tool publish --list + +# Or use the shorthand +release-tool publish -l +``` + +This shows a table with: +- Code Repository +- Version +- Type (RC or Final) +- Created timestamp +- File path + +### Auto-Finding Draft Notes + +When publishing without specifying `--notes-file`, the tool automatically searches for matching draft notes: + +```bash +# Automatically finds .release_tool_cache/draft-releases/{repo}/9.1.0.md +release-tool publish 9.1.0 +``` + +If no matching draft is found, it displays all available drafts and exits with an error. + +### List Releases + +View releases with various filters: + +```bash +# Show last 10 releases (default) +release-tool list-releases + +# Show all releases +release-tool list-releases --limit 0 + +# Filter by version prefix +release-tool list-releases --version "9.3" + +# Filter by type +release-tool list-releases --type final +release-tool list-releases --type rc --type final + +# Filter by date +release-tool list-releases --after 2024-01-01 +release-tool list-releases --before 2024-06-01 +``` + +### Branch Management + +The tool automatically manages release branches: +- New major versions (9.0.0) branch from `main` +- New minor versions (9.1.0) branch from previous release branch (`release/9.0`) +- Patches and RCs use existing release branches + +See [Branching Strategy](branching-strategy.md) for details. diff --git a/examples/MASTER_TEMPLATE_FEATURE.md b/examples/MASTER_TEMPLATE_FEATURE.md new file mode 100644 index 0000000..42b855c --- /dev/null +++ b/examples/MASTER_TEMPLATE_FEATURE.md @@ -0,0 +1,325 @@ +# Master Template Feature (`release_output_template` and `doc_output_template`) + +## Overview + +The master template system provides complete control over release notes structure through Jinja2 templates. As of v1.4, the tool supports **dual template output**: + +- **`release_output_template`**: Template for GitHub release notes +- **`doc_output_template`**: Template for Docusaurus/documentation (wraps GitHub notes with frontmatter) + +## What's New + +### Before (Legacy Format) +- Fixed category-based structure +- Limited to: Title → Description → Categories with Entries +- No way to customize overall layout +- `description_template` only accepts version variable + +### After (Master Template) +- **Full control** over entire release notes structure +- **Iterate** over categories, notes, migrations, descriptions +- **Custom sections** (e.g., migrations, contributors, breaking changes) +- **Entry sub-template** rendered via `render_entry()` function +- **Dual output** - separate templates for GitHub and Docusaurus (v1.4+) +- **Backward compatible** - legacy format still works when `release_output_template` is not set + +## Key Features + +### 1. GitHub Release Template (`release_output_template`) +```toml +[release_notes] +release_release_output_template = '''# {{ title }} + +{% for category in categories %} +## {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %}''' +``` + +### 2. Entry Sub-Template +```toml +[release_notes] +entry_template = '''- {{ title }} + {% if url %}([#{{ pr_numbers[0] }}]({{ url }})){% endif %} + {% if authors %}by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}''' +``` + +The `render_entry(note)` function renders the `entry_template` with the note's data. + +### 3. Docusaurus Template (`doc_output_template`) - NEW in v1.4 + +The `doc_output_template` wraps the GitHub release notes with documentation-specific formatting: + +```toml +[release_notes] +doc_release_output_template = '''--- +id: release-{{version}} +title: {{title}} +--- + +{{ render_release_notes() }}
''' + +[output] +release_output_path = "docs/releases/{version}.md" +doc_output_path = "docs/docusaurus/docs/releases/release-{major}.{minor}/release-{major}.{minor}.{patch}.md" +``` + +**Key Points:** +- `render_release_notes()` embeds the GitHub release notes (from `release_output_template`) +- Has access to all variables: `version`, `title`, `categories`, `all_notes`, `render_entry()` +- Optional - only generates when both `doc_output_template` and `doc_output_path` are configured +- Generate command creates both files automatically + +### 4. Available Variables + +In `release_output_template`: +- `{{ version }}` - Version string (e.g., "1.2.3") +- `{{ title }}` - Rendered release title (from `title_template`) +- `{{ categories }}` - List of category dicts: `[{name: str, notes: [...]}, ...]` +- `{{ all_notes }}` - Flat list of all note dicts (across all categories) +- `{{ render_entry(note) }}` - Function to render a note using `entry_template` + +In `doc_output_template` (additional variable): +- `{{ render_release_notes() }}` - Function to render the GitHub release notes (from `release_output_template`) +- Plus all variables from `release_output_template` above + +Each note dict contains: +- `title` - Note title +- `url` - PR/ticket URL +- `pr_numbers` - List of PR numbers +- `commit_shas` - List of commit SHAs +- `labels` - List of label strings +- `ticket_key` - Ticket identifier +- `category` - Category name +- `description` - Processed description (may be None) +- `migration_notes` - Processed migration notes (may be None) +- `authors` - List of author dicts with all fields (name, username, email, company, etc.) + +### 5. HTML-like Whitespace Processing + +Both `entry_template`, `release_output_template`, and `doc_output_template` use HTML-like whitespace behavior: +- **Multiple spaces collapse** to single space +- **Newlines are ignored** unless using `
` or `
` +- **Leading/trailing whitespace** stripped from lines + +This allows readable multi-line templates with clean output. + +## Use Cases + +### 1. Separate Migrations Section +```toml +release_output_template = '''# {{ title }} + +## Changes +{% for category in categories %} +### {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %} + +## Migration Guide +{% for note in all_notes %} +{% if note.migration_notes %} +### {{ note.title }} +{{ note.migration_notes }} +{% endif %} +{% endfor %}''' +``` + +### 2. Flat List (No Categories) +```toml +release_output_template = '''# {{ title }} + +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}''' +``` + +### 3. Full Descriptions as Sections +```toml +release_output_template = '''# {{ title }} + +{% for note in all_notes %} +## {{ note.title }} +{% if note.description %} +{{ note.description }} +{% endif %} +{% if note.migration_notes %} +**Migration:** {{ note.migration_notes }} +{% endif %} +{% endfor %}''' +``` + +### 4. Contributors Section +```toml +release_output_template = '''# {{ title }} + +## Changes +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %} + +## Contributors +{% set all_authors = [] %} +{% for note in all_notes %} + {% for author in note.authors %} + {% if author not in all_authors %} + {% set _ = all_authors.append(author) %} + {% endif %} + {% endfor %} +{% endfor %} +{% for author in all_authors %} +- {{ author.mention }}{% if author.company %} ({{ author.company }}){% endif %} +{% endfor %}''' +``` + +## Implementation Details + +### Architecture + +1. **Config Model** (`config.py`): + - `release_output_template: Optional[str]` - GitHub release notes template + - `doc_output_template: Optional[str]` - Docusaurus template (v1.4+) + - `release_output_path: str` - Path for GitHub release notes + - `doc_output_path: Optional[str]` - Path for Docusaurus output (v1.4+) + - Maintains backward compatibility (None = use legacy format) + +2. **Policy Class** (`policies.py`): + - `format_markdown()` - Entry point, returns tuple or single string + - `_format_with_master_template()` - Renders `release_output_template` + - `_format_with_doc_template()` - Renders `doc_output_template` (v1.4+) + - `_format_with_legacy_layout()` - Original category-based rendering + - `_prepare_note_for_template()` - Processes notes (media, authors) + - `_process_html_like_whitespace()` - HTML-like whitespace processing + +3. **Template Rendering**: + - Creates `render_entry(note_dict)` function closure + - Passes to master template as callable + - Processes output with HTML-like whitespace rules + +### Data Flow + +``` +grouped_notes → _format_with_master_template() + ↓ +Prepare categories_data & all_notes_data + ↓ +Create render_entry() closure + ↓ +Render master template with: + - version, title + - categories, all_notes + - render_entry function + ↓ +Process HTML-like whitespace + ↓ +Return formatted markdown +``` + +## Testing + +Comprehensive test suite in `tests/test_output_template.py`: +- ✅ Category-based layout +- ✅ Flat list layout +- ✅ Migrations section layout +- ✅ Legacy format compatibility +- ✅ render_entry() with all fields +- ✅ HTML whitespace processing + +All 56 tests pass (50 original + 6 new). + +## Examples + +See: +- `example_output_template.toml` - Migrations + contributors layout +- `example_flat_list.toml` - Simple flat list +- `example_detailed_descriptions.toml` - Full descriptions as sections +- `OUTPUT_TEMPLATE_EXAMPLES.md` - Complete guide with examples + +## Migration Guide + +### From v1.3 to v1.4 + +**Automatic Migration**: The tool automatically migrates your configuration from v1.3 to v1.4: +- `output_template` → `release_output_template` +- `output_path` → `release_output_path` +- Adds placeholders for `doc_output_template` and `doc_output_path` (optional) + +Your existing customizations are preserved! + +### From Legacy Format to Master Template + +If you have: +```toml +[release_notes] +title_template = "Release {{ version }}" +description_template = "Some description" +entry_template = "- {{ title }}" +``` + +To use master template, add: +```toml +[release_notes] +release_output_template = '''# {{ title }} + +Some description + +{% for category in categories %} +## {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %}''' +``` + +The `description_template` is now deprecated in favor of including it directly in `release_output_template`. + +### Adding Docusaurus Support (v1.4+) + +To generate Docusaurus documentation alongside GitHub release notes: + +```toml +[release_notes] +# Your existing release_output_template +doc_output_template = '''--- +id: release-{{version}} +title: {{title}} +--- +{{ render_release_notes() }}''' + +[output] +release_output_path = "docs/releases/{version}.md" +doc_output_path = "docs/docusaurus/docs/releases/release-{major}.{minor}/release-{major}.{minor}.{patch}.md" +``` + +## Backward Compatibility + +✅ **100% backward compatible** +- If `release_output_template` is not set (None), uses legacy format +- Existing v1.3 configs are automatically migrated to v1.4 +- `description_template` still works in legacy mode +- `doc_output_template` is optional - GitHub-only output still works + +## Benefits + +1. ✨ **Flexibility** - Complete control over layout +2. 🔄 **Iteration** - Loop over migrations, descriptions, authors +3. 📦 **Custom Sections** - Add breaking changes, contributors, etc. +4. 🎨 **Clean Templates** - HTML-like whitespace for readable code +5. 🔌 **Extensible** - Easy to add new sections and features +6. 📝 **Dual Output** - Generate GitHub and Docusaurus files simultaneously (v1.4+) +7. ↩️ **Backward Compatible** - Existing configs work unchanged + +## Future Enhancements + +Possible future additions: +- Filters for grouping (e.g., by label, author) +- Helper functions for common operations +- Template includes/macros +- More template examples for different use cases diff --git a/examples/OUTPUT_TEMPLATE_EXAMPLES.md b/examples/OUTPUT_TEMPLATE_EXAMPLES.md new file mode 100644 index 0000000..f065759 --- /dev/null +++ b/examples/OUTPUT_TEMPLATE_EXAMPLES.md @@ -0,0 +1,329 @@ +# Output Template Examples + +This document showcases different ways to use the `output_template` feature to customize your release notes layout. + +## What is `output_template`? + +The `output_template` is a **master Jinja2 template** that gives you complete control over the structure and layout of your release notes. When set, it replaces the default category-based layout. + +### Key Concepts + +1. **Master Template**: Controls the entire release notes structure +2. **Entry Template**: Sub-template for individual changes (used via `render_entry()`) +3. **Variables Available**: + - `{{ version }}` - Version string (e.g., "1.2.3") + - `{{ title }}` - Rendered release title + - `{{ categories }}` - List of category dicts with 'name' and 'notes' + - `{{ all_notes }}` - Flat list of all notes (across categories) + - `{{ render_entry(note) }}` - Function to render individual entries + +4. **Note Fields**: Each note dict contains: + - `title`, `url`, `pr_numbers`, `commit_shas`, `labels`, `ticket_key`, `category` + - `description`, `migration_notes` (may be None) + - `authors` (list of author dicts with name, username, email, etc.) + +## Example Configurations + +### 1. Default Category-Based Layout (example_output_template.toml) + +**Use Case**: Traditional changelog with categories and a dedicated migrations section + +**Features**: +- Changes grouped by category (Features, Bug Fixes, etc.) +- Separate "Migration Guide" section for all migration notes +- Contributors list at the bottom + +**Config**: `example_output_template.toml` + +**Output Structure**: +```markdown +# Release 1.2.3 + +## What's Changed +### Features +- Add new feature ([#123](url)) by @alice +- Improve performance ([#124](url)) by @bob + +### Bug Fixes +- Fix critical bug ([#125](url)) by @alice + +## Migration Guide +### Fix critical bug +Update your config to use new format. +See PR #125 for details. + +## Contributors +- @alice (Acme Corp) +- @bob +``` + +--- + +### 2. Flat List Layout (example_flat_list.toml) + +**Use Case**: Simple chronological list without category grouping + +**Features**: +- No category headers +- All changes in one flat list +- Rich entry template with inline descriptions + +**Config**: `example_flat_list.toml` + +**Output Structure**: +```markdown +# Release 1.2.3 + +All changes in this release: + +- **Add new feature** + Detailed description here + [#123](url) by @alice `feature` + +- **Fix critical bug** + [#124](url) by @bob `bug` `urgent` +``` + +--- + +### 3. Detailed Descriptions Layout (example_detailed_descriptions.toml) + +**Use Case**: Technical release notes with full ticket details + +**Features**: +- Each ticket gets its own section (##) +- Full descriptions displayed +- Metadata block (category, PR, labels, authors) +- Inline migration notes +- Summary at the end + +**Config**: `example_detailed_descriptions.toml` + +**Output Structure**: +```markdown +# Release 1.2.3 + +## Add new feature + +This feature allows users to do X, Y, and Z. +It improves performance by 50%. + +**Details:** +- Category: Features +- Pull Request: [#123](url) +- Labels: `feature`, `enhancement` +- Authors: @alice, @bob + +**Migration Notes:** +Run `npm run migrate` after upgrading. + +--- + +## Fix critical bug + +Fixed an issue where... + +**Details:** +- Category: Bug Fixes +- Pull Request: [#124](url) +- Labels: `bug` +- Authors: @alice + +--- + +## Summary + +This release includes 2 changes across the following categories: +- **Features**: 1 change(s) +- **Bug Fixes**: 1 change(s) +``` + +--- + +## Creating Your Own Template + +### Step 1: Choose Your Layout + +Decide how you want to structure your release notes: +- **Category-based** (default): Group by Features, Bug Fixes, etc. +- **Flat list**: Simple chronological order +- **Detailed**: Each change as a full section +- **Custom**: Mix and match! + +### Step 2: Define Entry Template + +The `entry_template` defines how individual changes appear: + +```toml +# Minimal +entry_template = "- {{ title }}" + +# With PR link and authors +entry_template = '''- {{ title }} + {% if url %}([#{{ pr_numbers[0] }}]({{ url }})){% endif %} + {% if authors %}by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}''' +``` + +### Step 3: Create Output Template + +Use the master template to control overall structure: + +```toml +# Iterate over categories +output_template = '''# {{ title }} + +{% for category in categories %} +## {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %}''' + +# OR iterate over all notes (flat) +output_template = '''# {{ title }} + +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}''' +``` + +### Step 4: Add Custom Sections + +You can add any custom sections you want: + +```toml +output_template = '''# {{ title }} + +## Changes +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %} + +## Breaking Changes +{% for note in all_notes %} +{% if "breaking" in note.labels %} +- {{ note.title }}: {{ note.migration_notes }} +{% endif %} +{% endfor %} + +## Contributors +{% set all_authors = [] %} +{% for note in all_notes %} + {% for author in note.authors %} + {% if author.username not in (all_authors | map(attribute='username') | list) %} + {% set _ = all_authors.append(author) %} + {% endif %} + {% endfor %} +{% endfor %} +{% for author in all_authors %} +- [{{ author.mention }}]({{ author.profile_url }}) +{% endfor %}''' +``` + +## Tips & Tricks + +### HTML-like Whitespace Behavior + +Templates use HTML-like whitespace processing: +- Multiple spaces collapse to single space +- Newlines are ignored (use `
` or `
` for explicit line breaks) +- Leading/trailing whitespace is stripped from lines + +```toml +# These are equivalent: +entry_template = "- {{ title }} by {{ authors[0].mention }}" + +entry_template = '''- {{ title }} + by {{ authors[0].mention }}''' +``` + +### Accessing Author Fields + +Each author dict has many fields: +- `{{ author.mention }}` - Smart @mention (username or name) +- `{{ author.username }}` - GitHub username +- `{{ author.name }}` - Git author name +- `{{ author.email }}` - Email address +- `{{ author.company }}` - Company name +- `{{ author.avatar_url }}` - Profile picture URL +- `{{ author.profile_url }}` - GitHub profile URL + +### Conditional Sections + +Show sections only when they have content: + +```toml +output_template = '''# {{ title }} + +## Changes +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %} + +{% if all_notes | selectattr('migration_notes') | list | length > 0 %} +## Migrations +{% for note in all_notes %} +{% if note.migration_notes %} +- {{ note.title }}: {{ note.migration_notes }} +{% endif %} +{% endfor %} +{% endif %}''' +``` + +### Custom Filtering + +Use Jinja2 filters to manipulate data: + +```toml +# Count changes by category +{% for category in categories %} +- {{ category.name }}: {{ category.notes | length }} changes +{% endfor %} + +# Show only certain labels +{% for note in all_notes %} +{% if "feature" in note.labels %} +- NEW: {{ note.title }} +{% endif %} +{% endfor %} +``` + +## Migration from Legacy Format + +If you're not using `output_template`, the tool uses the legacy format with: +- `title_template` - For the main title +- `description_template` - For optional description (deprecated) +- `entry_template` - For each entry +- Hardcoded category-based structure + +To migrate to `output_template`: + +1. Start with the default template: +```toml +output_template = '''# {{ title }} + +{% for category in categories %} +## {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %}''' +``` + +2. Add your custom sections (migrations, contributors, etc.) + +3. Customize as needed! + +## Testing Your Template + +Use the CLI to preview your release notes: + +```bash +# Generate release notes without writing to file +poetry run release-tool generate 1.2.3 + +# Write to file +poetry run release-tool generate 1.2.3 --output +``` + +Check the output and adjust your template until it looks perfect! diff --git a/examples/example_detailed_descriptions.toml b/examples/example_detailed_descriptions.toml new file mode 100644 index 0000000..768d5dd --- /dev/null +++ b/examples/example_detailed_descriptions.toml @@ -0,0 +1,71 @@ +# Example configuration showcasing detailed descriptions and full ticket information +# This layout displays each ticket as a full section with description and metadata + +[repository] +code_repo = "owner/repo" + +[github] +# token will be read from GITHUB_TOKEN environment variable + +[release_notes] +title_template = "Release {{ version }}" + +# Simple entry template - not used in this layout since we render manually +entry_template = "- {{ title }}" + +# Detailed output template - each ticket gets its own section +output_template = '''# {{ title }} + +{% for note in all_notes %} +## {{ note.title }} + +{% if note.description %} +{{ note.description }} +{% endif %} + +**Details:** +- Category: {{ note.category }} +{% if note.url %}- Pull Request: [#{{ note.pr_numbers[0] }}]({{ note.url }}){% endif %} +{% if note.labels %}- Labels: `{{ note.labels|join('`, `') }}`{% endif %} +{% if note.authors %}- Authors: {% for author in note.authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + +{% if note.migration_notes %} +**Migration Notes:** + +{{ note.migration_notes }} +{% endif %} + +--- +{% endfor %} + +## Summary + +This release includes {{ all_notes|length }} changes across the following categories: +{% for category in categories %} +- **{{ category.name }}**: {{ category.notes|length }} change(s) +{% endfor %}''' + +[[release_notes.categories]] +name = "Features" +labels = ["feature", "enhancement"] +order = 1 + +[[release_notes.categories]] +name = "Bug Fixes" +labels = ["bug", "fix"] +order = 2 + +[[release_notes.categories]] +name = "Documentation" +labels = ["docs"] +order = 3 + +[[release_notes.categories]] +name = "Other" +labels = [] +order = 99 + +[output] +output_path = "docs/releases/{version}.md" +download_media = true +assets_path = "docs/releases/assets/{version}" diff --git a/examples/example_flat_list.toml b/examples/example_flat_list.toml new file mode 100644 index 0000000..d744088 --- /dev/null +++ b/examples/example_flat_list.toml @@ -0,0 +1,45 @@ +# Example configuration with flat list (no categories) +# This shows a simple chronological list of all changes + +[repository] +code_repo = "owner/repo" + +[github] +# token will be read from GITHUB_TOKEN environment variable + +[release_notes] +title_template = "Release {{ version }}" + +# Entry template with full details +entry_template = '''- **{{ title }}** + {% if description %}{{ description }}{% endif %} + {% if url %}[#{{ pr_numbers[0] }}]({{ url }}){% endif %} + {% if authors %}by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + {% if labels %}`{{ labels|join('` `') }}`{% endif %}''' + +# Flat list output - no category grouping +output_template = '''# {{ title }} + +All changes in this release: + +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}''' + +[[release_notes.categories]] +name = "Features" +labels = ["feature"] +order = 1 + +[[release_notes.categories]] +name = "Bug Fixes" +labels = ["bug"] +order = 2 + +[[release_notes.categories]] +name = "Other" +labels = [] +order = 99 + +[output] +output_path = "CHANGELOG.md" diff --git a/examples/example_output_template.toml b/examples/example_output_template.toml new file mode 100644 index 0000000..2376201 --- /dev/null +++ b/examples/example_output_template.toml @@ -0,0 +1,85 @@ +# Example configuration showcasing the output_template master template feature +# This example shows how to create custom release notes with migrations section + +[repository] +code_repo = "owner/repo" + +[github] +# token will be read from GITHUB_TOKEN environment variable + +# ============================================================================= +# Release Notes Configuration +# ============================================================================= +[release_notes] +title_template = "Release {{ version }}" + +# Entry template - defines how each individual change is rendered +# This template is used by render_entry() in the output_template +entry_template = '''- {{ title }} + {% if url %}([#{{ pr_numbers[0] }}]({{ url }})){% endif %} + {% if authors %} + by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %}''' + +# Output template - master template for entire release notes +# Example 1: Custom layout with separate migrations section +output_template = '''# {{ title }} + +## What's Changed +{% for category in categories %} +### {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %} + +## Migration Guide +{% for note in all_notes %} +{% if note.migration_notes %} +### {{ note.title }} +{{ note.migration_notes }} +{% if note.url %} +See [PR #{{ note.pr_numbers[0] }}]({{ note.url }}) for details. +{% endif %} +{% endif %} +{% endfor %} + +## Contributors +{% set all_authors = [] %} +{% for note in all_notes %} + {% for author in note.authors %} + {% if author not in all_authors %} + {% set _ = all_authors.append(author) %} + {% endif %} + {% endfor %} +{% endfor %} +Thank you to all contributors: +{% for author in all_authors %} +- {{ author.mention }}{% if author.company %} ({{ author.company }}){% endif %} +{% endfor %}''' + +# Categories for grouping changes +[[release_notes.categories]] +name = "Features" +labels = ["feature", "enhancement"] +order = 1 + +[[release_notes.categories]] +name = "Bug Fixes" +labels = ["bug", "fix"] +order = 2 + +[[release_notes.categories]] +name = "Documentation" +labels = ["docs"] +order = 3 + +[[release_notes.categories]] +name = "Other" +labels = [] +order = 99 + +[output] +output_path = "docs/releases/{version}.md" +download_media = true +assets_path = "docs/releases/assets/{version}" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..50c45b7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1248 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2025.11.12" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "cryptography" +version = "46.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "gitdb" +version = "4.0.12" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.45" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, + {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy" +version = "1.18.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, + {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.12.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e"}, + {file = "pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygithub" +version = "2.8.1" +description = "Use the full Github API v3" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0"}, + {file = "pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9"}, +] + +[package.dependencies] +pyjwt = {version = ">=2.4.0", extras = ["crypto"]} +pynacl = ">=1.4.0" +requests = ">=2.14.0" +typing-extensions = ">=4.5.0" +urllib3 = ">=1.26.0" + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pynacl" +version = "1.6.1" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pynacl-1.6.1-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:7d7c09749450c385301a3c20dca967a525152ae4608c0a096fe8464bfc3df93d"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc734c1696ffd49b40f7c1779c89ba908157c57345cf626be2e0719488a076d3"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3cd787ec1f5c155dc8ecf39b1333cfef41415dc96d392f1ce288b4fe970df489"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b35d93ab2df03ecb3aa506be0d3c73609a51449ae0855c2e89c7ed44abde40b"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dece79aecbb8f4640a1adbb81e4aa3bfb0e98e99834884a80eb3f33c7c30e708"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c2228054f04bf32d558fb89bb99f163a8197d5a9bf4efa13069a7fa8d4b93fc3"}, + {file = "pynacl-1.6.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:2b12f1b97346f177affcdfdc78875ff42637cb40dcf79484a97dae3448083a78"}, + {file = "pynacl-1.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e735c3a1bdfde3834503baf1a6d74d4a143920281cb724ba29fb84c9f49b9c48"}, + {file = "pynacl-1.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3384a454adf5d716a9fadcb5eb2e3e72cd49302d1374a60edc531c9957a9b014"}, + {file = "pynacl-1.6.1-cp314-cp314t-win32.whl", hash = "sha256:d8615ee34d01c8e0ab3f302dcdd7b32e2bcf698ba5f4809e7cc407c8cdea7717"}, + {file = "pynacl-1.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5f5b35c1a266f8a9ad22525049280a600b19edd1f785bccd01ae838437dcf935"}, + {file = "pynacl-1.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d984c91fe3494793b2a1fb1e91429539c6c28e9ec8209d26d25041ec599ccf63"}, + {file = "pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7"}, + {file = "pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8"}, + {file = "pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0"}, + {file = "pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0"}, + {file = "pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c"}, + {file = "pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe"}, + {file = "pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde"}, + {file = "pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21"}, + {file = "pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf"}, + {file = "pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""} + +[package.extras] +docs = ["sphinx (<7)", "sphinx_rtd_theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "smmap" +version = "5.0.2" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, +] + +[[package]] +name = "tomli" +version = "2.3.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "cd9a2057040232c91c2df92ba4cfd3f90462630695d1e914bfbfce9c1adf0187" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88bb81a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[tool.poetry] +name = "release-tool" +version = "0.1.0" +description = "A tool to manage releases using semantic versioning for release flow." +authors = ["Sequent Tech Inc "] +readme = "README.md" +packages = [{include = "release_tool", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.10" +PyGithub = "^2.1.1" +tomli = "^2.0.1" +tomli-w = "^1.0.0" +tomlkit = "^0.12.0" +click = "^8.1.7" +jinja2 = "^3.1.2" +gitpython = "^3.1.40" +pydantic = "^2.5.0" +rich = "^13.7.0" +requests = "^2.31.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +black = "^23.11.0" +isort = "^5.12.0" +mypy = "^1.7.1" + +[tool.poetry.scripts] +release-tool = "release_tool.main:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/release.py b/release.py deleted file mode 100755 index c6e2937..0000000 --- a/release.py +++ /dev/null @@ -1,1047 +0,0 @@ -#!/usr/bin/env python3 - -# This file is part of release-tool. -# Copyright (C) 2016-2021 Sequent Tech Inc - -# release-tool is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License. - -# release-tool is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. - -# You should have received a copy of the GNU Lesser General Public License -# along with release-tool. If not, see . - -import argparse -import yaml -import requests -import tempfile -from datetime import datetime -import subprocess -import os -import re -from jinja2 import Environment, FileSystemLoader, select_autoescape -from github import Github -from collections import defaultdict -from release_notes import ( - get_sem_release, - get_release_head, - get_release_notes, - create_release_notes_md, -) - -def read_text_file(file_path): - textfile = open(file_path, "r") - text = textfile.read() - textfile.close() - return text - -def write_text_file(file_path, text): - textfile = open(file_path, "w") - textfile.write(text) - textfile.close() - -def get_project_type(dir_path): - config_file = read_text_file(os.path.join(dir_path, ".git/config")) - my_match = re.search('url\s*=\s*git@(github|gitlab).com:(sequentech)/(?P.+?)(\.git)?\\n', config_file) - - try: - my_match.group('proj_name') - except: - my_match = re.search('url\s*=\s*https://(github|gitlab).com/(sequentech)/(?P.+?)(\.git)?\\n', config_file) - - return my_match.group('proj_name') - -def do_gui_common(dir_path, version): - invalid_version = re.match(r"^[a-zA-Z]+", version) is not None - - print("SequentConfig.js...") - SequentConfig = read_text_file(os.path.join(dir_path, "SequentConfig.js")) - SequentConfig = re.sub( - "var\s+SEQUENT_CONFIG_VERSION\s*=\s*'[^']+';", - "var SEQUENT_CONFIG_VERSION = '" + version + "';", - SequentConfig - ) - SequentConfig = re.sub( - "mainVersion\s*[^,]+,\n", - "mainVersion: '" + version + "',\n", - SequentConfig - ) - write_text_file(os.path.join(dir_path, "SequentConfig.js"), SequentConfig) - - print("package.json...") - if not invalid_version: - package = read_text_file(os.path.join(dir_path, "package.json")) - package = re.sub('"version"\s*:\s*"[^"]+"', '"version" : "'+ version + '"', package) - write_text_file(os.path.join(dir_path, "package.json"), package) - else: - print("leaving package.json as is because of invalid version name") - - print("Gruntfile.js...") - Gruntfile = read_text_file(os.path.join(dir_path, "Gruntfile.js")) - Gruntfile = re.sub("var\s+SEQUENT_CONFIG_VERSION\s*=\s*'[^']+';", "var SEQUENT_CONFIG_VERSION = '" + version + "';", Gruntfile) - Gruntfile = re.sub("appCommon-v[0-9a-zA-Z.\-+]+\.js", "appCommon-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("libCommon-v[0-9a-zA-Z.\-+]+\.js", "libCommon-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("libnocompat-v[0-9a-zA-Z.\-+]+\.js", "libnocompat-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("libcompat-v[0-9a-zA-Z.\-+]+\.js", "libcompat-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("SequentConfig-v[0-9a-zA-Z.\-+]+\.js", "SequentConfig-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("SequentThemes-v[0-9a-zA-Z.\-+]+\.js", "SequentThemes-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("SequentPlugins-v[0-9a-zA-Z.\-+]+\.js", "SequentPlugins-v" + version + ".js", Gruntfile) - write_text_file(os.path.join(dir_path, "Gruntfile.js"), Gruntfile) - - print("running grunt build..") - call_process("grunt build", shell=True, cwd=dir_path) - -def do_gui_other(dir_path, version): - print("index.html...") - index = read_text_file(os.path.join(dir_path, "index.html")) - index = re.sub("libnocompat-v.*\.js", "libnocompat-v" + version + ".js", index) - index = re.sub("libcompat-v.*\.js", "libcompat-v" + version + ".js", index) - index = re.sub("SequentTheme-v.*\.js", "SequentTheme-v" + version + ".js", index) - index = re.sub("appCommon-v.*\.js", "appCommon-v" + version + ".js", index) - index = re.sub("libCommon-v.*\.js", "libCommon-v" + version + ".js", index) - write_text_file(os.path.join(dir_path, "index.html"), index) - - print("SequentConfig.js...") - SequentConfig = read_text_file(os.path.join(dir_path, "SequentConfig.js")) - SequentConfig = re.sub( - "var\s+SEQUENT_CONFIG_VERSION\s*=\s*'[^']+';", - "var SEQUENT_CONFIG_VERSION = '" + version + "';", - SequentConfig - ) - SequentConfig = re.sub( - "mainVersion\s*[^,]+,\n", - "mainVersion: '" + version + "',\n", - SequentConfig - ) - write_text_file(os.path.join(dir_path, "SequentConfig.js"), SequentConfig) - - av_plugins_config_path = os.path.join(dir_path, "SequentPluginsConfig.js") - if os.path.isfile(av_plugins_config_path): - print("SequentPluginsConfig.js...") - SequentPluginsConfig = read_text_file(av_plugins_config_path) - SequentPluginsConfig = re.sub("var\s+SEQUENT_PLUGINS_CONFIG_VERSION\s*=\s*'[^']+';", "var SEQUENT_PLUGINS_CONFIG_VERSION = '" + version + "';", SequentPluginsConfig) - write_text_file(av_plugins_config_path, SequentPluginsConfig) - - print("Gruntfile.js...") - Gruntfile = read_text_file(os.path.join(dir_path, "Gruntfile.js")) - Gruntfile = re.sub("var\s+SEQUENT_CONFIG_VERSION\s*=\s*'[^']+';", "var SEQUENT_CONFIG_VERSION = '" + version + "';", Gruntfile) - Gruntfile = re.sub("appCommon-v[0-9a-zA-Z.\-+]+\.js", "appCommon-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("libCommon-v[0-9a-zA-Z.\-+]+\.js", "libCommon-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("libnocompat-v[0-9a-zA-Z.\-+]+\.min\.js", "libnocompat-v" + version + ".min.js", Gruntfile) - Gruntfile = re.sub("libcompat-v[0-9a-zA-Z.\-+]+\.min\.js", "libcompat-v" + version + ".min.js", Gruntfile) - Gruntfile = re.sub("SequentConfig-v[0-9a-zA-Z.\-+]+\.js", "SequentConfig-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("SequentThemes-v[0-9a-zA-Z.\-+]+\.js", "SequentThemes-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("SequentPlugins-v[0-9a-zA-Z.\-+]+\.js", "SequentPlugins-v" + version + ".js", Gruntfile) - Gruntfile = re.sub("app-v[0-9a-zA-Z.\-+]+\.min\.js", "app-v" + version + ".min.js", Gruntfile) - Gruntfile = re.sub("lib-v[0-9a-zA-Z.\-+]+\.min\.js", "lib-v" + version + ".min.js", Gruntfile) - write_text_file(os.path.join(dir_path, "Gruntfile.js"), Gruntfile) - - print("package.json...") - package = read_text_file(os.path.join(dir_path, "package.json")) - package = re.sub('"version"\s*:\s*"[^"]+"', '"version" : "'+ version + '"', package) - package = re.sub( - '"common-ui": "https://github.com/sequentech/common-ui\.git.*\"', - f'"common-ui": "https://github.com/sequentech/common-ui.git#{version}\"', - package - ) - write_text_file(os.path.join(dir_path, "package.json"), package) - -def do_ballot_box(dir_path, version): - print("build.sbt...") - build = read_text_file(os.path.join(dir_path, "build.sbt")) - build = re.sub('version\s*:=\s*"[^"]+"', 'version := "'+ version + '"', build) - write_text_file(os.path.join(dir_path, "build.sbt"), build) - -def do_election_verifier(dir_path, version): - print("build.sbt...") - build = read_text_file(os.path.join(dir_path, "build.sbt")) - build = re.sub('version\s*:=\s*"[^"]+"', 'version := "'+ version + '"', build) - m = re.search('scalaVersion := "(?P[0-9]+\.[0-9]+)\.[0-9]"', build) - scalaVersion = m.group('scalaVersion') - print("scalaVersion is " + scalaVersion) - write_text_file(os.path.join(dir_path, "build.sbt"), build) - - print("package.sh...") - package = read_text_file(os.path.join(dir_path, "package.sh")) - package = re.sub( - 'cp target/scala-.*/proguard/election-verifier_.*\.jar dist', - 'cp target/scala-' + scalaVersion + '/proguard/election-verifier_' + scalaVersion + '-' + version + '.jar dist', - package - ) - write_text_file(os.path.join(dir_path, "package.sh"), package) - - print('pverify.sh..') - pverify = read_text_file(os.path.join(dir_path, "pverify.sh")) - pverify = re.sub( - 'java -Djava\.security\.egd=file:/dev/\./urandom -classpath election-verifier_.*\.jar org\.sequent\.sequent\.Verifier \$1 \$2', - 'java -Djava.security.egd=file:/dev/./urandom -classpath election-verifier_' + scalaVersion + '-' + version + '.jar org.sequent.sequent.Verifier $1 $2', - pverify - ) - write_text_file(os.path.join(dir_path, "pverify.sh"), pverify) - - print('vmnc.sh..') - vmnc = read_text_file(os.path.join(dir_path, "vmnc.sh")) - vmnc = re.sub( - 'java -Djava.security\.egd=file:/dev/\./urandom -classpath \$DIR/election-verifier_.*\.jar org\.sequent\.sequent\.Vmnc "\$@"', - 'java -Djava.security.egd=file:/dev/./urandom -classpath $DIR/election-verifier_' + scalaVersion + '-' + version + '.jar org.sequent.sequent.Vmnc "$@"', - vmnc - ) - write_text_file(os.path.join(dir_path, "vmnc.sh"), vmnc) - - print('README.md..') - readme = read_text_file(os.path.join(dir_path, "README.md")) - readme = re.sub( - 'using version `[^`]+`', - 'using version `' + version + '`', - readme - ) - readme = re.sub( - 'export INTERNAL_GIT_VERSION=.*', - 'export INTERNAL_GIT_VERSION="' + version + '"', - readme - ) - write_text_file(os.path.join(dir_path, "README.md"), readme) - - print("project.spdx.yml..") - spdx = read_text_file(os.path.join(dir_path, "project.spdx.yml")) - spdx = re.sub( - "name:\s*\"election-verifier-[^\"]+\"\s*", - "name: \"election-verifier-" + version +"\"\n", - spdx - ) - spdx = re.sub( - " name:\s*\"election-verifier\"\s*\n versionInfo:\s*\"[^\"]+\"", - f" name: \"election-verifier\"\n versionInfo: \"{version}\"", - spdx, - flags=re.MULTILINE - ) - spdx = re.sub( - 'downloadLocation: "git\+https://github.com/sequentech/election-verifier\.git@.*\"', - f'downloadLocation: "git+https://github.com/sequentech/election-verifier.git@{version}\"', - spdx - ) - write_text_file(os.path.join(dir_path, "project.spdx.yml"), spdx) - - print(".github/workflows/unittests.yml...") - unittests_yml_path = os.path.join( - dir_path, ".github", "workflows", "unittests.yml" - ) - unittests_yml = read_text_file(unittests_yml_path) - unittests_yml = re.sub( - 'export INTERNAL_GIT_VERSION=.*', - f'export INTERNAL_GIT_VERSION="{version}"', - unittests_yml - ) - write_text_file(unittests_yml_path, unittests_yml) - - print("config.json in unit tests tarfdiles") - testdata_path = os.path.join(dir_path, "testdata") - tar_files = [ - filename - for filename in os.listdir(testdata_path) - if ( - os.path.isfile(os.path.join(testdata_path, filename)) and - filename.endswith(".tar") - ) - ] - # untar the tarfiles, edit them and recreate them - for tarfile_name in tar_files: - tarfile_path = os.path.join(testdata_path, tarfile_name) - with tempfile.TemporaryDirectory() as temp_dir: - call_process( - f"tar xf {tarfile_path} -C {temp_dir}", - shell=True, - cwd=dir_path - ) - config_json_path = os.path.join(temp_dir, "config.json") - config_json = read_text_file(config_json_path) - config_json = re.sub( - "{\"version\"\s*:\s*\"[^\"]+\"\s*,", - "{\"version\": \"" + version +"\",", - config_json - ) - write_text_file(config_json_path, config_json) - call_process( - f"tar cf {tarfile_path} -C {temp_dir} .", - shell=True, - cwd=dir_path - ) - -def do_frestq(dir_path, version): - invalid_version = re.match(r"^[a-zA-Z]+", version) is not None - - print("setup.py...") - if not invalid_version: - repos = read_text_file(os.path.join(dir_path, "setup.py")) - repos = re.sub("version\s*=\s*'[^']+'\s*,", "version='" + version +"',", repos) - write_text_file(os.path.join(dir_path, "setup.py"), repos) - else: - print("leaving setup.py as is because of invalid version name") - -def do_election_orchestra(dir_path, version): - print("requirements.txt...") - requirements = read_text_file(os.path.join(dir_path, "requirements.txt")) - requirements = re.sub( - 'git\+https://github.com/sequentech/frestq\.git@.*', - 'git+https://github.com/sequentech/frestq.git@'+ version, - requirements - ) - write_text_file(os.path.join(dir_path, "requirements.txt"), requirements) - - setup_py = read_text_file(os.path.join(dir_path, "setup.py")) - setup_py = re.sub( - "version\s*=\s*'[^']+'\s*,", - "version='" + version +"',", - setup_py - ) - setup_py = re.sub( - 'git\+https://github.com/sequentech/frestq\.git@[^\'"]+', - 'git+https://github.com/sequentech/frestq.git@'+ version, - setup_py - ) - write_text_file(os.path.join(dir_path, "setup.py"), setup_py) - -def do_deployment_tool(dir_path, version): - print("repos.yml...") - repos = read_text_file(os.path.join(dir_path, "repos.yml")) - repos = re.sub('version:\s*.*\n', 'version: \''+ version + '\'\n', repos) - write_text_file(os.path.join(dir_path, "repos.yml"), repos) - - print("config.yml...") - repos = read_text_file(os.path.join(dir_path, "config.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub( - '"version":\s*"[^"]+",\n', - '"version": "'+ version + '",\n', - repos - ) - repos = re.sub( - 'mainVersion:\s*\'[^\']+\'\n', - f'mainVersion: \'{version}\'\n', - repos - ) - write_text_file(os.path.join(dir_path, "config.yml"), repos) - - print("doc/devel/sequent.config.yml...") - repos = read_text_file(os.path.join(dir_path, "doc/devel/sequent.config.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "doc/devel/sequent.config.yml"), repos) - - print("doc/devel/auth1.config.yml...") - repos = read_text_file(os.path.join(dir_path, "doc/devel/auth1.config.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "doc/devel/auth1.config.yml"), repos) - - print("doc/devel/auth2.config.yml...") - repos = read_text_file(os.path.join(dir_path, "doc/devel/auth2.config.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "doc/devel/auth2.config.yml"), repos) - - print("doc/production/config.auth.yml...") - repos = read_text_file(os.path.join(dir_path, "doc/production/config.auth.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "doc/production/config.auth.yml"), repos) - - print("doc/production/config.master.yml...") - repos = read_text_file(os.path.join(dir_path, "doc/production/config.master.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "doc/production/config.master.yml"), repos) - - print("helper-tools/config_prod_env.py...") - helper_script = read_text_file(os.path.join(dir_path, "helper-tools/config_prod_env.py")) - rx = re.compile("\s*OUTPUT_PROD_VERSION\s*=\s*['|\"]?([0-9a-zA-Z.\-+]*)['|\"]?\s*\n", re.MULTILINE) - search = rx.search(helper_script) - old_version = search.group(1) - helper_script = re.sub("INPUT_PROD_VERSION\s*=\s*['|\"]?[0-9a-zA-Z.\-+]*['|\"]?\s*\n", "INPUT_PROD_VERSION=\""+ old_version + "\"\n", helper_script) - helper_script = re.sub("INPUT_PRE_VERSION\s*=\s*['|\"]?[0-9a-zA-Z.\-+]*['|\"]?\s*\n", "INPUT_PRE_VERSION=\""+ version + "\"\n", helper_script) - helper_script = re.sub("OUTPUT_PROD_VERSION\s*=\s*['|\"]?[0-9a-zA-Z.\-+]*['|\"]?\s*\n", "OUTPUT_PROD_VERSION=\""+ version + "\"\n", helper_script) - write_text_file(os.path.join(dir_path, "helper-tools/config_prod_env.py"), helper_script) - - print("sequent-ui/templates/SequentConfig.js...") - Gruntfile = read_text_file(os.path.join(dir_path, "sequent-ui/templates/SequentConfig.js")) - Gruntfile = re.sub("var\s+SEQUENT_CONFIG_VERSION\s*=\s*'[^']+';", "var SEQUENT_CONFIG_VERSION = '" + version + "';", Gruntfile) - write_text_file(os.path.join(dir_path, "sequent-ui/templates/SequentConfig.js"), Gruntfile) - -def do_tally_methods(dir_path, version): - invalid_version = re.match(r"^[a-zA-Z]+", version) is not None - - print("setup.py...") - if not invalid_version: - repos = read_text_file(os.path.join(dir_path, "setup.py")) - repos = re.sub("version\s*=\s*'[^']+'\s*,", "version='" + version +"',", repos) - write_text_file(os.path.join(dir_path, "setup.py"), repos) - else: - print("leaving setup.py as is because of invalid version name") - -def do_tally_pipes(dir_path, version): - print("setup.py...") - repos = read_text_file(os.path.join(dir_path, "setup.py")) - repos = re.sub("version\s*=\s*'[^']+'\s*,", "version='" + version +"',", repos) - repos = re.sub('git\+https://github.com/sequentech/tally-methods\.git@.*', 'git+https://github.com/sequentech/tally-methods.git@'+ version + '\'', repos) - write_text_file(os.path.join(dir_path, "setup.py"), repos) - - print("requirements.txt...") - requirements = read_text_file(os.path.join(dir_path, "requirements.txt")) - requirements = re.sub('git\+https://github.com/sequentech/tally-methods\.git@.*', 'git+https://github.com/sequentech/tally-methods.git@'+ version + "#egg=tally-methods", requirements) - write_text_file(os.path.join(dir_path, "requirements.txt"), requirements) - - print("tally_pipes/main.py...") - main_path = os.path.join(dir_path, "tally_pipes/main.py") - if os.path.isfile(main_path): - main_file = read_text_file(main_path) - main_file = re.sub("VERSION\s*=\s*\"[^\"]+\"", "VERSION = \"" + version + "\"", main_file) - write_text_file(main_path, main_file) - -def do_sequent_payment_api(dir_path, version): - print("setup.py...") - repos = read_text_file(os.path.join(dir_path, "setup.py")) - repos = re.sub("version\s*=\s*'[^']+'\s*,", "version='" + version +"',", repos) - write_text_file(os.path.join(dir_path, "setup.py"), repos) - -def do_iam(dir_path, version): - pass - -def do_misc_tools(dir_path, version): - print("setup.py...") - repos = read_text_file(os.path.join(dir_path, "setup.py")) - repos = re.sub("version\s*=\s*'[^']+'\s*,", "version='" + version +"',", repos) - write_text_file(os.path.join(dir_path, "setup.py"), repos) - -def do_mixnet(dir_path, version): - print("project.spdx.yml..") - spdx = read_text_file(os.path.join(dir_path, "project.spdx.yml")) - str_datetime = datetime.now().isoformat(timespec="seconds") - spdx = re.sub( - "created:\s*\"[^\"]+\"\s*\n", - "created: \"" + str_datetime + "Z\"\n", - spdx - ) - spdx = re.sub( - "^name:\s*\"mixnet-[^\"]+\"\s*", - "name: \"mixnet-" + version + "\"\n", - spdx - ) - spdx = re.sub( - " name:\s*\"mixnet\"\s*\n versionInfo:\s*\"[^\"]+\"", - f" name: \"mixnet\"\n versionInfo: \"{version}\"", - spdx, - flags=re.MULTILINE - ) - spdx = re.sub( - 'downloadLocation: "git\+https://github.com/sequentech/mixnet\.git@.*\"', - f'downloadLocation: "git+https://github.com/sequentech/mixnet.git@{version}\"', - spdx - ) - write_text_file(os.path.join(dir_path, "project.spdx.yml"), spdx) - -def do_ballot_verifier(dir_path, version): - print("README.md...") - print("project.spdx.yml..") - spdx = read_text_file(os.path.join(dir_path, "project.spdx.yml")) - str_datetime = datetime.now().isoformat(timespec="seconds") - spdx = re.sub( - "created:\s*\"[^\"]+\"\s*\n", - "created: \"" + str_datetime + "Z\"\n", - spdx - ) - spdx = re.sub( - "^name:\s*\"ballot-verifier-[^\"]+\"\s*", - "name: \"ballot-verifier-" + version + "\"\n", - spdx - ) - spdx = re.sub( - " name:\s*\"ballot-verifier\"\s*\n versionInfo:\s*\"[^\"]+\"", - f" name: \"ballot-verifier\"\n versionInfo: \"{version}\"", - spdx, - flags=re.MULTILINE - ) - spdx = re.sub( - 'downloadLocation: "git\+https://github.com/sequentech/ballot-verifier\.git@.*\"', - f'downloadLocation: "git+https://github.com/sequentech/ballot-verifier.git@{version}\"', - spdx - ) - write_text_file(os.path.join(dir_path, "project.spdx.yml"), spdx) - - readme = read_text_file(os.path.join(dir_path, "README.md")) - readme = re.sub( - 'https://github\.com/sequentech/ballot-verifier/releases/download/[^/]+/', - f'https://github.com/sequentech/ballot-verifier/releases/download/{version}/', - readme) - write_text_file(os.path.join(dir_path, "README.md"), readme) - -def do_documentation(dir_path, version): - print("package.json...") - package = read_text_file(os.path.join(dir_path, "package.json")) - package = re.sub('"version"\s*:\s*"[^"]+"', '"version" : "'+ version + '"', package) - write_text_file(os.path.join(dir_path, "package.json"), package) - - print("docs/general/guides/deployment/assets/config.auth.yml...") - repos = read_text_file(os.path.join(dir_path, "docs/general/guides/deployment/assets/config.auth.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub( - 'mainVersion:\s*\'[^\']+\'\n', - f'mainVersion: \'{version}\'\n', - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "docs/general/guides/deployment/assets/config.auth.yml"), repos) - - print("docs/general/guides/deployment/assets/config.master.yml...") - repos = read_text_file(os.path.join(dir_path, "docs/general/guides/deployment/assets/config.master.yml")) - repos = re.sub('version:\s*.*[^,]\n', 'version: \''+ version + '\'\n', repos) - repos = re.sub( - "tallyPipesConfig: {\n(\s*)version:\s*\'[^\']+\',?\n", - f"tallyPipesConfig: {{\n\\1version: \'{version}\',\n", - repos - ) - repos = re.sub( - 'mainVersion:\s*\'[^\']+\'\n', - f'mainVersion: \'{version}\'\n', - repos - ) - repos = re.sub('"version":\s*"[^"]+",\n', '"version": "'+ version + '",\n', repos) - write_text_file(os.path.join(dir_path, "docs/general/guides/deployment/assets/config.master.yml"), repos) - -def do_release_tool(dir_path, version): - pass - -def apply_base_branch(dir_path, base_branches): - print("applying base_branch..") - call_process(f"git stash", shell=True, cwd=dir_path) - call_process(f"git fetch origin", shell=True, cwd=dir_path) - - final_branch_name = base_branches[0] - branch_to_fork = None - for base_branch in base_branches: - ret_code = call_process( - f"git show-ref refs/heads/{base_branch}", - shell=True, - cwd=dir_path - ) - if ret_code == 0: - branch_to_fork = base_branch - break - - if branch_to_fork is None: - print(f"Error: branches {base_branches} do not exist") - exit(1) - - call_process("git clean -f -d", shell=True, cwd=dir_path) - call_process(f"git checkout {branch_to_fork}", shell=True, cwd=dir_path) - call_process(f"git reset --hard origin/{branch_to_fork}", shell=True, cwd=dir_path) - - if final_branch_name != branch_to_fork: - print(f"creating '{final_branch_name}' branch from '{branch_to_fork}' branch") - call_process(f"git checkout -b {final_branch_name}", shell=True, cwd=dir_path) - -def do_commit_push_branch(dir_path, base_branch, version): - print(f"commit and push to base branch='{base_branch}'..") - call_process(f"git add -u && git add *", shell=True, cwd=dir_path) - call_process( - f"git status && git commit -m \"Release for version {version}\"", - shell=True, - cwd=dir_path - ) - call_process( - f"git push origin {base_branch} --force", - shell=True, - cwd=dir_path - ) - -def do_create_branch(dir_path, create_branch, version): - print("creating branch..") - call_process(f"git branch -D {create_branch}", shell=True, cwd=dir_path) - call_process(f"git checkout -b {create_branch}", shell=True, cwd=dir_path) - call_process(f"git add -u && git add *", shell=True, cwd=dir_path) - call_process( - f"git status && git commit -m \"Release for version {version}\"", - shell=True, - cwd=dir_path - ) - call_process(f"git push origin {create_branch} --force", shell=True, cwd=dir_path) - -def do_create_tag(dir_path, version): - print("creating tag..") - call_process( - f"git tag {version} --force -a -m \"Release tag for version {version}\"", - shell=True, - cwd=dir_path - ) - call_process(f"git push origin {version} --force", shell=True, cwd=dir_path) - -def call_process(command, *args, **kwargs): - print(f"Executing: {command}") - return subprocess.call(command, *args, **kwargs) - -def do_create_release( - dir_path, - version, - release_draft, - release_title, - release_notes_file, - generate_release_notes, - previous_tag_name, - prerelease -): - if not generate_release_notes: - release_notes_md = "" - else: - github_token = os.getenv("GITHUB_TOKEN") - - gh = Github(github_token) - release_notes = defaultdict(list) - project_name = os.path.basename(dir_path) - repo_path = f"sequentech/{project_name}" - print(f"Generating release notes for repo {repo_path}..") - repo = gh.get_repo(repo_path) - - with open(".github/release.yml") as release_template_yaml: - config = yaml.safe_load(release_template_yaml) - - prev_major, prev_minor, prev_patch = get_sem_release(previous_tag_name) - new_major, new_minor, new_patch = get_sem_release(version) - - prev_release_head = get_release_head(prev_major, prev_minor, prev_patch) - if new_patch or prev_major == new_major: - new_release_head = get_release_head(new_major, new_minor, new_patch) - else: - new_release_head = repo.default_branch - - print(f"Previous Release Head: {prev_release_head}") - print(f"New Release Head: {new_release_head}") - if prev_major != new_major: - # if we are going to do a new major release for example - # new_release="8.0.0", we need to obtain a list of all the changes made - # in the previous major release cycle (from 7.0.0 to - # previous_release="7.4.0") and mark them as hidden. - prev_major_release_head = get_release_head(prev_major, 0, "0") - else: - prev_major_release_head = None - print(f"Previous Major Release Head: {prev_major_release_head}") - - hidden_links = [] - # if we are going to do a new major release for example - # new_release="8.0.0", we need to obtain a list of all the changes made - # in the previous major release cycle (from 7.0.0 to - # previous_release="7.4.0") and mark them as hidden. - if prev_major_release_head: - print(f"Generating release notes for hidden links:") - (_, hidden_links) = get_release_notes( - gh, repo, prev_major_release_head, prev_release_head, config, - hidden_links=[] - ) - - print(f"Generating release notes:") - (repo_notes, _) = get_release_notes( - gh, repo, prev_release_head, new_release_head, config, - hidden_links=hidden_links - ) - print(f"..generated") - for category, notes in repo_notes.items(): - release_notes[category].extend(notes) - release_notes_md = create_release_notes_md(release_notes, new_release_head) - print(f"Generated Release Notes markdown: {release_notes_md}") - generated_release_title = f"{new_release_head} release" - - with tempfile.NamedTemporaryFile() as temp_release_file: - temp_release_file.write(release_notes_md.encode('utf-8')) - temp_release_file.flush() - - print("checking if release exists to overwrite it..") - ret_code = call_process( - f"gh release view {version}", - shell=True, - cwd=dir_path - ) - if ret_code == 0: - # release exists, so remove it first - ret_code = call_process( - f"gh release delete {version}", - shell=True, - cwd=dir_path - ) - if ret_code != 0: - print("Error: couldn't remove existing release") - exit(1) - - print("creating release..") - release_file_path = ( - release_notes_file - if release_notes_file is not None - else temp_release_file.name - ) - release_notes_opt = f"--notes-file \"{release_file_path}\"\\\n" - release_title_opt = ( - f"--title \"{release_title}\"\\\n" - if release_title is not None - else generated_release_title - ) - release_draft_opt = "--draft\\\n" if release_draft else "" - prerelease_opt = "--prerelease\\\n" if prerelease else "" - - call_process(f"git fetch --tags origin", shell=True, cwd=dir_path) - release_opts_str = " ".join([ - version, - release_title_opt, - release_notes_opt, - release_draft_opt, - prerelease_opt - ]) - ret_code = call_process( - f"gh release create {release_opts_str}", - shell=True, - cwd=dir_path - ) - if ret_code != 0: - print("Error: couldn't create the release") - exit(1) - -def do_set_dependabot_branches(project_path, branches): - ''' - Configures the repository branches that will get dependabot security alerts. - ''' - # change to pristine master branch - call_process(f"git stash", shell=True, cwd=project_path) - call_process(f"git fetch origin master", shell=True, cwd=project_path) - call_process(f"git clean -f -d", shell=True, cwd=project_path) - call_process(f"git checkout master", shell=True, cwd=project_path) - call_process(f"git reset --hard origin/master", shell=True, cwd=project_path) - - # load the .github/dependabot.yml.tpl template and render it - env = Environment( - loader=FileSystemLoader(project_path), - autoescape=select_autoescape(), - lstrip_blocks=True, - trim_blocks=True - ) - template = None - try: - template = env.get_template(".github/dependabot.yml.tpl") - except: - print("Error: couldn't load .github/dependabot.yml.tpl") - exit(1) - dependabot_yml_rendered = template.render( - branches=branches - ) - - # check if we don't have to update the dependabot.yml file and finish if - # that is the case - dependabot_file_path = os.path.join(project_path, ".github", "dependabot.yml") - current_dependabot_yml = read_text_file(dependabot_file_path) - if dependabot_yml_rendered == current_dependabot_yml: - print("Nothing to do, the dependabot.yml file is already correct") - return - - # update the dependabot.yml file, commit and push to master - print("Updating .github/dependabot.yml..") - write_text_file(dependabot_file_path, dependabot_yml_rendered) - print(f"commit and push to master branch..") - - call_process(f"git add .github/dependabot.yml", shell=True, cwd=project_path) - call_process(f"git diff --cached", shell=True, cwd=project_path) - - call_process( - f"git status && git commit -m \"Updating branches in .github/dependabot.yml\"", - shell=True, - cwd=project_path - ) - call_process( - f"git push origin master --force", - shell=True, - cwd=project_path - ) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "--change-version", - action="store_true", - help="Execute the version changing scripts specific for the project" - ) - parser.add_argument( - "--version", - type=str, - help="version name", - metavar="1.3.2" - ) - parser.add_argument( - "--path", - type=str, - help="project directory path", - metavar="../voting-booth" - ) - parser.add_argument( - "--parent-path", - type=str, - help="directory parent to all the projects", - metavar="path/to/dir" - ) - parser.add_argument( - "--base-branch", - type=str, - help="use a specific base branch (if it doesn't exist, try the next one in the list) instead of the current one", - metavar="v2.x", - nargs='+' - ) - parser.add_argument( - "--create-branch", - type=str, - help="create the branch for this release", - metavar="v1.x" - ) - parser.add_argument( - "--push-current-branch", - action="store_true", - help="push and commit changes to the current branch" - ) - parser.add_argument( - "--create-tag", - action="store_true", - help="create the tag for this release" - ) - parser.add_argument( - "--create-release", - action="store_true", - help="create the github release, requires gh command" - ) - parser.add_argument( - "--release-draft", - action="store_true", - help="github draft release" - ) - parser.add_argument( - "--release-title", - type=str, - help="github release title", - metavar="\"v1.3.2 (beta 1)\"" - ) - parser.add_argument( - "--previous-tag-name", - type=str, - help="previous release tag name", - metavar="\"5.3.4\"" - ) - parser.add_argument( - "--release-notes-file", - type=str, - help="github release notes file", - metavar="path/to/notes-file" - ) - parser.add_argument( - "--generate-release-notes", - action="store_true", - help="use github automatic release generation" - ) - parser.add_argument( - "--prerelease", - action="store_true", - help="github release notes" - ) - parser.add_argument( - "--set-dependabot-branches", - metavar="BRANCH-NAME", - type=str, - nargs="+", - help="Set the dependabot alerts only for the given repository branches" - ) - parser.add_argument( - '--dry-run', - action='store_true', - help=( - 'Output the release notes but do not create any tag, release or ' - 'new branch.' - ) - ) - args = parser.parse_args() - change_version = args.change_version - version = args.version - base_branch = args.base_branch - create_branch = args.create_branch - push_current_branch = args.push_current_branch - create_tag = args.create_tag - create_release = args.create_release - release_draft = args.release_draft - release_title = args.release_title - prerelease = args.prerelease - generate_release_notes = args.generate_release_notes - previous_tag_name = args.previous_tag_name - set_dependabot_branches = args.set_dependabot_branches - - path = args.path - parent_path = args.parent_path - if path is not None: - if not os.path.isdir(path): - raise Exception(path + ": path does not exist or is not a directory") - path = os.path.realpath(path) - parent_path = os.path.dirname(path) - elif parent_path is not None: - if not os.path.isdir(parent_path): - raise Exception(parent_path + ": path does not exist or is not a directory") - parent_path = os.path.realpath(parent_path) - - release_notes_file = args.release_notes_file - if release_notes_file is not None: - if not os.path.isfile(release_notes_file): - raise Exception(release_notes_file + ": path does not exist or is not a file") - release_notes_file = os.path.realpath(release_notes_file) - - print(f"""Options: - - change-version: {change_version} - - version: {version} - - path: {path} - - parent_path: {parent_path} - - base_branch: {base_branch} - - create_branch: {create_branch} - - push_current_branch: {push_current_branch} - - create_tag: {create_tag} - - create_release: {create_release} - - release_draft: {release_draft} - - release_title: {release_title} - - release_notes_file: {release_notes_file} - - generate_release_notes: {generate_release_notes} - - previous_tag_name: {previous_tag_name} - - prerelease: {prerelease} - - set_dependabot_branches: {set_dependabot_branches} - """) - - if path is not None: - projects = [ get_project_type(path) ] - else: - projects = [ - "common-ui", - "admin-console", - "election-portal", - "voting-booth", - "election-verifier", - "ballot_box", - "deployment-tool", - "tally-pipes", - "tally-methods", - "frestq", - "election-orchestra", - "iam", - "misc-tools", - "mixnet", - "documentation", - "release-tool" - ] - - for project_type in projects: - if path is not None: - project_path = path - else: - project_path = os.path.join(parent_path, project_type) - - print("project: " + project_type) - - if base_branch is not None: - apply_base_branch(project_path, base_branch) - - if change_version: - if 'common-ui' == project_type: - do_gui_common(project_path, version) - elif 'admin-console' == project_type: - do_gui_other(project_path, version) - elif 'election-portal' == project_type: - do_gui_other(project_path, version) - elif 'voting-booth' == project_type: - do_gui_other(project_path, version) - elif 'election-orchestra' == project_type: - do_election_orchestra(project_path, version) - elif 'election-verifier' == project_type: - do_election_verifier(project_path, version) - elif 'ballot_box' == project_type: - do_ballot_box(project_path, version) - elif 'deployment-tool' == project_type: - do_deployment_tool(project_path, version) - elif 'tally-pipes' == project_type: - do_tally_pipes(project_path, version) - elif 'tally-methods' == project_type: - do_tally_methods(project_path, version) - elif 'frestq' == project_type: - do_frestq(project_path, version) - elif 'iam' == project_type: - do_iam(project_path, version) - elif 'misc-tools' == project_type: - do_misc_tools(project_path, version) - elif 'mixnet' == project_type: - do_mixnet(project_path, version) - elif 'documentation' == project_type: - do_documentation(project_path, version) - elif 'ballot-verifier' == project_type: - do_ballot_verifier(project_path, version) - elif 'release-tool' == project_type: - do_release_tool(project_path, version) - - if create_branch is not None: - do_create_branch(project_path, create_branch, version) - elif push_current_branch: - do_commit_push_branch(project_path, base_branch[0], version) - if create_tag: - do_create_tag(project_path, version) - if create_release: - do_create_release( - project_path, - version, - release_draft, - release_title, - release_notes_file, - generate_release_notes, - previous_tag_name, - prerelease - ) - if set_dependabot_branches is not None: - do_set_dependabot_branches( - project_path=project_path, - branches=set_dependabot_branches - ) - - print("done") - -if __name__ == "__main__": - main() diff --git a/release_notes.py b/release_notes.py deleted file mode 100644 index 55e0fb1..0000000 --- a/release_notes.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -import os -import re -import yaml -import argparse -from datetime import datetime -from github import Github - -# Text to detect in PR descriptions which is followed by the link to the parent -# issue -PARENT_ISSUE_TEXT = 'Parent issue: ' - -# If a commit is older than this, we will ignore it in the release notes because -# we were not following the same github procedures at that time -CUTOFF_DATE = 'Sun, 01 Jan 2023 00:00:00 GMT' - -def get_label_category(labels, categories): - """ - Get the category that matches the given labels. - - Args: - labels (list): A list of labels from a pull request. - categories (list): A list of categories from the configuration. - - Returns: - dict: The matching category, or None if no category matches. - """ - for category in categories: - for label in labels: - if label.name in category["labels"] or '*' in category["labels"]: - return category - return None - -def get_commit_pull(commit): - """ - Get the pull request (PR) object associated with a commit. - - This function uses the PyGithub library to find the PR associated with a given commit. - It first checks if the commit message's first line contains one or more PR numbers (e.g. "#33"). - If so, it chooses the largest PR number and returns the corresponding PR object. - Otherwise, it returns the PR object with the lowest PR number from commit.get_pulls(). - - :param commit: A PyGithub Commit object representing a commit. - :return: A PyGithub PullRequest object corresponding to the associated PR, or None if no PR is found. - """ - # Extract PR numbers from the commit message's first line - first_line = commit.commit.message.split("\n")[0] - pr_numbers = [int(x[1:]) for x in re.findall(r"#\d+", first_line)] - - if pr_numbers: - # Find the PR with the largest number mentioned in the commit message - max_pr_number = max(pr_numbers) - associated_pr = None - - # Iterate over the available pull requests and find the one with the max_pr_number - for pr in commit.get_pulls(): - if pr.number == max_pr_number: - associated_pr = pr - break - - return associated_pr - else: - # If no PR numbers are mentioned in the commit message, find the PR with the lowest number - min_pr = None - - for pr in commit.get_pulls(): - if min_pr is None or pr.number < min_pr.number: - min_pr = pr - - return min_pr - -def get_github_issue_from_link(link_text, github): - """ - Retrieve the Github issue object from the provided link. - - Args: - link_text (str): The link to the Github issue, in the format 'https://github.com/owner/repo/issues/123'. - github (Github): A PyGithub object that represents a connection to a Github account. - - Returns: - object: A PyGithub object that represents the retrieved Github issue. - """ - # Extracting the owner, repository name, and issue number from the link - if '#' in link_text and not link_text.startswith('https://'): - owner, repo_issue_number = link_text.split("/") - repo, issue_number = repo_issue_number.split("#") - else: - owner, repo, _, issue_number = link_text.split("/")[-4:] - - # Getting the repository object - repo = github.get_repo(f"{owner}/{repo}") - - # Getting the issue object - issue = repo.get_issue(int(issue_number)) - - return issue - -def get_release_notes( - github, - repo, - previous_release_head, - new_release_head, - config, - hidden_links=[], - args=type('', (), {'silent': False})() - ): - """ - Retrieve release notes from a GitHub repository based on the given configuration. - - :param github: A PyGithub object that represents a connection to a Github account. - :param repo: A Repository object representing the GitHub repository. - :param previous_release_head: str, the previous release's head commit. - :param new_release_head: str, the new release's head commit. - :param config: dict, the configuration for generating release notes. - :return: tuple with ( - dict: the release notes categorized by their labels, - list: list of links to the PRs included - ) - """ - compare_branches = repo.compare(previous_release_head, new_release_head) - - release_notes = {} - parent_issues = [] - links = [] - cutoff_date = datetime.strptime( - CUTOFF_DATE, - '%a, %d %b %Y %H:%M:%S %Z' - ) - - for commit in compare_branches.commits: - pr = get_commit_pull(commit) - if pr == None: - verbose_print(args, f"[has no PR associated] ignoring commit:\n\tcommit={commit}\n") - continue - - title = pr.title.strip() - - if any(label.name in config["changelog"]["exclude"]["labels"] for label in pr.labels): - verbose_print(args, f"[has excluded label] ignoring PR:\n\tcommit={commit}\n\tpr.title={title}\n\tpr.url={pr.html_url}\n") - continue - - category = get_label_category(pr.labels, config["changelog"]["categories"]) - if category is None: - verbose_print(args, f"[has no category] ignoring PR:\n\tcommit={commit}\n\tpr.title={title}\n\tpr.url={pr.html_url}\n") - continue - - if pr.closed_at is None: - verbose_print(args, f"[PR not closed] ignoring PR:\n\tcommit={commit}\n\tpr.title={title}\n\tpr.url={pr.html_url}\n\n") - continue - - if pr.closed_at < cutoff_date: - verbose_print(args, f"[before cut-off date] ignoring PR:\n\tcommit={commit}\n\tpr.title={title}\n\tpr.url={pr.html_url}\n") - continue - - parent_issue = None - if isinstance(pr.body, str): - for line in pr.body.split("\n"): - if line.startswith(PARENT_ISSUE_TEXT): - parent_issue = line[len(PARENT_ISSUE_TEXT):] - break - - if parent_issue: - if parent_issue in parent_issues: - continue - else: - parent_issues.append(parent_issue) - link = parent_issue - issue = get_github_issue_from_link(link, github) - title = issue.title.strip() - else: - link = pr.html_url - - if link in links or link in hidden_links: - continue - else: - links.append(link) - - if category['title'] not in release_notes: - release_notes[category['title']] = [] - - development = f"* {title} by @{pr.user.login} in {link}" - release_notes[category['title']].append(development) - - release_notes_yaml = yaml.dump(release_notes, default_flow_style=False) - verbose_print(args, f"release notes:\n{release_notes_yaml}") - return (release_notes, links) - -def create_release_notes_md(release_notes, new_release): - """ - Convert the generated release notes into Markdown format. - - Args: - release_notes (dict): The release notes, organized by category. - new_release (str): The new release version (e.g. "1.1.0"). - - Returns: - str: The release notes in Markdown format. - """ - md = f"\n\n" - md += "## What's Changed\n" - for category, notes in release_notes.items(): - if notes: - md += f"### {category}\n" - md += "\n".join(notes) + "\n" - return md - -def parse_arguments(): - """ - Parse command-line arguments. - - Returns: - argparse.Namespace: An object containing parsed arguments. - """ - parser = argparse.ArgumentParser( - description='Generate release notes and create a new release.' - ) - parser.add_argument( - 'repo_path', - help='Github Repository path, i.e. `sequentech/ballot-box`' - ) - parser.add_argument( - 'previous_release', - help='Previous release version in format `.`, i.e. `7.2`' - ) - parser.add_argument( - 'new_release', - help=( - 'New release version in format `.`, i.e. `7.2` ' - 'or full semver release if it already exists i.e. `7.3.0`' - ) - ) - parser.add_argument( - '--dry-run', - action='store_true', - help=( - 'Output the release notes but do not create any tag, release or ' - 'new branch.' - ) - ) - parser.add_argument( - '--silent', - action='store_true', - help='Disables verbose output' - ) - parser.add_argument( - '--draft', - action='store_true', - help='Mark the new release be as draft' - ) - parser.add_argument( - '--prerelease', - action='store_true', - help='Mark the new release be as a prerelease' - ) - return parser.parse_args() - -def create_new_branch(repo, new_branch): - """ - Create a new branch for the release. - - Args: - repo (github.Repository.Repository): The repository object. - prev_major (int): The previous major version. - new_major (int): The new major version. - new_minor (int): The new minor version. - """ - default_branch = repo.default_branch - repo.create_git_ref(f"refs/heads/{new_branch}", repo.get_branch(default_branch).commit.sha) - -def get_sem_release(release_string): - """ - Returns the major, minor, and patch numbers of a software release in - Semantic Versioning format given as a string. - - Args: - release_string (str): A string representing the software release in - Semantic Versioning format. Example: "2.4.1". - - Returns: - A tuple of three integers representing the major, minor, and patch - numbers of the release respectively. If the release_string does not - contain a patch number, the third element of the tuple will be None. - - Example: - >>> get_sem_release_numbers("2.4.1") - (2, 4, 1) - - >>> get_sem_release_numbers("1.10") - (1, 10, None) - """ - release_string_list = release_string.split(".") - major = int(release_string_list[0]) - minor = int(release_string_list[1]) - patch = release_string_list[2] if len(release_string_list) >= 3 else None - return (major, minor, patch) - -def get_release_head(major, minor, patch): - """ - Returns a formatted version string based on the given major, minor, and patch version numbers. - - Args: - major (int): The major version number. - minor (int): The minor version number. - patch (int or None): The patch version number, or None if there is no patch. - - Returns: - str: A formatted version string with the structure "major.minor.patch" or "major.minor.x" if patch is None. - """ - if not patch: - return f"{major}.{minor}.x" - else: - return f"{major}.{minor}.{patch}" - -def verbose_print(args, message): - if not args.silent: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"[{timestamp}] {message}") - -def main(): - args = parse_arguments() - - repo_path = args.repo_path - previous_release = args.previous_release - new_release = args.new_release - dry_run = args.dry_run - github_token = os.getenv("GITHUB_TOKEN") - - gh = Github(github_token) - repo = gh.get_repo(repo_path) - - with open(".github/release.yml") as f: - config = yaml.safe_load(f) - - prev_major, prev_minor, prev_patch = get_sem_release(previous_release) - new_major, new_minor, new_patch = get_sem_release(new_release) - - prev_release_head = get_release_head(prev_major, prev_minor, prev_patch) - if new_patch or prev_major == new_major: - new_release_head = get_release_head(new_major, new_minor, new_patch) - else: - new_release_head = repo.default_branch - - verbose_print(args, f"Input Parameters: {args}") - verbose_print(args, f"Previous Release Head: {prev_release_head}") - verbose_print(args, f"New Release Head: {new_release_head}") - - (release_notes, _) = get_release_notes( - gh, repo, prev_release_head, new_release_head, config, args - ) - - if not new_patch: - latest_release = repo.get_releases()[0] - latest_tag = latest_release.tag_name - major, minor, new_patch = map(int, latest_tag.split(".")) - if new_major == major and new_minor == minor: - new_patch += 1 - else: - new_patch = 0 - - new_tag = f"{new_major}.{new_minor}.{new_patch}" - new_title = f"{new_tag} release" - verbose_print(args, f"New Release Tag: {new_tag}") - verbose_print(args, f"New Release Title: {new_title}") - - release_notes_md = create_release_notes_md(release_notes, new_tag) - - verbose_print(args, f"Generated Release Notes: {release_notes_md}") - - if not dry_run: - if prev_major < new_major: - verbose_print(args, "Creating new branch") - create_new_branch(repo, new_release_head) - verbose_print(args, "Creating new release") - repo.create_git_tag_and_release( - tag=new_tag, - tag_message=new_title, - type='commit', - object=repo.get_branch(new_release_head).commit.sha, - release_name=new_title, - release_message=release_notes_md, - prerelease=args.prerelease, - draft=args.draft - ) - verbose_print(args, f"Executed Actions: Branch created and new release created") - else: - verbose_print(args, "Dry Run: No actions executed") - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2a0d015..0000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -aiohttp==3.9.4 -aiosignal==1.3.1 -async-timeout==4.0.2 -attrs==22.2.0 -certifi==2023.7.22 -cffi==1.15.1 -charset-normalizer==2.0.7 -cryptography==42.0.4 -Deprecated==1.2.13 -frozenlist==1.3.3 -idna==3.7 -Jinja2==3.1.3 -MarkupSafe==2.1.1 -multidict==6.0.4 -pycparser==2.21 -PyGithub==1.58.1 -PyJWT==2.6.0 -PyNaCl==1.5.0 -PyYAML==6.0 -requests==2.31.0 -typing_extensions==4.5.0 -urllib3==1.26.18 -wrapt==1.15.0 -yarl==1.8.2 \ No newline at end of file diff --git a/src/release_tool/__init__.py b/src/release_tool/__init__.py new file mode 100644 index 0000000..83276f7 --- /dev/null +++ b/src/release_tool/__init__.py @@ -0,0 +1,4 @@ +""" +Release Tool package. +""" +__version__ = "0.1.0" diff --git a/src/release_tool/commands/__init__.py b/src/release_tool/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/release_tool/commands/generate.py b/src/release_tool/commands/generate.py new file mode 100644 index 0000000..ec6b948 --- /dev/null +++ b/src/release_tool/commands/generate.py @@ -0,0 +1,1056 @@ +import sys +import click +from typing import Optional, List, Set +from collections import defaultdict +from rich.console import Console + +from ..config import Config, PolicyAction +from ..db import Database +from ..github_utils import GitHubClient +from ..git_ops import GitOperations, get_release_commit_range, determine_release_branch_strategy, find_comparison_version +from ..models import SemanticVersion +from ..template_utils import render_template, TemplateError +from ..policies import ( + TicketExtractor, + CommitConsolidator, + ReleaseNoteGenerator, + VersionGapChecker, + PartialTicketMatch, + PartialTicketReason +) + +console = Console() + + +def _get_issues_repo(config: Config) -> str: + """ + Get the issues repository from config. + + Returns the first ticket_repos entry if available, otherwise falls back to code_repo. + """ + if config.repository.ticket_repos and len(config.repository.ticket_repos) > 0: + return config.repository.ticket_repos[0] + return config.repository.code_repo + + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.argument('version', required=False) +@click.option('--from-version', help='Compare from this version (auto-detected if not specified)') +@click.option('--repo-path', type=click.Path(exists=True), help='Path to local git repository (defaults to synced repo)') +@click.option('--output', '-o', type=click.Path(), help='Output file for release notes') +@click.option('--dry-run', is_flag=True, help='Show what would be generated without creating files') +@click.option('--new', type=click.Choice(['major', 'minor', 'patch', 'rc'], case_sensitive=False), help='Auto-bump version') +@click.option('--detect-mode', type=click.Choice(['all', 'published'], case_sensitive=False), default='published', help='Detection mode for existing releases (default: published)') +@click.option('--format', type=click.Choice(['markdown', 'json'], case_sensitive=False), default='markdown', help='Output format (default: markdown)') +@click.option('--debug', is_flag=True, help='Show detailed pattern matching debug output') +@click.pass_context +def generate(ctx, version: Optional[str], from_version: Optional[str], repo_path: Optional[str], + output: Optional[str], dry_run: bool, new: Optional[str], detect_mode: str, + format: str, debug: bool): + """ + Generate release notes for a version. + + Analyzes commits between versions, consolidates by ticket, and generates + formatted release notes. + + VERSION can be specified explicitly (e.g., "9.1.0") or auto-calculated using + --new option. Partial versions are supported (e.g., "9.2" + --new patch creates 9.2.1). + + Examples: + + release-tool generate 9.1.0 + + release-tool generate --new minor + + release-tool generate --new rc + + release-tool generate 9.1.0 --dry-run + + release-tool generate --new patch --repo-path /custom/path + + release-tool generate 9.2 --new patch + """ + if not version and not new: + console.print("[red]Error: VERSION argument or --new option is required[/red]") + return + + # Check if version is provided WITH a bump flag (partial version support) + if version and new: + # Parse as partial version to use as base + try: + base_version = SemanticVersion.parse(version, allow_partial=True) + console.print(f"[blue]Using base version: {base_version.to_string()}[/blue]") + + # For patch bumps, check if the base version exists first + # If it doesn't exist, use the base version instead of bumping + if new == 'patch' and base_version.patch == 0: + # Need to check if base version exists in Git + # Load config early to access repo path + cfg = ctx.obj['config'] + try: + git_ops_temp = GitOperations(cfg.get_code_repo_path()) + existing_versions = git_ops_temp.get_version_tags() + base_exists = any( + v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.prerelease == base_version.prerelease + for v in existing_versions + ) + + if not base_exists: + # Base version doesn't exist, use it instead of bumping + target_version = base_version + console.print(f"[blue]Base version {base_version.to_string()} does not exist → Creating {target_version.to_string()}[/blue]") + else: + # Base version exists, bump to next patch + target_version = base_version.bump_patch() + console.print(f"[blue]Base version {base_version.to_string()} exists → Bumping to {target_version.to_string()}[/blue]") + except Exception as e: + # If we can't check, default to bumping + console.print(f"[yellow]Warning: Could not check existing versions ({e}), bumping patch[/yellow]") + target_version = base_version.bump_patch() + console.print(f"[blue]Bumping patch version → {target_version.to_string()}[/blue]") + # Apply the bump for other cases + elif new == 'major': + target_version = base_version.bump_major() + console.print(f"[blue]Bumping major version → {target_version.to_string()}[/blue]") + elif new == 'minor': + target_version = base_version.bump_minor() + console.print(f"[blue]Bumping minor version → {target_version.to_string()}[/blue]") + elif new == 'patch': + # Patch != 0, just bump it + target_version = base_version.bump_patch() + console.print(f"[blue]Bumping patch version → {target_version.to_string()}[/blue]") + elif new == 'rc': + # Check if a final release exists for this base version + # If yes, bump patch first, then create RC + # Strategy: Check database FIRST, then optionally check git as fallback + import re + + cfg = ctx.obj['config'] + final_exists = False + rc_number = 0 + matching_rcs = [] + checked_db = False + checked_git = False + + # Step 1: Check database for existing versions (primary source of truth) + try: + db = Database(cfg.database.path) + db.connect() + + repo_name = cfg.repository.code_repo + repo = db.get_repository(repo_name) + + if repo: + # Check for final release of this base version + final_version_str = f"{base_version.major}.{base_version.minor}.{base_version.patch}" + all_releases = db.get_all_releases( + repo_id=repo.id, + version_prefix=final_version_str + ) + + for release in all_releases: + # Filter by detect_mode + if detect_mode == 'published' and release.is_draft: + continue + + try: + v = SemanticVersion.parse(release.version) + # Check for exact final version match + if (v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.is_final()): + final_exists = True + # Also collect RCs while we're here + if (v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.prerelease and v.prerelease.startswith('rc.')): + matching_rcs.append(v) + except ValueError: + continue + + checked_db = True + + db.close() + except Exception as e: + console.print(f"[yellow]Warning: Could not check database for existing versions: {e}[/yellow]") + + # Step 2: Optionally check git tags (only if repo exists locally) + # Only check git if detect_mode is 'all' or if we failed to check DB + # If detect_mode is 'published', we should rely on DB because git tags don't have draft status + if (detect_mode == 'all' or not checked_db): + try: + repo_path = cfg.get_code_repo_path() + from pathlib import Path + if Path(repo_path).exists(): + git_ops_temp = GitOperations(repo_path) + git_versions = git_ops_temp.get_version_tags() + + for v in git_versions: + # Check for final version + if (v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.is_final()): + final_exists = True + # Check for RCs + if (v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.prerelease and v.prerelease.startswith('rc.')): + if v not in matching_rcs: + matching_rcs.append(v) + + checked_git = True + except Exception as e: + # Git check is optional - not a critical failure + pass + + # Step 3: Determine target version based on what we found + if final_exists: + # Final release exists - bump patch and create rc.0 + source = "database" if checked_db else "git" if checked_git else "unknown" + console.print(f"[blue]Final release {base_version.to_string()} exists ({source}) → Bumping to next patch[/blue]") + base_version = base_version.bump_patch() + # Reset matching_rcs since we're now working with a new base version + matching_rcs = [] + target_version = base_version.bump_rc(0) + console.print(f"[blue]Creating RC version → {target_version.to_string()}[/blue]") + else: + # No final release - find existing RCs and auto-increment + if matching_rcs: + # Extract RC numbers and find the highest + rc_numbers = [] + for v in matching_rcs: + match = re.match(r'rc\.(\d+)', v.prerelease) + if match: + rc_numbers.append(int(match.group(1))) + + if rc_numbers: + rc_number = max(rc_numbers) + 1 + source = "database" if checked_db else "git" if checked_git else "unknown" + console.print(f"[blue]Found existing RCs in {source}, incrementing to rc.{rc_number}[/blue]") + elif not checked_db and not checked_git: + console.print(f"[yellow]Warning: Could not check existing versions, creating rc.0[/yellow]") + + target_version = base_version.bump_rc(rc_number) + console.print(f"[blue]Creating RC version → {target_version.to_string()}[/blue]") + + version = target_version.to_string() + # Skip the auto-calculation below + new = None + except ValueError as e: + console.print(f"[red]Error parsing version: {e}[/red]") + return + + config: Config = ctx.obj['config'] + + # Determine repo path (use synced repo as default) + if not repo_path: + repo_path = config.get_code_repo_path() + console.print(f"[blue]Using synced repository: {repo_path}[/blue]") + + # Verify repo path exists + from pathlib import Path + if not Path(repo_path).exists(): + console.print(f"[red]Error: Repository path does not exist: {repo_path}[/red]") + if not config.sync.code_repo_path: + console.print("[yellow]Tip: Run 'release-tool sync' first to clone the repository[/yellow]") + return + + try: + # Initialize components + db = Database(config.database.path) + db.connect() + + try: + # Get repository + repo_name = config.repository.code_repo + repo = db.get_repository(repo_name) + if not repo: + console.print(f"[yellow]Repository {repo_name} not found in database. Running sync...[/yellow]") + github_client = GitHubClient(config) + repo = github_client.get_repository_info(repo_name) + repo.id = db.upsert_repository(repo) + repo_id = repo.id + + # Initialize Git operations + git_ops = GitOperations(repo_path) + + # Auto-calculate version if using bump options + if new: + # Get all version tags from Git + all_tags = git_ops.get_version_tags() + all_tags.sort(reverse=True) + + latest_tag = None + for tag in all_tags: + # Check if this tag is a draft in DB if detect_mode is published + if detect_mode == 'published': + release = db.get_release(repo_id, tag.to_string()) + if release and release.is_draft: + continue + + # For patch bumps, we only want final versions + if new == 'patch' and not tag.is_final(): + continue + + latest_tag = tag + break + + if not latest_tag: + console.print("[red]Error: No suitable tags found in repository. Cannot auto-bump version.[/red]") + console.print("[yellow]Tip: Specify version explicitly or create an initial tag[/yellow]") + return + + base_version = latest_tag + console.print(f"[blue]Latest version: {base_version.to_string()}[/blue]") + + if new == 'major': + target_version = base_version.bump_major() + console.print(f"[blue]Bumping major version → {target_version.to_string()}[/blue]") + elif new == 'minor': + target_version = base_version.bump_minor() + console.print(f"[blue]Bumping minor version → {target_version.to_string()}[/blue]") + elif new == 'patch': + target_version = base_version.bump_patch() + console.print(f"[blue]Bumping patch version → {target_version.to_string()}[/blue]") + elif new == 'rc': + # Check if base_version is final - if so, bump patch first + import re + + if base_version.is_final(): + # Base version is final - bump patch and create rc.0 + console.print(f"[blue]Latest version {base_version.to_string()} is final → Bumping to next patch[/blue]") + base_version = base_version.bump_patch() + target_version = base_version.bump_rc(0) + console.print(f"[blue]Creating RC version → {target_version.to_string()}[/blue]") + else: + # Base version is not final - find the next RC number for this version + rc_number = 0 + + # Check existing versions for RCs of the same base version + matching_rcs = [] + for v in all_tags: + if (v.major == base_version.major and + v.minor == base_version.minor and + v.patch == base_version.patch and + v.prerelease and v.prerelease.startswith('rc.')): + + if detect_mode == 'published': + release = db.get_release(repo_id, v.to_string()) + if release and release.is_draft: + continue + matching_rcs.append(v) + + if matching_rcs: + # Extract RC numbers and find the highest + rc_numbers = [] + for v in matching_rcs: + match = re.match(r'rc\.(\d+)', v.prerelease) + if match: + rc_numbers.append(int(match.group(1))) + + if rc_numbers: + rc_number = max(rc_numbers) + 1 + + target_version = base_version.bump_rc(rc_number) + console.print(f"[blue]Creating RC version → {target_version.to_string()}[/blue]") + + version = target_version.to_string() + else: + # Parse explicitly provided version + target_version = SemanticVersion.parse(version) + + if dry_run: + console.print(f"[yellow]DRY RUN: Generating release notes for version {version}[/yellow]") + else: + console.print(f"[blue]Generating release notes for version {version}[/blue]") + + # Determine release branch strategy + available_versions = git_ops.get_version_tags() + release_branch, source_branch, should_create_branch = determine_release_branch_strategy( + target_version, + git_ops, + available_versions, + branch_template=config.branch_policy.release_branch_template, + default_branch=config.branch_policy.default_branch, + branch_from_previous=config.branch_policy.branch_from_previous_release + ) + + # Display branch information + console.print(f"[blue]Release branch: {release_branch}[/blue]") + if should_create_branch: + console.print(f"[yellow]→ Branch does not exist, will create from: {source_branch}[/yellow]") + else: + console.print(f"[blue]→ Using existing branch (source: {source_branch})[/blue]") + + # Create branch if needed (unless dry-run) + if should_create_branch and config.branch_policy.create_branches: + if dry_run: + console.print(f"[yellow]DRY RUN: Would create branch '{release_branch}' from '{source_branch}'[/yellow]") + else: + try: + # Ensure source branch exists locally + current_branch = git_ops.get_current_branch() + + # Create the new release branch + git_ops.create_branch(release_branch, source_branch) + console.print(f"[green]✓ Created branch '{release_branch}' from '{source_branch}'[/green]") + + # Optionally checkout the new branch + # git_ops.checkout_branch(release_branch) + # console.print(f"[green]✓ Checked out branch '{release_branch}'[/green]") + except ValueError as e: + console.print(f"[yellow]Warning: {e}[/yellow]") + except Exception as e: + console.print(f"[red]Error creating branch: {e}[/red]") + elif should_create_branch: + console.print(f"[yellow]→ Branch creation disabled in config[/yellow]") + + # Determine comparison version and get commits + from_ver = SemanticVersion.parse(from_version) if from_version else None + + if not from_ver: + # Calculate from_ver respecting detect_mode + available_versions = git_ops.get_version_tags() + + if detect_mode == 'published': + # Filter out drafts + filtered_versions = [] + for v in available_versions: + release = db.get_release(repo_id, v.to_string()) + if release and release.is_draft: + continue + filtered_versions.append(v) + available_versions = filtered_versions + elif detect_mode == 'all': + # Add releases from DB that might be missing from local tags + # This ensures we detect previous RCs even if tags aren't fetched yet + try: + db_releases = db.get_all_releases(repo_id) + for release in db_releases: + try: + v = SemanticVersion.parse(release.version) + if v not in available_versions: + available_versions.append(v) + except ValueError: + continue + available_versions.sort() + except Exception as e: + console.print(f"[yellow]Warning: Could not fetch releases from DB: {e}[/yellow]") + + from_ver = find_comparison_version(target_version, available_versions) + + # Determine head_ref for commit range + # If we are creating a new branch, the head is the source branch + # If we are using an existing branch, the head is that branch + if should_create_branch: + head_ref = source_branch + else: + head_ref = release_branch + + # If the branch exists remotely but not locally, we might need to prefix with origin/ + # However, git_ops usually handles local branches. + # If we are in dry-run and the branch exists only on remote, we should use origin/branch + if not should_create_branch and not git_ops.branch_exists(head_ref) and git_ops.branch_exists(head_ref, remote=True): + head_ref = f"origin/{head_ref}" + + comparison_version, commits = get_release_commit_range( + git_ops, + target_version, + from_ver, + head_ref=head_ref + ) + + if comparison_version: + console.print(f"[blue]Comparing {comparison_version.to_string()} → {version}[/blue]") + + # Check for version gaps + gap_checker = VersionGapChecker(config) + gap_checker.check_gap(comparison_version.to_string(), version) + else: + console.print(f"[blue]Generating notes for all commits up to {version}[/blue]") + + console.print(f"[blue]Found {len(commits)} commits[/blue]") + + # Convert git commits to our models and store them + commit_models = [] + for git_commit in commits: + commit_model = git_ops.commit_to_model(git_commit, repo_id) + db.upsert_commit(commit_model) + commit_models.append(commit_model) + + # Build PR map + pr_map = {} + for commit in commit_models: + if commit.pr_number: + pr = db.get_pull_request(repo_id, commit.pr_number) + if pr: + pr_map[commit.pr_number] = pr + + # Extract tickets and consolidate + extractor = TicketExtractor(config, debug=debug) + consolidator = CommitConsolidator(config, extractor, debug=debug) + consolidated_changes = consolidator.consolidate(commit_models, pr_map) + + console.print(f"[blue]Consolidated into {len(consolidated_changes)} changes[/blue]") + + # Handle missing tickets + consolidator.handle_missing_tickets(consolidated_changes) + + # Load ticket information from database (offline) with partial detection + # Tickets must be synced first using: release-tool sync + partial_matches: List[PartialTicketMatch] = [] + resolved_ticket_keys: Set[str] = set() # Track successfully resolved tickets + + # Get expected ticket repository IDs + expected_repos = config.get_ticket_repos() + expected_repo_ids = [] + for ticket_repo_name in expected_repos: + repo = db.get_repository(ticket_repo_name) + if repo: + expected_repo_ids.append(repo.id) + + for change in consolidated_changes: + if change.ticket_key: + # Query ticket from database across all repos + ticket = db.get_ticket_by_key(change.ticket_key) + + if not ticket: + # NOT FOUND - create partial match + extraction_source = _get_extraction_source(change) + partial = PartialTicketMatch( + ticket_key=change.ticket_key, + extracted_from=extraction_source, + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF, + PartialTicketReason.TYPO, + PartialTicketReason.SYNC_NOT_RUN + } + ) + partial_matches.append(partial) + + if debug: + console.print(f"\n[yellow]⚠️ Ticket {change.ticket_key} not found in DB[/yellow]") + + elif ticket.repo_id not in expected_repo_ids: + # DIFFERENT REPO - create partial match + found_repo = db.get_repository_by_id(ticket.repo_id) + extraction_source = _get_extraction_source(change) + partial = PartialTicketMatch( + ticket_key=change.ticket_key, + extracted_from=extraction_source, + match_type="different_repo", + found_in_repo=found_repo.full_name if found_repo else "unknown", + ticket_url=ticket.url, + potential_reasons={ + PartialTicketReason.REPO_CONFIG_MISMATCH, + PartialTicketReason.WRONG_TICKET_REPOS + } + ) + partial_matches.append(partial) + + if debug: + console.print(f"\n[yellow]⚠️ Ticket {change.ticket_key} in different repo: {found_repo.full_name if found_repo else 'unknown'}[/yellow]") + + else: + # Found in correct repo - mark as resolved + resolved_ticket_keys.add(change.ticket_key) + if debug: + console.print(f"\n[dim]📋 Found ticket in DB: #{ticket.number} - {ticket.title}[/dim]") + + change.ticket = ticket + + # Apply partial ticket policy (with resolved/unresolved tracking) + _handle_partial_tickets(partial_matches, resolved_ticket_keys, config, debug) + + # Check for inter-release duplicate tickets + consolidated_changes = _check_inter_release_duplicates( + consolidated_changes, + target_version, + db, + repo_id, + config, + debug + ) + + # Generate release notes + note_generator = ReleaseNoteGenerator(config) + release_notes = [] + for change in consolidated_changes: + note = note_generator.create_release_note(change, change.ticket) + release_notes.append(note) + + # Group and format + grouped_notes = note_generator.group_by_category(release_notes) + + # Format output based on format option + if format == 'json': + import json + # Convert to JSON + json_output = { + 'version': version, + 'from_version': comparison_version.to_string() if comparison_version else None, + 'num_commits': len(commits), + 'num_changes': len(consolidated_changes), + 'categories': {} + } + for category, notes in grouped_notes.items(): + json_output['categories'][category] = [ + { + 'title': note.title, + 'ticket_key': note.ticket_key, + 'description': note.description, + 'labels': note.labels + } + for note in notes + ] + formatted_output = json.dumps(json_output, indent=2) + doc_formatted_output = None + else: + # Determine release_output_path and doc_output_path + release_output_path = output + doc_output_path = None + + # If no explicit output provided, use config templates + if not release_output_path: + # Build default draft path from config template + template_context = { + 'code_repo': repo_name.replace('/', '-'), # Sanitized for filesystem + 'issue_repo': _get_issues_repo(config), # First ticket_repos or code_repo + 'version': version, + 'major': str(target_version.major), + 'minor': str(target_version.minor), + 'patch': str(target_version.patch), + 'output_file_type': 'release' + } + try: + release_output_path = render_template(config.output.draft_output_path, template_context) + except TemplateError as e: + console.print(f"[red]Error rendering draft_output_path template: {e}[/red]") + sys.exit(1) + + # Compute doc_output_path if configured (as a draft) + if config.output.doc_output_path and config.release_notes.doc_output_template: + # Use draft_output_path for doc draft as well, but with type='doc' + template_context['output_file_type'] = 'doc' + try: + doc_output_path = render_template(config.output.draft_output_path, template_context) + except TemplateError as e: + console.print(f"[red]Error rendering draft_output_path template for docs: {e}[/red]") + sys.exit(1) + + # If explicit output provided, we use that for release notes. + # For docs, we use the configured doc_output_path (final path) as fallback? + # Or we skip doc generation? + # The previous behavior was to generate docs to doc_output_path. + elif config.output.doc_output_path and config.release_notes.doc_output_template: + template_context = { + 'code_repo': repo_name.replace('/', '-'), + 'issue_repo': _get_issues_repo(config), + 'version': version, + 'major': str(target_version.major), + 'minor': str(target_version.minor), + 'patch': str(target_version.patch), + 'output_file_type': 'doc' + } + try: + doc_output_path = render_template(config.output.doc_output_path, template_context) + except TemplateError as e: + console.print(f"[red]Error rendering doc_output_path template: {e}[/red]") + sys.exit(1) + + # Format markdown with media processing + result = note_generator.format_markdown( + grouped_notes, + version, + release_output_path=release_output_path, + doc_output_path=doc_output_path + ) + + # Handle return value (tuple or single string) + if isinstance(result, tuple): + formatted_output, doc_formatted_output = result + else: + formatted_output = result + doc_formatted_output = None + + # Output handling + if dry_run: + console.print(f"\n[yellow]{'='*80}[/yellow]") + console.print(f"[yellow]DRY RUN - Release notes for {version}:[/yellow]") + console.print(f"[yellow]{'='*80}[/yellow]\n") + console.print(formatted_output) + console.print(f"\n[yellow]{'='*80}[/yellow]") + if doc_formatted_output: + console.print(f"\n[bold]Documentation Release Notes Output:[/bold]") + console.print(f"[dim]{'─' * 60}[/dim]") + console.print(doc_formatted_output) + console.print(f"[dim]{'─' * 60}[/dim]") + console.print(f"[yellow]{'='*80}[/yellow]\n") + console.print(f"[yellow]DRY RUN complete. No files were created.[/yellow]") + else: + # Write release notes file + release_path_obj = Path(release_output_path) + release_path_obj.parent.mkdir(parents=True, exist_ok=True) + release_path_obj.write_text(formatted_output) + console.print(f"[green]✓ Release notes written to:[/green]") + console.print(f"[green] {release_path_obj.absolute()}[/green]") + + # Write doc output file if configured + if doc_formatted_output and doc_output_path: + doc_path_obj = Path(doc_output_path) + doc_path_obj.parent.mkdir(parents=True, exist_ok=True) + doc_path_obj.write_text(doc_formatted_output) + console.print(f"[green]✓ Docusaurus release notes written to:[/green]") + console.print(f"[green] {doc_path_obj.absolute()}[/green]") + + console.print(f"[blue]→ Review and edit the files, then use 'release-tool publish {version} -f {release_output_path}' to upload to GitHub[/blue]") + + finally: + db.close() + + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + if '--debug' in sys.argv: + raise + sys.exit(1) + + +def _get_extraction_source(change, commits_map=None, prs_map=None): + """ + Get human-readable description of where a ticket was extracted from. + + Args: + change: ConsolidatedChange object + commits_map: Optional dict mapping sha to commit for lookups + prs_map: Optional dict mapping pr_number to PR for lookups + + Returns: + String like "branch feat/meta-8624/main, pattern #1" + """ + # Try to get from PR first (most reliable) + if change.prs and len(change.prs) > 0: + pr = change.prs[0] + if pr.head_branch: + return f"branch {pr.head_branch}, PR #{pr.number}" + return f"PR #{pr.number}" + + # Fall back to commit info + if change.commits and len(change.commits) > 0: + commit = change.commits[0] + return f"commit {commit.sha[:7]}" + + return "unknown source" + + +def _extract_ticket_keys_from_release_notes(release_body: str) -> Set[str]: + """ + Extract ticket keys from release notes body. + + Args: + release_body: The markdown body of release notes + + Returns: + Set of ticket keys found in the release notes + """ + import re + ticket_keys = set() + + # Pattern 1: #1234 format + for match in re.finditer(r'#(\d+)', release_body): + ticket_keys.add(match.group(1)) + + # Pattern 2: owner/repo#1234 format + for match in re.finditer(r'[\w-]+/[\w-]+#(\d+)', release_body): + ticket_keys.add(match.group(1)) + + return ticket_keys + + +def _check_inter_release_duplicates( + consolidated_changes: List, + target_version: SemanticVersion, + db: Database, + repo_id: int, + config, + debug: bool +) -> List: + """ + Check for tickets that appear in earlier releases and apply deduplication policy. + + Args: + consolidated_changes: List of consolidated changes with tickets + target_version: The version being generated + db: Database instance + repo_id: Repository ID + config: Config with inter_release_duplicate_action policy + debug: Debug mode flag + + Returns: + Filtered list of consolidated changes (with duplicates removed if policy is IGNORE) + """ + from ..models import SemanticVersion + + action = config.ticket_policy.inter_release_duplicate_action + + if action == PolicyAction.IGNORE and not debug: + # Skip the check entirely if ignoring and not debugging + pass + + # Get all earlier releases (semantically before target version) + all_releases = db.get_all_releases(repo_id=repo_id, limit=None) + + earlier_releases = [] + for release in all_releases: + try: + release_version = SemanticVersion.parse(release.version) + if release_version < target_version: + earlier_releases.append(release) + except ValueError: + # Skip releases with invalid version strings + continue + + if not earlier_releases: + # No earlier releases to check against + return consolidated_changes + + # Extract ticket keys from all earlier releases + tickets_in_earlier_releases = {} # ticket_key -> list of (version, release) + for release in earlier_releases: + if release.body: + ticket_keys = _extract_ticket_keys_from_release_notes(release.body) + for ticket_key in ticket_keys: + if ticket_key not in tickets_in_earlier_releases: + tickets_in_earlier_releases[ticket_key] = [] + tickets_in_earlier_releases[ticket_key].append((release.version, release)) + + # Check current changes against earlier releases + duplicate_tickets = {} # ticket_key -> list of versions + for change in consolidated_changes: + if change.ticket_key and change.ticket_key in tickets_in_earlier_releases: + versions = [v for v, r in tickets_in_earlier_releases[change.ticket_key]] + duplicate_tickets[change.ticket_key] = versions + + if not duplicate_tickets: + # No duplicates found + return consolidated_changes + + # Apply policy + if action == PolicyAction.IGNORE: + # Filter out duplicate tickets from consolidated_changes + filtered_changes = [ + change for change in consolidated_changes + if not (change.ticket_key and change.ticket_key in duplicate_tickets) + ] + + if debug: + console.print(f"\n[dim]Filtered out {len(duplicate_tickets)} duplicate ticket(s) found in earlier releases:[/dim]") + for ticket_key, versions in duplicate_tickets.items(): + console.print(f" [dim]• #{ticket_key} (in releases: {', '.join(versions)})[/dim]") + + return filtered_changes + + elif action == PolicyAction.WARN: + # Include duplicates but warn + msg_lines = [] + msg_lines.append("") + msg_lines.append(f"[yellow]⚠️ Warning: Found {len(duplicate_tickets)} ticket(s) that appear in earlier releases:[/yellow]") + for ticket_key, versions in duplicate_tickets.items(): + msg_lines.append(f" • [bold]#{ticket_key}[/bold] (in releases: {', '.join(versions)})") + msg_lines.append("") + msg_lines.append("[dim]These tickets will be included in this release but also exist in earlier releases.[/dim]") + msg_lines.append("[dim]To exclude duplicates, set ticket_policy.inter_release_duplicate_action = 'ignore'[/dim]") + msg_lines.append("") + console.print("\n".join(msg_lines)) + + return consolidated_changes + + elif action == PolicyAction.ERROR: + # Fail with error + msg_lines = [] + msg_lines.append(f"[red]Error: Found {len(duplicate_tickets)} ticket(s) that appear in earlier releases:[/red]") + for ticket_key, versions in duplicate_tickets.items(): + msg_lines.append(f" • [bold]#{ticket_key}[/bold] (in releases: {', '.join(versions)})") + console.print("\n".join(msg_lines)) + raise RuntimeError(f"Inter-release duplicate tickets found ({len(duplicate_tickets)} total). Policy: error") + + return consolidated_changes + + +def _display_partial_section(partials: List[PartialTicketMatch], section_title: str) -> List[str]: + """ + Helper function to display a section of partial tickets with details. + + Args: + partials: List of PartialTicketMatch objects to display + section_title: Title for this section + + Returns: + List of formatted message lines + """ + msg_lines = [] + + if not partials: + return msg_lines + + # Group by type + not_found = [p for p in partials if p.match_type == "not_found"] + different_repo = [p for p in partials if p.match_type == "different_repo"] + + msg_lines.append(f"[cyan]{section_title}:[/cyan]") + msg_lines.append("") + + # Handle different_repo partials + if different_repo: + msg_lines.append(f"[yellow]Tickets in different repository ({len(different_repo)}):[/yellow]") + + # Group tickets by reason + tickets_by_reason = defaultdict(list) + for p in different_repo: + for reason in p.potential_reasons: + tickets_by_reason[reason].append(p) + + # Show reasons with associated tickets + msg_lines.append(f" [dim]This might be because of:[/dim]") + for reason, tickets in tickets_by_reason.items(): + ticket_keys = [p.ticket_key for p in tickets] + msg_lines.append(f" • {reason.description}") + msg_lines.append(f" [dim]Tickets:[/dim] {', '.join(ticket_keys)}") + + msg_lines.append("") + msg_lines.append(" [dim]Details:[/dim]") + for p in different_repo: + msg_lines.append(f" • [bold]{p.ticket_key}[/bold] (from {p.extracted_from})") + if p.found_in_repo: + msg_lines.append(f" [dim]Found in:[/dim] {p.found_in_repo}") + if p.ticket_url: + msg_lines.append(f" [dim]URL:[/dim] {p.ticket_url}") + msg_lines.append("") + + # Handle not_found partials + if not_found: + msg_lines.append(f"[yellow]Tickets not found in database ({len(not_found)}):[/yellow]") + + # Group tickets by reason + tickets_by_reason = defaultdict(list) + for p in not_found: + for reason in p.potential_reasons: + tickets_by_reason[reason].append(p) + + # Show reasons with associated tickets + msg_lines.append(f" [dim]This might be because of:[/dim]") + for reason, tickets in tickets_by_reason.items(): + ticket_keys = [p.ticket_key for p in tickets] + msg_lines.append(f" • {reason.description}") + msg_lines.append(f" [dim]Tickets:[/dim] {', '.join(ticket_keys)}") + + msg_lines.append("") + msg_lines.append(" [dim]Details:[/dim]") + for p in not_found: + msg_lines.append(f" • [bold]{p.ticket_key}[/bold] (from {p.extracted_from})") + msg_lines.append("") + + return msg_lines + + +def _handle_partial_tickets( + all_partials: List[PartialTicketMatch], + resolved_ticket_keys: Set[str], + config, + debug: bool +): + """ + Handle partial ticket matches based on policy configuration. + + Args: + all_partials: List of ALL PartialTicketMatch objects (resolved and unresolved) + resolved_ticket_keys: Set of ticket keys that were eventually resolved + config: Config object with ticket_policy.partial_ticket_action + debug: Whether debug mode is enabled + + Raises: + RuntimeError: If policy is ERROR and unresolved partials exist + """ + if not all_partials: + return + + action = config.ticket_policy.partial_ticket_action + + if action == PolicyAction.IGNORE: + return + + # Split into resolved and unresolved + unresolved_partials = [p for p in all_partials if p.ticket_key not in resolved_ticket_keys] + resolved_partials = [p for p in all_partials if p.ticket_key in resolved_ticket_keys] + + # DEBUG MODE: Show both resolved and unresolved with full details + if debug: + msg_lines = [] + msg_lines.append("") + + # Header with counts + if unresolved_partials and resolved_partials: + msg_lines.append(f"[yellow]⚠️ Found {len(unresolved_partials)} unresolved and {len(resolved_partials)} resolved partial ticket match(es)[/yellow]") + elif unresolved_partials: + msg_lines.append(f"[yellow]⚠️ Found {len(unresolved_partials)} unresolved partial ticket match(es)[/yellow]") + else: + msg_lines.append(f"[green]✓ {len(resolved_partials)} partial ticket match(es) were fully resolved[/green]") + msg_lines.append("") + + # Show unresolved section first (if any) + if unresolved_partials: + unresolved_section = _display_partial_section(unresolved_partials, "Unresolved Partial Matches") + msg_lines.extend(unresolved_section) + + # Show resolved section (if any) + if resolved_partials: + resolved_section = _display_partial_section(resolved_partials, "Resolved Partial Matches") + msg_lines.extend(resolved_section) + + # Add resolution tips for unresolved + if unresolved_partials: + msg_lines.append("[dim]To resolve:[/dim]") + msg_lines.append(" 1. Run [bold]'release-tool sync'[/bold] to fetch latest tickets") + msg_lines.append(" 2. Check [bold]repository.ticket_repos[/bold] in config") + msg_lines.append(" 3. Verify ticket numbers in branches/PRs") + msg_lines.append("") + + console.print("\n".join(msg_lines)) + + # WARN MODE: Brief message if all resolved, full details if any unresolved + elif action == PolicyAction.WARN: + if not unresolved_partials: + # All resolved - brief message only + console.print(f"[dim]ℹ️ {len(resolved_partials)} partial ticket match(es) were fully resolved. Use --debug for details.[/dim]") + else: + # Has unresolved - show full details for unresolved only + msg_lines = [] + msg_lines.append("") + msg_lines.append(f"[yellow]⚠️ Warning: Found {len(unresolved_partials)} unresolved partial ticket match(es)[/yellow]") + if resolved_partials: + msg_lines.append(f"[dim]({len(resolved_partials)} were resolved)[/dim]") + msg_lines.append("") + + # Show unresolved details + unresolved_section = _display_partial_section(unresolved_partials, "Unresolved Partial Matches") + msg_lines.extend(unresolved_section) + + # Add resolution tips + msg_lines.append("[dim]To resolve:[/dim]") + msg_lines.append(" 1. Run [bold]'release-tool sync'[/bold] to fetch latest tickets") + msg_lines.append(" 2. Check [bold]repository.ticket_repos[/bold] in config") + msg_lines.append(" 3. Verify ticket numbers in branches/PRs") + msg_lines.append("") + + console.print("\n".join(msg_lines)) + + # ERROR MODE: Fail if any unresolved + if action == PolicyAction.ERROR and unresolved_partials: + raise RuntimeError(f"Unresolved partial ticket matches found ({len(unresolved_partials)} total). Policy: error") diff --git a/src/release_tool/commands/init_config.py b/src/release_tool/commands/init_config.py new file mode 100644 index 0000000..c81640f --- /dev/null +++ b/src/release_tool/commands/init_config.py @@ -0,0 +1,166 @@ +from pathlib import Path +import click +from rich.console import Console + +console = Console() + + +@click.command('init-config', context_settings={'help_option_names': ['-h', '--help']}) +@click.option('-y', '--assume-yes', is_flag=True, help='Assume "yes" for confirmation prompts') +@click.pass_context +def init_config(ctx, assume_yes: bool): + """Create an example configuration file.""" + # Load template from config_template.toml + # Path is relative to this file (commands/init_config.py), so go up two levels + template_path = Path(__file__).parent.parent / "config_template.toml" + try: + example_config = template_path.read_text(encoding='utf-8') + except Exception as e: + console.print(f"[red]Error loading config template: {e}[/red]") + console.print("[yellow]Falling back to minimal config...[/yellow]") + example_config = """ +config_version = "1.4" + +[repository] +code_repo = "sequentech/step" +ticket_repos = ["sequentech/meta"] +default_branch = "main" + +[github] +api_url = "https://api.github.com" + +[database] +path = "release_tool.db" + +[sync] +cutoff_date = "2025-01-01" +parallel_workers = 10 +clone_code_repo = true +show_progress = true + +[[ticket_policy.patterns]] +order = 1 +strategy = "branch_name" +pattern = "/(?P\\\\w+)-(?P\\\\d+)" +description = "Branch name format: type/repo-123/target" + +[[ticket_policy.patterns]] +order = 2 +strategy = "pr_body" +pattern = "Parent issue:.*?/issues/(?P\\\\d+)" +description = "Parent issue URL in PR description" + +[[ticket_policy.patterns]] +order = 3 +strategy = "pr_title" +pattern = "#(?P\\\\d+)" +description = "GitHub issue reference (#123) in PR title" + +[ticket_policy] +no_ticket_action = "warn" +unclosed_ticket_action = "warn" +consolidation_enabled = true +description_section_regex = "(?:## Description|## Summary)\\\\n(.*?)(?=\\\\n##|\\\\Z)" +migration_section_regex = "(?:## Migration|## Migration Notes)\\\\n(.*?)(?=\\\\n##|\\\\Z)" + +[version_policy] +gap_detection = "warn" +tag_prefix = "v" + +[branch_policy] +release_branch_template = "release/{major}.{minor}" +default_branch = "main" +create_branches = true +branch_from_previous_release = true + +[[release_notes.categories]] +name = "💥 Breaking Changes" +labels = ["breaking-change", "breaking"] +order = 1 +alias = "breaking" + +[[release_notes.categories]] +name = "🚀 Features" +labels = ["feature", "enhancement", "feat"] +order = 2 +alias = "features" + +[[release_notes.categories]] +name = "🛠 Bug Fixes" +labels = ["bug", "fix", "bugfix", "hotfix"] +order = 3 +alias = "bugfixes" + +[[release_notes.categories]] +name = "Other Changes" +labels = [] +order = 99 +alias = "other" + +[release_notes] +excluded_labels = ["skip-changelog", "internal", "wip", "do-not-merge"] +title_template = "Release {{ version }}" +entry_template = '''- {{ title }} + {% if url %}{{ url }}{% endif %} + {% if authors %} + by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %}''' +output_template = '''# {{ title }} + +{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %} +{% if breaking_with_desc|length > 0 %} +## 💥 Breaking Changes +{% for note in breaking_with_desc %} +### {{ note.title }} +{{ note.description }} +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} +## 📋 All Changes +{% for category in categories %} +### {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} + +{% endfor %}''' + +[output] +output_path = "docs/docusaurus/docs/releases/release-{major}.{minor}/release-{major}.{minor}.{patch}.md" +draft_output_path = ".release_tool_cache/draft-releases/{repo}/{version}.md" +assets_path = "docs/docusaurus/docs/releases/release-{major}.{minor}/assets" +download_media = false +create_github_release = false +create_pr = false + +[output.pr_templates] +branch_template = "release-notes-{version}" +title_template = "Release notes for {version}" +body_template = '''Automated release notes for version {version}. + +## Summary +This PR adds release notes for {version} with {num_changes} changes across {num_categories} categories.''' +""" + + config_path = Path("release_tool.toml") + if config_path.exists(): + console.print("[yellow]Configuration file already exists at release_tool.toml[/yellow]") + + # Get flags from context (for global -y flag) and merge with local parameter + auto = ctx.obj.get('auto', False) + assume_yes_global = ctx.obj.get('assume_yes', False) + assume_yes_effective = assume_yes or assume_yes_global + + # Check both flags before prompting + if not (auto or assume_yes_effective): + if not click.confirm("Overwrite?"): + return + + config_path.write_text(example_config) + console.print(f"[green]Created configuration file: {config_path}[/green]") + console.print("\n[blue]Next steps:[/blue]") + console.print("1. Edit release_tool.toml and set your repository") + console.print("2. Set GITHUB_TOKEN environment variable") + console.print("3. Run: release-tool sync") + console.print("4. Run: release-tool generate --repo-path /path/to/repo") diff --git a/src/release_tool/commands/list_releases.py b/src/release_tool/commands/list_releases.py new file mode 100644 index 0000000..26a16a0 --- /dev/null +++ b/src/release_tool/commands/list_releases.py @@ -0,0 +1,139 @@ +from typing import Optional +import click +from rich.console import Console +from rich.table import Table +from datetime import datetime + +from ..config import Config +from ..db import Database +from ..models import SemanticVersion + +console = Console() + + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.argument('version', required=False) +@click.option('--repository', '-r', help='Filter by repository (owner/name)') +@click.option('--limit', '-n', type=int, default=10, help='Number of releases to show (default: 10, use 0 for all)') +@click.option('--type', '-t', multiple=True, type=click.Choice(['final', 'rc', 'beta', 'alpha'], case_sensitive=False), help='Release types to include (can be specified multiple times)') +@click.option('--after', type=str, help='Only show releases published after this date (YYYY-MM-DD)') +@click.option('--before', type=str, help='Only show releases published before this date (YYYY-MM-DD)') +@click.pass_context +def list_releases(ctx, version: Optional[str], repository: Optional[str], limit: int, type: tuple, after: Optional[str], before: Optional[str]): + """ + List releases in the database. + + By default shows the last 10 releases. Use --limit 0 to show all releases. + + Examples: + + release-tool list-releases 9 # All 9.x.x releases + + release-tool list-releases 9.3 # All 9.3.x releases + + release-tool list-releases --type final # Only final releases + + release-tool list-releases --type final --type rc # Finals and RCs + + release-tool list-releases --after 2024-01-01 # Since 2024 + + release-tool list-releases --before 2024-06-01 # Before June 2024 + """ + config: Config = ctx.obj['config'] + repo_name = repository or config.repository.code_repo + + # Parse after date if provided + after_date = None + if after: + try: + after_date = datetime.fromisoformat(after) + except ValueError: + console.print(f"[red]Invalid date format for --after. Use YYYY-MM-DD[/red]") + return + + # Parse before date if provided + before_date = None + if before: + try: + before_date = datetime.fromisoformat(before) + except ValueError: + console.print(f"[red]Invalid date format for --before. Use YYYY-MM-DD[/red]") + return + + db = Database(config.database.path) + db.connect() + + try: + repo = db.get_repository(repo_name) + if not repo: + console.print(f"[red]Repository {repo_name} not found. Run 'sync' first.[/red]") + return + + # Convert type tuple to list, default to all types if not specified + release_types = list(type) if type else None + + # First get total count (without limit) to show "X out of Y" + total_releases = db.get_all_releases( + repo.id, + limit=None, # No limit for count + version_prefix=version, + release_types=release_types, + after=after_date, + before=before_date + ) + total_count = len(total_releases) + + # Now get the limited results + releases = db.get_all_releases( + repo.id, + limit=limit if limit > 0 else None, + version_prefix=version, + release_types=release_types, + after=after_date, + before=before_date + ) + + if not releases: + console.print("[yellow]No releases found.[/yellow]") + return + + # Build title with filter info + title_parts = [f"Releases for {repo_name}"] + if limit and limit > 0 and total_count > len(releases): + title_parts.append(f"(showing {len(releases)} out of {total_count})") + elif total_count > 0: + title_parts.append(f"({total_count} total)") + if version: + title_parts.append(f"version {version}.x") + if release_types: + title_parts.append(f"types: {', '.join(release_types)}") + if after: + title_parts.append(f"after {after}") + if before: + title_parts.append(f"before {before}") + + table = Table(title=" ".join(title_parts)) + table.add_column("Version", style="cyan") + table.add_column("Tag", style="green") + table.add_column("Type", style="yellow") + table.add_column("Published", style="magenta") + table.add_column("URL", style="blue") + + for release in releases: + version_obj = SemanticVersion.parse(release.version) + rel_type = "RC" if not version_obj.is_final() else "Final" + published = release.published_at.strftime("%Y-%m-%d") if release.published_at else "Draft" + url = release.url if release.url else f"https://github.com/{repo_name}/releases/tag/{release.tag_name}" + + table.add_row( + release.version, + release.tag_name, + rel_type, + published, + url + ) + + console.print(table) + + finally: + db.close() diff --git a/src/release_tool/commands/publish.py b/src/release_tool/commands/publish.py new file mode 100644 index 0000000..15a9a06 --- /dev/null +++ b/src/release_tool/commands/publish.py @@ -0,0 +1,1110 @@ +import sys +from pathlib import Path +from typing import Optional +from collections import defaultdict +from datetime import datetime +import click +from rich.console import Console +from rich.table import Table + +from ..config import Config +from ..db import Database +from ..github_utils import GitHubClient +from ..models import SemanticVersion, Release +from ..template_utils import render_template, validate_template_vars, get_template_variables, TemplateError +from ..git_ops import GitOperations, determine_release_branch_strategy + +console = Console() + + +def _get_issues_repo(config: Config) -> str: + """ + Get the issues repository from config. + + Returns the first ticket_repos entry if available, otherwise falls back to code_repo. + """ + if config.repository.ticket_repos and len(config.repository.ticket_repos) > 0: + return config.repository.ticket_repos[0] + return config.repository.code_repo + + +def _create_release_ticket( + config: Config, + github_client: GitHubClient, + db: Database, + template_context: dict, + version: str, + override: bool = False, + dry_run: bool = False, + debug: bool = False +) -> Optional[dict]: + """ + Create or update a GitHub issue for tracking the release. + + Args: + config: Configuration object + github_client: GitHub client instance + db: Database instance for checking/saving associations + template_context: Template context for rendering ticket templates + version: Release version + override: If True, reuse existing ticket if found + dry_run: If True, only show what would be created + debug: If True, show verbose output + + Returns: + Dictionary with 'number' and 'url' keys if created, None otherwise + """ + if not config.output.create_ticket: + if debug: + console.print("[dim]Ticket creation disabled (create_ticket=false)[/dim]") + return None + + issues_repo = _get_issues_repo(config) + repo_full_name = config.repository.code_repo + + # Prepare labels + final_labels = config.output.ticket_templates.labels.copy() + # Note: Issue type is handled separately via GraphQL, not as a label + + # Prepare milestone + milestone_obj = None + milestone_name = None + + if config.output.ticket_templates.milestone: + try: + milestone_name = render_template( + config.output.ticket_templates.milestone, + template_context + ) + if not dry_run: + milestone_obj = github_client.get_milestone_by_title(issues_repo, milestone_name) + except TemplateError as e: + console.print(f"[red]Error rendering milestone template: {e}[/red]") + + # Check for existing ticket association if override is enabled + existing_association = db.get_ticket_association(repo_full_name, version) if not dry_run else None + result = None + + # Render ticket templates + try: + title = render_template( + config.output.ticket_templates.title_template, + template_context + ) + body = render_template( + config.output.ticket_templates.body_template, + template_context + ) + except TemplateError as e: + console.print(f"[red]Error rendering ticket template: {e}[/red]") + return None + + if existing_association and override: + # Reuse existing ticket + if debug or not dry_run: + console.print(f"[blue]Reusing existing ticket #{existing_association['ticket_number']} (--force)[/blue]") + console.print(f"[dim] URL: {existing_association['ticket_url']}[/dim]") + + if not dry_run: + github_client.update_issue( + repo_full_name=issues_repo, + issue_number=existing_association['ticket_number'], + title=title, + body=body, + labels=final_labels, + milestone=milestone_obj + ) + + # Update issue type if specified + if config.output.ticket_templates.type: + github_client.set_issue_type( + repo_full_name=issues_repo, + issue_number=existing_association['ticket_number'], + type_name=config.output.ticket_templates.type + ) + + console.print(f"[green]Updated ticket #{existing_association['ticket_number']} details (title, body, labels, milestone, type)[/green]") + + result = { + 'number': str(existing_association['ticket_number']), + 'url': existing_association['ticket_url'] + } + elif existing_association and not override: + console.print(f"[yellow]Warning: Ticket already exists for {version} (#{existing_association['ticket_number']})[/yellow]") + console.print(f"[yellow]Use --force \\[draft|release] to reuse the existing ticket[/yellow]") + console.print(f"[dim] URL: {existing_association['ticket_url']}[/dim]") + return None + else: + # Create new ticket + if dry_run or debug: + console.print("\n[cyan]Release Tracking Ticket:[/cyan]") + console.print(f" Repository: {issues_repo}") + console.print(f" Title: {title}") + console.print(f" Labels: {', '.join(final_labels)}") + if config.output.ticket_templates.type: + console.print(f" Issue Type: {config.output.ticket_templates.type}") + if milestone_name: + console.print(f" Milestone: {milestone_name}") + + # Show assignee if configured + assignee = config.output.ticket_templates.assignee + if not assignee and not dry_run: + # Get current user if not dry-run + assignee = github_client.get_authenticated_user() if github_client else "current user" + console.print(f" Assignee: {assignee or 'current user'}") + + # Show project assignment if configured + if config.output.ticket_templates.project_id: + console.print(f" Project ID: {config.output.ticket_templates.project_id}") + if config.output.ticket_templates.project_status: + console.print(f" Project Status: {config.output.ticket_templates.project_status}") + if config.output.ticket_templates.project_fields: + console.print(f" Project Fields: {config.output.ticket_templates.project_fields}") + + if debug: + console.print(f"\n[dim]Body:[/dim]") + console.print(f"[dim]{'─' * 60}[/dim]") + console.print(f"[dim]{body}[/dim]") + console.print(f"[dim]{'─' * 60}[/dim]\n") + + if dry_run: + return {'number': 'XXXX', 'url': f'https://github.com/{issues_repo}/issues/XXXX'} + + # Create the issue + if debug: + console.print(f"[cyan]Creating ticket in {issues_repo}...[/cyan]") + + result = github_client.create_issue( + repo_full_name=issues_repo, + title=title, + body=body, + labels=final_labels, + milestone=milestone_obj, + issue_type=config.output.ticket_templates.type + ) + + if result: + # Save association to database + db.save_ticket_association( + repo_full_name=repo_full_name, + version=version, + ticket_number=int(result['number']), + ticket_url=result['url'] + ) + + if debug: + console.print(f"[dim]Saved ticket association to database[/dim]") + + if not result: + return None + + # Assign issue to user (for both new and updated tickets) + if not dry_run: + assignee = config.output.ticket_templates.assignee + if not assignee: + assignee = github_client.get_authenticated_user() + + if assignee: + github_client.assign_issue( + repo_full_name=issues_repo, + issue_number=int(result['number']), + assignee=assignee + ) + + # Add to project if configured (for both new and updated tickets) + if config.output.ticket_templates.project_id: + # Resolve project ID (number) to node ID + org_name = issues_repo.split('/')[0] + try: + project_number = int(config.output.ticket_templates.project_id) + project_node_id = github_client.get_project_node_id(org_name, project_number) + + if project_node_id: + github_client.assign_issue_to_project( + issue_url=result['url'], + project_id=project_node_id, + status=config.output.ticket_templates.project_status, + custom_fields=config.output.ticket_templates.project_fields, + debug=debug + ) + except ValueError: + console.print(f"[yellow]Warning: Invalid project ID '{config.output.ticket_templates.project_id}'. Expected a number.[/yellow]") + + return result + + +def _find_draft_releases(config: Config, version_filter: Optional[str] = None) -> list[Path]: + """ + Find draft release files matching the configured path template. + + Args: + config: Configuration object + version_filter: Optional version string to filter results (e.g., "9.2.0") + + Returns: + List of Path objects for draft release files, sorted by modification time (newest first) + """ + template = config.output.draft_output_path + + # Create a glob pattern that matches ALL repos and versions + # We replace Jinja2 placeholders {{variable}} with * to match any value + if version_filter: + # If filtering by version, keep the version in the pattern + glob_pattern = template.replace("{{code_repo}}", "*")\ + .replace("{{issue_repo}}", "*")\ + .replace("{{version}}", version_filter)\ + .replace("{{major}}", "*")\ + .replace("{{minor}}", "*")\ + .replace("{{minor}}", "*")\ + .replace("{{patch}}", "*")\ + .replace("{{output_file_type}}", "*") + else: + # Match all versions + glob_pattern = template.replace("{{code_repo}}", "*")\ + .replace("{{issue_repo}}", "*")\ + .replace("{{version}}", "*")\ + .replace("{{major}}", "*")\ + .replace("{{minor}}", "*")\ + .replace("{{patch}}", "*")\ + .replace("{{output_file_type}}", "*") + + # Use glob on the current directory to find all matching files + draft_files = list(Path('.').glob(glob_pattern)) + + # Sort by modification time desc (newest first) + draft_files.sort(key=lambda p: p.stat().st_mtime, reverse=True) + + return draft_files + + +def _display_draft_releases(draft_files: list[Path], title: str = "Draft Releases"): + """ + Display a table of draft release files. + + Args: + draft_files: List of Path objects for draft release files + title: Title for the table + """ + if not draft_files: + console.print("[yellow]No draft releases found.[/yellow]") + return + + table = Table(title=title) + table.add_column("Code Repository", style="green") + table.add_column("Version", style="cyan") + table.add_column("Release Type", style="yellow") # RC or Final + table.add_column("Content Type", style="magenta") # Doc, Release + table.add_column("Created", style="blue") + table.add_column("Path(s)", style="dim") + + # Group by (repo_name, version_str) + grouped_drafts = defaultdict(list) + + for file_path in draft_files: + # Extract repo name + repo_name = file_path.parent.name + + # Extract version from filename + # Filename format: version-type.md or version.md + filename = file_path.stem + + # Simple heuristic to strip suffix if present + version_str = filename + content_type = "Release" + + if filename.endswith("-doc"): + version_str = filename[:-4] + content_type = "Doc" + elif filename.endswith("-release"): + version_str = filename[:-8] + content_type = "Release" + + grouped_drafts[(repo_name, version_str)].append({ + 'path': file_path, + 'content_type': content_type, + 'mtime': file_path.stat().st_mtime + }) + + # Sort groups by mtime of newest file in group + sorted_groups = sorted( + grouped_drafts.items(), + key=lambda item: max(f['mtime'] for f in item[1]), + reverse=True + ) + + for (repo_name, version_str), files in sorted_groups: + try: + version_obj = SemanticVersion.parse(version_str) + rel_type = "RC" if not version_obj.is_final() else "Final" + except ValueError: + rel_type = "Unknown" + + # Collect content types and paths + content_types = sorted(list(set(f['content_type'] for f in files))) + content_type_str = ", ".join(content_types) + + # Get newest creation time + newest_mtime = max(f['mtime'] for f in files) + created_str = datetime.fromtimestamp(newest_mtime).strftime("%Y-%m-%d %H:%M") + + # Format paths (newline separated) + paths_str = "\n".join(str(f['path']) for f in files) + + table.add_row( + repo_name, + version_str, + rel_type, + content_type_str, + created_str, + paths_str + ) + + console.print(table) + + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.argument('version', required=False) +@click.option('--list', '-l', 'list_drafts', is_flag=True, help='List draft releases ready to be published') +@click.option('--delete', '-d', 'delete_drafts', is_flag=True, help='Delete draft releases for the specified version') +@click.option('--notes-file', '-f', type=click.Path(), help='Path to release notes file (markdown, optional - will auto-find if not specified)') +@click.option('--release/--no-release', 'create_release', default=None, help='Create GitHub release (default: from config)') +@click.option('--pr/--no-pr', 'create_pr', default=None, help='Create PR with release notes (default: from config)') +@click.option('--release-mode', type=click.Choice(['draft', 'published'], case_sensitive=False), default=None, help='Release mode (default: from config)') +@click.option('--prerelease', type=click.Choice(['auto', 'true', 'false'], case_sensitive=False), default=None, + help='Mark as prerelease: auto (detect from version), true, or false (default: from config)') +@click.option('--force', type=click.Choice(['none', 'draft', 'published'], case_sensitive=False), default='none', help='Force overwrite existing release (default: none)') +@click.option('--dry-run', is_flag=True, help='Show what would be published without making changes') +@click.option('--debug', is_flag=True, help='Show detailed debug information') +@click.pass_context +def publish(ctx, version: Optional[str], list_drafts: bool, delete_drafts: bool, notes_file: Optional[str], create_release: Optional[bool], + create_pr: Optional[bool], release_mode: Optional[str], prerelease: Optional[str], force: str, + dry_run: bool, debug: bool): + """ + Publish a release to GitHub. + + Creates a GitHub release and/or pull request with release notes. + Release notes can be read from a file or will be loaded from the database. + + Flags default to config values but can be overridden via CLI. + + Examples: + + release-tool publish 9.1.0 -f docs/releases/9.1.0.md + + release-tool publish 9.1.0-rc.0 --release-mode draft + + release-tool publish 9.1.0 --pr --no-release + + release-tool publish 9.1.0 -f notes.md --dry-run + + release-tool publish 9.1.0 -f notes.md --debug + """ + config: Config = ctx.obj['config'] + + if list_drafts: + draft_files = _find_draft_releases(config) + _display_draft_releases(draft_files) + + # Show tip with an example version if drafts exist + if draft_files: + # Extract version from the first (newest) draft + first_file = draft_files[0] + filename = first_file.stem + version_str = filename + if filename.endswith("-doc"): + version_str = filename[:-4] + elif filename.endswith("-release"): + version_str = filename[:-8] + + console.print(f"\n[yellow]Tip: Publish a release with:[/yellow]") + console.print(f"[dim] release-tool publish {version_str}[/dim]") + + return + + # Handle --delete flag + if delete_drafts: + if not version: + console.print("[red]Error: VERSION required when using --delete[/red]") + console.print("\nUsage: release-tool publish --delete VERSION") + sys.exit(1) + + # Find drafts for this version + matching_drafts = _find_draft_releases(config, version_filter=version) + + if not matching_drafts: + console.print(f"[yellow]No draft releases found for version {version}[/yellow]") + console.print("\n[dim]Available drafts:[/dim]") + all_drafts = _find_draft_releases(config) + _display_draft_releases(all_drafts, title="Available Draft Releases") + return + + # Display what will be deleted + console.print(f"\n[yellow]Found {len(matching_drafts)} draft file(s) for version {version}:[/yellow]") + for draft_path in matching_drafts: + console.print(f" - {draft_path}") + + # Confirm deletion (skip confirmation if non-interactive or dry-run) + if not dry_run: + response = input(f"\nDelete {len(matching_drafts)} file(s)? [y/N]: ").strip().lower() + if response not in ['y', 'yes']: + console.print("[yellow]Deletion cancelled.[/yellow]") + return + + # Delete the files + deleted_count = 0 + for draft_path in matching_drafts: + if dry_run: + console.print(f"[yellow]Would delete: {draft_path}[/yellow]") + else: + try: + draft_path.unlink() + console.print(f"[green]✓ Deleted: {draft_path}[/green]") + deleted_count += 1 + except Exception as e: + console.print(f"[red]Error deleting {draft_path}: {e}[/red]") + + if not dry_run: + console.print(f"\n[green]✓ Deleted {deleted_count} of {len(matching_drafts)} draft file(s)[/green]") + + return + + if not version: + console.print("[red]Error: Missing argument 'VERSION'.[/red]") + console.print("\nUsage: release-tool publish [OPTIONS] VERSION") + console.print("Try 'release-tool publish --help' for help.") + sys.exit(1) + + try: + # Use config defaults when CLI values are None + create_release = create_release if create_release is not None else config.output.create_github_release + create_pr = create_pr if create_pr is not None else config.output.create_pr + + # Resolve release mode + # If force is set to a mode, it overrides release_mode + if force != 'none': + mode = force + else: + mode = release_mode if release_mode is not None else config.output.release_mode + + is_draft = (mode == 'draft') + + # Handle tri-state prerelease: "auto", "true", "false" + prerelease_value = prerelease if prerelease is not None else config.output.prerelease + doc_output_enabled = config.output.doc_output_path is not None + + # Convert string values to appropriate types + if isinstance(prerelease_value, str): + if prerelease_value.lower() == "true": + prerelease_flag = True + prerelease_auto = False + elif prerelease_value.lower() == "false": + prerelease_flag = False + prerelease_auto = False + else: # "auto" + prerelease_flag = False # Will be set based on version + prerelease_auto = True + else: + # Boolean value from config + prerelease_flag = prerelease_value + prerelease_auto = False + + if debug: + console.print("\n[bold cyan]Debug Mode: Configuration & Settings[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Repository:[/dim] {config.repository.code_repo}") + console.print(f"[dim]Dry run:[/dim] {dry_run}") + console.print(f"[dim]Operations that will be performed:[/dim]") + console.print(f"[dim] • Create GitHub release: {create_release} (CLI override: {create_release is not None})[/dim]") + console.print(f"[dim] • Create PR: {create_pr} (CLI override: {create_pr is not None})[/dim]") + console.print(f"[dim] • Release mode: {mode} (CLI override: {release_mode is not None or force != 'none'})[/dim]") + console.print(f"[dim] • Force: {force}[/dim]") + console.print(f"[dim] • Prerelease setting: {prerelease_value} (CLI override: {prerelease is not None})[/dim]") + console.print(f"[dim] • Prerelease auto-detect: {prerelease_auto}[/dim]") + console.print(f"[dim] • Documentation output enabled: {doc_output_enabled}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + # Parse version + target_version = SemanticVersion.parse(version) + + if debug: + console.print("[bold cyan]Debug Mode: Version Information[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Version:[/dim] {target_version.to_string()}") + console.print(f"[dim] • Major: {target_version.major}[/dim]") + console.print(f"[dim] • Minor: {target_version.minor}[/dim]") + console.print(f"[dim] • Patch: {target_version.patch}[/dim]") + console.print(f"[dim] • Is final release: {target_version.is_final()}[/dim]") + console.print(f"[dim]Git tag:[/dim] v{version}") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + # Auto-detect prerelease if set to "auto" and version is not final + if prerelease_auto and not target_version.is_final(): + prerelease_flag = True + if debug: + console.print(f"[blue]✓ Auto-detected as prerelease version (version is not final)[/blue]") + elif not dry_run: + console.print(f"[blue]Auto-detected as prerelease version[/blue]") + + if debug: + console.print(f"[dim]Final prerelease flag:[/dim] {prerelease_flag}\n") + + # Read release notes + notes_path = None + release_notes = None + doc_notes_path = None + doc_notes_content = None + + if debug: + console.print("[bold cyan]Debug Mode: Release Notes[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + + if notes_file: + notes_path = Path(notes_file) + if not notes_path.exists(): + console.print(f"[red]Error: Notes file not found: {notes_file}[/red]") + sys.exit(1) + release_notes = notes_path.read_text() + if debug: + console.print(f"[dim]Source:[/dim] Explicit file (--notes-file)") + console.print(f"[dim]Path:[/dim] {notes_path}") + console.print(f"[dim]Size:[/dim] {len(release_notes)} characters") + console.print(f"[dim]File exists:[/dim] Yes") + elif not dry_run: + console.print(f"[blue]Loaded release notes from {notes_file}[/blue]") + else: + # Try to auto-find draft notes for this version + if debug: + console.print(f"[dim]Source:[/dim] Auto-finding (no --notes-file specified)") + console.print(f"[dim]Search pattern:[/dim] {config.output.draft_output_path}") + console.print(f"[dim]Searching for version:[/dim] {version}") + + matching_drafts = _find_draft_releases(config, version_filter=version) + + if debug: + console.print(f"[dim]Matches found:[/dim] {len(matching_drafts)}") + + if len(matching_drafts) == 1: + # Found exactly one match, use it + notes_path = matching_drafts[0] + release_notes = notes_path.read_text() + if debug: + console.print(f"[dim]Path:[/dim] {notes_path}") + console.print(f"[dim]Size:[/dim] {len(release_notes)} characters") + else: + console.print(f"[blue]Auto-found release notes from {notes_path}[/blue]") + elif len(matching_drafts) == 0: + # No draft found, error and list all available drafts + console.print(f"[red]Error: No draft release notes found for version {version}[/red]") + console.print("[yellow]Available draft releases:[/yellow]\n") + all_drafts = _find_draft_releases(config) + _display_draft_releases(all_drafts, title="Available Draft Releases") + + # Show tip with an available version if any exist + if all_drafts: + # Extract version from the first (newest) draft + first_file = all_drafts[0] + filename = first_file.stem + example_version = filename + if filename.endswith("-doc"): + example_version = filename[:-4] + elif filename.endswith("-release"): + example_version = filename[:-8] + + console.print(f"\n[yellow]Tip: Use an existing draft or generate new notes:[/yellow]") + console.print(f"[dim] release-tool publish {example_version}[/dim]") + console.print(f"[dim] release-tool generate {version}[/dim]") + else: + console.print(f"\n[yellow]Tip: Generate release notes first with:[/yellow]") + console.print(f"[dim] release-tool generate {version}[/dim]") + sys.exit(1) + else: + # Multiple matches found. Separate release and doc drafts + release_candidates = [d for d in matching_drafts if "doc" not in d.name.lower()] + doc_candidates = [d for d in matching_drafts if "doc" in d.name.lower()] + + # Handle release notes + if len(release_candidates) == 1: + notes_path = release_candidates[0] + release_notes = notes_path.read_text() + if debug: + console.print(f"[dim]Multiple drafts found, selected release candidate:[/dim] {notes_path}") + else: + console.print(f"[blue]Auto-found release notes from {notes_path}[/blue]") + elif len(release_candidates) == 0: + # If we have doc drafts but no release drafts, that might be an issue if we expected release notes + # But maybe the user only wants to publish docs? + # For now, let's assume we need release notes. + console.print(f"[red]Error: No release notes draft found for version {version}[/red]") + if doc_candidates: + console.print(f"[dim](Found {len(doc_candidates)} doc drafts, but need release notes)[/dim]") + sys.exit(1) + else: + # Ambiguous release notes + console.print(f"[red]Error: Multiple release note drafts found for version {version}[/red]") + _display_draft_releases(release_candidates, title="Ambiguous Release Drafts") + sys.exit(1) + + # Handle doc notes + if len(doc_candidates) == 1: + doc_notes_path = doc_candidates[0] + doc_notes_content = doc_notes_path.read_text() + if debug: + console.print(f"[dim]Selected doc candidate:[/dim] {doc_notes_path}") + elif len(doc_candidates) > 1: + if debug: + console.print(f"[yellow]Warning: Multiple doc drafts found, ignoring:[/yellow]") + for d in doc_candidates: + console.print(f" - {d}") + + if debug: + # Show full release notes in debug mode + if release_notes: + console.print(f"\n[bold]Full Release Notes Content:[/bold]") + console.print(f"[dim]{'─' * 60}[/dim]") + console.print(release_notes) + console.print(f"[dim]{'─' * 60}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + # Initialize GitHub client and database + github_client = None if dry_run else GitHubClient(config) + repo_name = config.repository.code_repo + + # Calculate issue_repo_name + issues_repo = _get_issues_repo(config) + issue_repo_name = issues_repo.split('/')[-1] if '/' in issues_repo else issues_repo + + # Initialize GitOperations and determine target_branch + git_ops = GitOperations('.') + available_versions = git_ops.get_version_tags() + + target_branch, _, _ = determine_release_branch_strategy( + version=target_version, + git_ops=git_ops, + available_versions=available_versions, + branch_template=config.branch_policy.release_branch_template, + default_branch=config.repository.default_branch, + branch_from_previous=config.branch_policy.branch_from_previous_release + ) + + # Initialize database connection + db = Database(config.database.path) + db.connect() + + # Check for existing release + repo = db.get_repository(repo_name) + if repo: + existing_release = db.get_release(repo.id, version) + if existing_release: + if force == 'none': + console.print(f"[red]Error: Release {version} already exists.[/red]") + console.print(f"[yellow]Use --force \\[draft|published] to overwrite.[/yellow]") + sys.exit(1) + elif not dry_run: + console.print(f"[yellow]Warning: Overwriting existing release {version} (--force {force})[/yellow]") + + if debug: + console.print("[bold cyan]Debug Mode: GitHub Operations[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Repository:[/dim] {repo_name}") + console.print(f"[dim]Git tag:[/dim] v{version}") + console.print(f"[dim]GitHub client initialized:[/dim] {not dry_run}") + console.print(f"[dim]Database path:[/dim] {config.database.path}") + console.print(f"[dim]Force mode:[/dim] {force}") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + # Dry-run banner + if dry_run: + console.print(f"\n[yellow]{'='*80}[/yellow]") + console.print(f"[yellow]DRY RUN - Publish release {version}[/yellow]") + console.print(f"[yellow]{'='*80}[/yellow]\n") + + # Create GitHub release + if create_release: + status = "draft " if is_draft else ("prerelease " if prerelease_flag else "") + release_type = "draft" if is_draft else ("prerelease" if prerelease_flag else "final release") + + if dry_run: + console.print(f"[yellow]Would create {status}GitHub release:[/yellow]") + console.print(f"[yellow] Repository: {repo_name}[/yellow]") + console.print(f"[yellow] Version: {version}[/yellow]") + console.print(f"[yellow] Tag: v{version}[/yellow]") + console.print(f"[yellow] Type: {release_type.capitalize()}[/yellow]") + console.print(f"[yellow] Status: {'Draft' if is_draft else 'Published'}[/yellow]") + console.print(f"[yellow] URL: https://github.com/{repo_name}/releases/tag/v{version}[/yellow]") + + # Show release notes preview (only if not in debug mode to avoid duplication) + if not debug: + preview_length = 500 + preview = release_notes[:preview_length] + if len(release_notes) > preview_length: + preview += "\n[... truncated ...]" + console.print(f"\n[yellow]Release notes preview ({len(release_notes)} characters):[/yellow]") + console.print(f"[dim]{preview}[/dim]\n") + else: + console.print(f"[blue]Creating {status}GitHub release for {version}...[/blue]") + + release_name = f"Release {version}" + github_client.create_release( + repo_name, + version, + release_name, + release_notes, + prerelease=prerelease_flag, + draft=is_draft, + target_commitish=target_branch + ) + console.print(f"[green]✓ GitHub release created successfully[/green]") + console.print(f"[blue]→ https://github.com/{repo_name}/releases/tag/v{version}[/blue]") + elif dry_run: + console.print(f"[yellow]Would NOT create GitHub release (--no-release or config setting)[/yellow]\n") + + # Save release to database + if not dry_run and repo: + release = Release( + repo_id=repo.id, + version=version, + tag_name=f"v{version}", + name=f"Release {version}", + body=release_notes, + created_at=datetime.now(), + published_at=datetime.now() if not is_draft else None, + is_draft=is_draft, + is_prerelease=prerelease_flag, + url=f"https://github.com/{repo_name}/releases/tag/v{version}", + target_commitish=target_branch + ) + db.upsert_release(release) + if debug: + console.print(f"[dim]Saved release to database (is_draft={is_draft})[/dim]") + + # Create PR + if create_pr: + if not notes_path: + console.print("[yellow]Warning: No release notes available, skipping PR creation.[/yellow]") + console.print("[dim]Tip: Generate release notes first or specify with --notes-file[/dim]") + else: + # Build template context with all available variables + # Try to count changes from release notes for PR body template + num_changes = 0 + num_categories = 0 + if release_notes: + # Simple heuristic: count markdown list items (lines starting with - or *) + lines = release_notes.split('\n') + num_changes = sum(1 for line in lines if line.strip().startswith(('- ', '* '))) + # Count category headers (lines starting with ###) + num_categories = sum(1 for line in lines if line.strip().startswith('###')) + + # Build initial template context + issues_repo = _get_issues_repo(config) + + # Calculate date-based variables + now = datetime.now() + quarter = (now.month - 1) // 3 + 1 + quarter_uppercase = f"Q{quarter}" + + template_context = { + 'code_repo': config.repository.code_repo.replace('/', '-'), + 'issue_repo': issues_repo, + 'issue_repo_name': issue_repo_name, + 'pr_link': 'PR_LINK_PLACEHOLDER', + 'version': version, + 'major': str(target_version.major), + 'minor': str(target_version.minor), + 'patch': str(target_version.patch), + 'year': str(now.year), + 'quarter_uppercase': quarter_uppercase, + 'num_changes': num_changes if num_changes > 0 else 'several', + 'num_categories': num_categories if num_categories > 0 else 'multiple', + 'target_branch': target_branch + } + + # Create release tracking ticket if enabled + ticket_result = None + + # If force=draft, try to find existing ticket interactively first if not in DB + if config.output.create_ticket and force == 'draft' and not dry_run: + existing_association = db.get_ticket_association(repo_name, version) + if not existing_association: + ticket_result = _find_existing_ticket_interactive(config, github_client, version) + if ticket_result: + console.print(f"[blue]Reusing existing ticket #{ticket_result['number']}[/blue]") + # Save association + db.save_ticket_association( + repo_full_name=repo_name, + version=version, + ticket_number=int(ticket_result['number']), + ticket_url=ticket_result['url'] + ) + + if not ticket_result: + ticket_result = _create_release_ticket( + config=config, + github_client=github_client, + db=db, + template_context=template_context, + version=version, + override=(force != 'none'), + dry_run=dry_run, + debug=debug + ) + + # Add ticket variables to context if ticket was created + if ticket_result: + template_context.update({ + 'issue_number': ticket_result['number'], + 'issue_link': ticket_result['url'] + }) + + if debug: + console.print("\n[bold cyan]Debug Mode: Ticket Information[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Ticket created:[/dim] Yes") + console.print(f"[dim]Issue number:[/dim] {ticket_result['number']}") + console.print(f"[dim]Issue URL:[/dim] {ticket_result['url']}") + console.print(f"[dim]Repository:[/dim] {_get_issues_repo(config)}") + console.print(f"\n[dim]Template variables now available:[/dim]") + for var in sorted(template_context.keys()): + console.print(f"[dim] • {{{{{var}}}}}: {template_context[var]}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + elif debug: + console.print("\n[bold cyan]Debug Mode: Ticket Information[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Ticket creation:[/dim] {'Disabled (create_ticket=false)' if not config.output.create_ticket else 'Failed or dry-run'}") + console.print(f"\n[dim]Template variables available (without ticket):[/dim]") + for var in sorted(template_context.keys()): + console.print(f"[dim] • {{{{{{var}}}}}}: {template_context[var]}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + # Define which variables are available + available_vars = set(template_context.keys()) + ticket_vars = {'issue_number', 'issue_link'} + + # Validate templates don't use ticket variables when they're not available + # (either because create_ticket=false or ticket creation failed) + if not ticket_result: + # Check each template for ticket variables + for template_name, template_str in [ + ('branch_template', config.output.pr_templates.branch_template), + ('title_template', config.output.pr_templates.title_template), + ('body_template', config.output.pr_templates.body_template) + ]: + try: + used_vars = get_template_variables(template_str) + invalid_vars = used_vars & ticket_vars + if invalid_vars: + console.print( + f"[red]Error: PR {template_name} uses ticket variables " + f"({', '.join(sorted(invalid_vars))}) but create_ticket is disabled[/red]" + ) + console.print("[yellow]Either enable create_ticket in config or update the template.[/yellow]") + sys.exit(1) + except TemplateError as e: + console.print(f"[red]Error in PR {template_name}: {e}[/red]") + sys.exit(1) + + # Render templates using Jinja2 + try: + branch_name = render_template(config.output.pr_templates.branch_template, template_context) + pr_title = render_template(config.output.pr_templates.title_template, template_context) + pr_body = render_template(config.output.pr_templates.body_template, template_context) + + # Determine which file(s) to include in the PR + # If Docusaurus output is enabled, we prioritize it and suppress the default release output + # to avoid double commits and unwanted files. + additional_files = {} + + if doc_output_enabled and doc_notes_content: + # Use Docusaurus file as the primary file + pr_file_path = render_template(config.output.doc_output_path, template_context) + pr_content = doc_notes_content + if debug: + console.print(f"[dim]Using Docusaurus output as primary PR file: {pr_file_path}[/dim]") + else: + # Use default release output + pr_file_path = render_template(config.output.release_output_path, template_context) + pr_content = release_notes + if debug: + console.print(f"[dim]Using standard release output as primary PR file: {pr_file_path}[/dim]") + + except TemplateError as e: + console.print(f"[red]Error rendering PR template: {e}[/red]") + if debug: + raise + sys.exit(1) + + if debug: + console.print("\n[bold cyan]Debug Mode: Pull Request Details[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Branch name:[/dim] {branch_name}") + console.print(f"[dim]PR title:[/dim] {pr_title}") + console.print(f"[dim]Target branch:[/dim] {target_branch}") + console.print(f"[dim]Primary file path:[/dim] {pr_file_path}") + if additional_files: + for path in additional_files: + console.print(f"[dim]Additional file:[/dim] {path}") + console.print(f"\n[dim]PR body:[/dim]") + console.print(f"[dim]{pr_body}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + if dry_run: + console.print(f"[yellow]Would create pull request:[/yellow]") + console.print(f"[yellow] Branch: {branch_name}[/yellow]") + console.print(f"[yellow] Title: {pr_title}[/yellow]") + console.print(f"[yellow] Target: {target_branch}[/yellow]") + console.print(f"[yellow] Primary file:[/yellow] {pr_file_path}") + if additional_files: + for path in additional_files: + console.print(f"[yellow] Additional file:[/yellow] {path}") + console.print(f"\n[yellow]PR body:[/yellow]") + console.print(f"[dim]{pr_body}[/dim]\n") + else: + console.print(f"[blue]Creating PR with release notes...[/blue]") + pr_url = github_client.create_pr_for_release_notes( + repo_name, + pr_title, + pr_file_path, + pr_content, + branch_name, + target_branch, + pr_body, + additional_files=additional_files + ) + + if pr_url: + console.print(f"[green]✓ Pull request processed successfully[/green]") + + # Update ticket body with real PR link + if ticket_result and not dry_run: + try: + repo = github_client.gh.get_repo(issues_repo) + issue = repo.get_issue(int(ticket_result['number'])) + + # Check if we need to update the link + if issue.body and 'PR_LINK_PLACEHOLDER' in issue.body: + new_body = issue.body.replace('PR_LINK_PLACEHOLDER', pr_url) + github_client.update_issue_body(issues_repo, int(ticket_result['number']), new_body) + console.print(f"[green]Updated ticket #{ticket_result['number']} with PR link[/green]") + except Exception as e: + console.print(f"[yellow]Warning: Could not update ticket body with PR link: {e}[/yellow]") + else: + console.print(f"[red]Failed to create or find PR[/red]") + elif dry_run: + console.print(f"[yellow]Would NOT create pull request (--no-pr or config setting)[/yellow]\n") + + # Handle Docusaurus file if configured (only if PR creation didn't handle it) + if doc_output_enabled and not create_pr: + template_context = { + 'version': version, + 'major': str(target_version.major), + 'minor': str(target_version.minor), + 'patch': str(target_version.patch) + } + try: + doc_path = render_template(config.output.doc_output_path, template_context) + except TemplateError as e: + console.print(f"[red]Error rendering doc_output_path template: {e}[/red]") + if debug: + raise + sys.exit(1) + doc_file = Path(doc_path) + + if debug: + console.print("\n[bold cyan]Debug Mode: Documentation Release Notes[/bold cyan]") + console.print("[dim]" + "=" * 60 + "[/dim]") + console.print(f"[dim]Doc template configured:[/dim] {config.release_notes.doc_output_template is not None}") + console.print(f"[dim]Doc path template:[/dim] {config.output.doc_output_path}") + console.print(f"[dim]Resolved final path:[/dim] {doc_path}") + console.print(f"[dim]Draft source path:[/dim] {doc_notes_path if doc_notes_path else 'None'}") + console.print(f"[dim]Draft content found:[/dim] {doc_notes_content is not None}") + + if doc_notes_content: + # We have draft content to write + console.print(f"\n[bold]Full Doc Notes Content:[/bold]") + console.print(f"[dim]{'─' * 60}[/dim]") + console.print(doc_notes_content) + console.print(f"[dim]{'─' * 60}[/dim]") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + if dry_run: + console.print(f"[yellow]Would write documentation to: {doc_path}[/yellow]") + console.print(f"[yellow] Source: {doc_notes_path}[/yellow]") + console.print(f"[yellow] Size: {len(doc_notes_content)} characters[/yellow]") + else: + doc_file.parent.mkdir(parents=True, exist_ok=True) + doc_file.write_text(doc_notes_content) + console.print(f"[green]✓ Documentation written to:[/green]") + console.print(f"[green] {doc_file}[/green]") + + elif doc_file.exists(): + # Fallback: File exists but we didn't find a draft. + # This might happen if we didn't run generate or if we're just re-publishing. + # Just report it. + if debug: + try: + existing_content = doc_file.read_text() + console.print(f"[dim]Existing file size:[/dim] {len(existing_content)} characters") + except Exception as e: + console.print(f"[dim]Error reading file:[/dim] {e}") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + + if dry_run: + console.print(f"[yellow]Existing Docusaurus file found: {doc_path}[/yellow]") + console.print(f"[dim]No new draft content found to update it.[/dim]") + elif not debug: + console.print(f"[blue]Existing Docusaurus file found at {doc_file}[/blue]") + else: + if debug: + console.print(f"[dim]Status:[/dim] No draft found and no existing file") + console.print("[dim]" + "=" * 60 + "[/dim]\n") + elif dry_run: + console.print(f"[dim]No documentation draft found and no existing file at {doc_path}[/dim]") + + # Dry-run summary + if dry_run: + console.print(f"\n[yellow]{'='*80}[/yellow]") + console.print(f"[yellow]DRY RUN complete. No changes were made.[/yellow]") + console.print(f"[yellow]{'='*80}[/yellow]\n") + + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + if debug: + raise + sys.exit(1) + finally: + # Close database connection if it was opened + if 'db' in locals() and db: + db.close() + + +def _find_existing_ticket_interactive(config: Config, github_client: GitHubClient, version: str) -> Optional[dict]: + """Find existing ticket interactively.""" + issues_repo = _get_issues_repo(config) + query = f"repo:{issues_repo} is:issue {version} in:title" + console.print(f"[cyan]Searching for existing tickets in {issues_repo}...[/cyan]") + + # We need to use the underlying github client to search + # This is a bit of a hack, but we don't have a search method in GitHubClient + # Assuming github_client.gh is available + issues = list(github_client.gh.search_issues(query)[:5]) + + if not issues: + console.print("[yellow]No matching tickets found.[/yellow]") + return None + + table = Table(title="Found Tickets") + table.add_column("#", style="cyan") + table.add_column("Number", style="green") + table.add_column("Title", style="white") + table.add_column("State", style="yellow") + + for i, issue in enumerate(issues): + table.add_row(str(i+1), str(issue.number), issue.title, issue.state) + + console.print(table) + + response = input("\nSelect ticket to reuse (1-5) or 'n' to create new: ").strip().lower() + if response.isdigit() and 1 <= int(response) <= len(issues): + selected = issues[int(response)-1] + return {'number': str(selected.number), 'url': selected.html_url} + + return None diff --git a/src/release_tool/commands/sync.py b/src/release_tool/commands/sync.py new file mode 100644 index 0000000..f1a7ede --- /dev/null +++ b/src/release_tool/commands/sync.py @@ -0,0 +1,55 @@ +import click +from rich.console import Console +from ..config import Config +from ..db import Database +from ..github_utils import GitHubClient +from ..sync import SyncManager + +console = Console() + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.argument('repository', required=False) +@click.option('--repo-path', type=click.Path(exists=True), help='Path to local git repository') +@click.pass_context +def sync(ctx, repository, repo_path): + """ + Sync repository data to local database. + + Fetches tickets, PRs, releases, and commits from GitHub and stores them locally. + Uses highly parallelized fetching with incremental sync. + """ + config: Config = ctx.obj['config'] + repo_name = repository or config.repository.code_repo + + # Initialize components + db = Database(config.database.path) + db.connect() + + try: + github_client = GitHubClient(config) + sync_manager = SyncManager(config, db, github_client) + + # Use the new sync manager for parallelized, incremental sync + console.print(f"[bold blue]Starting comprehensive sync...[/bold blue]") + stats = sync_manager.sync_all() + + # Also fetch releases (not yet in SyncManager) + console.print("[blue]Fetching releases...[/blue]") + repo_info = github_client.get_repository_info(repo_name) + repo_id = db.upsert_repository(repo_info) + releases = github_client.fetch_releases(repo_name, repo_id) + for release in releases: + db.upsert_release(release) + console.print(f"[green]Synced {len(releases)} releases[/green]") + + console.print("[bold green]Sync complete![/bold green]") + console.print(f"[dim]Summary:[/dim]") + console.print(f" Tickets: {stats['tickets']}") + console.print(f" Pull Requests: {stats['pull_requests']}") + console.print(f" Releases: {len(releases)}") + console.print(f" Repositories: {', '.join(stats['repos_synced'])}") + if stats.get('git_repo_path'): + console.print(f" Git repo: {stats['git_repo_path']}") + + finally: + db.close() diff --git a/src/release_tool/commands/tickets.py b/src/release_tool/commands/tickets.py new file mode 100644 index 0000000..05fd423 --- /dev/null +++ b/src/release_tool/commands/tickets.py @@ -0,0 +1,297 @@ +import sys +from pathlib import Path +from typing import List, Optional +import click +from rich.console import Console +from rich.table import Table +import csv +import json +from io import StringIO +import re + +from ..config import Config +from ..db import Database + +console = Console() + + +def _parse_ticket_key_arg(ticket_key_arg: str) -> tuple[Optional[str], Optional[str], bool]: + """ + Parse smart TICKET_KEY argument into components. + + Supports multiple formats: + - "1234" or "#1234" -> (None, "1234", False) + - "meta#1234" -> ("meta", "1234", False) + - "meta#1234~" -> ("meta", "1234", True) # proximity search + - "owner/repo#1234" -> ("owner/repo", "1234", False) + - "owner/repo#1234~" -> ("owner/repo", "1234", True) + + Args: + ticket_key_arg: The TICKET_KEY argument from CLI + + Returns: + Tuple of (repo_filter, ticket_key, is_proximity) + - repo_filter: Repository to filter by (None if not specified) + - ticket_key: Ticket number/key + - is_proximity: True if proximity search (~) was specified + """ + # Check for proximity indicator (~) + is_proximity = ticket_key_arg.endswith('~') + if is_proximity: + ticket_key_arg = ticket_key_arg[:-1] # Remove trailing ~ + + # Check if there's a # separator (indicating repo#ticket format) + if '#' in ticket_key_arg: + # Split on the last # to handle cases like "owner/repo#1234" + parts = ticket_key_arg.rsplit('#', 1) + if len(parts) == 2 and parts[1].isdigit(): + repo_part = parts[0] if parts[0] else None + ticket_num = parts[1] + return repo_part, ticket_num, is_proximity + + # No # separator, treat as plain ticket number (with optional leading #) + match = re.match(r'^#?(\d+)$', ticket_key_arg) + if match: + return None, match.group(1), is_proximity + + # Invalid format, return as-is + return None, ticket_key_arg, is_proximity + + +def _display_tickets_table(tickets: List, limit: int, offset: int): + """Display tickets in a formatted table.""" + if not tickets: + console.print("[yellow]No tickets found.[/yellow]") + console.print("[dim]Tip: Run 'release-tool sync' to fetch latest tickets.[/dim]") + return + + table = Table(title="Tickets" if offset == 0 else f"Tickets (offset: {offset})") + table.add_column("Key", style="cyan", no_wrap=True) + table.add_column("Repository", style="blue") + table.add_column("Title") + table.add_column("State", style="dim") + table.add_column("URL", style="dim", max_width=80, overflow="fold") + + for ticket in tickets: + # Get repo name (from bypassed Pydantic attribute or fallback) + repo_name = getattr(ticket, '_repo_full_name', 'unknown') + + # Color code state + state_style = "green" if ticket.state == "open" else "dim" + state_text = f"[{state_style}]{ticket.state}[/{state_style}]" + + # Truncate title if too long + title = ticket.title[:60] + "..." if len(ticket.title) > 60 else ticket.title + + # Don't truncate URL - let Rich handle it with max_width and overflow + url = ticket.url if ticket.url else "" + + table.add_row( + f"#{ticket.key}", + repo_name, + title, + state_text, + url + ) + + console.print(table) + + # Show pagination info + total_shown = len(tickets) + start_num = offset + 1 + end_num = offset + total_shown + + if total_shown == limit: + console.print(f"\n[dim]Showing {start_num}-{end_num} tickets (use --offset to see more)[/dim]") + else: + console.print(f"\n[dim]Showing {start_num}-{end_num} tickets (all results)[/dim]") + + +def _display_tickets_csv(tickets: List): + """Display tickets in CSV format.""" + if not tickets: + return + + # Use StringIO to build CSV, then print + output = StringIO() + + # Define all fields to export + fieldnames = [ + 'id', 'repo_id', 'number', 'key', 'title', 'body', 'state', + 'labels', 'url', 'created_at', 'closed_at', 'category', 'tags', + 'repo_full_name' + ] + + writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction='ignore') + writer.writeheader() + + for ticket in tickets: + row = { + 'id': ticket.id, + 'repo_id': ticket.repo_id, + 'number': ticket.number, + 'key': ticket.key, + 'title': ticket.title, + 'body': ticket.body[:500] if ticket.body else "", # Truncate long bodies + 'state': ticket.state, + 'labels': json.dumps([l.name for l in ticket.labels]), + 'url': ticket.url or "", + 'created_at': ticket.created_at.isoformat() if ticket.created_at else "", + 'closed_at': ticket.closed_at.isoformat() if ticket.closed_at else "", + 'category': ticket.category or "", + 'tags': json.dumps(ticket.tags), + 'repo_full_name': getattr(ticket, '_repo_full_name', '') + } + writer.writerow(row) + + # Print to stdout (user can redirect with >) + print(output.getvalue(), end='') + + +@click.command(name='tickets', context_settings={'help_option_names': ['-h', '--help']}) +@click.argument('ticket_key', required=False) +@click.option('--repo', '-r', help='Filter by repository (owner/name)') +@click.option('--limit', '-n', type=int, default=20, help='Max number of results (default: 20)') +@click.option('--offset', type=int, default=0, help='Skip first N results (for pagination)') +@click.option('--format', '-f', 'output_format', type=click.Choice(['table', 'csv']), default='table', help='Output format') +@click.option('--starts-with', help='Find tickets starting with prefix (fuzzy match)') +@click.option('--ends-with', help='Find tickets ending with suffix (fuzzy match)') +@click.option('--close-to', help='Find tickets numerically close to this number') +@click.option('--range', 'close_range', type=int, default=10, help='Range for --close-to (default: ±10)') +@click.pass_context +def tickets(ctx, ticket_key, repo, limit, offset, output_format, starts_with, ends_with, close_to, close_range): + """Query tickets from local database (offline). + + IMPORTANT: This command works offline and only searches synced data. + Run 'release-tool sync' first to ensure you have the latest tickets. + + TICKET_KEY supports smart formats: + + \b + 1234 Find ticket by number + #1234 Find ticket by number (with # prefix) + meta#1234 Find ticket 1234 in repo 'meta' + meta#1234~ Find tickets close to 1234 (±20) + owner/repo#1234 Find ticket in specific full repo path + + Examples: + + release-tool tickets 8624 + + release-tool tickets meta#8624 + + release-tool tickets meta#8624~ + + release-tool tickets --repo sequentech/meta --limit 50 + + release-tool tickets --starts-with 86 + + release-tool tickets --ends-with 24 + + release-tool tickets --close-to 8624 --range 50 + + release-tool tickets --repo sequentech/meta --format csv > tickets.csv + """ + config: Config = ctx.obj['config'] + + # Parse smart TICKET_KEY format if provided + parsed_repo = None + parsed_ticket = None + parsed_proximity = False + + if ticket_key: + # Special case: if ticket_key looks like a repo name (contains "/" but no "#" or ticket number), + # treat it as a --repo filter instead of a ticket key + if '/' in ticket_key and '#' not in ticket_key and not any(c.isdigit() for c in ticket_key.split('/')[-1]): + # This looks like "owner/repo" format, use as repo filter + if not repo: + repo = ticket_key + ticket_key = None + else: + parsed_repo, parsed_ticket, parsed_proximity = _parse_ticket_key_arg(ticket_key) + + # If repo was parsed from ticket_key, use it (unless --repo was also specified) + if parsed_repo and not repo: + repo = parsed_repo + + # If proximity search (~) was indicated, use close_to + if parsed_proximity and not close_to: + close_to = parsed_ticket + parsed_ticket = None # Don't use as exact match + + # Use parsed ticket as the key + ticket_key = parsed_ticket + + # Validation + if close_range < 0: + console.print("[red]Error: --range must be >= 0[/red]") + sys.exit(1) + + if limit <= 0: + console.print("[red]Error: --limit must be > 0[/red]") + sys.exit(1) + + if offset < 0: + console.print("[red]Error: --offset must be >= 0[/red]") + sys.exit(1) + + # Cannot combine close_to with starts_with or ends_with + if close_to and (starts_with or ends_with): + console.print("[red]Error: Cannot combine --close-to with --starts-with or --ends-with[/red]") + sys.exit(1) + + # Open database + db_path = Path(config.database.path) + if not db_path.exists(): + console.print("[red]Error: Database not found. Please run 'release-tool sync' first.[/red]") + sys.exit(1) + + db = Database(str(db_path)) + db.connect() + + # Convert repo name to repo_id if needed + repo_id = None + if repo: + # Try as full name first + repo_obj = db.get_repository(repo) + + # If not found and repo doesn't contain '/', try finding by name only + if not repo_obj and '/' not in repo: + # Search for repos matching this name + all_repos = db.get_all_repositories() + matching = [r for r in all_repos if r.name == repo] + if len(matching) == 1: + repo_obj = matching[0] + elif len(matching) > 1: + console.print(f"[red]Error: Multiple repositories match '{repo}'. Please specify as owner/repo:[/red]") + for r in matching: + console.print(f" - {r.full_name}") + sys.exit(1) + + if not repo_obj: + console.print(f"[red]Error: Repository '{repo}' not found in database.[/red]") + console.print("[yellow]Tip: Run 'release-tool sync' to fetch repository data.[/yellow]") + sys.exit(1) + repo_id = repo_obj.id + + # Query tickets + try: + tickets = db.query_tickets( + ticket_key=ticket_key, + repo_id=repo_id, + starts_with=starts_with, + ends_with=ends_with, + close_to=close_to, + close_range=close_range, + limit=limit, + offset=offset + ) + except Exception as e: + console.print(f"[red]Error querying tickets: {e}[/red]") + sys.exit(1) + + # Display results + if output_format == 'table': + _display_tickets_table(tickets, limit, offset) + else: + _display_tickets_csv(tickets) diff --git a/src/release_tool/commands/update_config.py b/src/release_tool/commands/update_config.py new file mode 100644 index 0000000..e68559d --- /dev/null +++ b/src/release_tool/commands/update_config.py @@ -0,0 +1,250 @@ +import sys +from pathlib import Path +from typing import Optional +import click +from rich.console import Console +import tomlkit + +console = Console() + + +def _merge_config_with_template(user_data: dict, template_doc) -> dict: + """Merge user config with template, preserving comments and structure. + + Args: + user_data: User's config as plain dict (from tomli) + template_doc: Template loaded with tomlkit (has comments) + + Returns: + Merged tomlkit document with template comments and user values + """ + def to_tomlkit_value(value): + """Convert plain Python value to tomlkit type to preserve comments.""" + if isinstance(value, dict): + result = tomlkit.table() + for k, v in value.items(): + result[k] = to_tomlkit_value(v) + return result + elif isinstance(value, list): + result = tomlkit.array() + for item in value: + result.append(to_tomlkit_value(item)) + return result + else: + # Scalars (str, int, bool, etc.) are fine as-is + return value + + def values_equal(val1, val2): + """Check if two values are equal for merge purposes.""" + # Convert both to comparable types + if isinstance(val1, (list, dict)) and isinstance(val2, (list, dict)): + # Use unwrap to get plain Python objects for comparison + v1 = val1.unwrap() if hasattr(val1, 'unwrap') else val1 + v2 = val2.unwrap() if hasattr(val2, 'unwrap') else val2 + return v1 == v2 + else: + # For scalars, convert to string for comparison + return str(val1) == str(val2) + + def update_values_in_place(template_item, user_value): + """Update template values in-place with user values.""" + if isinstance(template_item, dict) and isinstance(user_value, dict): + # Update each key in template with user's value + # Create list of keys first to avoid "dictionary changed during iteration" + for key in list(template_item.keys()): + if key in user_value: + template_val = template_item[key] + user_val = user_value[key] + + # SKIP updating if values are identical - this preserves comments! + if values_equal(template_val, user_val): + continue + + # Check if we need to recurse + if isinstance(template_val, dict) and isinstance(user_val, dict): + update_values_in_place(template_val, user_val) + # Special handling for AoT (Array of Tables) - preserve the type + elif isinstance(template_val, tomlkit.items.AoT) and isinstance(user_val, list): + # Clear existing items and repopulate with user data + template_val.clear() + for item in user_val: + template_val.append(to_tomlkit_value(item)) + elif isinstance(template_val, list) and isinstance(user_val, list): + # For regular lists, preserve trivia and convert to tomlkit array + old_trivia = template_val.trivia if hasattr(template_val, 'trivia') else None + new_val = to_tomlkit_value(user_val) + if old_trivia and hasattr(new_val, 'trivia'): + new_val.trivia.indent = old_trivia.indent + new_val.trivia.comment_ws = old_trivia.comment_ws + new_val.trivia.comment = old_trivia.comment + new_val.trivia.trail = old_trivia.trail + template_item[key] = new_val + else: + # Primitive value - preserve trivia and convert to tomlkit type + old_trivia = template_val.trivia if hasattr(template_val, 'trivia') else None + new_val = to_tomlkit_value(user_val) + if old_trivia and hasattr(new_val, 'trivia'): + new_val.trivia.indent = old_trivia.indent + new_val.trivia.comment_ws = old_trivia.comment_ws + new_val.trivia.comment = old_trivia.comment + new_val.trivia.trail = old_trivia.trail + template_item[key] = new_val + + # Add any keys from user that template doesn't have + for key in user_value: + if key not in template_item: + template_item[key] = to_tomlkit_value(user_value[key]) + + # Modify template in-place to preserve comments + # Create list of keys first to avoid "dictionary changed during iteration" + for key in list(template_doc.keys()): + if key in user_data: + template_item = template_doc[key] + + # Update values in place + update_values_in_place(template_item, user_data[key]) + + # Add any top-level keys user has that template doesn't have + for key in user_data: + if key not in template_doc: + template_doc[key] = to_tomlkit_value(user_data[key]) + + return template_doc + + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.option( + '--dry-run', + is_flag=True, + help='Show what would be upgraded without making changes' +) +@click.option( + '--target-version', + help='Target version to upgrade to (default: latest)' +) +@click.option( + '--restore-comments', + is_flag=True, + help='Restore comments and reformat templates (works on same version)' +) +@click.option('-y', '--assume-yes', is_flag=True, help='Assume "yes" for confirmation prompts') +@click.pass_context +def update_config(ctx, dry_run: bool, target_version: Optional[str], restore_comments: bool, assume_yes: bool): + """Update configuration file to the latest version. + + This command upgrades your release_tool.toml configuration file to the + latest format version, applying any necessary migrations. + """ + from ..migrations import MigrationManager + + # Determine config file path + config_path = ctx.parent.params.get('config') if ctx.parent else None + if not config_path: + # Look for default config files + default_paths = [ + "release_tool.toml", + ".release_tool.toml", + "config/release_tool.toml" + ] + for path in default_paths: + if Path(path).exists(): + config_path = path + break + + if not config_path: + console.print("[red]Error: No configuration file found[/red]") + console.print("Please specify a config file with --config or create one using:") + console.print(" release-tool init-config") + sys.exit(1) + + config_path = Path(config_path) + console.print(f"[blue]Checking configuration file: {config_path}[/blue]\n") + + # Load current config (use tomlkit to preserve comments) + try: + with open(config_path, 'r', encoding='utf-8') as f: + data = tomlkit.load(f) + except Exception as e: + console.print(f"[red]Error reading config file: {e}[/red]") + sys.exit(1) + + # Check version first to determine if we need to merge with template + manager = MigrationManager() + current_version = data.get('config_version', '1.0') + target_ver = target_version or manager.CURRENT_VERSION + + # Always load template and merge to preserve comments during upgrades + # This ensures user's values are kept but template comments are restored + if manager.needs_upgrade(current_version) or restore_comments: + try: + template_path = Path(__file__).parent.parent / "config_template.toml" + with open(template_path, 'r', encoding='utf-8') as f: + template_doc = tomlkit.load(f) + + # Merge: use template structure/comments but user's values + data = _merge_config_with_template(data, template_doc) + console.print("[dim]✓ Loaded comments from template[/dim]") + except Exception as e: + console.print(f"[yellow]Warning: Could not load template comments: {e}[/yellow]") + # Continue without comments - not critical + + console.print(f"Current version: [yellow]{current_version}[/yellow]") + console.print(f"Target version: [green]{target_ver}[/green]\n") + + # Check if upgrade is needed (unless restoring comments) + if not manager.needs_upgrade(current_version) and not restore_comments: + console.print("[green]✓ Configuration is already up to date![/green]") + return + + # If restoring comments on same version, show different message + if restore_comments and current_version == target_ver: + console.print("[blue]Restoring comments and reformatting templates...[/blue]\n") + else: + # Show changes + changes = manager.get_changes_description(current_version, target_ver) + console.print("[blue]Changes:[/blue]") + console.print(changes) + console.print() + + if dry_run: + console.print("[yellow]Dry-run mode: No changes made[/yellow]") + return + + # Get flags from context (for global -y flag) and merge with local parameter + auto = ctx.obj.get('auto', False) + assume_yes_global = ctx.obj.get('assume_yes', False) + assume_yes_effective = assume_yes or assume_yes_global + + # Confirm upgrade + if not (auto or assume_yes_effective): + if not click.confirm(f"Upgrade config from v{current_version} to v{target_ver}?"): + console.print("[yellow]Upgrade cancelled[/yellow]") + return + + # Apply migrations + try: + console.print(f"[blue]Upgrading configuration...[/blue]") + + # If restoring comments on same version, force run current version migration + if restore_comments and current_version == target_ver: + # For v1.1, reapply the v1.0 -> v1.1 migration to reformat templates + if current_version == "1.1": + from ..migrations.v1_0_to_v1_1 import migrate as v1_1_migrate + upgraded_data = v1_1_migrate(data) + else: + # For other versions, just use the data as-is + upgraded_data = data + else: + # Normal upgrade path + upgraded_data = manager.upgrade_config(data, target_ver) + + # Save back to file + with open(config_path, 'w', encoding='utf-8') as f: + f.write(tomlkit.dumps(upgraded_data)) + + console.print(f"[green]✓ Configuration upgraded to v{target_ver}![/green]") + console.print(f"[green]✓ Saved to {config_path}[/green]") + + except Exception as e: + console.print(f"[red]Error during upgrade: {e}[/red]") + sys.exit(1) diff --git a/src/release_tool/config.py b/src/release_tool/config.py new file mode 100644 index 0000000..e5315c3 --- /dev/null +++ b/src/release_tool/config.py @@ -0,0 +1,645 @@ +"""Configuration management for the release tool.""" + +import os +from pathlib import Path +from typing import Dict, List, Optional, Any, Union, Literal +from enum import Enum +from pydantic import BaseModel, Field, model_validator +import tomli +import tomlkit + + +class PolicyAction(str, Enum): + """Policy action types.""" + IGNORE = "ignore" + WARN = "warn" + ERROR = "error" + + +class TicketExtractionStrategy(str, Enum): + """Ticket extraction strategies.""" + BRANCH_NAME = "branch_name" + COMMIT_MESSAGE = "commit_message" + PR_BODY = "pr_body" + PR_TITLE = "pr_title" + + +class TicketPattern(BaseModel): + """A ticket extraction pattern with its associated strategy.""" + order: int + strategy: TicketExtractionStrategy + pattern: str + description: Optional[str] = None + + +class CategoryConfig(BaseModel): + """Category configuration.""" + name: str + labels: List[str] + order: int = 0 + alias: Optional[str] = None # Short alias for template references + + def matches_label(self, label: str, source: str) -> bool: + """ + Check if a label matches this category. + + Args: + label: The label name to check + source: Either "pr" or "ticket" indicating where the label comes from + + Returns: + True if the label matches this category + """ + for pattern in self.labels: + # Check for prefix (pr:, ticket:, or no prefix for any) + if pattern.startswith("pr:"): + # Only match PR labels + if source == "pr" and pattern[3:] == label: + return True + elif pattern.startswith("ticket:"): + # Only match ticket labels + if source == "ticket" and pattern[7:] == label: + return True + else: + # No prefix = match from any source + if pattern == label: + return True + return False + + +class PRTemplateConfig(BaseModel): + """Pull request template configuration for release notes.""" + branch_template: str = Field( + default="docs/{{issue_repo}}-{{issue_number}}/{{target_branch}}", + description="Branch name template for release notes PR (Jinja2 syntax)" + ) + title_template: str = Field( + default="Release notes for {{version}}", + description="PR title template (Jinja2 syntax)" + ) + body_template: str = Field( + default="Parent issue: {{issue_link}}\n\n" + "Automated release notes for version {{version}}.\n\n" + "## Summary\n" + "This PR adds release notes for {{version}} with {{num_changes}} changes across {{num_categories}} categories.", + description="PR body template (Jinja2 syntax)" + ) + + +class TicketTemplateConfig(BaseModel): + """Ticket template configuration for release tracking.""" + title_template: str = Field( + default="✨ Prepare Release {{version}}", + description="Ticket title template (Jinja2 syntax). Available variables: {{version}}, {{major}}, {{minor}}, {{patch}}" + ) + body_template: str = Field( + default=( + "### DevOps Tasks\n\n" + "- [ ] Github release notes: correct and complete\n" + "- [ ] Docusaurus release notes: correct and complete\n" + "- [ ] BEYOND-PR-HERE for a new default tenant/election-event template and any new other changes (branch should be `release/{{major}}.{{minor}}`)\n" + "- [ ] GITOPS-PR-HERE for a new default tenant/election-event template and any new other changes (branch should be `release/{{major}}.{{minor}}`)\n" + "- [ ] Request in [Environment spreadsheet](https://docs.google.com/spreadsheets/d/1TDxb8r9dZKwNxHc3lAL0mtFDSsoou985NX_Y44eA7V4/edit#gid=0) to get deployment approval by environment owners\n\n" + "NOTE: Please also update deployment status when a release is deployed in an environment.\n\n" + "### QA Flight List\n\n" + "- [ ] Deploy in `dev`\n" + "- [ ] Positive Test in `dev`\n" + "- [ ] Deploy in `qa`\n" + "- [ ] Positive Test in `qa`\n\n" + "### PRs to deploy new version in different environments\n\n" + "- [ ] PR 1" + ), + description="Ticket body template (Jinja2 syntax). Available variables: {{version}}, {{major}}, {{minor}}, {{patch}}, " + "{{num_changes}}, {{num_categories}}" + ) + labels: List[str] = Field( + default_factory=lambda: ["release", "devops", "infrastructure"], + description="Labels to apply to the release tracking ticket" + ) + assignee: Optional[str] = Field( + default=None, + description="GitHub username to assign the ticket to. If None, assigns to the authenticated user from the GitHub token." + ) + project_id: Optional[str] = Field( + default=None, + description="GitHub Project ID (number) to add the ticket to. Find this in the project URL: github.com/orgs/ORG/projects/ID" + ) + project_status: Optional[str] = Field( + default=None, + description="Status to set in the GitHub Project (e.g., 'Todo', 'In Progress', 'Done')" + ) + project_fields: Dict[str, str] = Field( + default_factory=dict, + description="Custom fields to set in the GitHub Project. Maps field name to field value. " + "Example: {'Priority': 'High', 'Sprint': '2024-Q1'}" + ) + type: Optional[str] = Field( + default=None, + description="Issue type to set (e.g., 'Task', 'Bug'). This is often mapped to a label or a specific field." + ) + milestone: Optional[str] = Field( + default=None, + description="Milestone name to assign the ticket to (e.g., 'v1.0.0')." + ) + + +class TicketPolicyConfig(BaseModel): + """Ticket extraction and consolidation policy configuration.""" + patterns: List[TicketPattern] = Field( + default_factory=lambda: [ + TicketPattern( + order=1, + strategy=TicketExtractionStrategy.BRANCH_NAME, + pattern=r'/(?P\w+)-(?P\d+)', + description="Branch names like feat/meta-123/main" + ), + TicketPattern( + order=2, + strategy=TicketExtractionStrategy.PR_BODY, + pattern=r'Parent issue:.*?/issues/(?P\d+)', + description="Parent issue URL in PR body" + ), + TicketPattern( + order=3, + strategy=TicketExtractionStrategy.PR_TITLE, + pattern=r'#(?P\d+)', + description="GitHub issue reference in PR title" + ), + TicketPattern( + order=4, + strategy=TicketExtractionStrategy.COMMIT_MESSAGE, + pattern=r'#(?P\d+)', + description="GitHub issue reference in commit message" + ), + TicketPattern( + order=5, + strategy=TicketExtractionStrategy.COMMIT_MESSAGE, + pattern=r'(?P[A-Z]+)-(?P\d+)', + description="JIRA-style tickets in commit message" + ), + ], + description="Ordered list of patterns with their extraction strategies" + ) + no_ticket_action: PolicyAction = Field( + default=PolicyAction.WARN, + description="What to do when no ticket is found" + ) + unclosed_ticket_action: PolicyAction = Field( + default=PolicyAction.WARN, + description="What to do with unclosed tickets" + ) + partial_ticket_action: PolicyAction = Field( + default=PolicyAction.WARN, + description="What to do with partial ticket matches (extracted but not found or wrong repo)" + ) + inter_release_duplicate_action: PolicyAction = Field( + default=PolicyAction.WARN, + description="What to do when a ticket appears in multiple releases (ignore=exclude from new release, warn=include but warn, error=fail)" + ) + consolidation_enabled: bool = Field( + default=True, + description="Whether to consolidate commits by parent ticket" + ) + description_section_regex: Optional[str] = Field( + default=r'(?:## Description|## Summary)\n(.*?)(?=\n##|\Z)', + description="Regex to extract description from ticket body" + ) + migration_section_regex: Optional[str] = Field( + default=r'(?:## Migration|## Migration Notes)\n(.*?)(?=\n##|\Z)', + description="Regex to extract migration notes from ticket body" + ) + + +class VersionPolicyConfig(BaseModel): + """Version comparison and gap policy configuration.""" + gap_detection: PolicyAction = Field( + default=PolicyAction.WARN, + description="What to do when version gaps are detected" + ) + tag_prefix: str = Field( + default="v", + description="Prefix for version tags" + ) + + +class BranchPolicyConfig(BaseModel): + """Branch management policy for releases.""" + release_branch_template: str = Field( + default="release/{major}.{minor}", + description="Template for release branch names. Use {major}, {minor}, {patch} placeholders" + ) + default_branch: str = Field( + default="main", + description="Default branch for new major versions" + ) + create_branches: bool = Field( + default=True, + description="Automatically create release branches if they don't exist" + ) + branch_from_previous_release: bool = Field( + default=True, + description="Branch new minor versions from previous release branch (if it exists)" + ) + + +class ReleaseNoteConfig(BaseModel): + """Release note generation configuration.""" + categories: List[CategoryConfig] = Field( + default_factory=lambda: [ + CategoryConfig( + name="💥 Breaking Changes", + labels=["breaking-change", "breaking"], + order=1, + alias="breaking" + ), + CategoryConfig( + name="🚀 Features", + labels=["feature", "enhancement", "feat"], + order=2, + alias="features" + ), + CategoryConfig( + name="🛠 Bug Fixes", + labels=["bug", "fix", "bugfix", "hotfix"], + order=3, + alias="bugfixes" + ), + CategoryConfig( + name="📖 Documentation", + labels=["docs", "documentation"], + order=4, + alias="docs" + ), + CategoryConfig( + name="🛡 Security Updates", + labels=["security"], + order=5, + alias="security" + ), + CategoryConfig( + name="Other Changes", + labels=[], + order=99, + alias="other" + ) + ], + description="Categories for grouping release notes" + ) + excluded_labels: List[str] = Field( + default_factory=lambda: ["skip-changelog", "internal", "wip", "do-not-merge"], + description="Labels that exclude items from release notes" + ) + title_template: str = Field( + default="Release {{ version }}", + description="Jinja2 template for release title" + ) + description_template: str = Field( + default="", + description="Jinja2 template for release description (deprecated - use output_template)" + ) + entry_template: str = Field( + default=( + "- {{ title }}\n" + " {% if short_repo_link %}{{ short_repo_link }}{% endif %}\n" + " {% if authors %}\n" + " by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}\n" + " {% endif %}" + ), + description="Jinja2 template for each release note entry (used as sub-template in output_template)" + ) + release_output_template: Optional[str] = Field( + default=( + "{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if breaking_with_desc|length > 0 %}\n" + "## 💥 Breaking Changes\n" + "\n" + "{% for note in breaking_with_desc %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.description }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "{% set migration_notes = all_notes|selectattr('migration_notes')|list %}\n" + "{% if migration_notes|length > 0 %}\n" + "## 🔄 Migrations\n" + "\n" + "{% for note in migration_notes %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.migration_notes }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "{% set non_breaking_with_desc = all_notes|rejectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if non_breaking_with_desc|length > 0 %}\n" + "## 📝 Highlights\n" + "\n" + "{% for note in non_breaking_with_desc %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.description }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "## 📋 All Changes\n" + "\n" + "{% for category in categories %}\n" + "### {{ category.name }}\n" + "\n" + "{% for note in category.notes %}\n" + "{{ render_entry(note) }}\n" + "\n" + "{% endfor %}\n" + "{% endfor %}" + ), + description="Master Jinja2 template for GitHub release notes output. " + "Available variables: version, title, categories (with 'alias' field), " + "all_notes, render_entry (function to render entry_template). " + "Note variables: title, url (prioritizes ticket_url over pr_url), ticket_url, pr_url, " + "short_link (#1234), short_repo_link (owner/repo#1234), pr_numbers, authors, description, etc." + ) + doc_output_template: Optional[str] = Field( + default=None, + description="Jinja2 template for Docusaurus/documentation release notes output. " + "Wraps the GitHub release notes with documentation-specific formatting (e.g., frontmatter). " + "Available variables: version, title, categories, all_notes, render_entry, " + "render_release_notes (function to render release_output_template). " + "Example: '---\\nid: release-{{version}}\\ntitle: {{title}}\\n---\\n{{ render_release_notes() }}'" + ) + + +class SyncConfig(BaseModel): + """Sync configuration for GitHub data fetching.""" + cutoff_date: Optional[str] = Field( + default=None, + description="ISO format date (YYYY-MM-DD) to limit historical fetching. Only fetch tickets/PRs from this date onwards." + ) + parallel_workers: int = Field( + default=20, + description="Number of parallel workers for GitHub API calls" + ) + clone_code_repo: bool = Field( + default=True, + description="Whether to clone/sync the code repository locally for offline operation" + ) + code_repo_path: Optional[str] = Field( + default=None, + description="Local path to clone code repository. Defaults to .release_tool_cache/{repo_name}" + ) + show_progress: bool = Field( + default=True, + description="Show progress updates during sync (e.g., 'syncing 13 / 156 tickets')" + ) + + +class RepositoryConfig(BaseModel): + """Repository configuration.""" + code_repo: str = Field( + description="Full name of code repository (owner/name)" + ) + ticket_repos: List[str] = Field( + default_factory=list, + description="List of ticket repository names (owner/name). If empty, uses code_repo." + ) + default_branch: Optional[str] = Field( + default=None, + description="Default branch name (deprecated: use branch_policy.default_branch instead)" + ) + + +class GitHubConfig(BaseModel): + """GitHub configuration.""" + token: Optional[str] = Field( + default=None, + description="GitHub API token (can also use GITHUB_TOKEN env var)" + ) + api_url: str = Field( + default="https://api.github.com", + description="GitHub API URL" + ) + + +class DatabaseConfig(BaseModel): + """Database configuration.""" + path: str = Field( + default="release_tool.db", + description="Path to SQLite database file" + ) + + +class OutputConfig(BaseModel): + """Output configuration for release notes.""" + release_output_path: str = Field( + default="docs/releases/{version}.md", + description="File path template for GitHub release notes (supports {version}, {major}, {minor}, {patch})" + ) + doc_output_path: Optional[str] = Field( + default=None, + description="File path template for Docusaurus/documentation release notes (supports {version}, {major}, {minor}, {patch}). " + "If set, doc_output_template must also be configured." + ) + draft_output_path: str = Field( + default=".release_tool_cache/draft-releases/{{code_repo}}/{{version}}.md", + description="File path template for draft release notes (supports {{code_repo}}, {{version}}, {{major}}, {{minor}}, {{patch}})" + ) + assets_path: str = Field( + default="docs/releases/assets/{version}", + description="Path template for downloaded media assets (images, videos)" + ) + download_media: bool = Field( + default=True, + description="Download and include images/videos from ticket descriptions" + ) + create_github_release: bool = Field( + default=False, + description="Whether to create a GitHub release" + ) + create_pr: bool = Field( + default=False, + description="Whether to create a PR with release notes" + ) + release_mode: Literal["draft", "published"] = Field( + default="draft", + description="Default release mode: 'draft' or 'published'" + ) + prerelease: Union[bool, Literal["auto"]] = Field( + default="auto", + description="Mark GitHub releases as prereleases. Options: 'auto' (detect from version), true, false" + ) + create_ticket: bool = Field( + default=True, + description="Whether to create a tracking issue for the release. " + "When true, a GitHub issue will be created and PR templates can use {{issue_repo}}, {{issue_number}}, and {{issue_link}} variables." + ) + ticket_templates: TicketTemplateConfig = Field( + default_factory=TicketTemplateConfig, + description="Templates for release tracking ticket (title, body, labels)" + ) + pr_templates: PRTemplateConfig = Field( + default_factory=PRTemplateConfig, + description="Templates for PR branch, title, and body" + ) + + +class Config(BaseModel): + """Main configuration model.""" + config_version: str = Field(default="1.1", description="Config file format version") + repository: RepositoryConfig + github: GitHubConfig = Field(default_factory=GitHubConfig) + database: DatabaseConfig = Field(default_factory=DatabaseConfig) + sync: SyncConfig = Field(default_factory=SyncConfig) + ticket_policy: TicketPolicyConfig = Field(default_factory=TicketPolicyConfig) + version_policy: VersionPolicyConfig = Field(default_factory=VersionPolicyConfig) + branch_policy: BranchPolicyConfig = Field(default_factory=BranchPolicyConfig) + release_notes: ReleaseNoteConfig = Field(default_factory=ReleaseNoteConfig) + output: OutputConfig = Field(default_factory=OutputConfig) + + @model_validator(mode='after') + def validate_doc_output(self): + """Validate that doc_output_path requires doc_output_template.""" + if self.output.doc_output_path and not self.release_notes.doc_output_template: + raise ValueError( + "doc_output_path is configured but doc_output_template is not set. " + "Both must be configured together for Docusaurus output." + ) + return self + + @classmethod + def from_file(cls, config_path: str, auto_upgrade: bool = False) -> "Config": + """Load configuration from TOML file. + + Args: + config_path: Path to the config file + auto_upgrade: If True, automatically upgrade old configs without prompting + + Returns: + Config object + """ + path = Path(config_path) + if not path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + with open(path, 'rb') as f: + data = tomli.load(f) + + # Check and upgrade config version if needed + from .migrations import MigrationManager + manager = MigrationManager() + current_version = data.get('config_version', '1.0') + + if manager.needs_upgrade(current_version): + # Config is out of date + target_version = manager.CURRENT_VERSION + changes = manager.get_changes_description(current_version, target_version) + + if auto_upgrade: + # Auto-upgrade without prompting + print(f"Auto-upgrading config from v{current_version} to v{target_version}...") + data = manager.upgrade_config(data, target_version) + + # Save upgraded config back to file + with open(path, 'w', encoding='utf-8') as f: + f.write(tomlkit.dumps(data)) + print(f"Config upgraded and saved to {config_path}") + else: + # Prompt user to upgrade + print(f"\n⚠️ Config file is version {current_version}, but current version is {target_version}") + print(f"\nChanges in v{target_version}:") + print(changes) + print(f"\nYou need to upgrade your config file to continue.") + + response = input("\nUpgrade now? [Y/n]: ").strip().lower() + if response in ['', 'y', 'yes']: + data = manager.upgrade_config(data, target_version) + + # Save upgraded config back to file + with open(path, 'w', encoding='utf-8') as f: + f.write(tomlkit.dumps(data)) + print(f"✓ Config upgraded to v{target_version} and saved to {config_path}") + else: + raise ValueError( + f"Config version {current_version} is not supported. " + f"Please upgrade to v{target_version} using: release-tool update-config" + ) + + # Override GitHub token from environment if present + if 'github' not in data: + data['github'] = {} + if not data['github'].get('token'): + data['github']['token'] = os.getenv('GITHUB_TOKEN') + + return cls(**data) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Config": + """Load configuration from dictionary.""" + # Override GitHub token from environment if present + if 'github' not in data: + data['github'] = {} + if not data['github'].get('token'): + data['github']['token'] = os.getenv('GITHUB_TOKEN') + + return cls(**data) + + def get_ticket_repos(self) -> List[str]: + """Get the list of ticket repositories (defaults to code repo if not specified).""" + if self.repository.ticket_repos: + return self.repository.ticket_repos + return [self.repository.code_repo] + + def get_code_repo_path(self) -> str: + """Get the local path for the cloned code repository.""" + if self.sync.code_repo_path: + return self.sync.code_repo_path + # Default to .release_tool_cache/{repo_name} + repo_name = self.repository.code_repo.split('/')[-1] + return str(Path.cwd() / '.release_tool_cache' / repo_name) + + def get_category_map(self) -> Dict[str, List[str]]: + """Get a mapping of category names to their labels.""" + return {cat.name: cat.labels for cat in self.release_notes.categories} + + def get_ordered_categories(self) -> List[str]: + """Get category names in order.""" + sorted_cats = sorted(self.release_notes.categories, key=lambda c: c.order) + return [cat.name for cat in sorted_cats] + + +def load_config(config_path: Optional[str] = None, auto_upgrade: bool = False) -> Config: + """Load configuration from file or use defaults. + + Args: + config_path: Path to config file (optional, will search default locations if not provided) + auto_upgrade: If True, automatically upgrade old configs without prompting + + Returns: + Config object + """ + if config_path and Path(config_path).exists(): + return Config.from_file(config_path, auto_upgrade=auto_upgrade) + + # Look for default config files + default_paths = [ + "release_tool.toml", + ".release_tool.toml", + "config/release_tool.toml" + ] + + for default_path in default_paths: + if Path(default_path).exists(): + return Config.from_file(default_path, auto_upgrade=auto_upgrade) + + # Return minimal config if no file found (will fail validation if required fields missing) + raise FileNotFoundError( + "No configuration file found. Please create release_tool.toml with required settings." + ) diff --git a/src/release_tool/config_template.toml b/src/release_tool/config_template.toml new file mode 100644 index 0000000..9b0f41a --- /dev/null +++ b/src/release_tool/config_template.toml @@ -0,0 +1,979 @@ +config_version = "1.4" + +# ============================================================================= +# Release Tool Configuration +# ============================================================================= +# This file controls how the release tool generates release notes by managing: +# - Repository information and GitHub integration +# - Ticket extraction and consolidation policies +# - Version comparison and gap detection +# - Release note categorization and formatting +# - Output destinations (file, GitHub release, PR) + +# ============================================================================= +# Repository Configuration +# ============================================================================= +[repository] +# code_repo (REQUIRED): The GitHub repository containing the code +# Format: "owner/repo" (e.g., "sequentech/voting-booth") +code_repo = "sequentech/step" + +# ticket_repos: List of repositories where tickets/issues are tracked +# If empty, uses code_repo for tickets as well +# This is useful when tickets are tracked in different repos than the code +# Default: [] (uses code_repo) +ticket_repos = ["sequentech/meta"] + +# ============================================================================= +# GitHub API Configuration +# ============================================================================= +[github] +# token: GitHub Personal Access Token for API authentication +# RECOMMENDED: Use GITHUB_TOKEN environment variable instead of storing here +# The token needs the following permissions: +# - repo (for accessing repositories, PRs, issues) +# - write:packages (if creating releases) +# How to create: https://github.com/settings/tokens +# Default: reads from GITHUB_TOKEN environment variable +# token = "ghp_..." + +# api_url: GitHub API base URL +# Change this only if using GitHub Enterprise Server +# Default: "https://api.github.com" +# For GitHub Enterprise: "https://github.yourcompany.com/api/v3" +api_url = "https://api.github.com" + +# ============================================================================= +# Database Configuration +# ============================================================================= +[database] +# path: Location of the SQLite database file for caching GitHub data +# The database stores PRs, commits, tickets, and releases to minimize API calls +# Relative paths are relative to the current working directory +# Default: "release_tool.db" +path = "release_tool.db" + +# ============================================================================= +# Sync Configuration +# ============================================================================= +[sync] +# cutoff_date: Only fetch tickets/PRs created after this date (ISO format: YYYY-MM-DD) +# This limits historical data fetching and speeds up initial sync +# Example: "2024-01-01" to only fetch data from 2024 onwards +# Default: null (fetch all historical data) +cutoff_date = "2025-01-01" + +# parallel_workers: Number of parallel workers for GitHub API calls +# Higher values = faster sync, but may hit rate limits more quickly +# Recommended: 5-20 depending on your API rate limit +# Default: 10 +parallel_workers = 10 + +# clone_code_repo: Whether to clone the code repository locally for offline operation +# When true, the generate-notes command can work without internet access +# Default: true +clone_code_repo = true + +# code_repo_path: Local path where to clone/sync the code repository +# If not specified, defaults to .release_tool_cache/{repo_name} +# Example: "/tmp/release_tool_repos/voting-booth" +# Default: null (uses .release_tool_cache/{repo_name}) +# code_repo_path = "/path/to/local/repo" + +# show_progress: Show progress updates during sync +# When true, displays messages like "syncing 13 / 156 tickets (10% done)" +# Default: true +show_progress = true + +# ============================================================================= +# Ticket Extraction and Consolidation Policy +# ============================================================================= +# patterns: Ordered list of ticket extraction patterns +# Each pattern is associated with a specific extraction strategy (where to look) +# and uses Python regex with NAMED CAPTURE GROUPS (use "ticket" group for the ID) +# +# Patterns are tried in ORDER (by the "order" field). First match wins. +# Lower order numbers = higher priority. You can reorder by changing the numbers. +# TIP: Put more specific/reliable patterns first (lower order), generic ones last +# +# Available strategies: +# - "branch_name": Extract from PR branch name (e.g., feat/meta-123/main) +# - "pr_body": Extract from PR description text +# - "pr_title": Extract from PR title +# - "commit_message": Extract from commit message text +# +# Pattern structure: +# [[ticket_policy.patterns]] +# order = 1 # Priority (lower = tried first) +# strategy = "branch_name" # Where to look +# pattern = "regex_here" # What to match (use (?P\\d+) for ID) +# description = "explanation" # Optional: what this pattern matches + +# ORDER 1: Branch name (most reliable, structured format) +# Matches: feat/meta-123/main, fix/repo-456.whatever/develop +# Format: /-[.optional]/ +[[ticket_policy.patterns]] +order = 1 +strategy = "branch_name" +pattern = "/(?P\\w+)-(?P\\d+)" +description = "Branch name format: type/repo-123/target" + +# ORDER 2: Parent issue URL in PR body (backup policy) +# Matches: "Parent issue: https://github.com/owner/repo/issues/999" +# Use this when the branch name doesn't follow convention +[[ticket_policy.patterns]] +order = 2 +strategy = "pr_body" +pattern = "(Parent issue:.*?/issues/|sequentech/meta#)(?P\\d+)" +description = "Parent issue URL in PR description" + +# ORDER 3: GitHub issue reference in PR title +# Matches: "#123" in the PR title +[[ticket_policy.patterns]] +order = 3 +strategy = "pr_title" +pattern = "#(?P\\d+)" +description = "GitHub issue reference (#123) in PR title" + +[ticket_policy] +# no_ticket_action: What to do when a commit/PR has no associated ticket +# Valid values: +# - "ignore": Silently skip the warning, include in release notes +# - "warn": Print a warning but continue (RECOMMENDED for most teams) +# - "error": Stop the release note generation with an error +# Default: "warn" +# Use "error" for strict ticket tracking, "warn" for flexibility +no_ticket_action = "warn" + +# unclosed_ticket_action: What to do with tickets that are still open +# Valid values: +# - "ignore": Include open tickets in release notes without warning +# - "warn": Print a warning but include them (RECOMMENDED) +# - "error": Stop if any tickets are still open +# Default: "warn" +unclosed_ticket_action = "warn" + +# partial_ticket_action: What to do when a ticket is extracted but not found in DB +# or found in a different repository than expected +# +# "Partial matches" occur when: +# - Ticket key extracted from branch/PR, but ticket not in database (common causes): +# * Ticket created before your sync cutoff date +# * Ticket doesn't exist (typo in branch name) +# * Sync hasn't been run yet +# - Ticket found in database, but in wrong repository: +# * Branch says "meta-8624" but ticket is in "step" repo +# * Mismatch between ticket_repos config and actual ticket location +# +# Valid values: +# - "ignore": Silently skip partial matches, continue with next pattern +# - "warn": Print detailed warnings with potential causes and ticket links (RECOMMENDED) +# - "error": Stop release note generation if any partial matches found +# +# Default: "warn" +# When in warn mode, the tool will list: +# - Which tickets had partial matches +# - Whether they were not found or in different repo +# - Potential reasons (cutoff date, typo, wrong repo, etc.) +# - Links to tickets if found in different repos +partial_ticket_action = "warn" + +# inter_release_duplicate_action: What to do when a ticket appears in multiple releases +# This checks if tickets in the new release already exist in semantically-earlier releases +# (e.g., creating 9.3.1 and finding tickets that already appeared in 9.2.0) +# +# This happens when a single ticket includes multiple PRs that land in different releases, +# or when backporting fixes to older release branches. +# +# Valid values: +# - "ignore": Exclude duplicate tickets from the new release (keep only in earlier release) +# - "warn": Include duplicates but show a warning (RECOMMENDED) +# - "error": Stop release note generation if duplicates are found +# +# Default: "warn" +# Use "ignore" if you want strict deduplication (one ticket per release across history) +# Use "warn" for visibility into which tickets span multiple releases +inter_release_duplicate_action = "warn" + +# consolidation_enabled: Group multiple commits by their parent ticket +# When true: Commits with the same ticket (e.g., TICKET-123) are grouped +# into a single release note entry +# When false: Each commit appears as a separate entry in release notes +# Default: true +# RECOMMENDED: true (makes release notes more concise and readable) +consolidation_enabled = true + +# description_section_regex: Regex to extract description from ticket body +# Uses Python regex with capturing group (group 1 is extracted) +# Looks for sections like "## Description" or "## Summary" in ticket text +# The tool gracefully handles tickets without description sections +# Default: r'(?:## Description|## Summary)\\n(.*?)(?=\\n##|\\Z)' +# Set to empty string "" to disable description extraction +# NOTE: In TOML, backslashes must be doubled: \\n becomes \\\\n, \\Z becomes \\\\Z +description_section_regex = "(?:## Description|## Summary)\\\\n(.*?)(?=\\\\n##|\\\\Z)" + +# migration_section_regex: Regex to extract migration notes from ticket body +# Useful for database migrations, breaking changes, upgrade steps +# Looks for sections like "## Migration" or "## Migration Notes" +# The tool gracefully handles tickets without migration sections +# Default: r'(?:## Migration|## Migration Notes)\\n(.*?)(?=\\n##|\\Z)' +# Set to empty string "" to disable migration notes extraction +# NOTE: In TOML, backslashes must be doubled: \\n becomes \\\\n, \\Z becomes \\\\Z +migration_section_regex = "(?:## Migration|## Migration Notes)\\\\n(.*?)(?=\\\\n##|\\\\Z)" + +# ============================================================================= +# Version Comparison and Gap Detection Policy +# ============================================================================= +[version_policy] +# gap_detection: Check for missing versions between releases +# Detects gaps like 1.0.0 → 1.2.0 (missing 1.1.0) +# Valid values: +# - "ignore": Don't check for version gaps +# - "warn": Print a warning if gaps detected (RECOMMENDED) +# - "error": Stop the process if gaps are detected +# Default: "warn" +gap_detection = "warn" + +# tag_prefix: Prefix used for version tags in Git +# The tool will look for tags like "v1.0.0" if prefix is "v" +# Common values: "v", "release-", "" (empty for no prefix) +# Default: "v" +tag_prefix = "v" + +# ============================================================================= +# Branch Management Policy +# ============================================================================= +# Controls how release branches are created and managed +[branch_policy] +# release_branch_template: Template for release branch names (Jinja2 syntax) +# Use {{major}}, {{minor}}, {{patch}} as placeholders +# Examples: +# - "release/{{major}}.{{minor}}" → "release/9.1" +# - "rel-{{major}}.{{minor}}.x" → "rel-9.1.x" +# - "v{{major}}.{{minor}}" → "v9.1" +# Default: "release/{{major}}.{{minor}}" +release_branch_template = "release/{{major}}.{{minor}}" + +# default_branch: The default branch for new major versions +# New major versions (e.g., 9.0.0 when coming from 8.x.x) will branch from this +# Common values: "main", "master", "develop" +# Default: "main" +default_branch = "main" + +# create_branches: Automatically create release branches if they don't exist +# When true, the tool will create a new release branch automatically +# When false, you must create branches manually +# Default: true +create_branches = true + +# branch_from_previous_release: Branch new minor versions from previous release +# Controls the branching strategy for new minor versions: +# - true: 9.1.0 branches from release/9.0 (if it exists) +# - false: 9.1.0 branches from main (default_branch) +# This enables hotfix workflows where release branches persist +# Default: true +branch_from_previous_release = true + +# ============================================================================= +# Release Notes Categorization +# ============================================================================= +# Categories group release notes by the labels on tickets/PRs +# Each category can match multiple labels, and has a display order +# +# Label Matching with Source Prefixes: +# You can specify where labels should match from using prefixes: +# - "pr:label_name" = Only match this label from Pull Requests +# - "ticket:label_name" = Only match this label from Tickets/Issues +# - "label_name" = Match from EITHER PRs or tickets (default) +# +# This is useful when PRs and tickets use the same label names differently. +# For example: +# - PRs might use "bug" for any bug-related code change +# - Tickets might use "bug" only for confirmed bugs needing fixes +# - You can categorize them separately: ["pr:bug"] vs ["ticket:bug"] +# +# Category structure: +# [[release_notes.categories]] +# name = "Display Name" # Shown in the release notes +# labels = ["label1", "pr:label2", "ticket:label3"] # With optional prefixes +# order = 1 # Display order (lower numbers appear first) + +[release_notes] +# excluded_labels: Skip tickets/PRs with these labels from release notes +# Useful for internal changes, CI updates, etc. +# Default: ["skip-changelog", "internal"] +excluded_labels = ["skip-changelog", "internal", "wip", "do-not-merge"] + +# title_template: Jinja2 template for the release notes title +# Available variables: +# - {{ version }}: The version being released (e.g., "1.2.3") +# Default: "Release {{ version }}" +title_template = "Release {{ version }}" + +# entry_template: Jinja2 template for each individual release note entry +# This is a POWERFUL template that lets you customize exactly how each change +# appears in the release notes. You can use Jinja2 syntax including conditionals, +# loops, filters, and all available variables. +# +# IMPORTANT: HTML-like behavior for whitespace and line breaks +# - Multiple spaces/tabs are collapsed into a single space (like HTML) +# - New lines in the template are ignored unless you use
or
+# - Use
or
for explicit line breaks in the output +# - Use   for explicit spaces that won't collapse (e.g.,    = two spaces) +# - This allows multi-line templates for readability while controlling output +# +# Available variables for each entry: +# - {{ title }} : The title/summary of the change (string) +# Example: "Fix authentication bug in login flow" +# - {{ url }} : Smart link prioritizing ticket over PR (string or None) +# Example: "https://github.com/owner/repo/issues/123" +# Priority: ticket_url > pr_url +# - {{ ticket_url }} : Direct link to the ticket/issue (string or None) +# Example: "https://github.com/owner/repo/issues/123" +# - {{ pr_url }} : Direct link to the pull request (string or None) +# Example: "https://github.com/owner/repo/pull/456" +# - {{ short_link }} : Short format link (string or None) +# Example: "#123" +# - {{ short_repo_link }} : Short format with repo name (string or None) +# Example: "owner/repo#123" +# - {{ pr_numbers }} : List of related PR numbers (list of int) +# Example: [123, 124] +# - {{ authors }} : List of author objects (list of dict) +# Each author is a dict with comprehensive information: +# - name: Git author name (e.g., "John Doe") +# - email: Git author email (e.g., "john@example.com") +# - username: GitHub login (e.g., "johndoe") +# - github_id: GitHub user ID (e.g., 12345) +# - display_name: GitHub display name +# - avatar_url: Profile picture URL +# - profile_url: GitHub profile URL +# - company: Company name +# - location: Location +# - bio: Bio text +# - blog: Blog URL +# - user_type: "User", "Bot", or "Organization" +# - identifier: Best identifier (username > name > email) +# - mention: @mention format (e.g., "@johndoe") +# - full_display_name: Best display name +# - {{ description }} : Extracted description text (string or None) +# Example: "This fixes the login flow by..." +# - {{ migration_notes }} : Extracted migration notes (string or None) +# Example: "Run: python manage.py migrate" +# - {{ labels }} : List of label names (list of string) +# Example: ["bug", "critical", "security"] +# - {{ ticket_key }} : Ticket identifier (string or None) +# Example: "#123" or "JIRA-456" +# - {{ category }} : Assigned category name (string or None) +# Example: "Bug Fixes" +# - {{ commit_shas }} : List of commit SHA hashes (list of string) +# Example: ["a1b2c3d", "e4f5g6h"] +# +# Jinja2 syntax examples: +# - Conditionals: {% if url %}...{% endif %} +# - Loops: {% for author in authors %}@{{ author.username }}{% endfor %} +# - Filters: {{ description|truncate(100) }} +# - Boolean check: {% if pr_numbers %}(#{{ pr_numbers[0] }}){% endif %} +# - Line breaks: Use
or
for new lines in output +# - Author fields: {{ author.username }}, {{ author.name }}, {{ author.mention }} +# +# Template examples: +# 1. Minimal (single line): +# entry_template = "- {{ title }}" +# +# 2. With PR link (single line): +# entry_template = "- {{ title }}{% if url %} ([#{{ pr_numbers[0] }}]({{ url }})){% endif %}" +# +# 3. With GitHub @mentions (uses author.mention for smart @username or name): +# entry_template = '''- {{ title }} +# {% if url %}([#{{ pr_numbers[0] }}]({{ url }})){% endif %} +# {% if authors %}
by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}''' +# +# 4. With author names and emails: +# entry_template = '''- {{ title }} +# {% if authors %}
by {% for author in authors %}{{ author.name }} <{{ author.email }}>{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}''' +# +# 5. With author avatars and profile links (for markdown/HTML): +# entry_template = '''- {{ title }} +# {% if authors %}
by {% for author in authors %} {{ author.display_name }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}''' +# +# 6. Complex multi-line with labels, migration notes, and rich authors: +# entry_template = '''- {{ title }} +# {% if url %}([#{{ pr_numbers[0] }}]({{ url }})){% endif %} +# {% if labels %} `{{ labels|join('` `') }}`{% endif %} +# {% if authors %}
Contributors: {% for author in authors %}@{{ author.username or author.name }}{% if author.company %} ({{ author.company }}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} +# {% if migration_notes %}
**Migration:** {{ migration_notes }}{% endif %}''' +# +# Default: Multi-line template with title, short_repo_link, and author mentions +# The whitespace will collapse,
tags create line breaks +# Uses author.mention which gives @username if available, otherwise falls back to name +# Uses short_repo_link for concise display (e.g., "meta#123" instead of full URL) +entry_template = '''- {{ title }} {% if short_repo_link %}({{ short_repo_link }}){% endif %}
+ {% if authors %} +   by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %}''' + +# release_output_template: MASTER Jinja2 template for GitHub release notes output +# This is an ADVANCED feature that gives you complete control over the release +# notes structure. When set, this template replaces the default category-based +# layout and lets you design your own custom format. +# +# WHEN TO USE: +# - You want a custom layout (e.g., flat list, grouped by type, etc.) +# - You need to iterate over migrations or descriptions across all tickets +# - You want full control over the output structure +# +# IMPORTANT: HTML-like behavior for whitespace (same as entry_template) +# - Multiple spaces collapse to single space +# - Line breaks: Use
or
for new lines in output +# - Non-breaking spaces: Use   for explicit spaces that won't collapse +# +# Available variables: +# - {{ version }} : Version string (e.g., "1.2.3") +# - {{ title }} : Rendered release title (from title_template) +# - {{ year }} : Current year (e.g., "2025") +# - {{ categories }} : List of category dicts with 'name' and 'notes' +# - {{ all_notes }} : Flat list of all note dicts (across categories) +# - {{ render_entry(note) }}: Function to render a note using entry_template +# +# Each note dict contains: +# - title, url (prioritizes ticket_url over pr_url), ticket_url, pr_url +# - short_link (#123), short_repo_link (meta#123) +# - pr_numbers, commit_shas, labels, ticket_key, category +# - description, migration_notes (processed, may be None) +# - authors (list of author dicts with all fields) +# +# Template examples: +# +# 1. Default category-based layout (equivalent to not setting output_template): +# output_template = '''# {{ title }} +# +# {% for category in categories %} +# ## {{ category.name }} +# {% for note in category.notes %} +# {{ render_entry(note) }} +# {% endfor %} +# {% endfor %}''' +# +# 2. Flat list without categories: +# output_template = '''# {{ title }} +# +# {% for note in all_notes %} +# {{ render_entry(note) }} +# {% endfor %}''' +# +# 3. Custom layout with migrations section: +# output_template = '''# {{ title }} +# +# ## Changes +# {% for note in all_notes %} +# {{ render_entry(note) }} +# {% endfor %} +# +# ## Migration Notes +# {% for note in all_notes %} +# {% if note.migration_notes %} +# ### {{ note.title }} +# {{ note.migration_notes }} +# {% endif %} +# {% endfor %}''' +# +# 4. Grouped by ticket with full descriptions: +# output_template = '''# {{ title }} +# +# {% for note in all_notes %} +# ## {{ note.title }} +# {% if note.description %} +# {{ note.description }} +# {% endif %} +# {% if note.url %} +# **Pull Request:** [#{{ note.pr_numbers[0] }}]({{ note.url }}) +# {% endif %} +# {% if note.authors %} +# **Authors:** {% for author in note.authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %} +# {% endif %} +# {% if note.migration_notes %} +# **Migration:** {{ note.migration_notes }} +# {% endif %} +# {% endfor %}''' +# +# 5. Custom grouping with manual entry rendering: +# output_template = '''# {{ title }} +# +# ## Features & Fixes +# {% for category in categories %} +# {% if category.name in ["Features", "Bug Fixes"] %} +# ### {{ category.name }} +# {% for note in category.notes %} +# {{ render_entry(note) }} +# {% endfor %} +# {% endif %} +# {% endfor %} +# +# ## Other Changes +# {% for category in categories %} +# {% if category.name not in ["Features", "Bug Fixes"] %} +# {% for note in category.notes %} +# - {{ note.title }}{% if note.url %} ([#{{ note.pr_numbers[0] }}]({{ note.url }})){% endif %} +# {% endfor %} +# {% endif %} +# {% endfor %}''' +# +# Default: Comprehensive template with breaking changes, migrations, descriptions, and categorized changes +release_output_template = '''{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %} +{% if breaking_with_desc|length > 0 %} +
## 💥 Breaking Changes +{% for note in breaking_with_desc %} +
### {{ note.title }} +{{ note.description }} +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} +{% set migration_notes = all_notes|selectattr('migration_notes')|list %} +{% if migration_notes|length > 0 %} +
## 🔄 Migrations
+{% for note in migration_notes %} +
### {{ note.title }}
+{{ note.migration_notes }} +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} +{% set non_breaking_with_desc = all_notes|rejectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %} +{% if non_breaking_with_desc|length > 0 %} +
## 📝 Highlights
+{% for note in non_breaking_with_desc %} +
### {{ note.title }} +{{ note.description }} +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} +
## 📋 All Changes +{% for category in categories %} +
### {{ category.name }} +{% for note in category.notes %} +{% if loop.first %}
{% endif %}{{ render_entry(note) }}
+{% endfor %} + +{% endfor %}''' + +# doc_output_template: Jinja2 template for Docusaurus/documentation release notes +# This optional template wraps the GitHub release notes with documentation-specific +# formatting such as Docusaurus frontmatter. It provides an additional render function +# to embed the GitHub release notes within the documentation format. +# +# WHEN TO USE: +# - You want to generate Docusaurus documentation alongside GitHub release notes +# - You need to add frontmatter (id, title, etc.) to the release notes +# - You want different formatting for documentation vs GitHub releases +# +# IMPORTANT: HTML-like behavior for whitespace (same as release_output_template) +# - Multiple spaces collapse to single space +# - Line breaks: Use
or
for new lines in output +# - Non-breaking spaces: Use   for explicit spaces that won't collapse +# +# Available variables (in addition to all release_output_template variables): +# - {{ version }} : Version string (e.g., "1.2.3") +# - {{ title }} : Rendered release title (from title_template) +# - {{ year }} : Current year (e.g., "2025") +# - {{ categories }} : List of category dicts with 'name' and 'notes' +# - {{ all_notes }} : Flat list of all note dicts +# - {{ render_entry(note) }} : Function to render a note using entry_template +# - {{ render_release_notes() }} : Function to render the GitHub release notes +# +# Template example for Docusaurus: +# doc_output_template = '''--- +# id: release-{{version}} +# title: {{title}} +# --- +# +# {{ render_release_notes() }}
''' +# +# You can set it to None to disable it so that only GitHub release notes are generated +doc_output_template = '''--- +id: release-{{version}} +title: {{title}} +--- + +# Release {{version}} + +{{ render_release_notes() }}
''' + +# ============================================================================= +# Output Configuration +# ============================================================================= + +[[release_notes.categories]] +name = "💥 Breaking Changes" +labels = ["breaking-change", "breaking"] +order = 1 +alias = "breaking" + +[[release_notes.categories]] +name = "🚀 Features" +labels = ["feature", "enhancement", "feat"] +order = 2 +alias = "features" + +[[release_notes.categories]] +name = "🛠 Bug Fixes" +labels = ["bug", "fix", "bugfix", "hotfix"] +order = 3 +alias = "bugfixes" + +[[release_notes.categories]] +name = "📖 Documentation" +labels = ["docs", "documentation"] +order = 4 +alias = "docs" + +[[release_notes.categories]] +name = "🛡 Security Updates" +labels = ["security"] +order = 5 +alias = "security" + +[[release_notes.categories]] +name = "Other Changes" +labels = [] +order = 99 +alias = "other" + +# ============================================================================= +# Release Notes Formatting and Content +# ============================================================================= + +[output] +# release_output_path: Path template for GitHub release notes file (Jinja2 syntax) +# This file will be created/updated when using the generate command +# Used for creating GitHub releases via the publish command +# +# Available variables for path substitution: +# - {{version}}: Full version string (e.g., "1.2.3", "2.0.0-rc.1") +# - {{major}}: Major version number only (e.g., "1") +# - {{minor}}: Minor version number only (e.g., "2") +# - {{patch}}: Patch version number only (e.g., "3") +# +# Path template examples: +# - "CHANGELOG.md": Single changelog file (appends/overwrites) +# - "docs/releases/{{version}}.md": Separate file per version +# - "releases/{{major}}.{{minor}}/{{patch}}.md": Organized by major.minor +# - "docs/{{major}}.x.md": One file per major version +# - "website/releases/v{{version}}.md": With prefix +# +# Default: "docs/releases/{{version}}.md" +release_output_path = "docs/releases/{{version}}.md" + +# doc_output_path: Path template for Docusaurus/documentation release notes file (Jinja2 syntax) +# This file will be created alongside the GitHub release notes when doc_output_template is configured +# Used for generating documentation that can be committed to your docs site +# +# Available variables for path substitution (same as release_output_path): +# - {{version}}: Full version string (e.g., "1.2.3", "2.0.0-rc.1") +# - {{major}}: Major version number only (e.g., "1") +# - {{minor}}: Minor version number only (e.g., "2") +# - {{patch}}: Patch version number only (e.g., "3") +# +# Path template examples: +# - "docs/docusaurus/docs/releases/release-{{major}}.{{minor}}/release-{{major}}.{{minor}}.{{patch}}.md" +# - "website/docs/releases/{{version}}.md" +# - "docs/releases/v{{major}}.{{minor}}.{{patch}}.md" +# +# You can set it to None to disable it so that only GitHub release notes are generated +doc_output_path = "docs/docusaurus/docs/releases/release-{{major}}.{{minor}}/release-{{major}}.{{minor}}.{{patch}}.md" + +# draft_output_path: Path template for draft release notes (generate command, Jinja2 syntax) +# This is where 'generate' command saves files by default (when --output not specified) +# Files are saved here for review/editing before publishing to GitHub +# +# Available variables: +# - {{code_repo}}: Sanitized code repository name (e.g., "sequentech-step") +# - {{version}}: Full version string +# - {{major}}: Major version number +# - {{minor}}: Minor version number +# - {{patch}}: Patch version number +# - {{output_file_type}}: Type of output file ("release" or "doc") +# +# Examples: +# - ".release_tool_cache/draft-releases/{{code_repo}}/{{version}}-{{output_file_type}}.md": Organized by repo and version (DEFAULT) +# - "drafts/{{major}}.{{minor}}.{{patch}}-{{output_file_type}}.md": Simple draft folder +# - "/tmp/releases/{{code_repo}}-{{version}}-{{output_file_type}}.md": Temporary location +# +# Default: ".release_tool_cache/draft-releases/{{code_repo}}/{{version}}-{{output_file_type}}.md" +draft_output_path = ".release_tool_cache/draft-releases/{{code_repo}}/{{version}}-{{output_file_type}}.md" + +# assets_path: Path template for downloaded media assets (images, videos, Jinja2 syntax) +# Images and videos referenced in ticket descriptions will be downloaded here +# and references will be updated to use local paths in the release notes +# This is useful for Docusaurus and other static site generators +# +# Available variables (same as output_path): +# - {{version}}: Full version string +# - {{major}}: Major version number +# - {{minor}}: Minor version number +# - {{patch}}: Patch version number +# +# Path must be relative to output_path for correct markdown references +# Examples: +# - "docs/releases/assets/{{version}}": Organized by version +# - "static/img/releases/{{major}}.{{minor}}": Shared across patches +# - "assets/{{version}}": Simple structure +# +# Default: "docs/releases/assets/{{version}}" +assets_path = "docs/docusaurus/docs/releases/release-{{major}}.{{minor}}/assets" + +# download_media: Download images and videos from ticket descriptions +# When true: Downloads media files and updates references to local paths +# When false: Keeps original URLs in release notes +# Default: true +# RECOMMENDED: true for static sites (Docusaurus), false for GitHub releases +download_media = false + +# create_github_release: Automatically create a GitHub release +# When true: Uploads release notes to GitHub Releases +# When false: Only generates markdown (no upload) +# Default: true +# CLI override: --release / --no-release +# SECURITY: Requires GitHub token with repo write permissions +create_github_release = true + +# create_pr: Automatically create a PR with the release notes file +# When true: Creates a PR to add/update the release notes file +# When false: No PR is created +# Requires: output_path to be configured +# Default: true +# CLI override: --pr / --no-pr +create_pr = true + +# release_mode: Default release mode for GitHub releases +# Options: +# - "draft": Releases are created as drafts (unpublished) +# - "published": Releases are published immediately +# Default: "draft" +# CLI override: --release-mode draft|published +# Use case: Set to "draft" if you want to review releases before publishing +release_mode = "draft" + +# prerelease: Mark GitHub releases as prereleases +# Options: +# - "auto": Auto-detect from version (e.g., "1.0.0-rc.1" → prerelease, "1.0.0" → stable) +# - true: Always mark as prerelease +# - false: Always mark as stable/final +# Default: "auto" +# CLI override: --prerelease auto|true|false +# Use cases: +# - "auto": Most flexible, automatically handles RCs and stable releases +# - true: If you typically create beta/RC releases and want to be explicit +# - false: For projects that only do stable releases +prerelease = "auto" + +# create_ticket: Whether to create a tracking issue for the release +# When true, a GitHub issue will be created automatically and PR templates can use +# {{issue_repo}}, {{issue_number}}, and {{issue_link}} variables +# Default: true +# Use case: Set to false if you don't want to track releases with GitHub issues +create_ticket = true + +# ============================================================================= +# Release Tracking Ticket Templates (for create_ticket) +# ============================================================================= +[output.ticket_templates] +# title_template: Template for the ticket title +# Available variables: +# - {{version}}: Full version string (e.g., "1.2.3") +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{num_changes}}, {{num_categories}}: Release notes statistics +# +# Examples: +# - "Release {{version}}": Simple format (DEFAULT) +# - "Release v{{version}} - {{num_changes}} changes": With stats +# +# Default: "✨ Prepare Release {{version}}" +title_template = "✨ Prepare Release {{version}}" + +# body_template: Template for the ticket description (supports multi-line) +# Available variables: +# - {{version}}: Full version string (e.g., "1.2.3") +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{num_changes}}, {{num_categories}}: Release notes statistics +# - {{pr_link}}: Link to the release notes PR (will be populated after PR creation) +# - {{issue_repo_name}}: Short name of issues repo (e.g. "meta" from "sequentech/meta") +# +# Default: Release preparation checklist based on step's .github release template +body_template = '''### DevOps Tasks + +- [ ] Github release notes: correct and complete +- [ ] Docusaurus release notes: correct and complete +- [ ] BEYOND-PR-HERE for a new default tenant/election-event template and any new other changes (branch should be `release/{{major}}.{{minor}}`) +- [ ] GITOPS-PR-HERE for a new default tenant/election-event template and any new other changes (branch should be `release/{{major}}.{{minor}}`) +- [ ] Request in Environments spreadsheet to get deployment approval by environment owners + +NOTE: Please also update deployment status when a release is deployed in an environment. + +### QA Flight List + +- [ ] Deploy in `dev` +- [ ] Positive Test in `dev` +- [ ] Deploy in `qa` +- [ ] Positive Test in `qa` + +### PRs + +- {{pr_link}}''' + +# labels: Labels to apply to the release tracking ticket +# Default: ["release", "devops", "infrastructure"] +labels = ["release", "devops", "infrastructure"] + +# assignee: GitHub username to assign the ticket to +# If not specified (or commented out), the ticket is assigned to the user +# authenticated with the GitHub token (the one running the tool). +# Set this if you want to assign the release ticket to a specific person or bot. +# Example: "johndoe" +# assignee = "johndoe" + +# project_id: GitHub Project V2 Number to add the ticket to +# This allows automatically adding the release ticket to a GitHub Project board. +# The value should be the project NUMBER found in the URL (e.g. 1 for .../projects/1), +# NOT the Node ID (PVT_...). +# NOTE: This requires the GitHub token to have 'project' scope (for PATs) or +# appropriate permissions (for GitHub Apps). +# Example: "1" +project_id = "1" + +# project_status: Status to set in the GitHub Project +# Sets the value of the "Status" field in the project board. +# The value must EXACTLY match a valid status option in your project (case-sensitive). +# This setting is ignored if project_id is not set. +# Common values: "Todo", "In Progress", "Backlog", "Ready" +# Example: "Todo" +project_status = "In progress" + +# project_fields: Custom fields to set in the GitHub Project +# Allows setting additional metadata fields in the project board. +# This is a key-value map where: +# - Key: The exact name of the field in the project (e.g., "Priority", "Sprint") +# - Value: The value to set. Supports: +# * Text/Number/Date fields: The value as a string +# * Single Select fields: The option name (case-insensitive) +# * Iteration fields: The iteration name OR "@current" to automatically select the active iteration +# This setting is ignored if project_id is not set. +# Example: { "Priority" = "High", "Estimate" = "3", "Iteration" = "@current" } +project_fields = { "Team" = "Product", "Estimate" = "3", "Iteration" = "@current" } + +# type: Issue type to set +# Specifies the type of the issue (e.g., "Task", "Bug", "Feature"). +# This is typically mapped to a label with the same name. +# Example: "Task" +type = "Task" + +# milestone: Milestone to assign the ticket to +# Specifies the name of the GitHub milestone to assign. +# The milestone must exist in the repository. +# +# Available variables: +# - {{version}}: Full version string (e.g., "1.2.3") +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{year}}: Current year (e.g., "2025") +# - {{quarter_uppercase}}: Current quarter (e.g., "Q4") +# +# Examples: +# - "{{year}}_{{quarter_uppercase}}": Time-based (DEFAULT) -> "2025_Q4" +# - "v{{major}}.{{minor}}": Version-based -> "v1.2" +# - "Release {{version}}": Full version -> "Release 1.2.3" +# +# Default: "{{year}}_{{quarter_uppercase}}" +milestone = "{{year}}_{{quarter_uppercase}}" + +# ============================================================================= +# Pull Request Templates (for create_pr) +# ============================================================================= +[output.pr_templates] +# ALL TEMPLATES USE JINJA2 SYNTAX: {{ variable_name }} +# +# branch_template: Template for the PR branch name +# Available variables (always): +# - {{version}}: Full version string (e.g., "1.2.3") +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{code_repo}}: Sanitized code repository name (e.g., "sequentech-step") +# - {{issue_repo}}: Issues repository name (e.g., "sequentech/meta") +# - {{issue_repo_name}}: Short name of issues repo (e.g. "meta" from "sequentech/meta") +# - {{target_branch}}: Target branch for PR +# +# Available variables (only when create_ticket=true and ticket was created): +# - {{issue_number}}: Auto-generated issue number +# - {{issue_link}}: Auto-generated issue URL +# +# Examples: +# - "release-notes-{{version}}": Simple format +# - "docs/{{issue_repo_name}}-{{issue_number}}/{{target_branch}}": With issue tracking (DEFAULT) +# - "docs/release-{{major}}.{{minor}}.{{patch}}": Structured branch +# - "chore/update-changelog-{{version}}": With prefix +# +# Default: "docs/{{issue_repo_name}}-{{issue_number}}/{{target_branch}}" +branch_template = "docs/{{issue_repo_name}}-{{issue_number}}/{{target_branch}}" + +# title_template: Template for the PR title +# Available variables (always): +# - {{version}}: Full version string +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{num_changes}}: Number of changes in release notes +# - {{num_categories}}: Number of categories +# - {{code_repo}}, {{issue_repo}}: Repository names +# +# Available variables (only when create_ticket=true and ticket was created): +# - {{issue_number}}, {{issue_link}}: Ticket information +# +# Examples: +# - "Release notes for {{version}}": Simple title (DEFAULT) +# - "docs: Add release notes for v{{version}}": Conventional commits +# - "Release {{version}} with {{num_changes}} changes": With counts +# +# Default: "Release notes for {{version}}" +title_template = "Release notes for {{version}}" + +# body_template: Template for the PR description (supports multi-line) +# Available variables: +# - {{version}}: Full version string +# - {{major}}, {{minor}}, {{patch}}: Version components +# - {{num_changes}}: Number of changes in release notes +# - {{num_categories}}: Number of categories +# - {{code_repo}}: Sanitized code repository name +# - {{issue_repo}}: Issues repository name +# - {{issue_repo_name}}: Short name of issues repo +# - {{target_branch}}: Target branch for PR +# +# Available variables (only when create_ticket=true and ticket was created): +# - {{issue_number}}: Auto-generated issue number +# - {{issue_link}}: Auto-generated issue URL +# +# Examples: +# - Simple: +# body_template = "Automated release notes for version {{version}}." +# +# - With issue tracking (DEFAULT) - use triple quotes for multi-line: +# body_template = '''Parent issue: {{issue_link}} +# +# Automated release notes for version {{version}}. +# +# ## Summary +# This PR adds release notes for {{version}} with {{num_changes}} changes across {{num_categories}} categories.''' +# +# Default: Multi-line with issue link and summary +body_template = '''Parent issue: {{issue_link}} + +Automated release notes for version {{version}}. + +## Summary +This PR adds release notes for {{version}} with {{num_changes}} changes across {{num_categories}} categories.''' + +# ============================================================================= +# End of Configuration +# ============================================================================= diff --git a/src/release_tool/db.py b/src/release_tool/db.py new file mode 100644 index 0000000..af8ec69 --- /dev/null +++ b/src/release_tool/db.py @@ -0,0 +1,1013 @@ +"""Database operations for the release tool.""" + +import sqlite3 +import json +from datetime import datetime +from typing import List, Dict, Any, Optional +from pathlib import Path + +from .models import ( + Repository, PullRequest, Commit, Ticket, Release, Label +) + + +class Database: + """SQLite database manager.""" + + def __init__(self, db_path: str = "release_tool.db"): + self.db_path = db_path + self.conn: Optional[sqlite3.Connection] = None + self.cursor: Optional[sqlite3.Cursor] = None + + def connect(self): + """Connect to the database and initialize schema.""" + Path(self.db_path).parent.mkdir(parents=True, exist_ok=True) + self.conn = sqlite3.connect(self.db_path) + self.conn.row_factory = sqlite3.Row + self.cursor = self.conn.cursor() + self._init_db() + + def _init_db(self): + """Create database schema.""" + # Repositories table + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS repositories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner TEXT NOT NULL, + name TEXT NOT NULL, + full_name TEXT NOT NULL UNIQUE, + url TEXT, + default_branch TEXT DEFAULT 'main' + ) + """) + + # Authors table - stores unique authors + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS authors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + email TEXT, + username TEXT, + github_id INTEGER, + display_name TEXT, + avatar_url TEXT, + profile_url TEXT, + company TEXT, + location TEXT, + bio TEXT, + blog TEXT, + user_type TEXT + ) + """) + + # Pull requests table + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS pull_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER NOT NULL, + number INTEGER NOT NULL, + title TEXT NOT NULL, + body TEXT, + state TEXT, + merged_at TEXT, + author_json TEXT, + base_branch TEXT, + head_branch TEXT, + head_sha TEXT, + labels TEXT, + url TEXT, + FOREIGN KEY (repo_id) REFERENCES repositories (id), + UNIQUE(repo_id, number) + ) + """) + + # Commits table + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS commits ( + sha TEXT PRIMARY KEY, + repo_id INTEGER NOT NULL, + message TEXT NOT NULL, + author_json TEXT NOT NULL, + date TEXT NOT NULL, + url TEXT, + pr_number INTEGER, + FOREIGN KEY (repo_id) REFERENCES repositories (id) + ) + """) + + # Tickets/Issues table + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER NOT NULL, + number INTEGER NOT NULL, + key TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT, + state TEXT, + labels TEXT, + url TEXT, + created_at TEXT, + closed_at TEXT, + category TEXT, + tags TEXT, + FOREIGN KEY (repo_id) REFERENCES repositories (id), + UNIQUE(repo_id, key) + ) + """) + + # Releases table + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS releases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER NOT NULL, + version TEXT NOT NULL, + tag_name TEXT NOT NULL, + name TEXT, + body TEXT, + created_at TEXT, + published_at TEXT, + is_draft INTEGER DEFAULT 0, + is_prerelease INTEGER DEFAULT 0, + url TEXT, + target_commitish TEXT, + FOREIGN KEY (repo_id) REFERENCES repositories (id), + UNIQUE(repo_id, version) + ) + """) + + # Migration for existing tables + try: + self.cursor.execute("ALTER TABLE releases ADD COLUMN target_commitish TEXT") + except sqlite3.OperationalError: + # Column likely already exists + pass + + # Sync metadata table - tracks last sync timestamp per repository + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS sync_metadata ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_full_name TEXT NOT NULL UNIQUE, + entity_type TEXT NOT NULL, + last_sync_at TEXT NOT NULL, + cutoff_date TEXT, + total_fetched INTEGER DEFAULT 0, + UNIQUE(repo_full_name, entity_type) + ) + """) + + # Release tickets table - tracks association between releases and tracking tickets + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS release_tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_full_name TEXT NOT NULL, + version TEXT NOT NULL, + ticket_number INTEGER NOT NULL, + ticket_url TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(repo_full_name, version) + ) + """) + + # Create indexes for performance + self.cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_pr_repo_merged + ON pull_requests(repo_id, merged_at) + """) + + self.cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_commit_repo + ON commits(repo_id, date) + """) + + self.cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_ticket_repo + ON tickets(repo_id, state) + """) + + self.cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_release_ticket_repo_version + ON release_tickets(repo_full_name, version) + """) + + self.conn.commit() + + def close(self): + """Close database connection.""" + if self.conn: + self.conn.close() + self.conn = None + self.cursor = None + + # ========================================================================= + # Sync Metadata Methods + # ========================================================================= + + def get_last_sync(self, repo_full_name: str, entity_type: str) -> Optional[datetime]: + """ + Get the last sync timestamp for a repository and entity type. + + Args: + repo_full_name: Full repository name (owner/repo) + entity_type: Type of entity ('tickets', 'pull_requests', 'commits') + + Returns: + Last sync datetime or None if never synced + """ + self.cursor.execute( + """SELECT last_sync_at FROM sync_metadata + WHERE repo_full_name=? AND entity_type=?""", + (repo_full_name, entity_type) + ) + row = self.cursor.fetchone() + if row: + return datetime.fromisoformat(row['last_sync_at']) + return None + + def update_sync_metadata( + self, + repo_full_name: str, + entity_type: str, + cutoff_date: Optional[str] = None, + total_fetched: int = 0 + ) -> None: + """ + Update sync metadata for a repository and entity type. + + Args: + repo_full_name: Full repository name (owner/repo) + entity_type: Type of entity ('tickets', 'pull_requests', 'commits') + cutoff_date: Optional cutoff date (ISO format) + total_fetched: Number of items fetched in this sync + """ + now = datetime.now().isoformat() + + self.cursor.execute( + """INSERT OR REPLACE INTO sync_metadata + (repo_full_name, entity_type, last_sync_at, cutoff_date, total_fetched) + VALUES (?, ?, ?, ?, ?)""", + (repo_full_name, entity_type, now, cutoff_date, total_fetched) + ) + self.conn.commit() + + def get_all_sync_status(self) -> List[Dict[str, Any]]: + """Get sync status for all repositories and entity types.""" + self.cursor.execute( + """SELECT repo_full_name, entity_type, last_sync_at, cutoff_date, total_fetched + FROM sync_metadata + ORDER BY repo_full_name, entity_type""" + ) + return [dict(row) for row in self.cursor.fetchall()] + + def get_existing_ticket_numbers(self, repo_full_name: str) -> set: + """Get set of ticket numbers already in database for a repository.""" + self.cursor.execute( + """SELECT t.number FROM tickets t + JOIN repositories r ON t.repo_id = r.id + WHERE r.full_name = ?""", + (repo_full_name,) + ) + return {row['number'] for row in self.cursor.fetchall()} + + def get_existing_pr_numbers(self, repo_full_name: str) -> set: + """Get set of PR numbers already in database for a repository.""" + self.cursor.execute( + """SELECT pr.number FROM pull_requests pr + JOIN repositories r ON pr.repo_id = r.id + WHERE r.full_name = ?""", + (repo_full_name,) + ) + return {row['number'] for row in self.cursor.fetchall()} + + # Repository operations + def upsert_repository(self, repo: Repository) -> int: + """Insert or update a repository.""" + try: + self.cursor.execute( + """INSERT INTO repositories (owner, name, full_name, url, default_branch) + VALUES (?, ?, ?, ?, ?)""", + (repo.owner, repo.name, repo.full_name, repo.url, repo.default_branch) + ) + self.conn.commit() + return self.cursor.lastrowid + except sqlite3.IntegrityError: + self.cursor.execute( + """UPDATE repositories SET owner=?, name=?, url=?, default_branch=? + WHERE full_name=?""", + (repo.owner, repo.name, repo.url, repo.default_branch, repo.full_name) + ) + self.conn.commit() + return self.get_repository_id(repo.full_name) + + def get_repository_id(self, full_name: str) -> Optional[int]: + """Get repository ID by full name.""" + self.cursor.execute("SELECT id FROM repositories WHERE full_name = ?", (full_name,)) + row = self.cursor.fetchone() + return row["id"] if row else None + + def get_repository(self, full_name: str) -> Optional[Repository]: + """Get repository by full name.""" + self.cursor.execute("SELECT * FROM repositories WHERE full_name = ?", (full_name,)) + row = self.cursor.fetchone() + if row: + return Repository(**dict(row)) + return None + + def get_repository_by_id(self, repo_id: int) -> Optional[Repository]: + """Get repository by ID.""" + self.cursor.execute("SELECT * FROM repositories WHERE id = ?", (repo_id,)) + row = self.cursor.fetchone() + if row: + return Repository(**dict(row)) + return None + + def get_all_repositories(self) -> List[Repository]: + """Get all repositories from the database.""" + self.cursor.execute("SELECT * FROM repositories") + rows = self.cursor.fetchall() + return [Repository(**dict(row)) for row in rows] + + # Pull request operations + def upsert_pull_request(self, pr: PullRequest) -> int: + """Insert or update a pull request.""" + labels_json = json.dumps([label.model_dump() for label in pr.labels]) + merged_at_str = pr.merged_at.isoformat() if pr.merged_at else None + author_json = json.dumps(pr.author.model_dump()) if pr.author else None + + try: + self.cursor.execute( + """INSERT INTO pull_requests ( + repo_id, number, title, body, state, merged_at, author_json, + base_branch, head_branch, head_sha, labels, url + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (pr.repo_id, pr.number, pr.title, pr.body, pr.state, merged_at_str, + author_json, pr.base_branch, pr.head_branch, pr.head_sha, labels_json, pr.url) + ) + self.conn.commit() + return self.cursor.lastrowid + except sqlite3.IntegrityError: + self.cursor.execute( + """UPDATE pull_requests SET + title=?, body=?, state=?, merged_at=?, author_json=?, + base_branch=?, head_branch=?, head_sha=?, labels=?, url=? + WHERE repo_id=? AND number=?""", + (pr.title, pr.body, pr.state, merged_at_str, author_json, + pr.base_branch, pr.head_branch, pr.head_sha, labels_json, pr.url, + pr.repo_id, pr.number) + ) + self.conn.commit() + return self.get_pull_request_id(pr.repo_id, pr.number) + + def get_pull_request_id(self, repo_id: int, number: int) -> Optional[int]: + """Get PR ID by repo and number.""" + self.cursor.execute( + "SELECT id FROM pull_requests WHERE repo_id=? AND number=?", + (repo_id, number) + ) + row = self.cursor.fetchone() + return row["id"] if row else None + + def get_pull_request(self, repo_id: int, number: int) -> Optional[PullRequest]: + """Get pull request by repo and number.""" + from .models import Author + + self.cursor.execute( + "SELECT * FROM pull_requests WHERE repo_id=? AND number=?", + (repo_id, number) + ) + row = self.cursor.fetchone() + if row: + data = dict(row) + data['labels'] = [Label(**l) for l in json.loads(data.get('labels', '[]'))] + if data.get('merged_at'): + data['merged_at'] = datetime.fromisoformat(data['merged_at']) + # Deserialize author from JSON + if data.get('author_json'): + data['author'] = Author(**json.loads(data['author_json'])) + del data['author_json'] + return PullRequest(**data) + return None + + def get_merged_prs_between_dates( + self, repo_id: int, start_date: Optional[datetime], end_date: Optional[datetime] + ) -> List[PullRequest]: + """Get merged PRs in a date range.""" + query = "SELECT * FROM pull_requests WHERE repo_id=? AND merged_at IS NOT NULL" + params = [repo_id] + + if start_date: + query += " AND merged_at >= ?" + params.append(start_date.isoformat()) + if end_date: + query += " AND merged_at <= ?" + params.append(end_date.isoformat()) + + query += " ORDER BY merged_at ASC" + + self.cursor.execute(query, params) + rows = self.cursor.fetchall() + + from .models import Author + + prs = [] + for row in rows: + data = dict(row) + data['labels'] = [Label(**l) for l in json.loads(data.get('labels', '[]'))] + if data.get('merged_at'): + data['merged_at'] = datetime.fromisoformat(data['merged_at']) + # Deserialize author from JSON + if data.get('author_json'): + data['author'] = Author(**json.loads(data['author_json'])) + del data['author_json'] + prs.append(PullRequest(**data)) + + return prs + + # Commit operations + def upsert_commit(self, commit: Commit) -> None: + """Insert or update a commit.""" + import json + date_str = commit.date.isoformat() + author_json = json.dumps(commit.author.model_dump()) + + self.cursor.execute( + """INSERT OR REPLACE INTO commits ( + sha, repo_id, message, author_json, date, url, pr_number + ) VALUES (?, ?, ?, ?, ?, ?, ?)""", + (commit.sha, commit.repo_id, commit.message, author_json, + date_str, commit.url, commit.pr_number) + ) + self.conn.commit() + + def get_commit(self, sha: str) -> Optional[Commit]: + """Get commit by SHA.""" + import json + from .models import Author + + self.cursor.execute("SELECT * FROM commits WHERE sha=?", (sha,)) + row = self.cursor.fetchone() + if row: + data = dict(row) + data['date'] = datetime.fromisoformat(data['date']) + # Deserialize author from JSON + if 'author_json' in data: + data['author'] = Author(**json.loads(data['author_json'])) + del data['author_json'] + return Commit(**data) + return None + + def get_commits_by_repo(self, repo_id: int) -> List[Commit]: + """Get all commits for a repository.""" + import json + from .models import Author + + self.cursor.execute( + "SELECT * FROM commits WHERE repo_id=? ORDER BY date ASC", + (repo_id,) + ) + rows = self.cursor.fetchall() + + commits = [] + for row in rows: + data = dict(row) + data['date'] = datetime.fromisoformat(data['date']) + # Deserialize author from JSON + if 'author_json' in data: + data['author'] = Author(**json.loads(data['author_json'])) + del data['author_json'] + commits.append(Commit(**data)) + + return commits + + # Ticket operations + def upsert_ticket(self, ticket: Ticket) -> int: + """Insert or update a ticket.""" + labels_json = json.dumps([label.model_dump() for label in ticket.labels]) + tags_json = json.dumps(ticket.tags) + created_at_str = ticket.created_at.isoformat() if ticket.created_at else None + closed_at_str = ticket.closed_at.isoformat() if ticket.closed_at else None + + try: + self.cursor.execute( + """INSERT INTO tickets ( + repo_id, number, key, title, body, state, labels, url, + created_at, closed_at, category, tags + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (ticket.repo_id, ticket.number, ticket.key, ticket.title, ticket.body, + ticket.state, labels_json, ticket.url, created_at_str, closed_at_str, + ticket.category, tags_json) + ) + self.conn.commit() + return self.cursor.lastrowid + except sqlite3.IntegrityError: + self.cursor.execute( + """UPDATE tickets SET + number=?, title=?, body=?, state=?, labels=?, url=?, + created_at=?, closed_at=?, category=?, tags=? + WHERE repo_id=? AND key=?""", + (ticket.number, ticket.title, ticket.body, ticket.state, labels_json, + ticket.url, created_at_str, closed_at_str, ticket.category, tags_json, + ticket.repo_id, ticket.key) + ) + self.conn.commit() + return self.get_ticket_id(ticket.repo_id, ticket.key) + + def get_ticket_id(self, repo_id: int, key: str) -> Optional[int]: + """Get ticket ID by repo and key.""" + self.cursor.execute( + "SELECT id FROM tickets WHERE repo_id=? AND key=?", + (repo_id, key) + ) + row = self.cursor.fetchone() + return row["id"] if row else None + + def get_ticket(self, repo_id: int, key: str) -> Optional[Ticket]: + """Get ticket by repo and key.""" + self.cursor.execute( + "SELECT * FROM tickets WHERE repo_id=? AND key=?", + (repo_id, key) + ) + row = self.cursor.fetchone() + if row: + data = dict(row) + data['labels'] = [Label(**l) for l in json.loads(data.get('labels', '[]'))] + data['tags'] = json.loads(data.get('tags', '{}')) + if data.get('created_at'): + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('closed_at'): + data['closed_at'] = datetime.fromisoformat(data['closed_at']) + return Ticket(**data) + return None + + def get_ticket_by_key(self, key: str) -> Optional[Ticket]: + """ + Get ticket by key across all repositories. + + This searches for a ticket by key without requiring a specific repo_id. + Useful when the ticket could be in any of the configured ticket repos. + + Args: + key: Ticket key (e.g., "8624", "#123", "JIRA-456") + + Returns: + Ticket if found, None otherwise + """ + # Normalize key: strip "#" prefix if present + normalized_key = key.lstrip('#') if key.startswith('#') else key + + self.cursor.execute( + "SELECT * FROM tickets WHERE key=? ORDER BY created_at DESC LIMIT 1", + (normalized_key,) + ) + row = self.cursor.fetchone() + if row: + data = dict(row) + data['labels'] = [Label(**l) for l in json.loads(data.get('labels', '[]'))] + data['tags'] = json.loads(data.get('tags', '{}')) + if data.get('created_at'): + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('closed_at'): + data['closed_at'] = datetime.fromisoformat(data['closed_at']) + return Ticket(**data) + return None + + def _parse_ticket_number(self, key: str) -> Optional[int]: + """ + Parse numeric portion from a ticket key. + + Handles various formats: + - "8624" -> 8624 + - "#8624" -> 8624 + - "ISSUE-8624" -> 8624 + - "meta-8624" -> 8624 + + Args: + key: Ticket key in any format + + Returns: + Integer ticket number if found, None otherwise + """ + import re + # Try to find a number in the key + match = re.search(r'\d+', key) + if match: + try: + return int(match.group()) + except ValueError: + return None + return None + + def query_tickets( + self, + ticket_key: Optional[str] = None, + repo_id: Optional[int] = None, + repo_full_name: Optional[str] = None, + starts_with: Optional[str] = None, + ends_with: Optional[str] = None, + close_to: Optional[str] = None, + close_range: int = 10, + limit: int = 20, + offset: int = 0 + ) -> List[Ticket]: + """ + Query tickets with flexible filtering and fuzzy matching. + + This method supports multiple query patterns: + - Exact match by ticket_key + - Filter by repository (id or full_name) + - Fuzzy matching: starts_with, ends_with + - Proximity search: close_to with configurable range + + Args: + ticket_key: Exact ticket key to search for + repo_id: Filter by repository ID + repo_full_name: Filter by repository full name (e.g., "owner/repo") + starts_with: Find tickets where key starts with this prefix + ends_with: Find tickets where key ends with this suffix + close_to: Find tickets numerically close to this number + close_range: Range for close_to search (default: ±10) + limit: Maximum number of results (default: 20) + offset: Skip first N results (for pagination) + + Returns: + List of Ticket objects matching the query + + Examples: + # Find specific ticket + query_tickets(ticket_key="8624") + + # Find all tickets in a repo + query_tickets(repo_full_name="sequentech/meta", limit=50) + + # Fuzzy match: tickets starting with "86" + query_tickets(starts_with="86") + + # Find tickets close to 8624 (8604-8644) + query_tickets(close_to="8624", close_range=10) + """ + # Build the SQL query dynamically based on filters + conditions = [] + params = [] + + # Handle repo filter (either by id or full_name) + if repo_id is not None: + conditions.append("t.repo_id = ?") + params.append(repo_id) + elif repo_full_name is not None: + # Need to join with repositories table + conditions.append("r.full_name = ?") + params.append(repo_full_name) + + # Handle exact ticket key + if ticket_key: + # Normalize key: strip "#" prefix if present + normalized_key = ticket_key.lstrip('#') if ticket_key.startswith('#') else ticket_key + conditions.append("t.key = ?") + params.append(normalized_key) + + # Handle fuzzy matching + if starts_with: + conditions.append("(t.key LIKE ? OR CAST(t.number AS TEXT) LIKE ?)") + params.append(f"{starts_with}%") + params.append(f"{starts_with}%") + + if ends_with: + conditions.append("(t.key LIKE ? OR CAST(t.number AS TEXT) LIKE ?)") + params.append(f"%{ends_with}") + params.append(f"%{ends_with}") + + # Handle proximity search + if close_to: + target_num = self._parse_ticket_number(close_to) + if target_num is not None: + lower = target_num - close_range + upper = target_num + close_range + conditions.append("t.number BETWEEN ? AND ?") + params.append(lower) + params.append(upper) + + # Build the WHERE clause + where_clause = " AND ".join(conditions) if conditions else "1=1" + + # Build the full query with JOIN to get repo information + query = f""" + SELECT + t.*, + r.full_name as repo_full_name, + r.owner as repo_owner, + r.name as repo_name + FROM tickets t + LEFT JOIN repositories r ON t.repo_id = r.id + WHERE {where_clause} + ORDER BY t.created_at DESC + LIMIT ? OFFSET ? + """ + + params.extend([limit, offset]) + + # Execute query + self.cursor.execute(query, params) + rows = self.cursor.fetchall() + + # Parse results into Ticket objects with repo info stored separately + tickets = [] + for row in rows: + data = dict(row) + # Extract the joined repo fields (not part of Ticket model) + repo_full_name_val = data.pop('repo_full_name', None) + data.pop('repo_owner', None) + data.pop('repo_name', None) + + # Parse JSON fields + data['labels'] = [Label(**l) for l in json.loads(data.get('labels', '[]'))] + data['tags'] = json.loads(data.get('tags', '{}')) + + # Parse dates + if data.get('created_at'): + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('closed_at'): + data['closed_at'] = datetime.fromisoformat(data['closed_at']) + + ticket = Ticket(**data) + # Store repo info in a way that won't conflict with Pydantic + # Use object.__setattr__ to bypass Pydantic's validation + object.__setattr__(ticket, '_repo_full_name', repo_full_name_val) + tickets.append(ticket) + + return tickets + + # Release operations + def upsert_release(self, release: Release) -> int: + """Insert or update a release.""" + created_at_str = release.created_at.isoformat() if release.created_at else None + published_at_str = release.published_at.isoformat() if release.published_at else None + + try: + self.cursor.execute( + """INSERT INTO releases ( + repo_id, version, tag_name, name, body, created_at, published_at, + is_draft, is_prerelease, url, target_commitish + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (release.repo_id, release.version, release.tag_name, release.name, + release.body, created_at_str, published_at_str, + int(release.is_draft), int(release.is_prerelease), release.url, + release.target_commitish) + ) + self.conn.commit() + return self.cursor.lastrowid + except sqlite3.IntegrityError: + self.cursor.execute( + """UPDATE releases SET + tag_name=?, name=?, body=?, created_at=?, published_at=?, + is_draft=?, is_prerelease=?, url=?, target_commitish=? + WHERE repo_id=? AND version=?""", + (release.tag_name, release.name, release.body, created_at_str, + published_at_str, int(release.is_draft), int(release.is_prerelease), + release.url, release.target_commitish, release.repo_id, release.version) + ) + self.conn.commit() + return self.get_release_id(release.repo_id, release.version) + + def get_release_id(self, repo_id: int, version: str) -> Optional[int]: + """Get release ID by repo and version.""" + self.cursor.execute( + "SELECT id FROM releases WHERE repo_id=? AND version=?", + (repo_id, version) + ) + row = self.cursor.fetchone() + return row["id"] if row else None + + def get_release(self, repo_id: int, version: str) -> Optional[Release]: + """Get release by repo and version.""" + self.cursor.execute( + "SELECT * FROM releases WHERE repo_id=? AND version=?", + (repo_id, version) + ) + row = self.cursor.fetchone() + if row: + data = dict(row) + data['is_draft'] = bool(data['is_draft']) + data['is_prerelease'] = bool(data['is_prerelease']) + if data.get('created_at'): + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('published_at'): + data['published_at'] = datetime.fromisoformat(data['published_at']) + return Release(**data) + return None + + def get_all_releases( + self, + repo_id: int, + limit: Optional[int] = None, + version_prefix: Optional[str] = None, + release_types: Optional[List[str]] = None, + after: Optional[datetime] = None, + before: Optional[datetime] = None, + # Deprecated parameters - kept for backwards compatibility + since: Optional[datetime] = None, + final_only: bool = False + ) -> List[Release]: + """ + Get releases for a repository with optional filtering. + + Args: + repo_id: Repository ID + limit: Maximum number of releases to return (None for all) + version_prefix: Filter by version prefix (e.g., "9" for 9.x.x, "9.3" for 9.3.x) + release_types: List of release types to include ('final', 'rc', 'beta', 'alpha') + after: Only return releases published after this date + before: Only return releases published before this date + since: (Deprecated) Use 'after' instead + final_only: (Deprecated) Use release_types=['final'] instead + + Returns: + List of releases ordered by published_at DESC + """ + from .models import SemanticVersion + + # Handle deprecated parameters + if since and not after: + after = since + if final_only and not release_types: + release_types = ['final'] + + # Build query with filters + query = "SELECT * FROM releases WHERE repo_id=?" + params = [repo_id] + + # Date range filters + if after: + query += " AND published_at >= ?" + params.append(after.isoformat()) + + if before: + query += " AND published_at <= ?" + params.append(before.isoformat()) + + query += " ORDER BY published_at DESC" + + # Don't apply LIMIT in SQL if we have client-side filters + # (version_prefix or release_types) because limit would be applied before filtering + apply_limit_in_sql = limit and not version_prefix and not release_types + + if apply_limit_in_sql: + query += " LIMIT ?" + params.append(limit) + + self.cursor.execute(query, params) + rows = self.cursor.fetchall() + + releases = [] + for row in rows: + data = dict(row) + data['is_draft'] = bool(data['is_draft']) + data['is_prerelease'] = bool(data['is_prerelease']) + if data.get('created_at'): + data['created_at'] = datetime.fromisoformat(data['created_at']) + if data.get('published_at'): + data['published_at'] = datetime.fromisoformat(data['published_at']) + + release = Release(**data) + + # Apply version prefix filter (client-side since version format varies) + if version_prefix: + # Match exact version, version starting with prefix followed by ".", or "-" (for prereleases) + version_matches = ( + release.version == version_prefix or + release.version.startswith(version_prefix + ".") or + release.version.startswith(version_prefix + "-") + ) + if not version_matches: + continue + + # Apply release type filter (client-side) + if release_types: + try: + sem_ver = SemanticVersion.parse(release.version) + release_type = sem_ver.get_type() + + # Map release type to filter values + if release_type == 'final' and 'final' not in release_types: + continue + elif release_type == 'rc' and 'rc' not in release_types: + continue + elif release_type == 'beta' and 'beta' not in release_types: + continue + elif release_type == 'alpha' and 'alpha' not in release_types: + continue + except ValueError: + # Skip releases with invalid version format + continue + + releases.append(release) + + # Apply limit after client-side filtering + if not apply_limit_in_sql and limit and len(releases) >= limit: + break + + return releases + + def migrate_ticket_keys_strip_hash(self) -> int: + """ + Migrate database: strip "#" prefix from all ticket keys. + + This is a one-time migration for version 1.3 which changes the ticket key + storage format from "#8624" to "8624". + + Returns: + Number of tickets updated + """ + # Update all ticket keys that start with # + self.cursor.execute(""" + UPDATE tickets + SET key = SUBSTR(key, 2) + WHERE key LIKE '#%' + """) + + updated_count = self.cursor.rowcount + self.conn.commit() + + return updated_count + + # Release ticket association operations + def save_ticket_association( + self, + repo_full_name: str, + version: str, + ticket_number: int, + ticket_url: str + ) -> None: + """ + Save or update the association between a release version and its tracking ticket. + + Args: + repo_full_name: Full repository name (owner/repo) + version: Release version (e.g., "1.2.3") + ticket_number: GitHub issue number + ticket_url: URL to the GitHub issue + + Example: + db.save_ticket_association( + repo_full_name="sequentech/step", + version="1.2.3", + ticket_number=8624, + ticket_url="https://github.com/sequentech/meta/issues/8624" + ) + """ + now = datetime.now().isoformat() + + self.cursor.execute( + """INSERT OR REPLACE INTO release_tickets + (repo_full_name, version, ticket_number, ticket_url, created_at) + VALUES (?, ?, ?, ?, ?)""", + (repo_full_name, version, ticket_number, ticket_url, now) + ) + self.conn.commit() + + def get_ticket_association( + self, + repo_full_name: str, + version: str + ) -> Optional[Dict[str, Any]]: + """ + Get the tracking ticket associated with a release version. + + Args: + repo_full_name: Full repository name (owner/repo) + version: Release version (e.g., "1.2.3") + + Returns: + Dictionary with ticket_number, ticket_url, created_at if found, None otherwise + + Example: + association = db.get_ticket_association("sequentech/step", "1.2.3") + if association: + print(f"Ticket #{association['ticket_number']}: {association['ticket_url']}") + """ + self.cursor.execute( + """SELECT ticket_number, ticket_url, created_at + FROM release_tickets + WHERE repo_full_name=? AND version=?""", + (repo_full_name, version) + ) + row = self.cursor.fetchone() + if row: + return dict(row) + return None + + def has_ticket_association(self, repo_full_name: str, version: str) -> bool: + """ + Check if a release version has an associated tracking ticket. + + Args: + repo_full_name: Full repository name (owner/repo) + version: Release version (e.g., "1.2.3") + + Returns: + True if association exists, False otherwise + + Example: + if db.has_ticket_association("sequentech/step", "1.2.3"): + print("This release already has a tracking ticket") + """ + return self.get_ticket_association(repo_full_name, version) is not None diff --git a/src/release_tool/git_ops.py b/src/release_tool/git_ops.py new file mode 100644 index 0000000..a091a5b --- /dev/null +++ b/src/release_tool/git_ops.py @@ -0,0 +1,437 @@ +"""Git operations for the release tool.""" + +import re +from datetime import datetime +from pathlib import Path +from typing import List, Optional, Tuple +from git import Repo, Commit as GitCommit +from .models import Commit, SemanticVersion +from .template_utils import render_template, TemplateError + + +class GitOperations: + """Git operations wrapper.""" + + def __init__(self, repo_path: str): + """Initialize with path to git repository.""" + self.repo_path = Path(repo_path) + self.repo = Repo(str(self.repo_path)) + + def get_tags(self) -> List[str]: + """Get all tags in the repository.""" + return [tag.name for tag in self.repo.tags] + + def get_version_tags(self) -> List[SemanticVersion]: + """Get all version tags, parsed as semantic versions.""" + versions = [] + for tag in self.get_tags(): + try: + version = SemanticVersion.parse(tag) + versions.append(version) + except ValueError: + # Skip non-semver tags + continue + return sorted(versions) + + def get_latest_tag(self, final_only: bool = False) -> Optional[str]: + """ + Get the most recent tag by semantic version (not by commit date). + + Args: + final_only: If True, only considers final releases (excludes prereleases) + + Returns: + Latest tag name, or None if no tags found + """ + try: + # Get all version tags sorted by semantic version + versions = self.get_version_tags() + if not versions: + return None + + # Filter to final versions only if requested + if final_only: + versions = [v for v in versions if v.is_final()] + if not versions: + return None + + # Return the highest semantic version + latest = max(versions) + return latest.to_string(include_v=True) if latest else None + except Exception: + return None + + def get_commits_between_refs( + self, base_ref: str, head_ref: str = "HEAD" + ) -> List[GitCommit]: + """Get commits between two refs.""" + try: + commit_range = f"{base_ref}..{head_ref}" + commits = list(self.repo.iter_commits(commit_range)) + return commits + except Exception as e: + raise ValueError(f"Failed to get commits between {base_ref} and {head_ref}: {e}") + + def get_commits_for_version_range( + self, from_version: SemanticVersion, to_version: SemanticVersion + ) -> List[GitCommit]: + """Get commits between two versions.""" + from_tag = self._find_tag_for_version(from_version) + to_tag = self._find_tag_for_version(to_version) + + if not from_tag: + raise ValueError(f"Tag not found for version {from_version.to_string()}") + if not to_tag: + raise ValueError(f"Tag not found for version {to_version.to_string()}") + + return self.get_commits_between_refs(from_tag, to_tag) + + def _find_tag_for_version(self, version: SemanticVersion) -> Optional[str]: + """Find tag name for a given version.""" + version_str = version.to_string() + version_str_with_v = version.to_string(include_v=True) + + for tag in self.repo.tags: + if tag.name == version_str or tag.name == version_str_with_v: + return tag.name + return None + + def extract_pr_number_from_commit(self, commit: GitCommit) -> Optional[int]: + """Extract PR number from commit message.""" + # Common patterns: + # - "Merge pull request #123 from..." + # - "... (#123)" + # - "PR #123:" + patterns = [ + r'[Mm]erge pull request #(\d+)', + r'\(#(\d+)\)', + r'[Pp][Rr]\s*#(\d+)', + ] + + message = commit.message + for pattern in patterns: + match = re.search(pattern, message) + if match: + return int(match.group(1)) + return None + + def commit_to_model(self, git_commit: GitCommit, repo_id: int) -> Commit: + """Convert GitPython commit to our model.""" + from .models import Author + + pr_number = self.extract_pr_number_from_commit(git_commit) + + # Create Author from git commit author info + # Note: GitPython provides GitAuthor which has name and email + # We don't have GitHub username here, but it may be enriched later + # via PR author lookup or GitHub API + author = Author( + name=git_commit.author.name if git_commit.author else "Unknown", + email=git_commit.author.email if git_commit.author else None + ) + + return Commit( + sha=git_commit.hexsha, + repo_id=repo_id, + message=git_commit.message, + author=author, + date=datetime.fromtimestamp(git_commit.committed_date), + pr_number=pr_number + ) + + def get_current_branch(self) -> str: + """Get the current branch name.""" + return self.repo.active_branch.name + + def get_commit_by_sha(self, sha: str) -> Optional[GitCommit]: + """Get a specific commit by SHA.""" + try: + return self.repo.commit(sha) + except Exception: + return None + + def get_default_branch(self) -> str: + """Get the default branch (usually main or master).""" + try: + # Try to get from remote + origin = self.repo.remote('origin') + return origin.refs.HEAD.ref.remote_head + except Exception: + # Fallback to common defaults + for branch in ['main', 'master']: + try: + self.repo.branches[branch] + return branch + except Exception: + continue + # Return current branch as last resort + return self.get_current_branch() + + def get_all_branches(self, remote: bool = False) -> List[str]: + """Get all branch names (local or remote).""" + if remote: + try: + origin = self.repo.remote('origin') + return [ref.remote_head for ref in origin.refs if ref.remote_head != 'HEAD'] + except Exception: + return [] + else: + return [branch.name for branch in self.repo.branches] + + def branch_exists(self, branch_name: str, remote: bool = False) -> bool: + """Check if a branch exists (locally or remotely).""" + branches = self.get_all_branches(remote=remote) + return branch_name in branches + + def create_branch(self, branch_name: str, start_point: str = "HEAD") -> None: + """Create a new branch from a starting point.""" + if self.branch_exists(branch_name): + raise ValueError(f"Branch {branch_name} already exists") + + self.repo.create_head(branch_name, start_point) + + def checkout_branch(self, branch_name: str, create: bool = False) -> None: + """Checkout a branch, optionally creating it first.""" + if create and not self.branch_exists(branch_name): + self.create_branch(branch_name) + + self.repo.git.checkout(branch_name) + + def find_release_branches(self, major: int, minor: Optional[int] = None) -> List[str]: + """ + Find release branches matching a pattern. + + Args: + major: Major version number + minor: Optional minor version number + + Returns: + List of branch names matching the pattern + """ + all_branches = self.get_all_branches() + self.get_all_branches(remote=True) + all_branches = list(set(all_branches)) # Deduplicate + + if minor is not None: + pattern = f"release/{major}.{minor}" + return [b for b in all_branches if b == pattern or b == f"origin/{pattern}"] + else: + pattern_prefix = f"release/{major}." + matches = [] + for branch in all_branches: + clean_branch = branch.replace("origin/", "") + if clean_branch.startswith(pattern_prefix): + matches.append(clean_branch) + return matches + + def get_latest_release_branch(self, major: int) -> Optional[str]: + """ + Get the most recent release branch for a given major version. + + Args: + major: Major version number + + Returns: + Branch name of the latest release, or None if no release branches exist + """ + branches = self.find_release_branches(major) + if not branches: + return None + + # Extract minor versions and sort + branch_versions = [] + for branch in branches: + # Parse release/X.Y format + match = re.match(r'release/(\d+)\.(\d+)', branch) + if match: + branch_major, branch_minor = int(match.group(1)), int(match.group(2)) + if branch_major == major: + branch_versions.append((branch_minor, branch)) + + if not branch_versions: + return None + + # Return branch with highest minor version + branch_versions.sort(reverse=True) + return branch_versions[0][1] + + +def determine_release_branch_strategy( + version: SemanticVersion, + git_ops: "GitOperations", + available_versions: List[SemanticVersion], + branch_template: str = "release/{major}.{minor}", + default_branch: str = "main", + branch_from_previous: bool = True +) -> Tuple[str, str, bool]: + """ + Determine the release branch name and source branch for a version. + + Args: + version: Target version + git_ops: GitOperations instance + available_versions: List of existing versions + branch_template: Template for branch names + default_branch: Default branch (e.g., "main") + branch_from_previous: Whether to branch from previous release + + Returns: + Tuple of (release_branch_name, source_branch, should_create_branch) + """ + # Format release branch name using Jinja2 template + template_context = { + 'major': str(version.major), + 'minor': str(version.minor), + 'patch': str(version.patch) + } + try: + release_branch = render_template(branch_template, template_context) + except TemplateError as e: + # Fall back to simple string if template rendering fails + # This maintains backwards compatibility + release_branch = f"release/{version.major}.{version.minor}" + + # Check if this branch already exists + branch_exists = git_ops.branch_exists(release_branch) or git_ops.branch_exists(release_branch, remote=True) + + # Check if this is the first release for this major.minor + same_version_releases = [ + v for v in available_versions + if v.major == version.major and v.minor == version.minor + ] + + # Determine source branch + source_branch = default_branch # Default fallback + + if same_version_releases: + # Not the first release for this version - use existing release branch + source_branch = release_branch + should_create = False + else: + # First release for this major.minor - need to determine source + should_create = not branch_exists + + if version.minor == 0: + # New major version - branch from default branch + source_branch = default_branch + elif branch_from_previous: + # New minor version - try to branch from previous minor's release branch + prev_release_branch = git_ops.get_latest_release_branch(version.major) + + if prev_release_branch: + source_branch = prev_release_branch + else: + # No previous release branch found - check if there are any releases for this major + same_major_releases = [ + v for v in available_versions + if v.major == version.major and v.is_final() + ] + + if same_major_releases: + # There are releases but no branches - branch from default + source_branch = default_branch + else: + # New major version - branch from default + source_branch = default_branch + else: + # Not branching from previous - use default branch + source_branch = default_branch + + return (release_branch, source_branch, should_create) + + +def find_comparison_version( + target_version: SemanticVersion, + available_versions: List[SemanticVersion] +) -> Optional[SemanticVersion]: + """ + Find the appropriate version to compare against based on the target version. + + Rules: + - Release candidates compare to previous RC of same version, or previous final version + - Final versions compare to previous final version + - Betas/alphas compare to previous prerelease of same major.minor, or previous final + """ + target_type = target_version.get_type() + + # Filter and sort versions before the target + earlier_versions = [v for v in available_versions if v < target_version] + if not earlier_versions: + return None + + earlier_versions = sorted(earlier_versions, reverse=True) + + # For release candidates, try to find previous RC of same version first + if target_type == target_version.get_type().RELEASE_CANDIDATE: + # Look for RCs of the same major.minor.patch + same_version_rcs = [ + v for v in earlier_versions + if (v.major == target_version.major and + v.minor == target_version.minor and + v.patch == target_version.patch and + v.prerelease is not None and + v.prerelease.startswith('rc')) + ] + if same_version_rcs: + return same_version_rcs[0] + + # For final versions or if no matching RC found, look for previous final version + if target_type == target_version.get_type().FINAL or target_version.prerelease: + # If this is a final version, find the previous final version + final_versions = [v for v in earlier_versions if v.is_final()] + if final_versions: + return final_versions[0] + + # If no final version exists, return the most recent version + return earlier_versions[0] + + return earlier_versions[0] if earlier_versions else None + + +def get_release_commit_range( + git_ops: GitOperations, + target_version: SemanticVersion, + from_version: Optional[SemanticVersion] = None, + head_ref: str = "HEAD" +) -> Tuple[Optional[SemanticVersion], List[GitCommit]]: + """ + Get the commit range for a release. + + Args: + git_ops: GitOperations instance + target_version: The version being released + from_version: Optional starting version (calculated if None) + head_ref: Reference to use as the end of the range (default: HEAD) + + Returns: (comparison_version, commits) + """ + available_versions = git_ops.get_version_tags() + + if from_version: + comparison_version = from_version + else: + comparison_version = find_comparison_version(target_version, available_versions) + + if not comparison_version: + # No previous version, get all commits up to target + try: + tag = git_ops._find_tag_for_version(target_version) + if tag: + # Get commits from beginning to target + commits = list(git_ops.repo.iter_commits(tag)) + else: + # Target tag doesn't exist yet, get all commits up to head_ref + commits = list(git_ops.repo.iter_commits(head_ref)) + return None, commits + except Exception: + return None, [] + + try: + commits = git_ops.get_commits_for_version_range(comparison_version, target_version) + return comparison_version, commits + except ValueError: + # Target version tag doesn't exist yet, compare from comparison to head_ref + from_tag = git_ops._find_tag_for_version(comparison_version) + if from_tag: + commits = git_ops.get_commits_between_refs(from_tag, head_ref) + return comparison_version, commits + return comparison_version, [] diff --git a/src/release_tool/github_utils.py b/src/release_tool/github_utils.py new file mode 100644 index 0000000..8b2f855 --- /dev/null +++ b/src/release_tool/github_utils.py @@ -0,0 +1,1959 @@ +"""GitHub API utilities.""" + +from datetime import datetime +from typing import List, Dict, Any, Optional +from github import Github, GithubException +from rich.console import Console + +from .models import ( + Repository, PullRequest, Ticket, Release, Label +) +from .config import Config + +console = Console() + + +class GitHubClient: + """GitHub API client wrapper.""" + + def __init__(self, config: Config): + """Initialize GitHub client.""" + self.config = config + token = config.github.token + if not token: + raise ValueError( + "GitHub token not found. Set GITHUB_TOKEN environment variable " + "or configure it in release_tool.toml" + ) + # Set per_page=100 (max) for efficient pagination across all API calls + self.gh = Github(token, base_url=config.github.api_url, per_page=100) + + def get_repository_info(self, full_name: str) -> Repository: + """Get repository information.""" + try: + repo = self.gh.get_repo(full_name) + owner, name = full_name.split('/') + return Repository( + owner=owner, + name=name, + full_name=full_name, + url=repo.html_url, + default_branch=repo.default_branch + ) + except GithubException as e: + raise ValueError(f"Failed to fetch repository {full_name}: {e}") + + def fetch_pull_requests( + self, + repo_full_name: str, + state: str = "closed", + base_branch: Optional[str] = None + ) -> List[PullRequest]: + """Fetch pull requests from GitHub with parallel processing.""" + from concurrent.futures import ThreadPoolExecutor, as_completed + from rich.progress import Progress, SpinnerColumn, TextColumn + + try: + repo = self.gh.get_repo(repo_full_name) + + console.print(f"Fetching pull requests from {repo_full_name}...") + + # First, get PR numbers in batches (this is fast) + gh_prs = repo.get_pulls(state=state, sort="updated", direction="desc") + + # Process PRs in batches with parallel fetching + prs_data = [] + batch_size = 100 # Increased for better GitHub API throughput + processed = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + task = progress.add_task("Processing pull requests...", total=None) + + pr_batch = [] + for pr in gh_prs: + # Quick filters before parallel processing + if base_branch and pr.base.ref != base_branch: + continue + if not pr.merged_at: + continue + + pr_batch.append(pr) + + # Process batch when full + if len(pr_batch) >= batch_size: + batch_results = self._process_pr_batch(pr_batch) + prs_data.extend(batch_results) + processed += len(pr_batch) + + progress.update( + task, + description=f"Processed {processed} pull requests ({len(prs_data)} merged)" + ) + pr_batch = [] + + # Process remaining PRs + if pr_batch: + batch_results = self._process_pr_batch(pr_batch) + prs_data.extend(batch_results) + processed += len(pr_batch) + + progress.update( + task, + description=f"Processed {processed} pull requests ({len(prs_data)} merged)" + ) + + console.print(f"[green]✓[/green] Fetched {len(prs_data)} merged PRs from {processed} candidates") + return prs_data + except GithubException as e: + console.print(f"[red]Error fetching PRs: {e}[/red]") + return [] + + def _process_pr_batch(self, pr_batch: List) -> List[PullRequest]: + """Process a batch of PRs in parallel.""" + from concurrent.futures import ThreadPoolExecutor, as_completed + + results = [] + + # Use ThreadPoolExecutor for parallel processing + with ThreadPoolExecutor(max_workers=20) as executor: + # Submit all PRs for processing + future_to_pr = { + executor.submit(self._pr_to_model, pr, 0): pr + for pr in pr_batch + } + + # Collect results as they complete + for future in as_completed(future_to_pr): + try: + pr_model = future.result() + if pr_model: + results.append(pr_model) + except Exception as e: + console.print(f"[yellow]Warning: Error processing PR: {e}[/yellow]") + + return results + + def _github_user_to_author(self, gh_user) -> Optional['Author']: + """Convert PyGithub NamedUser to Author model.""" + from .models import Author + + if not gh_user: + return None + + try: + # Use raw_data to avoid lazy loading of fields not in the partial response + # PyGithub 2.x exposes raw_data + raw = getattr(gh_user, '_rawData', None) + if raw is None: + raw = getattr(gh_user, 'raw_data', {}) + + return Author( + username=gh_user.login, + github_id=gh_user.id, + name=raw.get('name'), # Avoid gh_user.name which triggers fetch + email=raw.get('email'), + display_name=raw.get('name') or gh_user.login, + avatar_url=gh_user.avatar_url, + profile_url=gh_user.html_url, + company=raw.get('company'), + location=raw.get('location'), + bio=raw.get('bio'), + blog=raw.get('blog'), + user_type=gh_user.type + ) + except Exception as e: + console.print(f"[yellow]Warning: Error creating author from GitHub user: {e}[/yellow]") + # Return minimal author with just username + return Author(username=gh_user.login if hasattr(gh_user, 'login') else None) + + def _pr_to_model(self, gh_pr, repo_id: int) -> PullRequest: + """Convert PyGithub PR to our model, avoiding lazy loads.""" + # Use internal _rawData if available to avoid any property overhead or lazy loading checks + raw = getattr(gh_pr, '_rawData', None) + if raw is None: + raw = getattr(gh_pr, 'raw_data', {}) + + # Extract labels from raw data to avoid lazy load + labels = [] + for label_data in raw.get('labels', []): + labels.append(Label( + name=label_data.get('name', ''), + color=label_data.get('color', ''), + description=label_data.get('description') + )) + + # Extract base/head branches from raw data to avoid lazy load + base_data = raw.get('base', {}) + head_data = raw.get('head', {}) + + # Get user data from raw to avoid lazy load + user_data = raw.get('user') + # Create a mock object with raw_data for _github_user_to_author + from types import SimpleNamespace + gh_user = None + if user_data: + gh_user = SimpleNamespace( + login=user_data.get('login'), + id=user_data.get('id'), + avatar_url=user_data.get('avatar_url'), + html_url=user_data.get('html_url'), + type=user_data.get('type'), + raw_data=user_data + ) + + return PullRequest( + repo_id=repo_id, + number=raw.get('number'), + title=raw.get('title'), + body=raw.get('body'), + state=raw.get('state'), + merged_at=raw.get('merged_at'), + author=self._github_user_to_author(gh_user), + base_branch=base_data.get('ref'), + head_branch=head_data.get('ref'), + head_sha=head_data.get('sha'), + labels=labels, + url=raw.get('html_url') + ) + + def _issue_to_ticket(self, gh_issue, repo_id: int) -> Ticket: + """Convert PyGithub Issue to our Ticket model, avoiding lazy loads.""" + # Use internal _rawData if available to avoid any property overhead or lazy loading checks + # PyGithub stores the raw dictionary in _rawData + raw = getattr(gh_issue, '_rawData', None) + if raw is None: + # Fallback to public raw_data + raw = getattr(gh_issue, 'raw_data', {}) + + # Extract labels from raw data to avoid lazy load + labels = [] + for label_data in raw.get('labels', []): + labels.append(Label( + name=label_data.get('name', ''), + color=label_data.get('color', ''), + description=label_data.get('description') + )) + + # Get number from raw_data to be absolutely sure we avoid any lazy loads + number = raw.get('number') + if number is None: + # Only access gh_issue.number if absolutely necessary (fallback) + number = gh_issue.number + + ticket = Ticket( + repo_id=repo_id, + number=number, + key=str(number), + title=raw.get('title'), + body=raw.get('body'), + state=raw.get('state'), + labels=labels, + url=raw.get('html_url'), + created_at=raw.get('created_at'), + closed_at=raw.get('closed_at') + ) + + return ticket + + def fetch_issue(self, repo_full_name: str, issue_number: int, repo_id: int) -> Optional[Ticket]: + """Fetch a single issue/ticket from GitHub.""" + try: + repo = self.gh.get_repo(repo_full_name) + issue = repo.get_issue(issue_number) + + labels = [ + Label(name=label.name, color=label.color, description=label.description) + for label in issue.labels + ] + + return Ticket( + repo_id=repo_id, + number=issue.number, + key=str(issue.number), + title=issue.title, + body=issue.body, + state=issue.state, + labels=labels, + url=issue.html_url, + created_at=issue.created_at, + closed_at=issue.closed_at + ) + except GithubException as e: + console.print(f"[yellow]Warning: Could not fetch issue #{issue_number}: {e}[/yellow]") + return None + + def fetch_issue_by_key( + self, + repo_full_name: str, + ticket_key: str, + repo_id: int + ) -> Optional[Ticket]: + """Fetch issue by ticket key (e.g., '#123' or 'PROJ-123').""" + # Extract number from key + import re + match = re.search(r'(\d+)', ticket_key) + if not match: + return None + + issue_number = int(match.group(1)) + return self.fetch_issue(repo_full_name, issue_number, repo_id) + + def search_issue_numbers( + self, + repo_full_name: str, + since: Optional[datetime] = None + ) -> List[int]: + """ + Get issue numbers using Core API with explicit pagination. + + Uses GET /repos/{owner}/{repo}/issues endpoint with per_page=100. + Manually paginates to ensure we fetch 100 items per request. + + IMPORTANT: This endpoint returns both issues AND pull requests (PRs are issues in GitHub API). + Returns all numbers - filtering happens downstream if needed. + + Core API limit: 5000 req/hour (much higher than Search API's 30 req/min). + + Args: + repo_full_name: Full repository name (owner/repo) + since: Only include issues created after this datetime + + Returns: + List of issue numbers (includes both issues and PRs) + """ + from rich.progress import Progress, SpinnerColumn, TextColumn + + try: + repo = self.gh.get_repo(repo_full_name) + + # Use Core API with explicit pagination + # state='all' to get both open and closed + # Note: This returns both issues AND pull requests + issues_paginated = repo.get_issues( + state='all', + since=since, + sort='created', + direction='asc' + ) + + issue_numbers = [] + page_num = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + task = progress.add_task("Fetching issues...", total=None) + + # Explicitly paginates through results to fetch 100 at a time + while True: + try: + # Get page (PyGithub caches pages internally) + page = issues_paginated.get_page(page_num) + + if not page: + break + + # Process the page (100 items) - just get the numbers + # Note: This includes both issues AND PRs (PRs are issues in GitHub API) + for issue in page: + issue_numbers.append(issue.number) + + page_num += 1 + progress.update(task, description=f"Fetching issues... {len(issue_numbers)} found (page {page_num})") + + except Exception as e: + # No more pages + break + + console.print(f" [green]✓[/green] Found {len(issue_numbers)} issues") + return issue_numbers + + except GithubException as e: + console.print(f"[red]Error fetching issues from {repo_full_name}: {e}[/red]") + return [] + + def search_ticket_numbers(self, repo_full_name: str, since: Optional[datetime] = None) -> List[int]: + """Deprecated: Use search_issue_numbers() instead.""" + return self.search_issue_numbers(repo_full_name, since) + + def fetch_all_issues( + self, + repo_full_name: str, + repo_id: int, + since: Optional[datetime] = None + ) -> List[Ticket]: + """ + Fetch all issues as Ticket objects using Core API with efficient pagination. + + Uses GET /repos/{owner}/{repo}/issues endpoint with per_page=100. + Fetches full issue data and converts to Ticket objects in one pass. + + IMPORTANT: GitHub's /issues endpoint returns both issues AND pull requests. + PRs are filtered out by checking if pull_request field is None. + + Core API limit: 5000 req/hour. + + Args: + repo_full_name: Full repository name (owner/repo) + repo_id: Repository ID in database + since: Only include issues created after this datetime + + Returns: + List of Ticket objects (PRs excluded) + """ + from rich.progress import Progress, SpinnerColumn, TextColumn + import time + + try: + repo = self.gh.get_repo(repo_full_name) + + # Use Core API with explicit pagination + issues_paginated = repo.get_issues( + state='all', + since=since, + sort='created', + direction='asc' + ) + + tickets = [] + page_num = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + task = progress.add_task("Fetching issues...", total=None) + + # Explicitly paginate through results to fetch 100 at a time + while True: + try: + page_start = time.time() + progress.update(task, description=f"Fetching issues... page {page_num + 1} (fetching...)") + + # Get page (100 items) - force to list to avoid lazy iteration + page = issues_paginated.get_page(page_num) + + if not page: + break + + # Force page to list to ensure all data is loaded + page = list(page) + + page_fetch_time = time.time() - page_start + progress.update(task, description=f"Fetching issues... page {page_num + 1} ({len(page)} items in {page_fetch_time:.1f}s, converting...)") + + # Convert issues to Ticket objects directly + convert_start = time.time() + for idx, issue in enumerate(page): + item_start = time.time() + # Convert to Ticket using helper (doesn't trigger extra API calls) + ticket = self._issue_to_ticket(issue, repo_id) + tickets.append(ticket) + item_time = time.time() - item_start + + # Update every 10 items to show progress + if (idx + 1) % 10 == 0: + avg_time = (time.time() - convert_start) / (idx + 1) + progress.update(task, description=f"Fetching issues... page {page_num + 1} (converting {idx + 1}/{len(page)}... {avg_time*1000:.0f}ms/item)") + + convert_time = time.time() - convert_start + page_num += 1 + progress.update(task, description=f"Fetching issues... {len(tickets)} found (page {page_num} done in {page_fetch_time + convert_time:.1f}s)") + + except Exception as e: + # No more pages + break + + console.print(f" [green]✓[/green] Found {len(tickets)} issues") + return tickets + + except GithubException as e: + console.print(f"[red]Error fetching issues from {repo_full_name}: {e}[/red]") + return [] + + def search_tickets( + self, + repo_full_name: str, + repo_id: int, + since: Optional[datetime] = None + ) -> List[Ticket]: + """ + Search for tickets using GitHub Search API and return full Ticket objects. + + This is more efficient than search_ticket_numbers() + fetch_issue() for each, + as it extracts all ticket data directly from search results without additional API calls. + + GitHub Search API has a 1000-result limit per query. This method handles + that by chunking the date range when needed. + + Args: + repo_full_name: Full repository name (owner/repo) + repo_id: Repository ID in database + since: Only include tickets created after this datetime + + Returns: + List of Ticket objects with full data + """ + from datetime import timedelta + + try: + console.print(f" [cyan]Searching for tickets...[/cyan]") + + # NOTE: GitHub Search API has a 1000-result limit per query + # AND it lies about totalCount - it caps at 1000 even when there are more results + # So we must always chunk and check if we hit exactly 1000 results + + tickets = [] + current_start = since + + while True: + # Query for this chunk (sorted ascending to get oldest first in this range) + chunk_query = f"repo:{repo_full_name} is:issue" + if current_start: + chunk_query += f" created:>={current_start.strftime('%Y-%m-%d')}" + + chunk_issues = self.gh.search_issues(chunk_query, sort='created', order='asc') + chunk_count = chunk_issues.totalCount + + if chunk_count == 0: + break + + # Show progress + if len(tickets) == 0: + if chunk_count >= 1000: + console.print(f" [yellow]Note: API shows {chunk_count} tickets, but there may be more (API limit: 1000)[/yellow]") + else: + console.print(f" [dim]Total tickets to fetch: {chunk_count}[/dim]") + + # Fetch up to 1000 from this chunk + fetched_in_chunk = 0 + last_created_date = None + + for issue in chunk_issues: + # Convert to Ticket object directly (no additional API call needed!) + ticket = self._issue_to_ticket(issue, repo_id) + tickets.append(ticket) + last_created_date = issue.created_at + fetched_in_chunk += 1 + + if len(tickets) % 100 == 0: + console.print(f" [dim]Found {len(tickets)} tickets...[/dim]") + + # Stop at 1000 per chunk to avoid API limit + if fetched_in_chunk >= 1000: + break + + # If we fetched less than 1000, we're done (no more results) + if fetched_in_chunk < 1000: + break + + # We fetched exactly 1000 - there might be more, continue to next chunk + if last_created_date: + current_start = last_created_date + timedelta(seconds=1) + console.print(f" [yellow]Fetched 1000 results - chunking to continue from {current_start.strftime('%Y-%m-%d %H:%M:%S')}...[/yellow]") + else: + break + + console.print(f" [green]✓[/green] Found {len(tickets)} tickets with full data") + return tickets + + except GithubException as e: + console.print(f"[red]Error searching tickets from {repo_full_name}: {e}[/red]") + return [] + + def search_pr_numbers( + self, + repo_full_name: str, + since: Optional[datetime] = None + ) -> List[int]: + """ + Get merged PR numbers using Core API with explicit pagination. + + Uses GET /repos/{owner}/{repo}/pulls endpoint with per_page=100. + Manually paginates to ensure we fetch 100 items per request. + + Core API limit: 5000 req/hour (much higher than Search API's 30 req/min). + + Args: + repo_full_name: Full repository name (owner/repo) + since: Only include PRs merged after this datetime + + Returns: + List of PR numbers + """ + from rich.progress import Progress, SpinnerColumn, TextColumn + + try: + repo = self.gh.get_repo(repo_full_name) + + # Use Core API with explicit pagination + # state='closed' gets both merged and closed-without-merge + prs_paginated = repo.get_pulls( + state='closed', + sort='created', + direction='asc' + ) + + pr_numbers = [] + page_num = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + task = progress.add_task("Fetching PRs...", total=None) + + # Explicitly paginate through results to fetch 100 at a time + while True: + try: + # Get page (PyGithub caches pages internally) + page = prs_paginated.get_page(page_num) + + if not page: + break + + # Process the page (100 items) + for pr in page: + # Filter to only merged PRs and respect since date + if pr.merged_at: + if since is None or pr.merged_at >= since: + pr_numbers.append(pr.number) + + page_num += 1 + progress.update(task, description=f"Fetching PRs... {len(pr_numbers)} found (page {page_num})") + + except Exception as e: + # No more pages + break + + console.print(f" [green]✓[/green] Found {len(pr_numbers)} merged PRs") + return pr_numbers + + except GithubException as e: + console.print(f"[red]Error fetching PRs from {repo_full_name}: {e}[/red]") + return [] + + def fetch_all_pull_requests( + self, + repo_full_name: str, + repo_id: int, + since: Optional[datetime] = None + ) -> List[PullRequest]: + """ + Fetch all PRs as PullRequest objects using Core API with efficient pagination. + + Uses GET /repos/{owner}/{repo}/pulls endpoint with per_page=100. + Fetches full PR data and converts to PullRequest objects in one pass. + + Note: Gets all closed PRs. Filtering (merged vs closed, since date) happens downstream. + + Core API limit: 5000 req/hour. + + Args: + repo_full_name: Full repository name (owner/repo) + repo_id: Repository ID in database + since: Only include PRs created after this datetime (filtering done downstream) + + Returns: + List of PullRequest objects (all closed PRs) + """ + from rich.progress import Progress, SpinnerColumn, TextColumn + import time + + try: + repo = self.gh.get_repo(repo_full_name) + + # Use Core API with explicit pagination + # state='closed' gets both merged and closed-without-merge + prs_paginated = repo.get_pulls( + state='closed', + sort='created', + direction='asc' + ) + + pull_requests = [] + page_num = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + task = progress.add_task("Fetching PRs...", total=None) + + # Explicitly paginate through results to fetch 100 at a time + while True: + try: + page_start = time.time() + progress.update(task, description=f"Fetching PRs... page {page_num + 1} (fetching...)") + + # Get page (100 items) - force to list to avoid lazy iteration + page = prs_paginated.get_page(page_num) + + if not page: + break + + # Force page to list to ensure all data is loaded + page = list(page) + + page_fetch_time = time.time() - page_start + progress.update(task, description=f"Fetching PRs... page {page_num + 1} ({len(page)} items in {page_fetch_time:.1f}s, converting...)") + + # Convert all PRs to PullRequest objects directly (no filtering here) + convert_start = time.time() + for idx, pr in enumerate(page): + item_start = time.time() + pr_obj = self._pr_to_model(pr, repo_id) + pull_requests.append(pr_obj) + item_time = time.time() - item_start + + # Update every 10 items to show progress + if (idx + 1) % 10 == 0: + avg_time = (time.time() - convert_start) / (idx + 1) + progress.update(task, description=f"Fetching PRs... page {page_num + 1} (converting {idx + 1}/{len(page)}... {avg_time*1000:.0f}ms/item)") + + convert_time = time.time() - convert_start + page_num += 1 + progress.update(task, description=f"Fetching PRs... {len(pull_requests)} found (page {page_num} done in {page_fetch_time + convert_time:.1f}s)") + + except Exception as e: + # No more pages + break + + console.print(f" [green]✓[/green] Found {len(pull_requests)} PRs") + return pull_requests + + except GithubException as e: + console.print(f"[red]Error fetching PRs from {repo_full_name}: {e}[/red]") + return [] + + def search_pull_requests( + self, + repo_full_name: str, + repo_id: int, + since: Optional[datetime] = None + ) -> List[PullRequest]: + """ + Search for merged PRs using GitHub Search API and return full PullRequest objects. + + This is more efficient than search_pr_numbers() + get_pull_request() for each, + as it extracts all PR data directly from search results without additional API calls. + + GitHub Search API has a 1000-result limit per query. This method handles + that by chunking the date range when needed. + + Args: + repo_full_name: Full repository name (owner/repo) + repo_id: Repository ID in database + since: Only include PRs merged after this datetime + + Returns: + List of PullRequest objects with full data + """ + from datetime import timedelta + + try: + console.print(f" [cyan]Searching for merged PRs...[/cyan]") + + # NOTE: GitHub Search API has a 1000-result limit per query + # AND it lies about totalCount - it caps at 1000 even when there are more results + # So we must always chunk and check if we hit exactly 1000 results + + prs = [] + current_start = since + + while True: + # Query for this chunk (sorted ascending to get oldest first in this range) + chunk_query = f"repo:{repo_full_name} is:pr is:merged" + if current_start: + chunk_query += f" merged:>={current_start.strftime('%Y-%m-%d')}" + + chunk_prs = self.gh.search_issues(chunk_query, sort='created', order='asc') + chunk_count = chunk_prs.totalCount + + if chunk_count == 0: + break + + # Show progress + if len(prs) == 0: + if chunk_count >= 1000: + console.print(f" [yellow]Note: API shows {chunk_count} PRs, but there may be more (API limit: 1000)[/yellow]") + else: + console.print(f" [dim]Total PRs to fetch: {chunk_count}[/dim]") + + # Fetch up to 1000 from this chunk + fetched_in_chunk = 0 + last_created_date = None + + for pr in chunk_prs: + # Convert to PullRequest object directly (no additional API call needed!) + pr_obj = self._pr_to_model(pr, repo_id) + prs.append(pr_obj) + last_created_date = pr.created_at + fetched_in_chunk += 1 + + if len(prs) % 500 == 0: + console.print(f" [dim]Found {len(prs)} merged PRs...[/dim]") + + # Stop at 1000 per chunk to avoid API limit + if fetched_in_chunk >= 1000: + break + + # If we fetched less than 1000, we're done (no more results) + if fetched_in_chunk < 1000: + break + + # We fetched exactly 1000 - there might be more, continue to next chunk + if last_created_date: + current_start = last_created_date + timedelta(seconds=1) + console.print(f" [yellow]Fetched 1000 results - chunking to continue from {current_start.strftime('%Y-%m-%d %H:%M:%S')}...[/yellow]") + else: + break + + console.print(f" [green]✓[/green] Found {len(prs)} merged PRs with full data") + return prs + + except GithubException as e: + console.print(f"[red]Error searching PRs from {repo_full_name}: {e}[/red]") + return [] + + def get_ticket( + self, + repo_full_name: str, + ticket_number: int + ) -> Optional[Ticket]: + """ + Get a single ticket (convenience method for parallel fetching). + + Args: + repo_full_name: Full repository name (owner/repo) + ticket_number: Ticket number + + Returns: + Ticket model or None + """ + # Need to get repo_id first + repo_info = self.get_repository_info(repo_full_name) + # Assuming repo_id is stored - we'll need to look it up from DB + # For now, use a temporary value and let the caller handle it + return self.fetch_issue(repo_full_name, ticket_number, repo_id=0) + + def get_pull_request( + self, + repo_full_name: str, + pr_number: int + ) -> Optional[PullRequest]: + """ + Get a single pull request (convenience method for parallel fetching). + + Args: + repo_full_name: Full repository name (owner/repo) + pr_number: PR number + + Returns: + PullRequest model or None + """ + try: + repo = self.gh.get_repo(repo_full_name) + gh_pr = repo.get_pull(pr_number) + + # Need repo_id - use 0 for now and let caller handle it + return self._pr_to_model(gh_pr, repo_id=0) + except GithubException as e: + console.print(f"[yellow]Warning: Could not fetch PR #{pr_number}: {e}[/yellow]") + return None + + def fetch_releases(self, repo_full_name: str, repo_id: int) -> List[Release]: + """Fetch releases from GitHub with parallel processing.""" + from concurrent.futures import ThreadPoolExecutor, as_completed + + try: + repo = self.gh.get_repo(repo_full_name) + releases = [] + + # Get all release objects first (lightweight) + gh_releases = list(repo.get_releases()) + + if not gh_releases: + return [] + + # Process releases in parallel + def process_release(gh_release): + try: + return Release( + repo_id=repo_id, + version=gh_release.tag_name.lstrip('v'), + tag_name=gh_release.tag_name, + name=gh_release.title, + body=gh_release.body, + created_at=gh_release.created_at, + published_at=gh_release.published_at, + is_draft=gh_release.draft, + is_prerelease=gh_release.prerelease, + url=gh_release.html_url + ) + except Exception as e: + console.print(f"[yellow]Warning: Error processing release: {e}[/yellow]") + return None + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_release = { + executor.submit(process_release, gh_release): gh_release + for gh_release in gh_releases + } + + for future in as_completed(future_to_release): + release = future.result() + if release: + releases.append(release) + + return releases + except GithubException as e: + console.print(f"[yellow]Warning: Could not fetch releases: {e}[/yellow]") + return [] + + def create_release( + self, + repo_full_name: str, + version: str, + name: str, + body: str, + draft: bool = False, + prerelease: bool = False, + target_commitish: Optional[str] = None + ) -> Optional[str]: + """Create a GitHub release.""" + try: + repo = self.gh.get_repo(repo_full_name) + tag_name = f"{self.config.version_policy.tag_prefix}{version}" + + # Prepare arguments + kwargs = { + 'tag': tag_name, + 'name': name, + 'message': body, + 'draft': draft, + 'prerelease': prerelease + } + if target_commitish: + kwargs['target_commitish'] = target_commitish + + release = repo.create_git_release(**kwargs) + + console.print(f"[green]Created release: {release.html_url}[/green]") + return release.html_url + except GithubException as e: + console.print(f"[red]Error creating release: {e}[/red]") + return None + + def create_issue( + self, + repo_full_name: str, + title: str, + body: str, + labels: Optional[List[str]] = None, + milestone: Optional[Any] = None, + issue_type: Optional[str] = None + ) -> Optional[Dict[str, Any]]: + """Create a GitHub issue. + + Args: + repo_full_name: Repository in "owner/repo" format + title: Issue title + body: Issue body/description + labels: List of label names to apply + milestone: Milestone object or number to assign + issue_type: Issue type name (e.g. "Task", "Bug") + + Returns: + Dictionary with 'number' and 'url' keys if successful, None otherwise + """ + try: + repo = self.gh.get_repo(repo_full_name) + + # Get label objects if labels specified + label_objects = [] + if labels: + for label_name in labels: + try: + label = repo.get_label(label_name) + label_objects.append(label) + except GithubException: + # Label doesn't exist, skip it or create it + console.print(f"[yellow]Warning: Label '{label_name}' not found in {repo_full_name}, skipping[/yellow]") + + # Prepare kwargs + kwargs = { + 'title': title, + 'body': body, + 'labels': label_objects if label_objects else [] + } + + if milestone: + kwargs['milestone'] = milestone + + # Create the issue + issue = repo.create_issue(**kwargs) + + # Set issue type if specified + if issue_type: + self.set_issue_type(repo_full_name, issue.number, issue_type) + + console.print(f"[green]Created issue #{issue.number}: {issue.html_url}[/green]") + return { + 'number': str(issue.number), + 'url': issue.html_url + } + except GithubException as e: + console.print(f"[red]Error creating issue: {e}[/red]") + return None + + def get_milestone_by_title(self, repo_full_name: str, title: str) -> Optional[Any]: + """Get a milestone by its title.""" + try: + repo = self.gh.get_repo(repo_full_name) + milestones = repo.get_milestones(state='open') + for milestone in milestones: + if milestone.title == title: + return milestone + + console.print(f"[yellow]Warning: Milestone '{title}' not found in {repo_full_name}[/yellow]") + return None + except GithubException as e: + console.print(f"[yellow]Warning: Error fetching milestones: {e}[/yellow]") + return None + + def create_pr_for_release_notes( + self, + repo_full_name: str, + pr_title: str, + file_path: str, + content: str, + branch_name: str, + target_branch: str, + pr_body: Optional[str] = None, + additional_files: Optional[Dict[str, str]] = None + ) -> Optional[str]: + """ + Create a PR with release notes. + + Args: + repo_full_name: Full repository name (owner/repo) + pr_title: Title for the pull request + file_path: Path to the release notes file in the repo + content: Content of the release notes file + branch_name: Name of the branch to create + target_branch: Target branch for the PR (e.g., main) + pr_body: Optional body text for the PR + additional_files: Optional dictionary of {path: content} for extra files + + Returns: + URL of the created PR or None if failed + """ + try: + repo = self.gh.get_repo(repo_full_name) + + # Get base branch reference + base_ref = repo.get_git_ref(f"heads/{target_branch}") + base_sha = base_ref.object.sha + + # Create new branch + try: + repo.create_git_ref(f"refs/heads/{branch_name}", base_sha) + except GithubException: + # Branch might already exist + pass + + # Track if any changes were made + changes_made = False + + # Helper to update/create a file + def update_file(path: str, file_content: str) -> bool: + commit_msg = f"Update {path}" + try: + file_contents = repo.get_contents(path, ref=branch_name) + + # Check if content is identical + try: + existing_content = file_contents.decoded_content.decode('utf-8') + if existing_content == file_content: + console.print(f"[dim]Content unchanged for {path}, skipping commit[/dim]") + return False + except Exception: + # If comparison fails, proceed with update + pass + + # Update existing file + repo.update_file( + path, + commit_msg, + file_content, + file_contents.sha, + branch=branch_name + ) + return True + except GithubException: + # Create new file + repo.create_file( + path, + commit_msg, + file_content, + branch=branch_name + ) + return True + + # Update main release notes file + if update_file(file_path, content): + changes_made = True + + # Update additional files if any + if additional_files: + for path, file_content in additional_files.items(): + if update_file(path, file_content): + changes_made = True + + if not changes_made: + console.print("[yellow]No changes detected in release notes (diff is empty). Skipping commit/push.[/yellow]") + + # Create PR with custom title and body + pr_body_text = pr_body if pr_body else f"Automated release notes update" + try: + pr = repo.create_pull( + title=pr_title, + body=pr_body_text, + head=branch_name, + base=target_branch + ) + console.print(f"[green]Created PR: {pr.html_url}[/green]") + return pr.html_url + except GithubException as e: + if e.status == 422 and "A pull request already exists" in str(e.data): + console.print(f"[yellow]PR already exists for {branch_name}, finding it...[/yellow]") + # Find the existing PR + # head needs to be "owner:branch" or just "branch" depending on context + # Try searching for it + prs = repo.get_pulls(head=f"{repo.owner.login}:{branch_name}", base=target_branch, state='open') + if prs.totalCount > 0: + pr = prs[0] + console.print(f"[green]Found existing PR: {pr.html_url}[/green]") + + # Update PR title and body if they differ + if pr.title != pr_title or pr.body != pr_body_text: + console.print(f"[blue]Updating PR title/body...[/blue]") + pr.edit(title=pr_title, body=pr_body_text) + console.print(f"[green]Updated PR details[/green]") + + return pr.html_url + + # If it's another error or we couldn't find it + console.print(f"[red]Error creating PR: {e}[/red]") + return None + except GithubException as e: + console.print(f"[red]Error creating PR: {e}[/red]") + return None + + def get_authenticated_user(self) -> Optional[str]: + """ + Get the username of the currently authenticated user. + + Returns: + GitHub username or None if unable to fetch + """ + try: + user = self.gh.get_user() + return user.login + except GithubException as e: + console.print(f"[yellow]Warning: Could not fetch authenticated user: {e}[/yellow]") + return None + + def assign_issue_to_project( + self, + issue_url: str, + project_id: str, + status: Optional[str] = None, + custom_fields: Optional[Dict[str, str]] = None, + debug: bool = False + ) -> Optional[str]: + """ + Assign an issue to a GitHub Project and optionally set fields using GraphQL API. + + Args: + issue_url: Full URL of the issue (e.g., https://github.com/owner/repo/issues/123) + project_id: GitHub Project Node ID (e.g. PVT_...) + status: Status to set in the project (e.g., 'Todo', 'In Progress', 'Done') + custom_fields: Dictionary mapping custom field names to values + debug: Whether to show debug output + + Returns: + Project item ID if successful, None otherwise + + Example: + item_id = client.assign_issue_to_project( + issue_url="https://github.com/sequentech/meta/issues/8624", + project_id="PVT_kwDOBSDgG84ACa9s", + status="In Progress", + custom_fields={"Priority": "High", "Sprint": "2024-Q1"} + ) + """ + import requests + + try: + # Step 1: Get the issue node ID + issue_node_id = self._get_issue_node_id(issue_url) + if not issue_node_id: + return None + + # Step 2: Use the provided project ID (which is now the Node ID) + project_node_id = project_id + + # Step 3: Add the issue to the project + item_id = self._add_issue_to_project(issue_node_id, project_node_id) + if not item_id: + return None + + # Step 4: Set status if provided + if status: + self._set_project_status(project_node_id, item_id, status, debug=debug) + + # Step 5: Set custom fields if provided + if custom_fields: + for field_name, field_value in custom_fields.items(): + self._set_project_custom_field(project_node_id, item_id, field_name, field_value, debug=debug) + + return item_id + + except Exception as e: + console.print(f"[yellow]Warning: Error assigning issue to project: {e}[/yellow]") + return None + + def _get_issue_node_id(self, issue_url: str) -> Optional[str]: + """Extract issue node ID from URL using GraphQL.""" + import re + import requests + + # Parse issue owner/repo/number from URL + match = re.match(r'https?://github\.com/([^/]+)/([^/]+)/issues/(\d+)', issue_url) + if not match: + console.print(f"[yellow]Warning: Invalid issue URL format: {issue_url}[/yellow]") + return None + + owner, repo, number = match.groups() + + query = """ + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $number) { + id + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": {"owner": owner, "repo": repo, "number": int(number)}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error getting issue node ID: {data['errors']}[/yellow]") + return None + + return data["data"]["repository"]["issue"]["id"] + except Exception as e: + console.print(f"[yellow]Warning: Error getting issue node ID: {e}[/yellow]") + return None + + def get_project_node_id(self, org_name: str, project_number: int) -> Optional[str]: + """ + Get the project node ID (PVT_...) from the project number. + + Args: + org_name: Organization login (e.g. "sequentech") + project_number: Project number (e.g. 1) + + Returns: + Project node ID if found, None otherwise + """ + import requests + + query = """ + query($org: String!) { + organization(login: $org) { + projectsV2(first: 20) { + nodes { + id + title + url + } + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": {"org": org_name}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + # Check for permission errors + for error in data.get("errors", []): + if "FORBIDDEN" in str(error) or "Resource not accessible by integration" in str(error): + console.print(f"[red]Permission denied accessing organization projects.[/red]") + console.print(f"[yellow]Tip: Try refreshing your token permissions:[/yellow]") + console.print(f"[yellow] gh auth refresh -s read:org[/yellow]") + + console.print(f"[yellow]Warning: GraphQL error getting projects: {data['errors']}[/yellow]") + return None + + projects = data.get("data", {}).get("organization", {}).get("projectsV2", {}).get("nodes", []) + + # Look for matching project number in URL + # URL format: https://github.com/orgs/{org}/projects/{number} + target_suffix = f"/projects/{project_number}" + + for project in projects: + if project.get("url", "").endswith(target_suffix): + return project["id"] + + console.print(f"[yellow]Project number {project_number} not found in organization {org_name}[/yellow]") + return None + + except Exception as e: + console.print(f"[yellow]Warning: Error getting project node ID: {e}[/yellow]") + return None + + def _add_issue_to_project(self, issue_node_id: str, project_node_id: str) -> Optional[str]: + """Add an issue to a project using GraphQL.""" + import requests + + mutation = """ + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { + id + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://api.github.com/graphql", + json={"query": mutation, "variables": {"projectId": project_node_id, "contentId": issue_node_id}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error adding issue to project: {data['errors']}[/yellow]") + return None + + item_id = data["data"]["addProjectV2ItemById"]["item"]["id"] + console.print(f"[green]Added issue to project (internal project id: {item_id})[/green]") + return item_id + except Exception as e: + console.print(f"[yellow]Warning: Error adding issue to project: {e}[/yellow]") + return None + + def _set_project_status(self, project_node_id: str, item_id: str, status: str, debug: bool = False) -> bool: + """Set the status field of a project item.""" + import requests + + # First, get the status field ID + field_id = self._get_project_field_id(project_node_id, "Status") + if not field_id: + console.print(f"[yellow]Warning: Could not find 'Status' field in project[/yellow]") + return False + + # Get the status option ID + option_id = self._get_project_field_option_id(project_node_id, field_id, status, debug=debug) + if not option_id: + console.print(f"[yellow]Warning: Could not find status option '{status}' in project[/yellow]") + return False + + mutation = """ + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: $value + }) { + projectV2Item { + id + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://api.github.com/graphql", + json={"query": mutation, "variables": { + "projectId": project_node_id, + "itemId": item_id, + "fieldId": field_id, + "value": {"singleSelectOptionId": option_id} + }}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error setting status: {data['errors']}[/yellow]") + return False + + console.print(f"[green]Set project status to '{status}'[/green]") + return True + except Exception as e: + console.print(f"[yellow]Warning: Error setting project status: {e}[/yellow]") + return False + + def _set_project_custom_field(self, project_node_id: str, item_id: str, field_name: str, field_value: str, debug: bool = False) -> bool: + """ + Set a custom field of a project item. + + Supports: + - Text fields + - Number fields + - Date fields + - Single Select fields + - Iteration fields (supports "@current" to select current iteration) + """ + import requests + import json + from datetime import datetime, date + + # Get field details (ID, type, options/configuration) + field_info = self._get_project_field_details(project_node_id, field_name, debug=debug) + if not field_info: + console.print(f"[yellow]Warning: Could not find field '{field_name}' in project[/yellow]") + return False + + field_id = field_info["id"] + field_type = field_info.get("dataType", "TEXT") + + if debug: + console.print(f"[dim]Field '{field_name}' type: {field_type}[/dim]") + + # Prepare the value based on field type + value_arg = {} + + if field_type == "SINGLE_SELECT": + # Find option ID + options = field_info.get("options", []) + option_id = None + + # 1. Exact match + for opt in options: + if opt["name"].lower() == field_value.lower(): + option_id = opt["id"] + break + + # 2. Partial match if not found + if not option_id: + matches = [o for o in options if field_value.lower() in o["name"].lower()] + if len(matches) == 1: + option_id = matches[0]["id"] + if debug: + console.print(f"[dim]Using partial match '{matches[0]['name']}' for '{field_value}'[/dim]") + + if not option_id: + console.print(f"[yellow]Warning: Option '{field_value}' not found for field '{field_name}'[/yellow]") + return False + + value_arg = {"singleSelectOptionId": option_id} + + elif field_type == "ITERATION": + # Handle Iteration field + iterations = field_info.get("configuration", {}).get("iterations", []) + iteration_id = None + + # Check if user wants "Current" iteration + if field_value.lower() in ["@current"]: + # Find current iteration based on date + today = date.today().isoformat() + for iteration in iterations: + start_date = iteration.get("startDate") + duration = iteration.get("duration", 0) + + # Calculate end date (approximate, assuming duration is in days) + # Note: GitHub API returns duration in days + if start_date: + # We rely on GitHub's logic usually, but here we need to check locally + # or we could try to find one that is "active" if the API provided it + # But the API structure provided in _get_project_field_details needs to include dates + pass + + # Better approach: The API usually returns iterations in order. + # We need to parse dates to find the current one. + from datetime import timedelta + + now = datetime.now().date() + + for iteration in iterations: + start_str = iteration.get("startDate") + duration_days = iteration.get("duration", 14) # Default to 2 weeks if missing + + if start_str: + start_date = datetime.strptime(start_str, "%Y-%m-%d").date() + end_date = start_date + timedelta(days=duration_days) + + if start_date <= now < end_date: + iteration_id = iteration["id"] + if debug: + console.print(f"[dim]Found current iteration: {iteration['title']} ({start_str} - {end_date})[/dim]") + break + + if not iteration_id: + console.print(f"[yellow]Warning: Could not determine 'Current' iteration for field '{field_name}'[/yellow]") + return False + else: + # Find by name + for iteration in iterations: + if iteration["title"].lower() == field_value.lower(): + iteration_id = iteration["id"] + break + + if not iteration_id: + # Partial match + matches = [i for i in iterations if field_value.lower() in i["title"].lower()] + if len(matches) == 1: + iteration_id = matches[0]["id"] + else: + console.print(f"[yellow]Warning: Iteration '{field_value}' not found for field '{field_name}'[/yellow]") + return False + + value_arg = {"iterationId": iteration_id} + + elif field_type == "NUMBER": + try: + # GraphQL expects a float for number fields + value_arg = {"number": float(field_value)} + except ValueError: + console.print(f"[yellow]Warning: Value '{field_value}' is not a valid number for field '{field_name}'[/yellow]") + return False + + elif field_type == "DATE": + # Validate date format YYYY-MM-DD + try: + datetime.strptime(field_value, "%Y-%m-%d") + value_arg = {"date": field_value} + except ValueError: + console.print(f"[yellow]Warning: Value '{field_value}' is not a valid date (YYYY-MM-DD) for field '{field_name}'[/yellow]") + return False + + else: + # Default to text + value_arg = {"text": field_value} + + mutation = """ + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: $value + }) { + projectV2Item { + id + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + + if debug: + console.print(f"[dim]Setting field '{field_name}' ({field_id}) to {value_arg}[/dim]") + + response = requests.post( + "https://api.github.com/graphql", + json={"query": mutation, "variables": { + "projectId": project_node_id, + "itemId": item_id, + "fieldId": field_id, + "value": value_arg + }}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error setting field '{field_name}': {data['errors']}[/yellow]") + return False + + if debug: + console.print(f"[green]Successfully set field '{field_name}'[/green]") + return True + except Exception as e: + console.print(f"[yellow]Warning: Error setting custom field '{field_name}': {e}[/yellow]") + return False + + def _get_project_field_details(self, project_node_id: str, field_name: str, debug: bool = False) -> Optional[dict]: + """Get detailed information about a project field.""" + import requests + import json + + query = """ + query($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + fields(first: 20) { + nodes { + ... on ProjectV2Field { + id + name + dataType + } + ... on ProjectV2SingleSelectField { + id + name + dataType + options { + id + name + } + } + ... on ProjectV2IterationField { + id + name + dataType + configuration { + iterations { + id + title + startDate + duration + } + } + } + } + } + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + + if debug: + console.print(f"[dim]Fetching project fields...[/dim]") + + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": {"projectId": project_node_id}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error getting project fields: {data['errors']}[/yellow]") + return None + + fields = data["data"]["node"]["fields"]["nodes"] + for field in fields: + if field.get("name").lower() == field_name.lower(): + return field + + return None + except Exception as e: + console.print(f"[yellow]Warning: Error getting project field details: {e}[/yellow]") + return None + + def _get_project_field_id(self, project_node_id: str, field_name: str) -> Optional[str]: + """Get the field ID for a project field by name.""" + # Re-implement using the new detailed method for consistency + details = self._get_project_field_details(project_node_id, field_name) + return details["id"] if details else None + + + def _get_project_field_option_id(self, project_node_id: str, field_id: str, option_name: str, debug: bool = False) -> Optional[str]: + """Get the option ID for a single-select field.""" + import requests + import json + + query = """ + query($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + + if debug: + console.print(f"[dim]GraphQL Query (get options):[/dim]") + console.print(f"[dim]{query}[/dim]") + console.print(f"[dim]Variables: projectId={project_node_id}[/dim]") + + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": {"projectId": project_node_id}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if debug: + console.print(f"[dim]GraphQL Response (get options):[/dim]") + console.print(f"[dim]{json.dumps(data, indent=2)}[/dim]") + + if "errors" in data: + console.print(f"[yellow]Warning: GraphQL error getting field options: {data['errors']}[/yellow]") + return None + + fields = data["data"]["node"]["fields"]["nodes"] + for field in fields: + if field.get("id") == field_id and "options" in field: + options = field["options"] + target_name = option_name.lower() + + # 1. Exact match (case-insensitive) + for option in options: + if option["name"].lower() == target_name: + return option["id"] + + # 2. Partial match + matches = [o for o in options if target_name in o["name"].lower()] + + if len(matches) == 1: + matched = matches[0] + console.print(f"[yellow]Note: Using partial match '{matched['name']}' for status '{option_name}'[/yellow]") + return matched["id"] + elif len(matches) > 1: + names = [o["name"] for o in matches] + console.print(f"[yellow]Warning: Multiple status options match '{option_name}': {', '.join(names)}. Not applying.[/yellow]") + return None + else: + # No matches + available = [o["name"] for o in options] + console.print(f"[yellow]Warning: Status option '{option_name}' not found. Available: {', '.join(available)}[/yellow]") + return None + + return None + except Exception as e: + console.print(f"[yellow]Warning: Error getting field option ID: {e}[/yellow]") + return None + + def assign_issue( + self, + repo_full_name: str, + issue_number: int, + assignee: str + ) -> bool: + """ + Assign an issue to a user. + + Args: + repo_full_name: Repository in "owner/repo" format + issue_number: Issue number + assignee: GitHub username to assign to + + Returns: + True if successful, False otherwise + """ + try: + repo = self.gh.get_repo(repo_full_name) + issue = repo.get_issue(issue_number) + issue.add_to_assignees(assignee) + console.print(f"[green]Assigned issue #{issue_number} to @{assignee}[/green]") + return True + except GithubException as e: + console.print(f"[yellow]Warning: Could not assign issue: {e}[/yellow]") + return False + + def update_issue( + self, + repo_full_name: str, + issue_number: int, + title: Optional[str] = None, + body: Optional[str] = None, + labels: Optional[List[str]] = None, + milestone: Optional[Any] = None + ) -> bool: + """Update an existing issue.""" + try: + repo = self.gh.get_repo(repo_full_name) + issue = repo.get_issue(issue_number) + + kwargs = {} + if title is not None: + kwargs['title'] = title + if body is not None: + kwargs['body'] = body + if labels is not None: + kwargs['labels'] = labels + if milestone is not None: + kwargs['milestone'] = milestone + + if kwargs: + issue.edit(**kwargs) + console.print(f"[green]Updated issue #{issue_number}[/green]") + return True + except GithubException as e: + console.print(f"[red]Error updating issue #{issue_number}: {e}[/red]") + return False + + def update_issue_body( + self, + repo_full_name: str, + issue_number: int, + body: str + ) -> bool: + """Update the body of an existing issue.""" + return self.update_issue(repo_full_name, issue_number, body=body) + + def set_issue_type(self, repo_full_name: str, issue_number: int, type_name: str) -> bool: + """Set the issue type for an issue using GraphQL.""" + import requests + + owner, repo = repo_full_name.split('/') + + query = """ + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $number) { + id + } + issueTypes(first: 20) { + nodes { + id + name + } + } + } + } + """ + + try: + headers = { + "Authorization": f"Bearer {self.config.github.token}", + "Content-Type": "application/json" + } + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": {"owner": owner, "repo": repo, "number": int(issue_number)}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[red]GraphQL Error fetching issue/types: {data['errors']}[/red]") + return False + + repository = data.get("data", {}).get("repository") + if not repository: + return False + + issue_id = repository.get("issue", {}).get("id") + issue_types = repository.get("issueTypes", {}).get("nodes", []) + + if not issue_id: + console.print(f"[red]Could not find issue node ID for #{issue_number}[/red]") + return False + + # Find the type ID + type_id = None + for it in issue_types: + if it["name"].lower() == type_name.lower(): + type_id = it["id"] + break + + if not type_id: + console.print(f"[yellow]Warning: Issue type '{type_name}' not found in repository. Available types: {', '.join(t['name'] for t in issue_types)}[/yellow]") + return False + + # Mutation to update issue type + mutation = """ + mutation($issueId: ID!, $issueTypeId: ID!) { + updateIssue(input: {id: $issueId, issueTypeId: $issueTypeId}) { + issue { + id + } + } + } + """ + + response = requests.post( + "https://api.github.com/graphql", + json={"query": mutation, "variables": {"issueId": issue_id, "issueTypeId": type_id}}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + console.print(f"[red]GraphQL Error setting issue type: {data['errors']}[/red]") + return False + + console.print(f"[green]Set issue type to '{type_name}'[/green]") + return True + + except Exception as e: + console.print(f"[yellow]Warning: Error setting issue type: {e}[/yellow]") + return False diff --git a/src/release_tool/main.py b/src/release_tool/main.py new file mode 100644 index 0000000..4b570da --- /dev/null +++ b/src/release_tool/main.py @@ -0,0 +1,67 @@ +"""Main CLI for the release tool.""" + +import sys +from typing import Optional +import click +from rich.console import Console + +from .config import load_config +from .commands.sync import sync +from .commands.generate import generate +from .commands.publish import publish +from .commands.list_releases import list_releases +from .commands.init_config import init_config +from .commands.update_config import update_config +from .commands.tickets import tickets + +console = Console() + + +@click.group(context_settings={'help_option_names': ['-h', '--help']}) +@click.option( + '--config', + '-c', + type=click.Path(exists=True), + help='Path to configuration file' +) +@click.option( + '--auto', + is_flag=True, + help='Run in non-interactive mode (auto-apply defaults, skip prompts)' +) +@click.option( + '-y', '--assume-yes', + is_flag=True, + help='Assume "yes" for all confirmation prompts' +) +@click.pass_context +def cli(ctx, config: Optional[str], auto: bool, assume_yes: bool): + """Release tool for managing semantic versioned releases.""" + ctx.ensure_object(dict) + ctx.obj['auto'] = auto + ctx.obj['assume_yes'] = assume_yes + # Don't load config for init-config and update-config commands + if ctx.invoked_subcommand not in ['init-config', 'update-config']: + try: + ctx.obj['config'] = load_config(config, auto_upgrade=auto) + except FileNotFoundError as e: + console.print(f"[red]Error: {e}[/red]") + sys.exit(1) + + +# Register commands +cli.add_command(sync) +cli.add_command(generate) +cli.add_command(publish) +cli.add_command(list_releases) +cli.add_command(init_config) +cli.add_command(update_config) +cli.add_command(tickets) + + +def main(): + cli(obj={}) + + +if __name__ == "__main__": + main() diff --git a/src/release_tool/media_utils.py b/src/release_tool/media_utils.py new file mode 100644 index 0000000..b241d17 --- /dev/null +++ b/src/release_tool/media_utils.py @@ -0,0 +1,168 @@ +"""Media download and processing utilities for release notes.""" + +import re +import hashlib +from pathlib import Path +from typing import Optional, Dict, Tuple +from urllib.parse import urlparse +import requests +from rich.console import Console +from .template_utils import render_template, TemplateError + +console = Console() + + +class MediaDownloader: + """Download and manage media assets for release notes.""" + + def __init__(self, assets_path: str, download_enabled: bool = True): + """ + Initialize media downloader. + + Args: + assets_path: Path template for downloaded assets + download_enabled: Whether to download media or keep URLs + """ + self.assets_path = assets_path + self.download_enabled = download_enabled + self.downloaded_files: Dict[str, str] = {} # URL -> local path mapping + + def process_description( + self, + description: str, + version: str, + output_path: str + ) -> str: + """ + Process description text to download media and update references. + + Args: + description: Markdown text with potential media URLs + version: Version string for path substitution + output_path: Path to the output release notes file + + Returns: + Updated description with local media references + """ + if not self.download_enabled or not description: + return description + + # Find all image and video references in markdown + # Matches: ![alt](url) and videos with .mp4, .webm, etc. + media_pattern = r'!\[([^\]]*)\]\(([^)]+)\)' + + def replace_media(match): + alt_text = match.group(1) + url = match.group(2) + + # Skip if already a local path + if not url.startswith(('http://', 'https://')): + return match.group(0) + + # Download media and get local path + local_path = self._download_media(url, version, output_path) + if local_path: + return f'![{alt_text}]({local_path})' + + # If download fails, keep original + return match.group(0) + + return re.sub(media_pattern, replace_media, description) + + def _download_media( + self, + url: str, + version: str, + output_path: str + ) -> Optional[str]: + """ + Download media file and return relative path. + + Args: + url: URL of media file to download + version: Version string for path substitution + output_path: Path to the output release notes file + + Returns: + Relative path to downloaded file or None if failed + """ + # Check if already downloaded + if url in self.downloaded_files: + return self.downloaded_files[url] + + try: + # Parse version for path substitution + version_parts = self._parse_version(version) + + # Create assets directory using Jinja2 template + try: + assets_path_rendered = render_template(self.assets_path, version_parts) + assets_dir = Path(assets_path_rendered) + except TemplateError as e: + console.print(f"[red]Error rendering assets_path template: {e}[/red]") + return None + + assets_dir.mkdir(parents=True, exist_ok=True) + + # Generate filename from URL + parsed_url = urlparse(url) + original_filename = Path(parsed_url.path).name + + # Add hash to filename to avoid collisions + url_hash = hashlib.md5(url.encode()).hexdigest()[:8] + filename = f"{url_hash}_{original_filename}" + + local_file = assets_dir / filename + + # Download file + console.print(f"[blue]Downloading media: {url}[/blue]") + response = requests.get(url, timeout=30, stream=True) + response.raise_for_status() + + with open(local_file, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + # Calculate relative path from output_path to media file + output_dir = Path(output_path).parent + try: + relative_path = local_file.relative_to(output_dir) + except ValueError: + # If not relative, use absolute path + relative_path = local_file + + relative_path_str = str(relative_path).replace('\\', '/') + self.downloaded_files[url] = relative_path_str + + console.print(f"[green]Downloaded: {relative_path_str}[/green]") + return relative_path_str + + except Exception as e: + console.print(f"[yellow]Warning: Failed to download {url}: {e}[/yellow]") + return None + + def _parse_version(self, version: str) -> Dict[str, str]: + """ + Parse version string into components for path substitution. + + Args: + version: Version string (e.g., "1.2.3" or "1.2.3-rc.1") + + Returns: + Dictionary with version, major, minor, patch keys + """ + # Remove 'v' prefix if present + clean_version = version.lstrip('v') + + # Split by '-' to separate version from prerelease + version_base = clean_version.split('-')[0] + + # Split version into parts + parts = version_base.split('.') + + return { + 'version': version, + 'major': parts[0] if len(parts) > 0 else '0', + 'minor': parts[1] if len(parts) > 1 else '0', + 'patch': parts[2] if len(parts) > 2 else '0', + } diff --git a/src/release_tool/migrations/__init__.py b/src/release_tool/migrations/__init__.py new file mode 100644 index 0000000..d494231 --- /dev/null +++ b/src/release_tool/migrations/__init__.py @@ -0,0 +1,10 @@ +"""Config migration system. + +This package manages configuration file migrations between versions. +Individual migration scripts live in this directory (e.g., v1_0_to_v1_1.py). +The MigrationManager is defined in manager.py. +""" + +from .manager import MigrationManager, MigrationError + +__all__ = ['MigrationManager', 'MigrationError'] diff --git a/src/release_tool/migrations/manager.py b/src/release_tool/migrations/manager.py new file mode 100644 index 0000000..da7d4e8 --- /dev/null +++ b/src/release_tool/migrations/manager.py @@ -0,0 +1,237 @@ +"""Config migration system for release-tool. + +Handles upgrading config files between versions when format changes. +""" + +from pathlib import Path +from typing import Dict, List, Tuple, Optional, Callable +import importlib.util +from packaging import version + + +class MigrationError(Exception): + """Raised when a migration fails.""" + pass + + +class MigrationManager: + """Manages config file migrations.""" + + CURRENT_VERSION = "1.4" # Latest config version + + def __init__(self): + # Since manager.py is in the migrations/ directory, parent IS the migrations dir + self.migrations_dir = Path(__file__).parent + self._loaded_migrations: Dict[Tuple[str, str], Callable] = {} + + def compare_versions(self, v1: str, v2: str) -> int: + """ + Compare two version strings. + + Returns: + -1 if v1 < v2 + 0 if v1 == v2 + 1 if v1 > v2 + """ + ver1 = version.parse(v1) + ver2 = version.parse(v2) + + if ver1 < ver2: + return -1 + elif ver1 > ver2: + return 1 + else: + return 0 + + def needs_upgrade(self, current_version: str) -> bool: + """Check if config needs upgrade.""" + return self.compare_versions(current_version, self.CURRENT_VERSION) < 0 + + def get_migration_path(self, from_version: str, to_version: str) -> List[Tuple[str, str]]: + """ + Get ordered list of migrations needed to go from one version to another. + + Returns: + List of (from_version, to_version) tuples representing migration steps + """ + # For now, we support direct migrations + # In future, could support chained migrations (1.0 -> 1.1 -> 1.2) + available_migrations = self._discover_migrations() + + if (from_version, to_version) in available_migrations: + return [(from_version, to_version)] + + # Try to find a path through intermediate versions + # Simple implementation: try common version chain + path = [] + current = from_version + + while self.compare_versions(current, to_version) < 0: + # Find next migration + found = False + for (from_v, to_v) in available_migrations: + if from_v == current: + path.append((from_v, to_v)) + current = to_v + found = True + break + + if not found: + # No migration path found + raise MigrationError( + f"No migration path found from {from_version} to {to_version}" + ) + + return path + + def _discover_migrations(self) -> List[Tuple[str, str]]: + """Discover available migration scripts.""" + migrations = [] + + if not self.migrations_dir.exists(): + return migrations + + for file in self.migrations_dir.glob("v*_to_v*.py"): + # Parse filename: v1_0_to_v1_1.py -> ("1.0", "1.1") + name = file.stem + parts = name.split("_to_") + if len(parts) != 2: + continue + + from_part = parts[0].replace("v", "").replace("_", ".") + to_part = parts[1].replace("v", "").replace("_", ".") + + migrations.append((from_part, to_part)) + + return migrations + + def load_migration(self, from_version: str, to_version: str) -> Callable: + """Load a migration function from file.""" + key = (from_version, to_version) + + if key in self._loaded_migrations: + return self._loaded_migrations[key] + + # Build filename: v1_0_to_v1_1.py + from_part = "v" + from_version.replace(".", "_") + to_part = "v" + to_version.replace(".", "_") + filename = f"{from_part}_to_{to_part}.py" + + migration_file = self.migrations_dir / filename + + if not migration_file.exists(): + raise MigrationError( + f"Migration file not found: {migration_file}" + ) + + # Dynamically load the migration module + spec = importlib.util.spec_from_file_location( + f"migration_{from_part}_to_{to_part}", + migration_file + ) + if not spec or not spec.loader: + raise MigrationError(f"Failed to load migration: {migration_file}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if not hasattr(module, 'migrate'): + raise MigrationError( + f"Migration {filename} must have a 'migrate' function" + ) + + self._loaded_migrations[key] = module.migrate + return module.migrate + + def apply_migration( + self, + config_dict: Dict, + from_version: str, + to_version: str + ) -> Dict: + """Apply a single migration to config dict.""" + migrate_func = self.load_migration(from_version, to_version) + + try: + updated_config = migrate_func(config_dict) + # Ensure version is updated + updated_config['config_version'] = to_version + return updated_config + except Exception as e: + raise MigrationError( + f"Migration from {from_version} to {to_version} failed: {e}" + ) from e + + def upgrade_config( + self, + config_dict: Dict, + target_version: Optional[str] = None + ) -> Dict: + """ + Upgrade config dict to target version (or latest if not specified). + + Args: + config_dict: Config dictionary (from TOML) + target_version: Target version (defaults to latest) + + Returns: + Upgraded config dictionary + """ + if target_version is None: + target_version = self.CURRENT_VERSION + + current_version = config_dict.get('config_version', '1.0') + + if self.compare_versions(current_version, target_version) >= 0: + # Already at target version or newer + return config_dict + + # Get migration path + path = self.get_migration_path(current_version, target_version) + + # Apply migrations in sequence + updated_config = config_dict.copy() + for from_v, to_v in path: + updated_config = self.apply_migration(updated_config, from_v, to_v) + + return updated_config + + def get_changes_description(self, from_version: str, to_version: str) -> str: + """Get human-readable description of changes between versions.""" + descriptions = { + ("1.0", "1.1"): ( + "Version 1.1 adds:\n" + " • New template variables: ticket_url, pr_url for more flexible URL handling\n" + " • Improved output_template formatting with better spacing\n" + " • url field now intelligently uses ticket_url if available, else pr_url" + ), + ("1.1", "1.2"): ( + "Version 1.2 adds:\n" + " • New partial_ticket_action policy (ignore/warn/error)\n" + " • Handles tickets extracted but not found in database\n" + " • Handles tickets found in different repositories\n" + " • Provides diagnostics with potential reasons and links" + ), + ("1.2", "1.3"): ( + "Version 1.3 fixes:\n" + " • Ticket key format: removed '#' prefix from database storage\n" + " • Database queries now normalize keys (accept both '8624' and '#8624')\n" + " • BREAKING: Requires database migration to strip '#' from existing keys\n" + " • Display still shows '#' prefix for user-friendly output\n" + " • URL truncation fixed in tickets command" + ), + ("1.3", "1.4"): ( + "Version 1.4 adds:\n" + " • Dual template support: separate templates for GitHub and Docusaurus\n" + " • output_template renamed to release_output_template (GitHub release notes)\n" + " • output_path renamed to release_output_path (GitHub release notes file)\n" + " • New doc_output_template: wraps GitHub notes with Docusaurus frontmatter\n" + " • New doc_output_path: path for Docusaurus release notes file\n" + " • doc_output_template can use render_release_notes() to embed GitHub notes\n" + " • generate command creates both files when both templates configured\n" + " • Automatic config migration preserves your customizations" + ), + } + + key = (from_version, to_version) + return descriptions.get(key, "No description available") diff --git a/src/release_tool/migrations/v1_0_to_v1_1.py b/src/release_tool/migrations/v1_0_to_v1_1.py new file mode 100644 index 0000000..723ab0c --- /dev/null +++ b/src/release_tool/migrations/v1_0_to_v1_1.py @@ -0,0 +1,228 @@ +"""Migration from config version 1.0 to 1.1. + +Changes in 1.1: +- Added template variables: ticket_url, pr_url +- Improved output_template formatting (better spacing, blank lines) +- url field now smart: ticket_url if available, else pr_url +- Templates formatted as multiline literal strings for readability + +This migration: +- Updates output_template to new format (if it's still the default) +- Converts templates to multiline format using '''...''' +- Adds config_version field set to "1.1" +""" + +from typing import Dict, Any +import tomlkit + +# Default v1.0 output_template (for comparison) +V1_0_DEFAULT_OUTPUT_TEMPLATE = ( + "# {{ title }}\n" + "\n" + "{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if breaking_with_desc|length > 0 %}\n" + "## 💥 Breaking Changes\n" + "{% for note in breaking_with_desc %}\n" + "### {{ note.title }}\n" + "{{ note.description }}\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "{% set migration_notes = all_notes|selectattr('migration_notes')|list %}\n" + "{% if migration_notes|length > 0 %}\n" + "## 🔄 Migrations\n" + "{% for note in migration_notes %}\n" + "### {{ note.title }}\n" + "{{ note.migration_notes }}\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "{% set non_breaking_with_desc = all_notes|rejectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if non_breaking_with_desc|length > 0 %}\n" + "## 📝 Highlights\n" + "{% for note in non_breaking_with_desc %}\n" + "### {{ note.title }}\n" + "{{ note.description }}\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "## 📋 All Changes\n" + "{% for category in categories %}\n" + "### {{ category.name }}\n" + "{% for note in category.notes %}\n" + "{{ render_entry(note) }}\n" + "{% endfor %}\n" + "\n" + "{% endfor %}" +) + +# New v1.1 output_template (improved formatting) - as Python string +V1_1_DEFAULT_OUTPUT_TEMPLATE_STR = ( + "# {{ title }}\n" + "\n" + "{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if breaking_with_desc|length > 0 %}\n" + "## 💥 Breaking Changes\n" + "\n" + "{% for note in breaking_with_desc %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.description }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "{% set migration_notes = all_notes|selectattr('migration_notes')|list %}\n" + "{% if migration_notes|length > 0 %}\n" + "## 🔄 Migrations\n" + "\n" + "{% for note in migration_notes %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.migration_notes }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "{% set non_breaking_with_desc = all_notes|rejectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %}\n" + "{% if non_breaking_with_desc|length > 0 %}\n" + "## 📝 Highlights\n" + "\n" + "{% for note in non_breaking_with_desc %}\n" + "### {{ note.title }}\n" + "\n" + "{{ note.description }}\n" + "\n" + "{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %}\n" + "\n" + "{% endfor %}\n" + "{% endif %}\n" + "\n" + "## 📋 All Changes\n" + "\n" + "{% for category in categories %}\n" + "### {{ category.name }}\n" + "\n" + "{% for note in category.notes %}\n" + "{{ render_entry(note) }}\n" + "\n" + "{% endfor %}\n" + "{% endfor %}" +) + +# Multiline literal string version (for TOML formatting) +V1_1_DEFAULT_OUTPUT_TEMPLATE = '''# {{ title }} + +{% set breaking_with_desc = all_notes|selectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %} +{% if breaking_with_desc|length > 0 %} +## 💥 Breaking Changes + +{% for note in breaking_with_desc %} +### {{ note.title }} + +{{ note.description }} + +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} + +{% set migration_notes = all_notes|selectattr('migration_notes')|list %} +{% if migration_notes|length > 0 %} +## 🔄 Migrations + +{% for note in migration_notes %} +### {{ note.title }} + +{{ note.migration_notes }} + +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} + +{% set non_breaking_with_desc = all_notes|rejectattr('category', 'equalto', '💥 Breaking Changes')|selectattr('description')|list %} +{% if non_breaking_with_desc|length > 0 %} +## 📝 Highlights + +{% for note in non_breaking_with_desc %} +### {{ note.title }} + +{{ note.description }} + +{% if note.url %}See [#{{ note.pr_numbers[0] }}]({{ note.url }}) for details.{% endif %} + +{% endfor %} +{% endif %} + +## 📋 All Changes + +{% for category in categories %} +### {{ category.name }} + +{% for note in category.notes %} +{{ render_entry(note) }} + +{% endfor %} +{% endfor %}''' + +# Default entry template as multiline +V1_1_DEFAULT_ENTRY_TEMPLATE = '''- {{ title }} + {% if url %}{{ url }}{% endif %} + {% if authors %} + by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %}''' + + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Migrate config from version 1.0 to 1.1. + + Args: + config_dict: Config dictionary loaded from TOML + + Returns: + Upgraded config dictionary (can be regular dict, tomlkit will format it) + """ + # Parse the dict with tomlkit to preserve structure + doc = tomlkit.document() + + # Copy all existing content + for key, value in config_dict.items(): + doc[key] = value + + # Update config_version + doc['config_version'] = '1.1' + + # Update output_template if it's still the default v1.0 template + if 'release_notes' in doc: + current_template = doc['release_notes'].get('output_template') + current_entry = doc['release_notes'].get('entry_template') + + # Compare against both string formats (with \n and without) + if current_template in [V1_0_DEFAULT_OUTPUT_TEMPLATE, V1_1_DEFAULT_OUTPUT_TEMPLATE_STR]: + # Create a multiline literal string + multiline_output = tomlkit.string(V1_1_DEFAULT_OUTPUT_TEMPLATE, literal=True, multiline=True) + doc['release_notes']['output_template'] = multiline_output + elif current_template and '\n' in current_template: + # Convert any template with newlines to multiline format + multiline_output = tomlkit.string(current_template, literal=True, multiline=True) + doc['release_notes']['output_template'] = multiline_output + + # Also convert entry_template to multiline if it has newlines + if current_entry and '\n' in current_entry: + multiline_entry = tomlkit.string(current_entry, literal=True, multiline=True) + doc['release_notes']['entry_template'] = multiline_entry + elif not current_entry or current_entry == "- {{ title }}\n {% if url %}{{ url }}{% endif %}\n {% if authors %}\n by {% for author in authors %}{{ author.mention }}{% if not loop.last %}, {% endif %}{% endfor %}\n {% endif %}": + # Set default multiline entry template + multiline_entry = tomlkit.string(V1_1_DEFAULT_ENTRY_TEMPLATE, literal=True, multiline=True) + doc['release_notes']['entry_template'] = multiline_entry + + return doc diff --git a/src/release_tool/migrations/v1_1_to_v1_2.py b/src/release_tool/migrations/v1_1_to_v1_2.py new file mode 100644 index 0000000..54bb84a --- /dev/null +++ b/src/release_tool/migrations/v1_1_to_v1_2.py @@ -0,0 +1,46 @@ +"""Migration from config version 1.1 to 1.2. + +Changes in 1.2: +- Added ticket_policy.partial_ticket_action (ignore/warn/error) +- Handles tickets extracted but not found in database or found in different repo +- Provides diagnostics for partial matches with potential reasons + +This migration: +- Adds partial_ticket_action = "warn" to [ticket_policy] +- Updates config_version to "1.2" +""" + +from typing import Dict, Any +import tomlkit + + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Migrate config from version 1.1 to 1.2. + + Args: + config_dict: Config dictionary/document loaded from TOML + + Returns: + Upgraded config dictionary/document + """ + # If it's already a tomlkit document, modify in place to preserve comments + # Otherwise, create a new document + if hasattr(config_dict, 'add'): # tomlkit document has 'add' method + doc = config_dict + else: + doc = tomlkit.document() + for key, value in config_dict.items(): + doc[key] = value + + # Update config_version + doc['config_version'] = '1.2' + + # Add partial_ticket_action to ticket_policy if not already present + if 'ticket_policy' not in doc: + doc['ticket_policy'] = {} + + if 'partial_ticket_action' not in doc['ticket_policy']: + doc['ticket_policy']['partial_ticket_action'] = 'warn' + + return doc diff --git a/src/release_tool/migrations/v1_2_to_v1_3.py b/src/release_tool/migrations/v1_2_to_v1_3.py new file mode 100644 index 0000000..6ff9d6a --- /dev/null +++ b/src/release_tool/migrations/v1_2_to_v1_3.py @@ -0,0 +1,43 @@ +"""Migration from config version 1.2 to 1.3. + +Changes in 1.3: +- Fixed ticket key format: tickets now stored without "#" prefix in database +- Updated db.get_ticket_by_key() and db.query_tickets() to normalize keys +- Breaking change: requires database migration to strip "#" from existing ticket keys + +This migration: +- Updates config_version to "1.3" +- No config field changes required +- Note: Database will be automatically migrated on next sync, or run with --force-db-migration +""" + +from typing import Dict, Any +import tomlkit + + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Migrate config from version 1.2 to 1.3. + + Args: + config_dict: Config dictionary/document loaded from TOML + + Returns: + Upgraded config dictionary/document + """ + # If it's already a tomlkit document, modify in place to preserve comments + # Otherwise, create a new document + if hasattr(config_dict, 'add'): # tomlkit document has 'add' method + doc = config_dict + else: + doc = tomlkit.document() + for key, value in config_dict.items(): + doc[key] = value + + # Update config_version + doc['config_version'] = '1.3' + + # No other config changes needed for this version + # The main change is in database storage format (handled separately) + + return doc diff --git a/src/release_tool/migrations/v1_3_to_v1_4.py b/src/release_tool/migrations/v1_3_to_v1_4.py new file mode 100644 index 0000000..6276231 --- /dev/null +++ b/src/release_tool/migrations/v1_3_to_v1_4.py @@ -0,0 +1,83 @@ +"""Migration from config version 1.3 to 1.4. + +Changes in 1.4: +- Renamed output_template to release_output_template (for GitHub release notes) +- Added doc_output_template (for Docusaurus/documentation release notes) +- Renamed output_path to release_output_path (for GitHub release notes) +- Added doc_output_path (for Docusaurus output) +- Doc template can use render_release_notes() to wrap GitHub template + +This migration: +- Renames output_template → release_output_template (preserves customizations) +- Renames output_path → release_output_path (preserves user paths) +- Adds doc_output_template = None (optional, users can configure later) +- Adds doc_output_path = None (optional, users can configure later) +- Updates config_version to "1.4" +""" + +from typing import Dict, Any +import tomlkit + + +def migrate(config_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Migrate config from version 1.3 to 1.4. + + Args: + config_dict: Config dictionary/document loaded from TOML + + Returns: + Upgraded config dictionary/document + """ + # If it's already a tomlkit document, modify in place to preserve comments + # Otherwise, create a new document + if hasattr(config_dict, 'add'): # tomlkit document has 'add' method + doc = config_dict + else: + doc = tomlkit.document() + for key, value in config_dict.items(): + doc[key] = value + + # Update config_version + doc['config_version'] = '1.4' + + # Migrate release_notes section + if 'release_notes' in doc: + # Rename output_template → release_output_template + if 'output_template' in doc['release_notes']: + output_template_value = doc['release_notes']['output_template'] + del doc['release_notes']['output_template'] + doc['release_notes']['release_output_template'] = output_template_value + + # Add doc_output_template if not present + if 'doc_output_template' not in doc['release_notes']: + # Add a comment explaining the new field + doc['release_notes'].add(tomlkit.comment( + "doc_output_template: Optional Jinja2 template for Docusaurus/documentation output" + )) + doc['release_notes'].add(tomlkit.comment( + "Example: '---\\nid: release-{{version}}\\ntitle: {{title}}\\n---\\n{{ render_release_notes() }}'" + )) + # Note: tomlkit doesn't support None directly in some versions, so we comment it out + # Users can uncomment and configure when needed + + # Migrate output section + if 'output' in doc: + # Rename output_path → release_output_path + if 'output_path' in doc['output']: + output_path_value = doc['output']['output_path'] + del doc['output']['output_path'] + doc['output']['release_output_path'] = output_path_value + + # Add doc_output_path if not present + if 'doc_output_path' not in doc['output']: + # Add a comment explaining the new field + doc['output'].add(tomlkit.comment( + "doc_output_path: Optional path template for Docusaurus/documentation output" + )) + doc['output'].add(tomlkit.comment( + "Example: 'docs/docusaurus/docs/releases/release-{major}.{minor}/release-{major}.{minor}.{patch}.md'" + )) + # Note: tomlkit doesn't support None directly in some versions, so we comment it out + + return doc diff --git a/src/release_tool/models.py b/src/release_tool/models.py new file mode 100644 index 0000000..85cd687 --- /dev/null +++ b/src/release_tool/models.py @@ -0,0 +1,350 @@ +"""Data models for the release tool.""" + +from datetime import datetime +from typing import Optional, List, Dict, Any +from enum import Enum +from pydantic import BaseModel, Field, field_validator + + +class VersionType(str, Enum): + """Types of version releases.""" + FINAL = "final" + RELEASE_CANDIDATE = "rc" + BETA = "beta" + ALPHA = "alpha" + + +class SemanticVersion(BaseModel): + """Semantic version model.""" + major: int + minor: int + patch: int + prerelease: Optional[str] = None + + @classmethod + def parse(cls, version_str: str, allow_partial: bool = False) -> "SemanticVersion": + """ + Parse a semantic version string. + + Args: + version_str: Version string to parse (e.g., "1.2.3", "1.2.3-rc.1", "1.2") + allow_partial: If True, allows partial versions like "1.2" (patch defaults to 0) + + Returns: + SemanticVersion instance + + Raises: + ValueError: If version string is invalid + """ + import re + # Remove leading 'v' if present + version_str = version_str.lstrip('v') + + # Try full pattern first: major.minor.patch[-prerelease] + full_pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$' + match = re.match(full_pattern, version_str) + + if match: + major, minor, patch, prerelease = match.groups() + return cls( + major=int(major), + minor=int(minor), + patch=int(patch), + prerelease=prerelease + ) + + # Try partial pattern if allowed: major.minor + if allow_partial: + partial_pattern = r'^(\d+)\.(\d+)$' + match = re.match(partial_pattern, version_str) + if match: + major, minor = match.groups() + return cls( + major=int(major), + minor=int(minor), + patch=0, + prerelease=None + ) + + raise ValueError(f"Invalid semantic version: {version_str}") + + def to_string(self, include_v: bool = False) -> str: + """Convert to string representation.""" + version = f"{self.major}.{self.minor}.{self.patch}" + if self.prerelease: + version += f"-{self.prerelease}" + if include_v: + version = f"v{version}" + return version + + def is_final(self) -> bool: + """Check if this is a final release.""" + return self.prerelease is None + + def get_type(self) -> VersionType: + """Get the version type.""" + if not self.prerelease: + return VersionType.FINAL + + prerelease_lower = self.prerelease.lower() + if prerelease_lower.startswith('rc'): + return VersionType.RELEASE_CANDIDATE + elif prerelease_lower.startswith('beta'): + return VersionType.BETA + elif prerelease_lower.startswith('alpha'): + return VersionType.ALPHA + + return VersionType.FINAL + + def __lt__(self, other: "SemanticVersion") -> bool: + """Compare versions.""" + if self.major != other.major: + return self.major < other.major + if self.minor != other.minor: + return self.minor < other.minor + if self.patch != other.patch: + return self.patch < other.patch + + # Handle prerelease comparison + if self.prerelease is None and other.prerelease is None: + return False + if self.prerelease is None: + return False # Final version is greater + if other.prerelease is None: + return True # Prerelease is less than final + + return self.prerelease < other.prerelease + + def __eq__(self, other: object) -> bool: + """Check equality.""" + if not isinstance(other, SemanticVersion): + return False + return (self.major == other.major and + self.minor == other.minor and + self.patch == other.patch and + self.prerelease == other.prerelease) + + def __le__(self, other: "SemanticVersion") -> bool: + return self < other or self == other + + def __gt__(self, other: "SemanticVersion") -> bool: + return not self <= other + + def __ge__(self, other: "SemanticVersion") -> bool: + return not self < other + + def bump_major(self) -> "SemanticVersion": + """Create a new version with major version bumped.""" + return SemanticVersion(major=self.major + 1, minor=0, patch=0) + + def bump_minor(self) -> "SemanticVersion": + """Create a new version with minor version bumped.""" + return SemanticVersion(major=self.major, minor=self.minor + 1, patch=0) + + def bump_patch(self) -> "SemanticVersion": + """Create a new version with patch version bumped.""" + return SemanticVersion(major=self.major, minor=self.minor, patch=self.patch + 1) + + def bump_rc(self, rc_number: int = 0) -> "SemanticVersion": + """Create a new RC version.""" + return SemanticVersion( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=f"rc.{rc_number}" + ) + + +class Repository(BaseModel): + """Repository model.""" + id: Optional[int] = None + owner: str + name: str + full_name: str = "" + url: str = "" + default_branch: str = "main" + + def __init__(self, **data): + if 'full_name' not in data or not data['full_name']: + data['full_name'] = f"{data['owner']}/{data['name']}" + super().__init__(**data) + + +class Label(BaseModel): + """GitHub label model.""" + name: str + color: Optional[str] = None + description: Optional[str] = None + + +class Author(BaseModel): + """ + Author/contributor model with comprehensive information. + + Combines Git author info (name, email from commits) with GitHub user info + (login, username, profile data). Not all fields are always available. + """ + # Core identification (at least one should be present) + name: Optional[str] = None # Git author name (from commit) + email: Optional[str] = None # Git author email (from commit) + username: Optional[str] = None # GitHub login/username + + # GitHub user details (when available) + github_id: Optional[int] = None # GitHub user ID + display_name: Optional[str] = None # GitHub display name (may differ from git name) + avatar_url: Optional[str] = None # Profile picture URL + profile_url: Optional[str] = None # GitHub profile URL (html_url) + + # Extended profile info (optional, from GitHub API) + company: Optional[str] = None + location: Optional[str] = None + bio: Optional[str] = None + blog: Optional[str] = None + user_type: Optional[str] = None # "User", "Bot", "Organization", etc. + + def get_identifier(self) -> str: + """ + Get the best identifier for this author. + + Priority: username > name > email + """ + if self.username: + return self.username + if self.name: + return self.name + if self.email: + return self.email.split('@')[0] # Use email prefix as fallback + return "unknown" + + def get_display_name(self) -> str: + """ + Get the best display name for this author. + + Priority: display_name > name > username > email + """ + if self.display_name: + return self.display_name + if self.name: + return self.name + if self.username: + return self.username + if self.email: + return self.email.split('@')[0] + return "Unknown Author" + + def get_mention(self) -> str: + """Get @ mention format (for GitHub comments, release notes, etc.).""" + if self.username: + return f"@{self.username}" + return self.get_display_name() + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for templates.""" + return { + 'name': self.name, + 'email': self.email, + 'username': self.username, + 'github_id': self.github_id, + 'display_name': self.display_name, + 'avatar_url': self.avatar_url, + 'profile_url': self.profile_url, + 'company': self.company, + 'location': self.location, + 'bio': self.bio, + 'blog': self.blog, + 'user_type': self.user_type, + 'identifier': self.get_identifier(), + 'mention': self.get_mention(), + 'full_display_name': self.get_display_name(), + } + + +class PullRequest(BaseModel): + """Pull request model.""" + id: Optional[int] = None + repo_id: int + number: int + title: str + body: Optional[str] = None + state: str + merged_at: Optional[datetime] = None + author: Optional[Author] = None # Changed from str to Author + base_branch: Optional[str] = None + head_branch: Optional[str] = None + head_sha: Optional[str] = None + labels: List[Label] = Field(default_factory=list) + url: Optional[str] = None + + +class Commit(BaseModel): + """Git commit model.""" + sha: str + repo_id: int + message: str + author: Author # Changed from str to Author (includes name, email, etc.) + date: datetime + url: Optional[str] = None + pr_number: Optional[int] = None + + +class Ticket(BaseModel): + """Issue/ticket model.""" + id: Optional[int] = None + repo_id: int + number: int + key: str # e.g., "JIRA-123" or issue number + title: str + body: Optional[str] = None + state: str + labels: List[Label] = Field(default_factory=list) + url: Optional[str] = None + created_at: Optional[datetime] = None + closed_at: Optional[datetime] = None + category: Optional[str] = None + tags: Dict[str, str] = Field(default_factory=dict) + + +class Release(BaseModel): + """Release model.""" + id: Optional[int] = None + repo_id: int + version: str + tag_name: str + name: Optional[str] = None + body: Optional[str] = None + created_at: Optional[datetime] = None + published_at: Optional[datetime] = None + is_draft: bool = False + is_prerelease: bool = False + url: Optional[str] = None + target_commitish: Optional[str] = None + + +class ReleaseNote(BaseModel): + """Release note entry model.""" + ticket_key: Optional[str] = None + title: str + description: Optional[str] = None + migration_notes: Optional[str] = None + category: Optional[str] = None + labels: List[str] = Field(default_factory=list) + authors: List[Author] = Field(default_factory=list) # Changed from List[str] to List[Author] + pr_numbers: List[int] = Field(default_factory=list) + commit_shas: List[str] = Field(default_factory=list) + ticket_url: Optional[str] = None # URL to the ticket/issue + pr_url: Optional[str] = None # URL to the pull request + url: Optional[str] = None # Smart URL: ticket_url if available, else pr_url + short_link: Optional[str] = None # Short format: #1234 + short_repo_link: Optional[str] = None # Short format with repo: owner/repo#1234 + tags: Dict[str, str] = Field(default_factory=dict) + + +class ConsolidatedChange(BaseModel): + """Consolidated change from commits/PRs.""" + type: str # "ticket", "pr", "commit" + ticket_key: Optional[str] = None + pr_number: Optional[int] = None + commits: List[Commit] = Field(default_factory=list) + prs: List[PullRequest] = Field(default_factory=list) + ticket: Optional[Ticket] = None + release_note: Optional[ReleaseNote] = None diff --git a/src/release_tool/policies.py b/src/release_tool/policies.py new file mode 100644 index 0000000..88e870f --- /dev/null +++ b/src/release_tool/policies.py @@ -0,0 +1,997 @@ +"""Policy implementations for ticket extraction, consolidation, and release notes.""" + +import re +from dataclasses import dataclass, field +from typing import List, Dict, Optional, Any, Set +from enum import Enum +from rich.console import Console + +from .models import ( + Commit, PullRequest, Ticket, ReleaseNote, ConsolidatedChange, Label +) +from .config import ( + Config, PolicyAction, TicketExtractionStrategy +) + +console = Console() + + +class PartialTicketReason(Enum): + """Reasons why a ticket might be partially matched.""" + + # Not found reasons + OLDER_THAN_CUTOFF = "older_than_cutoff" # Ticket may be older than sync cutoff date + TYPO = "typo" # Ticket may not exist (typo in branch/PR) + SYNC_NOT_RUN = "sync_not_run" # Sync may not have been run yet + + # Different repo reasons + REPO_CONFIG_MISMATCH = "repo_config_mismatch" # Ticket found in different repo than configured + WRONG_TICKET_REPOS = "wrong_ticket_repos" # Mismatch between ticket_repos config and actual location + + @property + def description(self) -> str: + """Get human-readable description of the reason.""" + descriptions = { + PartialTicketReason.OLDER_THAN_CUTOFF: "Ticket may be older than sync cutoff date", + PartialTicketReason.TYPO: "Ticket may not exist (typo in branch/PR)", + PartialTicketReason.SYNC_NOT_RUN: "Sync may not have been run yet", + PartialTicketReason.REPO_CONFIG_MISMATCH: "Ticket found in different repo than configured", + PartialTicketReason.WRONG_TICKET_REPOS: "Check repository.ticket_repos in config", + } + return descriptions.get(self, self.value) + + +@dataclass +class PartialTicketMatch: + """ + Information about a partial ticket match. + + A partial match occurs when a ticket is extracted from a branch/PR/commit + but cannot be fully resolved to a ticket in the database, or is found + in an unexpected repository. + """ + ticket_key: str # The extracted ticket key (e.g., "8624", "#123") + extracted_from: str # Human-readable description of source (e.g., "branch feat/meta-8624/main, pattern #1") + match_type: str # "not_found" or "different_repo" + found_in_repo: Optional[str] = None # For different_repo type: which repo it was found in + ticket_url: Optional[str] = None # For different_repo type: URL to the ticket + potential_reasons: Set[PartialTicketReason] = field(default_factory=set) # Set of potential causes + + +class TicketExtractor: + """Extract ticket references from various sources.""" + + def __init__(self, config: Config, debug: bool = False): + self.config = config + self.debug = debug + # Sort patterns by order field, then group by strategy for efficient lookup + sorted_patterns = sorted(config.ticket_policy.patterns, key=lambda p: p.order) + self.pattern_configs = sorted_patterns # Store for debug output + self.patterns_by_strategy: Dict[TicketExtractionStrategy, List[re.Pattern]] = {} + for ticket_pattern in sorted_patterns: + strategy = ticket_pattern.strategy + if strategy not in self.patterns_by_strategy: + self.patterns_by_strategy[strategy] = [] + self.patterns_by_strategy[strategy].append(re.compile(ticket_pattern.pattern)) + + def _extract_with_patterns(self, text: str, patterns: List[re.Pattern], show_results: bool = False) -> List[str]: + """Extract ticket references using a list of patterns.""" + tickets = [] + for i, pattern in enumerate(patterns): + matches_found = [] + # Use finditer to get match objects and extract named groups + for match in pattern.finditer(text): + # Try to extract the 'ticket' named group + try: + ticket = match.group('ticket') + tickets.append(ticket) + matches_found.append(ticket) + except IndexError: + # If no 'ticket' group, fall back to the entire match or first group + if match.groups(): + ticket = match.group(1) + tickets.append(ticket) + matches_found.append(ticket) + else: + ticket = match.group(0) + tickets.append(ticket) + matches_found.append(ticket) + + if self.debug and show_results: + if matches_found: + console.print(f" [green]✅ MATCH! Extracted: {matches_found}[/green]") + else: + console.print(f" [dim]❌ No match[/dim]") + + return tickets + + def extract_from_commit(self, commit: Commit) -> List[str]: + """Extract ticket references from commit message.""" + if self.debug: + console.print(f"\n🔍 [bold cyan]Extracting from commit:[/bold cyan] {commit.sha[:7]} - {commit.message[:60]}{'...' if len(commit.message) > 60 else ''}") + + patterns = self.patterns_by_strategy.get(TicketExtractionStrategy.COMMIT_MESSAGE, []) + + # Debug: show which patterns apply to commit_message + if self.debug and patterns: + matching_configs = [p for p in self.pattern_configs if p.strategy == TicketExtractionStrategy.COMMIT_MESSAGE] + for pattern_config in matching_configs: + console.print(f" [dim]Trying pattern #{pattern_config.order} (strategy={pattern_config.strategy.value})[/dim]") + if pattern_config.description: + console.print(f" Description: \"{pattern_config.description}\"") + console.print(f" Regex: {pattern_config.pattern}") + console.print(f" Text: \"{commit.message[:100]}{'...' if len(commit.message) > 100 else ''}\"") + + tickets = list(set(self._extract_with_patterns(commit.message, patterns, show_results=self.debug))) + + if self.debug: + if tickets: + console.print(f" [green]✅ Extracted tickets: {tickets}[/green]") + else: + console.print(f" [yellow]- Extracted tickets: (none)[/yellow]") + + return tickets + + def extract_from_pr(self, pr: PullRequest) -> List[str]: + """Extract ticket references from PR using configured strategies.""" + if self.debug: + console.print(f"\n🔍 [bold cyan]Extracting from PR #{pr.number}:[/bold cyan] {pr.title[:60]}{'...' if len(pr.title) > 60 else ''}") + + tickets = [] + + # Try patterns in order (sorted by order field) + sorted_patterns = sorted(self.config.ticket_policy.patterns, key=lambda p: p.order) + for ticket_pattern in sorted_patterns: + strategy = ticket_pattern.strategy + text = None + source_name = None + + if strategy == TicketExtractionStrategy.PR_BODY and pr.body: + text = pr.body + source_name = "pr_body" + elif strategy == TicketExtractionStrategy.PR_TITLE and pr.title: + text = pr.title + source_name = "pr_title" + elif strategy == TicketExtractionStrategy.BRANCH_NAME and pr.head_branch: + text = pr.head_branch + source_name = "branch_name" + + if self.debug: + console.print(f" [dim]Pattern #{ticket_pattern.order} (strategy={strategy.value})[/dim]") + if ticket_pattern.description: + console.print(f" Description: \"{ticket_pattern.description}\"") + console.print(f" Regex: {ticket_pattern.pattern}") + + if text: + if self.debug: + console.print(f" Source: {source_name}") + console.print(f" Text: \"{text[:100]}{'...' if len(text) > 100 else ''}\"") + + pattern = re.compile(ticket_pattern.pattern) + extracted = self._extract_with_patterns(text, [pattern], show_results=self.debug) + if extracted: + tickets.extend(extracted) + if self.debug: + console.print(f" [yellow]🛑 Stopping (first match wins)[/yellow]") + # Stop on first match to respect priority order + break + else: + if self.debug: + console.print(f" [dim]❌ Skipped (no {strategy.value} available)[/dim]") + + if self.debug: + if tickets: + console.print(f" [green]✅ Extracted tickets: {list(set(tickets))}[/green]") + else: + console.print(f" [yellow]- Extracted tickets: (none)[/yellow]") + + return list(set(tickets)) + + def extract_from_branch(self, branch_name: str) -> List[str]: + """Extract ticket references from branch name.""" + if self.debug: + console.print(f"\n🔍 [bold cyan]Extracting from branch:[/bold cyan] {branch_name}") + + patterns = self.patterns_by_strategy.get(TicketExtractionStrategy.BRANCH_NAME, []) + + # Debug: show which patterns apply to branch_name + if self.debug and patterns: + matching_configs = [p for p in self.pattern_configs if p.strategy == TicketExtractionStrategy.BRANCH_NAME] + for pattern_config in matching_configs: + console.print(f" [dim]Trying pattern #{pattern_config.order} (strategy={pattern_config.strategy.value})[/dim]") + if pattern_config.description: + console.print(f" Description: \"{pattern_config.description}\"") + console.print(f" Regex: {pattern_config.pattern}") + console.print(f" Text: \"{branch_name}\"") + + tickets = list(set(self._extract_with_patterns(branch_name, patterns, show_results=self.debug))) + + if self.debug: + if tickets: + console.print(f" [green]✅ Extracted tickets: {tickets}[/green]") + else: + console.print(f" [yellow]- Extracted tickets: (none)[/yellow]") + + return tickets + + +class CommitConsolidator: + """Consolidate commits by parent ticket.""" + + def __init__(self, config: Config, extractor: TicketExtractor, debug: bool = False): + self.config = config + self.extractor = extractor + self.debug = debug + + def consolidate( + self, + commits: List[Commit], + prs: Dict[int, PullRequest] + ) -> List[ConsolidatedChange]: + """ + Consolidate commits by their parent ticket. + + Returns a list of ConsolidatedChange objects, grouped by ticket. + """ + if not self.config.ticket_policy.consolidation_enabled: + # Return each commit as a separate change + return [ + ConsolidatedChange( + type="commit", + commits=[commit] + ) + for commit in commits + ] + + consolidated: Dict[str, ConsolidatedChange] = {} + + if self.debug: + console.print(f"\n[bold magenta]{'='*60}[/bold magenta]") + console.print(f"[bold magenta]📦 CONSOLIDATION PHASE[/bold magenta]") + console.print(f"[bold magenta]{'='*60}[/bold magenta]\n") + + for commit in commits: + if self.debug: + console.print(f"\n📦 [bold]Consolidating commit {commit.sha[:7]}:[/bold] \"{commit.message[:60]}{'...' if len(commit.message) > 60 else ''}\"") + + # Try to find ticket from commit + tickets = self.extractor.extract_from_commit(commit) + + if self.debug: + console.print(f" → Tickets from commit: {tickets if tickets else '(none)'}") + + # Try to find associated PR + pr = prs.get(commit.pr_number) if commit.pr_number else None + if pr: + if self.debug: + console.print(f" ✅ Associated PR: #{pr.number} \"{pr.title[:50]}{'...' if len(pr.title) > 50 else ''}\"") + pr_tickets = self.extractor.extract_from_pr(pr) + if self.debug: + console.print(f" {'✅' if pr_tickets else '→'} Tickets from PR: {pr_tickets if pr_tickets else '(none)'}") + tickets.extend(pr_tickets) + elif self.debug: + console.print(f" → Associated PR: (none)") + + tickets = list(set(tickets)) # Remove duplicates + + if tickets: + # Use first ticket as the parent + ticket_key = tickets[0] + if self.debug: + console.print(f" [green]✅ Consolidated under ticket: {ticket_key}[/green]") + + if ticket_key not in consolidated: + consolidated[ticket_key] = ConsolidatedChange( + type="ticket", + ticket_key=ticket_key, + commits=[], + prs=[] + ) + consolidated[ticket_key].commits.append(commit) + if pr and pr not in consolidated[ticket_key].prs: + consolidated[ticket_key].prs.append(pr) + elif pr: + # No ticket but has PR + pr_key = f"pr-{pr.number}" + if self.debug: + console.print(f" [yellow]✅ Consolidated under PR: #{pr.number}[/yellow]") + + if pr_key not in consolidated: + consolidated[pr_key] = ConsolidatedChange( + type="pr", + pr_number=pr.number, + commits=[], + prs=[pr] + ) + consolidated[pr_key].commits.append(commit) + else: + # No ticket and no PR - standalone commit + commit_key = f"commit-{commit.sha[:8]}" + if self.debug: + console.print(f" [dim]✅ Standalone commit (no ticket or PR)[/dim]") + + consolidated[commit_key] = ConsolidatedChange( + type="commit", + commits=[commit] + ) + + return list(consolidated.values()) + + def handle_missing_tickets( + self, + consolidated_changes: List[ConsolidatedChange] + ): + """Handle changes that don't have a parent ticket.""" + action = self.config.ticket_policy.no_ticket_action + no_ticket_changes = [ + c for c in consolidated_changes + if c.type in ["commit", "pr"] or not c.ticket_key + ] + + if not no_ticket_changes: + return + + if action == PolicyAction.ERROR: + raise ValueError( + f"Found {len(no_ticket_changes)} changes without a parent ticket. " + "Configure no_ticket_action policy to allow this." + ) + elif action == PolicyAction.WARN: + console.print( + f"[yellow]WARNING: Found {len(no_ticket_changes)} changes without a parent ticket[/yellow]" + ) + for change in no_ticket_changes[:5]: # Show first 5 + if change.commits: + msg = change.commits[0].message.split('\n')[0] + console.print(f" - {msg[:80]}") + + +class ReleaseNoteGenerator: + """Generate release notes from consolidated changes.""" + + def __init__(self, config: Config): + self.config = config + + def create_release_note( + self, + change: ConsolidatedChange, + ticket: Optional[Ticket] = None + ) -> ReleaseNote: + """Create a release note from a consolidated change.""" + # Extract and deduplicate authors from commits and PRs + authors = self._deduplicate_authors(change) + + pr_numbers = list(set(pr.number for pr in change.prs)) + commit_shas = [commit.sha for commit in change.commits] + + # Determine title + if ticket: + title = ticket.title + elif change.prs: + title = change.prs[0].title + elif change.commits: + title = change.commits[0].message.split('\n')[0] + else: + title = "Unknown change" + + # Determine category from labels + category = self._determine_category(change, ticket) + + # Extract description and migration notes if we have a ticket + description = None + migration_notes = None + if ticket and ticket.body: + description = self._extract_section( + ticket.body, + self.config.ticket_policy.description_section_regex + ) + migration_notes = self._extract_section( + ticket.body, + self.config.ticket_policy.migration_section_regex + ) + + # Get URLs + ticket_url = None + pr_url = None + url = None # Smart URL: ticket_url if available, else pr_url + + if ticket: + ticket_url = ticket.url + url = ticket_url # Prefer ticket URL + + if change.prs: + pr_url = change.prs[0].url + if not url: # Use PR URL if no ticket URL + url = pr_url + + # Compute short links from the smart URL + short_link = None + short_repo_link = None + if url: + repo_name, number = self._extract_github_url_info(url) + if number: + short_link = f"#{number}" + if repo_name: + short_repo_link = f"{repo_name}#{number}" + + # Get labels + labels = [] + if ticket: + labels = [label.name for label in ticket.labels] + elif change.prs: + labels = [label.name for pr in change.prs for label in pr.labels] + + return ReleaseNote( + ticket_key=change.ticket_key, + title=title, + description=description, + migration_notes=migration_notes, + category=category, + labels=labels, + authors=authors, + pr_numbers=pr_numbers, + commit_shas=commit_shas, + ticket_url=ticket_url, + pr_url=pr_url, + url=url, + short_link=short_link, + short_repo_link=short_repo_link + ) + + def _deduplicate_authors(self, change: ConsolidatedChange) -> List[Any]: + """ + Deduplicate authors from commits and PRs. + + Prioritizes PR authors because they have GitHub usernames (needed for @mentions). + Commits often only have Name/Email, and Email might be private/missing in PR data. + """ + final_authors_map = {} # Key by identifier -> Author + + # 1. Index PR authors (they are our "source of truth" for GitHub identity) + pr_authors_by_pr_number = {} + known_authors_by_name = {} + known_authors_by_email = {} + + for pr in change.prs: + if pr.author: + pr_authors_by_pr_number[pr.number] = pr.author + + # Add to final map immediately + final_authors_map[pr.author.get_identifier()] = pr.author + + # Index for matching + if pr.author.name: + known_authors_by_name[pr.author.name] = pr.author + if pr.author.email: + known_authors_by_email[pr.author.email] = pr.author + + # 2. Process commits and try to link them to existing PR authors + for commit in change.commits: + resolved_author = commit.author + + # Strategy A: Link via PR number (Strongest link) + if commit.pr_number and commit.pr_number in pr_authors_by_pr_number: + resolved_author = pr_authors_by_pr_number[commit.pr_number] + + # Strategy B: Link via Email (Strong link, if available) + elif commit.author.email and commit.author.email in known_authors_by_email: + resolved_author = known_authors_by_email[commit.author.email] + + # Strategy C: Link via Name (Weaker link, but helps if email is missing) + elif commit.author.name and commit.author.name in known_authors_by_name: + resolved_author = known_authors_by_name[commit.author.name] + + # Add to map (deduplicates by identifier) + # If we resolved to a PR author, get_identifier() returns username. + # If we stayed with commit author, get_identifier() returns name/email. + final_authors_map[resolved_author.get_identifier()] = resolved_author + + return list(final_authors_map.values()) + + def _determine_category( + self, + change: ConsolidatedChange, + ticket: Optional[Ticket] + ) -> Optional[str]: + """Determine the category for a change based on labels with source prefix support.""" + # Get labels from ticket with source indicator + ticket_labels: List[str] = [] + pr_labels: List[str] = [] + + if ticket: + ticket_labels = [label.name for label in ticket.labels] + + if change.prs: + for pr in change.prs: + pr_labels.extend([label.name for label in pr.labels]) + + # Check against category mappings (respecting pr: and ticket: prefixes) + for category_config in self.config.release_notes.categories: + # Check ticket labels + for label in ticket_labels: + if category_config.matches_label(label, "ticket"): + return category_config.name + + # Check PR labels + for label in pr_labels: + if category_config.matches_label(label, "pr"): + return category_config.name + + return "Other" + + def _extract_section(self, text: str, regex: Optional[str]) -> Optional[str]: + """Extract a section from text using regex.""" + if not regex or not text: + return None + + match = re.search(regex, text, re.DOTALL | re.IGNORECASE) + if match: + return match.group(1).strip() + return None + + def _extract_github_url_info(self, url: str) -> tuple[Optional[str], Optional[str]]: + """ + Extract repository name and number from a GitHub URL. + + Args: + url: GitHub URL (e.g., "https://github.com/owner/repo/issues/1234") + + Returns: + Tuple of (owner_repo, number) where owner_repo is "owner/repo" format + and number is the issue/PR number as a string. + Returns (None, None) if URL is not a valid GitHub URL. + + Examples: + "https://github.com/sequentech/meta/issues/8853" -> ("sequentech/meta", "8853") + "https://github.com/owner/repo/pull/123" -> ("owner/repo", "123") + """ + # Pattern: https://github.com/owner/repo/(issues|pull)/number + pattern = r'github\.com/([^/]+)/([^/]+)/(issues|pull)/(\d+)' + match = re.search(pattern, url) + + if match: + owner = match.group(1) # Owner name (e.g., "sequentech") + repo = match.group(2) # Repo name (e.g., "meta") + number = match.group(4) # The issue/PR number + owner_repo = f"{owner}/{repo}" + return (owner_repo, number) + + return (None, None) + + def group_by_category( + self, + notes: List[ReleaseNote] + ) -> Dict[str, List[ReleaseNote]]: + """Group release notes by category.""" + # Filter out excluded notes + excluded_labels = set(self.config.release_notes.excluded_labels) + notes = [ + note for note in notes + if not any(label in excluded_labels for label in note.labels) + ] + + # Group by category + grouped: Dict[str, List[ReleaseNote]] = {} + for category_name in self.config.get_ordered_categories(): + grouped[category_name] = [] + + for note in notes: + category = note.category or "Other" + if category not in grouped: + grouped[category] = [] + grouped[category].append(note) + + return grouped + + def _process_html_like_whitespace(self, text: str, preserve_br: bool = False) -> str: + """ + Process template output with HTML-like whitespace behavior. + + - Multiple spaces/tabs collapse to single space + - Newlines are ignored unless using
or
+ -   entities are preserved as non-breaking spaces + - Leading/trailing whitespace stripped from lines + """ + import re + + # 1. Protect   entities from whitespace collapse + processed = text.replace(' ', '') + + # 2. Replace
and
with newline markers + # Use a unique marker that won't conflict with actual content + if not preserve_br: + processed = processed.replace('
', '').replace('
', '') + + # 3. Collapse multiple spaces/tabs into single space (like HTML) + processed = re.sub(r'[^\S\n]+', ' ', processed) + + # 4. Strip leading/trailing whitespace from each line + processed = '\n'.join(line.strip() for line in processed.split('\n')) + + # 5. Remove empty lines (unless they came from
tags) + # Process line by line and handle BR_MARKER specially + lines_list = [] + for line in processed.split('\n'): + # If line contains BR_MARKER, split it and add empty line after + if '' in line: + parts = line.split('') + for i, part in enumerate(parts): + if part.strip(): + lines_list.append(part) + # Add empty line after all BR_MARKER occurrences except the last part + if i < len(parts) - 1: + lines_list.append('') + elif line.strip(): + lines_list.append(line) + elif preserve_br: + lines_list.append('') + + result = '\n'.join(lines_list) + + # Note: We don't replace here because the output might be + # processed again (e.g., entry_template processed, then inserted into + # output_template which is also processed). Markers are replaced at the + # very end in the format_markdown methods. + + return result + + def _prepare_note_for_template( + self, + note: ReleaseNote, + version: str, + output_path: Optional[str], + media_downloader + ) -> Dict[str, Any]: + """ + Prepare a release note for template rendering. + + Returns a dict with processed description/migration and author dicts. + """ + # Process media in description and migration notes if enabled + processed_description = note.description + processed_migration = note.migration_notes + + if media_downloader and output_path: + if note.description: + processed_description = media_downloader.process_description( + note.description, version, output_path + ) + if note.migration_notes: + processed_migration = media_downloader.process_description( + note.migration_notes, version, output_path + ) + + # Convert Author objects to dicts for template access + authors_dicts = [author.to_dict() for author in note.authors] + + return { + 'title': note.title, + 'url': note.url, # Smart URL: ticket_url if available, else pr_url + 'ticket_url': note.ticket_url, # Direct ticket URL + 'pr_url': note.pr_url, # Direct PR URL + 'short_link': note.short_link, # Short format: #1234 + 'short_repo_link': note.short_repo_link, # Short format: owner/repo#1234 + 'pr_numbers': note.pr_numbers, + 'authors': authors_dicts, + 'description': processed_description, + 'migration_notes': processed_migration, + 'labels': note.labels, + 'ticket_key': note.ticket_key, + 'category': note.category, + 'commit_shas': note.commit_shas + } + + def format_markdown( + self, + grouped_notes: Dict[str, List[ReleaseNote]], + version: str, + release_output_path: Optional[str] = None, + doc_output_path: Optional[str] = None + ): + """ + Format release notes as markdown. + + Args: + grouped_notes: Release notes grouped by category + version: Version string + release_output_path: Optional GitHub release notes output file path (for media processing) + doc_output_path: Optional Docusaurus output file path (for media processing) + + Returns: + If doc_output_template is configured: tuple of (release_notes, doc_notes) + Otherwise: single release notes string + """ + from jinja2 import Template + from .media_utils import MediaDownloader + + # Initialize media downloader if enabled (use release_output_path for media) + media_downloader = None + if self.config.output.download_media and release_output_path: + media_downloader = MediaDownloader( + self.config.output.assets_path, + download_enabled=True + ) + + # If release_output_template is configured, use master template approach + if self.config.release_notes.release_output_template: + release_notes = self._format_with_master_template( + grouped_notes, version, release_output_path, media_downloader + ) + else: + # Otherwise, use legacy approach for backward compatibility + release_notes = self._format_with_legacy_layout( + grouped_notes, version, release_output_path, media_downloader + ) + + # If doc_output_template is configured, generate Docusaurus version as well + if self.config.release_notes.doc_output_template: + doc_notes = self._format_with_doc_template( + grouped_notes, version, doc_output_path, media_downloader, release_notes + ) + return (release_notes, doc_notes) + + # Return just release notes if no doc template + return release_notes + + def _format_with_master_template( + self, + grouped_notes: Dict[str, List[ReleaseNote]], + version: str, + output_path: Optional[str], + media_downloader, + preserve_br: bool = False + ) -> str: + """Format using the master release_output_template.""" + from jinja2 import Template + + # Create entry template for sub-rendering + entry_template = Template(self.config.release_notes.entry_template) + + # Create a render_entry function that can be called from the master template + def render_entry(note_dict: Dict[str, Any]) -> str: + """Render a single entry using the entry_template.""" + rendered = entry_template.render(**note_dict) + return self._process_html_like_whitespace(rendered, preserve_br=preserve_br) + + # Prepare all notes with processed data + categories_data = [] + all_notes_data = [] + + for category_name in self.config.get_ordered_categories(): + notes = grouped_notes.get(category_name, []) + if not notes: + continue + + notes_data = [] + for note in notes: + note_dict = self._prepare_note_for_template( + note, version, output_path, media_downloader + ) + notes_data.append(note_dict) + all_notes_data.append(note_dict) + + # Find category config for alias + category_alias = None + for cat_config in self.config.release_notes.categories: + if cat_config.name == category_name: + category_alias = cat_config.alias + break + + categories_data.append({ + 'name': category_name, + 'alias': category_alias, + 'notes': notes_data + }) + + # Render title + title_template = Template(self.config.release_notes.title_template) + title = title_template.render(version=version) + + # Render master template + from datetime import datetime + master_template = Template(self.config.release_notes.release_output_template) + output = master_template.render( + version=version, + title=title, + categories=categories_data, + all_notes=all_notes_data, + render_entry=render_entry, + year=datetime.now().year + ) + + # Process HTML-like whitespace + output = self._process_html_like_whitespace(output) + + # Replace   markers with actual spaces (done at the very end) + output = output.replace('', ' ') + + return output + + def _format_with_doc_template( + self, + grouped_notes: Dict[str, List[ReleaseNote]], + version: str, + output_path: Optional[str], + media_downloader, + release_notes: str + ) -> str: + """Format using the doc_output_template with render_release_notes() function.""" + from jinja2 import Template + + # Create entry template for sub-rendering + entry_template = Template(self.config.release_notes.entry_template) + + # Create a render_entry function + def render_entry(note_dict: Dict[str, Any]) -> str: + """Render a single entry using the entry_template.""" + rendered = entry_template.render(**note_dict) + return self._process_html_like_whitespace(rendered) + + # Create a render_release_notes function that returns the already-rendered release notes + # wrapped in a marker to prevent re-processing + def render_release_notes(preserve_br: bool = True) -> str: + """Render the GitHub release notes (already computed).""" + # Return the release notes wrapped in a marker to protect from re-processing + return '' + + # Prepare all notes with processed data + categories_data = [] + all_notes_data = [] + + for category_name in self.config.get_ordered_categories(): + notes = grouped_notes.get(category_name, []) + if not notes: + continue + + notes_data = [] + for note in notes: + note_dict = self._prepare_note_for_template( + note, version, output_path, media_downloader + ) + notes_data.append(note_dict) + all_notes_data.append(note_dict) + + # Find category config for alias + category_alias = None + for cat_config in self.config.release_notes.categories: + if cat_config.name == category_name: + category_alias = cat_config.alias + break + + categories_data.append({ + 'name': category_name, + 'alias': category_alias, + 'notes': notes_data + }) + + # Render title + title_template = Template(self.config.release_notes.title_template) + title = title_template.render(version=version) + + # Render doc template + from datetime import datetime + doc_template = Template(self.config.release_notes.doc_output_template) + output = doc_template.render( + version=version, + title=title, + categories=categories_data, + all_notes=all_notes_data, + render_entry=render_entry, + render_release_notes=render_release_notes, + year=datetime.now().year + ) + + # Process HTML-like whitespace WITHOUT preserve_br + # This will convert
tags to proper newlines without extra blank lines + output = self._process_html_like_whitespace(output, preserve_br=False) + + # Replace the marker with the actual release notes AFTER processing + # This way the release notes won't be re-processed + output = output.replace('', release_notes) + + # Replace   markers with actual spaces (done at the very end) + output = output.replace('', ' ') + + return output + + def _format_with_legacy_layout( + self, + grouped_notes: Dict[str, List[ReleaseNote]], + version: str, + output_path: Optional[str], + media_downloader, + preserve_br: bool = False + ) -> str: + """Format using the legacy category-based layout.""" + from jinja2 import Template + + lines = [] + + # Title + title_template = Template(self.config.release_notes.title_template) + title = title_template.render(version=version) + lines.append(f"# {title}") + lines.append("") + + # Description (legacy) + if self.config.release_notes.description_template: + desc_template = Template(self.config.release_notes.description_template) + description = desc_template.render(version=version) + lines.append(description) + lines.append("") + + # Categories + entry_template = Template(self.config.release_notes.entry_template) + + for category in self.config.get_ordered_categories(): + notes = grouped_notes.get(category, []) + if not notes: + continue + + lines.append(f"## {category}") + lines.append("") + + for note in notes: + note_dict = self._prepare_note_for_template( + note, version, output_path, media_downloader + ) + + rendered_entry = entry_template.render(**note_dict) + processed_entry = self._process_html_like_whitespace(rendered_entry, preserve_br=preserve_br) + lines.append(processed_entry) + + # Add description if present and not already in template + if note_dict['description'] and '{{ description }}' not in self.config.release_notes.entry_template: + lines.append(f" {note_dict['description'][:200]}...") + lines.append("") + + lines.append("") + + output = "\n".join(lines) + + # Replace   markers with actual spaces (done at the very end) + output = output.replace('', ' ') + + return output + + +class VersionGapChecker: + """Check for version gaps.""" + + def __init__(self, config: Config): + self.config = config + + def check_gap(self, from_version: str, to_version: str): + """Check if there's a gap between versions.""" + from .models import SemanticVersion + + action = self.config.version_policy.gap_detection + if action == PolicyAction.IGNORE: + return + + try: + prev = SemanticVersion.parse(from_version) + curr = SemanticVersion.parse(to_version) + + gap = False + if curr.major > prev.major + 1: + gap = True + elif curr.major == prev.major and curr.minor > prev.minor + 1: + gap = True + elif (curr.major == prev.major and + curr.minor == prev.minor and + curr.patch > prev.patch + 1): + gap = True + + if gap: + msg = f"Version gap detected between {from_version} and {to_version}" + if action == PolicyAction.ERROR: + raise ValueError(msg) + elif action == PolicyAction.WARN: + console.print(f"[yellow]WARNING: {msg}[/yellow]") + + except ValueError as e: + if action == PolicyAction.ERROR: + raise + elif action == PolicyAction.WARN: + console.print(f"[yellow]WARNING: {e}[/yellow]") diff --git a/src/release_tool/sync.py b/src/release_tool/sync.py new file mode 100644 index 0000000..46b06c6 --- /dev/null +++ b/src/release_tool/sync.py @@ -0,0 +1,465 @@ +"""Sync module for highly parallelized GitHub data fetching.""" + +import asyncio +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime +from typing import List, Dict, Any, Optional, Callable +from pathlib import Path + +from rich.console import Console +from rich.progress import Progress, TaskID, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn + +from .config import Config +from .db import Database +from .github_utils import GitHubClient +from .models import Ticket, PullRequest + +console = Console() + + +class SyncManager: + """Manager for parallelized GitHub data synchronization.""" + + def __init__(self, config: Config, db: Database, github_client: GitHubClient): + self.config = config + self.db = db + self.github = github_client + self.parallel_workers = config.sync.parallel_workers + + def sync_all(self) -> Dict[str, Any]: + """ + Sync all data from GitHub (tickets, PRs, commits). + + Returns: + Dictionary with sync statistics + """ + stats = { + 'tickets': 0, + 'pull_requests': 0, + 'commits': 0, + 'repos_synced': set() + } + + if self.config.sync.show_progress: + console.print("[bold cyan]Starting GitHub data sync...[/bold cyan]") + + # Sync tickets from all ticket repos + ticket_repos = self.config.get_ticket_repos() + for repo_full_name in ticket_repos: + if self.config.sync.show_progress: + console.print(f"[cyan]Syncing tickets from {repo_full_name}...[/cyan]") + + ticket_count = self._sync_tickets_for_repo(repo_full_name) + stats['tickets'] += ticket_count + stats['repos_synced'].add(repo_full_name) + + # Sync PRs from code repo + code_repo = self.config.repository.code_repo + if self.config.sync.show_progress: + console.print(f"[cyan]Syncing pull requests from {code_repo}...[/cyan]") + + pr_count = self._sync_pull_requests_for_repo(code_repo) + stats['pull_requests'] = pr_count + stats['repos_synced'].add(code_repo) + + # Sync git repository if enabled + if self.config.sync.clone_code_repo: + if self.config.sync.show_progress: + console.print(f"[cyan]Syncing git repository for {code_repo}...[/cyan]") + + git_path = self._sync_git_repository(code_repo) + stats['git_repo_path'] = git_path + + if self.config.sync.show_progress: + console.print("[bold green]Sync completed successfully![/bold green]") + console.print(f" Tickets: {stats['tickets']}") + console.print(f" Pull Requests: {stats['pull_requests']}") + if stats.get('git_repo_path'): + console.print(f" Git repo synced to: {stats['git_repo_path']}") + + stats['repos_synced'] = list(stats['repos_synced']) + return stats + + def _sync_tickets_for_repo(self, repo_full_name: str) -> int: + """ + Sync tickets for a specific repository with parallel fetching. + + Args: + repo_full_name: Full repository name (owner/repo) + + Returns: + Number of tickets synced + """ + # Ensure repository exists in DB and get repo_id + repo_info = self.github.get_repository_info(repo_full_name) + repo_id = self.db.upsert_repository(repo_info) + + # Get last sync time + last_sync = self.db.get_last_sync(repo_full_name, 'tickets') + + # Determine cutoff date + cutoff_date = None + cutoff_source = None + if self.config.sync.cutoff_date: + cutoff_date = datetime.fromisoformat(self.config.sync.cutoff_date) + # Ensure timezone awareness (assume UTC if naive) + if cutoff_date.tzinfo is None: + from datetime import timezone + cutoff_date = cutoff_date.replace(tzinfo=timezone.utc) + cutoff_source = f"configured cutoff date: {self.config.sync.cutoff_date}" + elif last_sync: + # Incremental sync - fetch from last sync + cutoff_date = last_sync + # Ensure timezone awareness + if cutoff_date.tzinfo is None: + from datetime import timezone + cutoff_date = cutoff_date.replace(tzinfo=timezone.utc) + cutoff_source = f"last sync: {last_sync.strftime('%Y-%m-%d %H:%M:%S')}" + + if self.config.sync.show_progress: + if cutoff_source: + console.print(f" [dim]Using {cutoff_source}[/dim]") + else: + console.print(f" [dim]Fetching all historical tickets[/dim]") + + # Fetch tickets directly with streaming (no discovery phase) + tickets = self._fetch_tickets_streaming( + repo_full_name, + repo_id, + cutoff_date + ) + + if not tickets: + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] All tickets up to date (0 new)") + return 0 + + # Update sync metadata + self.db.update_sync_metadata( + repo_full_name, + 'tickets', + cutoff_date=self.config.sync.cutoff_date, + total_fetched=len(tickets) + ) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Synced {len(tickets)} tickets") + + return len(tickets) + + def _sync_pull_requests_for_repo(self, repo_full_name: str) -> int: + """ + Sync pull requests for a specific repository with parallel fetching. + + Args: + repo_full_name: Full repository name (owner/repo) + + Returns: + Number of PRs synced + """ + # Ensure repository exists in DB and get repo_id + repo_info = self.github.get_repository_info(repo_full_name) + repo_id = self.db.upsert_repository(repo_info) + + # Get last sync time + last_sync = self.db.get_last_sync(repo_full_name, 'pull_requests') + + # Determine cutoff date + cutoff_date = None + cutoff_source = None + if self.config.sync.cutoff_date: + cutoff_date = datetime.fromisoformat(self.config.sync.cutoff_date) + # Ensure timezone awareness (assume UTC if naive) + if cutoff_date.tzinfo is None: + from datetime import timezone + cutoff_date = cutoff_date.replace(tzinfo=timezone.utc) + cutoff_source = f"configured cutoff date: {self.config.sync.cutoff_date}" + elif last_sync: + # Incremental sync - fetch from last sync + cutoff_date = last_sync + # Ensure timezone awareness + if cutoff_date.tzinfo is None: + from datetime import timezone + cutoff_date = cutoff_date.replace(tzinfo=timezone.utc) + cutoff_source = f"last sync: {last_sync.strftime('%Y-%m-%d %H:%M:%S')}" + + if self.config.sync.show_progress: + if cutoff_source: + console.print(f" [dim]Using {cutoff_source}[/dim]") + else: + console.print(f" [dim]Fetching all historical PRs[/dim]") + + # Fetch PRs directly with streaming (no discovery phase) + prs = self._fetch_prs_streaming( + repo_full_name, + repo_id, + cutoff_date + ) + + if not prs: + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] All PRs up to date (0 new)") + return 0 + + # Update sync metadata + self.db.update_sync_metadata( + repo_full_name, + 'pull_requests', + cutoff_date=self.config.sync.cutoff_date, + total_fetched=len(prs) + ) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Synced {len(prs)} PRs") + + return len(prs) + + def _get_ticket_numbers_to_fetch( + self, + repo_full_name: str, + cutoff_date: Optional[datetime] + ) -> List[int]: + """ + Get list of ticket numbers that need to be fetched. + + Args: + repo_full_name: Full repository name + cutoff_date: Only fetch tickets created after this date + + Returns: + List of ticket numbers to fetch + """ + # Get all ticket numbers from GitHub using fast Search API + all_ticket_numbers = self.github.search_ticket_numbers( + repo_full_name, + since=cutoff_date + ) + + # Get ticket numbers already in DB + existing_numbers = self.db.get_existing_ticket_numbers(repo_full_name) + + # Only fetch tickets not in DB + to_fetch = [num for num in all_ticket_numbers if num not in existing_numbers] + + return to_fetch + + def _get_pr_numbers_to_fetch( + self, + repo_full_name: str, + cutoff_date: Optional[datetime] + ) -> List[int]: + """ + Get list of PR numbers that need to be fetched. + + Args: + repo_full_name: Full repository name + cutoff_date: Only fetch PRs created after this date + + Returns: + List of PR numbers to fetch + """ + # Get all PR numbers from GitHub using fast Search API + all_pr_numbers = self.github.search_pr_numbers( + repo_full_name, + since=cutoff_date + ) + + # Get PR numbers already in DB + existing_numbers = self.db.get_existing_pr_numbers(repo_full_name) + + # Only fetch PRs not in DB + to_fetch = [num for num in all_pr_numbers if num not in existing_numbers] + + return to_fetch + + def _sync_git_repository(self, repo_full_name: str) -> str: + """ + Clone or update the git repository for offline operation. + + Args: + repo_full_name: Full repository name (owner/repo) + + Returns: + Path to the synced git repository + """ + repo_path = Path(self.config.get_code_repo_path()) + + # Check if repo already exists + if repo_path.exists() and (repo_path / '.git').exists(): + # Repository exists - update it + if self.config.sync.show_progress: + console.print(f" [dim]Updating existing repository at {repo_path}[/dim]") + + try: + # Fetch all updates + subprocess.run( + ['git', 'fetch', '--all', '--tags', '--prune'], + cwd=repo_path, + check=True, + capture_output=True, + text=True + ) + + # Reset to latest version of default branch + # Use branch_policy.default_branch (fallback to repository.default_branch for backward compatibility) + default_branch = self.config.branch_policy.default_branch + if self.config.repository.default_branch: + # Legacy config support + default_branch = self.config.repository.default_branch + subprocess.run( + ['git', 'reset', '--hard', f'origin/{default_branch}'], + cwd=repo_path, + check=True, + capture_output=True, + text=True + ) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Updated repository") + + except subprocess.CalledProcessError as e: + console.print(f"[yellow]Warning: Failed to update repository: {e}[/yellow]") + console.print(f"[yellow]Error output: {e.stderr}[/yellow]") + + else: + # Repository doesn't exist - clone it + if self.config.sync.show_progress: + console.print(f" [dim]Cloning repository to {repo_path}[/dim]") + + # Ensure parent directory exists + repo_path.parent.mkdir(parents=True, exist_ok=True) + + try: + # Construct clone URL (use https with token if available) + if self.config.github.token: + clone_url = f"https://{self.config.github.token}@github.com/{repo_full_name}.git" + else: + clone_url = f"https://github.com/{repo_full_name}.git" + + subprocess.run( + ['git', 'clone', clone_url, str(repo_path)], + check=True, + capture_output=True, + text=True + ) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Cloned repository") + + except subprocess.CalledProcessError as e: + console.print(f"[red]Error: Failed to clone repository: {e}[/red]") + console.print(f"[red]Error output: {e.stderr}[/red]") + raise + + return str(repo_path) + + def _fetch_tickets_streaming( + self, + repo_full_name: str, + repo_id: int, + cutoff_date: Optional[datetime] + ) -> List[Ticket]: + """ + Fetch tickets efficiently using Core API with paginated batch fetching. + + Uses GET /repos/{owner}/{repo}/issues with per_page=100 to fetch full issue data + in batches, then filters against existing tickets in DB. + + Args: + repo_full_name: Full repository name + repo_id: Repository ID in database + cutoff_date: Only fetch tickets created after this date + + Returns: + List of fetched tickets + """ + try: + existing_numbers = self.db.get_existing_ticket_numbers(repo_full_name) + + # Fetch all issues in one pass with paginated batches (100 per request) + all_tickets = self.github.fetch_all_issues(repo_full_name, repo_id, since=cutoff_date) + + # Filter out existing + if self.config.sync.show_progress and all_tickets: + console.print(f" [dim]Filtering {len(all_tickets)} issues against existing {len(existing_numbers)} in database...[/dim]") + new_tickets = [ticket for ticket in all_tickets if ticket.number not in existing_numbers] + + if not new_tickets: + if self.config.sync.show_progress: + console.print(f" [dim]No new tickets to sync[/dim]") + return [] + + if self.config.sync.show_progress: + console.print(f" [cyan]Storing {len(new_tickets)} new tickets...[/cyan]") + + # Insert tickets to database + for ticket in new_tickets: + self.db.upsert_ticket(ticket) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Synced {len(new_tickets)} new tickets") + + return new_tickets + + except Exception as e: + console.print(f"[red]Error fetching tickets: {e}[/red]") + return [] + + def _fetch_prs_streaming( + self, + repo_full_name: str, + repo_id: int, + cutoff_date: Optional[datetime] + ) -> List[PullRequest]: + """ + Fetch PRs efficiently using Core API with paginated batch fetching. + + Uses GET /repos/{owner}/{repo}/pulls with per_page=100 to fetch full PR data + in batches, then filters for merged PRs and against existing PRs in DB. + + Args: + repo_full_name: Full repository name + repo_id: Repository ID in database + cutoff_date: Only fetch PRs merged after this date + + Returns: + List of fetched PRs + """ + try: + existing_numbers = self.db.get_existing_pr_numbers(repo_full_name) + + # Fetch all PRs in one pass with paginated batches (100 per request) + all_prs = self.github.fetch_all_pull_requests(repo_full_name, repo_id, since=cutoff_date) + + # Filter to only merged PRs and respect cutoff date + merged_prs = [ + pr for pr in all_prs + if pr.merged_at and (cutoff_date is None or pr.merged_at >= cutoff_date) + ] + + # Filter out existing + if self.config.sync.show_progress and merged_prs: + console.print(f" [dim]Filtering {len(merged_prs)} merged PRs against existing {len(existing_numbers)} in database...[/dim]") + new_prs = [pr for pr in merged_prs if pr.number not in existing_numbers] + + if not new_prs: + if self.config.sync.show_progress: + console.print(f" [dim]No new PRs to sync[/dim]") + return [] + + if self.config.sync.show_progress: + console.print(f" [cyan]Storing {len(new_prs)} new PRs...[/cyan]") + + # Insert PRs to database + for pr in new_prs: + self.db.upsert_pull_request(pr) + + if self.config.sync.show_progress: + console.print(f" [green]✓[/green] Synced {len(new_prs)} new PRs") + + return new_prs + + except Exception as e: + console.print(f"[red]Error fetching PRs: {e}[/red]") + return [] diff --git a/src/release_tool/template_utils.py b/src/release_tool/template_utils.py new file mode 100644 index 0000000..50df857 --- /dev/null +++ b/src/release_tool/template_utils.py @@ -0,0 +1,104 @@ +"""Template rendering utilities using Jinja2.""" + +from typing import Dict, Set, Any +from jinja2 import Template, TemplateSyntaxError, UndefinedError, StrictUndefined + + +class TemplateError(Exception): + """Exception raised for template-related errors.""" + pass + + +def render_template(template_str: str, context: Dict[str, Any]) -> str: + """ + Render a Jinja2 template with the given context. + + Args: + template_str: Jinja2 template string using {{ variable }} syntax + context: Dictionary of variables available to the template + + Returns: + Rendered template string + + Raises: + TemplateError: If template syntax is invalid or uses undefined variables + """ + try: + # Use StrictUndefined to raise errors for undefined variables + template = Template(template_str, undefined=StrictUndefined) + return template.render(**context) + except TemplateSyntaxError as e: + raise TemplateError(f"Invalid template syntax: {e}") + except UndefinedError as e: + raise TemplateError(f"Template uses undefined variable: {e}") + except Exception as e: + raise TemplateError(f"Template rendering error: {e}") + + +def validate_template_vars( + template_str: str, + available_vars: Set[str], + template_name: str = "template" +) -> None: + """ + Validate that a template only uses variables that are available. + + This function parses the template and checks that all variable references + exist in the available_vars set. It raises an error if undefined variables are used. + + Args: + template_str: Jinja2 template string to validate + available_vars: Set of variable names that are available + template_name: Name of the template for error messages + + Raises: + TemplateError: If template uses variables not in available_vars + """ + try: + # Parse template to find variable references + from jinja2 import meta, Environment + env = Environment() + ast = env.parse(template_str) + referenced_vars = meta.find_undeclared_variables(ast) + + # Check if any referenced vars are not in available_vars + undefined = referenced_vars - available_vars + if undefined: + raise TemplateError( + f"{template_name} uses undefined variables: {', '.join(sorted(undefined))}. " + f"Available variables: {', '.join(sorted(available_vars))}" + ) + + except TemplateSyntaxError as e: + raise TemplateError(f"Invalid {template_name} syntax: {e}") + except TemplateError: + # Re-raise our own errors + raise + except Exception as e: + # Other errors are likely bugs, but we'll wrap them + raise TemplateError(f"Error validating {template_name}: {e}") + + +def get_template_variables(template_str: str) -> Set[str]: + """ + Extract all variable names used in a template. + + Args: + template_str: Jinja2 template string + + Returns: + Set of variable names referenced in the template + + Raises: + TemplateError: If template syntax is invalid + """ + try: + from jinja2 import meta + template = Template(template_str) + env = template.environment + ast = env.parse(template_str) + return meta.find_undeclared_variables(ast) + except TemplateSyntaxError as e: + raise TemplateError(f"Invalid template syntax: {e}") + except Exception as e: + raise TemplateError(f"Error parsing template: {e}") diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..813e43f --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,142 @@ +"""Tests for configuration management.""" + +import os +import pytest +from pathlib import Path +from release_tool.config import Config, load_config + + +def test_config_from_dict(): + """Test creating config from dictionary.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + assert config.repository.code_repo == "test/repo" + assert config.github.token == "test_token" + + +def test_load_from_file(tmp_path): + """Test loading config from TOML file.""" + config_file = tmp_path / "test_config.toml" + config_content = """ +config_version = "1.4" + +[repository] +code_repo = "owner/repo" +default_branch = "main" + +[github] +token = "fake-token" + +[version_policy] +tag_prefix = "release-" +""" + config_file.write_text(config_content) + + config = Config.from_file(str(config_file)) + assert config.repository.code_repo == "owner/repo" + assert config.version_policy.tag_prefix == "release-" + + +def test_env_var_override(monkeypatch): + """Test GitHub token override from environment.""" + monkeypatch.setenv("GITHUB_TOKEN", "env-token") + + config_dict = { + "repository": { + "code_repo": "test/repo" + } + } + config = Config.from_dict(config_dict) + assert config.github.token == "env-token" + + +def test_category_map(): + """Test category mapping generation.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + } + } + config = Config.from_dict(config_dict) + + category_map = config.get_category_map() + assert "🚀 Features" in category_map + assert "🛠 Bug Fixes" in category_map + assert "feature" in category_map["🚀 Features"] + + +def test_ordered_categories(): + """Test category ordering.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + } + } + config = Config.from_dict(config_dict) + + categories = config.get_ordered_categories() + assert isinstance(categories, list) + assert len(categories) > 0 + + +def test_category_label_matching_no_prefix(): + """Test label matching without prefix (matches any source).""" + from release_tool.config import CategoryConfig + + category = CategoryConfig( + name="Test", + labels=["bug", "feature"], + order=1 + ) + + # Should match from either source + assert category.matches_label("bug", "pr") + assert category.matches_label("bug", "ticket") + assert category.matches_label("feature", "pr") + assert category.matches_label("feature", "ticket") + assert not category.matches_label("other", "pr") + + +def test_category_label_matching_with_pr_prefix(): + """Test label matching with pr: prefix.""" + from release_tool.config import CategoryConfig + + category = CategoryConfig( + name="Test", + labels=["pr:bug", "feature"], + order=1 + ) + + # pr:bug should only match from PRs + assert category.matches_label("bug", "pr") + assert not category.matches_label("bug", "ticket") + + # feature (no prefix) should match from either + assert category.matches_label("feature", "pr") + assert category.matches_label("feature", "ticket") + + +def test_category_label_matching_with_ticket_prefix(): + """Test label matching with ticket: prefix.""" + from release_tool.config import CategoryConfig + + category = CategoryConfig( + name="Test", + labels=["ticket:critical", "normal"], + order=1 + ) + + # ticket:critical should only match from tickets + assert not category.matches_label("critical", "pr") + assert category.matches_label("critical", "ticket") + + # normal (no prefix) should match from either + assert category.matches_label("normal", "pr") + assert category.matches_label("normal", "ticket") diff --git a/tests/test_db.py b/tests/test_db.py new file mode 100644 index 0000000..539ca51 --- /dev/null +++ b/tests/test_db.py @@ -0,0 +1,655 @@ +"""Tests for database operations.""" + +import sqlite3 +import pytest +from datetime import datetime +from release_tool.db import Database +from release_tool.models import Repository, PullRequest, Commit, Label, Ticket, Release, Author + + +@pytest.fixture +def db(tmp_path): + """Create a test database.""" + db_path = tmp_path / "test.db" + database = Database(str(db_path)) + database.connect() + yield database + database.close() + + +def test_init_db(db): + """Test database initialization.""" + cursor = db.conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in cursor.fetchall()] + assert "repositories" in tables + assert "pull_requests" in tables + assert "commits" in tables + assert "tickets" in tables + assert "releases" in tables + + +def test_upsert_repository(db): + """Test repository upsert.""" + repo = Repository(owner="test", name="repo", url="http://example.com") + repo_id = db.upsert_repository(repo) + assert repo_id is not None + + fetched_repo = db.get_repository("test/repo") + assert fetched_repo.owner == "test" + assert fetched_repo.name == "repo" + assert fetched_repo.url == "http://example.com" + + +def test_upsert_pull_request(db): + """Test PR upsert.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + author = Author(name="dev", username="dev") + pr = PullRequest( + repo_id=repo_id, + number=1, + title="Test PR", + state="closed", + merged_at=datetime.now(), + author=author, + base_branch="main", + head_branch="feature", + head_sha="abc123", + labels=[Label(name="bug")] + ) + + pr_id = db.upsert_pull_request(pr) + assert pr_id is not None + + fetched_pr = db.get_pull_request(repo_id, 1) + assert fetched_pr.title == "Test PR" + assert fetched_pr.author.name == "dev" + assert fetched_pr.author.username == "dev" + assert len(fetched_pr.labels) == 1 + assert fetched_pr.labels[0].name == "bug" + + +def test_upsert_commit(db): + """Test commit upsert.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + author = Author(name="dev", email="dev@example.com") + commit = Commit( + sha="abc123", + repo_id=repo_id, + message="Test commit", + author=author, + date=datetime.now() + ) + + db.upsert_commit(commit) + + fetched_commit = db.get_commit("abc123") + assert fetched_commit.sha == "abc123" + assert fetched_commit.message == "Test commit" + assert fetched_commit.author.name == "dev" + assert fetched_commit.author.email == "dev@example.com" + + +def test_upsert_ticket(db): + """Test ticket upsert.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + ticket = Ticket( + repo_id=repo_id, + number=123, + key="#123", + title="Fix bug", + state="closed", + labels=[Label(name="bug")] + ) + + ticket_id = db.upsert_ticket(ticket) + assert ticket_id is not None + + fetched_ticket = db.get_ticket(repo_id, "#123") + assert fetched_ticket.title == "Fix bug" + assert fetched_ticket.number == 123 + + +def test_get_ticket_by_key(db): + """Test getting ticket by key across all repos.""" + # Create two different repos (simulating code repo and ticket repo) + code_repo = Repository(owner="org", name="code") + code_repo_id = db.upsert_repository(code_repo) + + ticket_repo = Repository(owner="org", name="tickets") + ticket_repo_id = db.upsert_repository(ticket_repo) + + # Create ticket in ticket repo + ticket = Ticket( + repo_id=ticket_repo_id, + number=8624, + key="8624", # Bare number as extracted from branch + title="Implement feature X", + state="closed", + labels=[Label(name="enhancement")] + ) + db.upsert_ticket(ticket) + + # Query by repo_id should fail when using wrong repo + wrong_ticket = db.get_ticket(code_repo_id, "8624") + assert wrong_ticket is None + + # Query by key only should succeed + found_ticket = db.get_ticket_by_key("8624") + assert found_ticket is not None + assert found_ticket.title == "Implement feature X" + assert found_ticket.number == 8624 + assert found_ticket.repo_id == ticket_repo_id + + +def test_upsert_release(db): + """Test release upsert.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + release = Release( + repo_id=repo_id, + version="1.0.0", + tag_name="v1.0.0", + name="Release 1.0.0", + is_prerelease=False + ) + + release_id = db.upsert_release(release) + assert release_id is not None + + fetched_release = db.get_release(repo_id, "1.0.0") + assert fetched_release.version == "1.0.0" + assert fetched_release.tag_name == "v1.0.0" + + +def test_get_all_releases(db): + """Test fetching all releases.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create multiple releases + for i in range(3): + release = Release( + repo_id=repo_id, + version=f"1.{i}.0", + tag_name=f"v1.{i}.0", + is_prerelease=False + ) + db.upsert_release(release) + + releases = db.get_all_releases(repo_id) + assert len(releases) == 3 + + +def test_get_all_releases_with_limit(db): + """Test fetching releases with limit.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create 15 releases + for i in range(15): + release = Release( + repo_id=repo_id, + version=f"1.{i}.0", + tag_name=f"v1.{i}.0", + is_prerelease=False, + published_at=datetime(2024, 1, i + 1) + ) + db.upsert_release(release) + + # Test limit + releases = db.get_all_releases(repo_id, limit=10) + assert len(releases) == 10 + + # Test limit=5 + releases = db.get_all_releases(repo_id, limit=5) + assert len(releases) == 5 + + # Test no limit (all) + releases = db.get_all_releases(repo_id, limit=None) + assert len(releases) == 15 + + +def test_get_all_releases_with_since_date(db): + """Test fetching releases filtered by date.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create releases across different dates + releases_data = [ + ("1.0.0", datetime(2024, 1, 1)), + ("1.1.0", datetime(2024, 2, 1)), + ("1.2.0", datetime(2024, 3, 1)), + ("1.3.0", datetime(2024, 4, 1)), + ("1.4.0", datetime(2024, 5, 1)), + ] + + for version, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=False, + published_at=published_at + ) + db.upsert_release(release) + + # Test since March 1st - should get 3 releases (March, April, May) + releases = db.get_all_releases(repo_id, since=datetime(2024, 3, 1)) + assert len(releases) == 3 + assert releases[0].version == "1.4.0" # Most recent first + assert releases[1].version == "1.3.0" + assert releases[2].version == "1.2.0" + + # Test since April 15th - should get 1 release (May) + releases = db.get_all_releases(repo_id, since=datetime(2024, 4, 15)) + assert len(releases) == 1 + assert releases[0].version == "1.4.0" + + +def test_get_all_releases_final_only(db): + """Test fetching only final releases (excluding prereleases).""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create mix of final and prerelease versions + releases_data = [ + ("1.0.0", False), # Final + ("1.1.0-rc.1", True), # RC + ("1.1.0-rc.2", True), # RC + ("1.1.0", False), # Final + ("2.0.0-beta.1", True),# Beta + ("2.0.0-rc.1", True), # RC + ("2.0.0", False), # Final + ] + + for version, is_prerelease in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=is_prerelease, + published_at=datetime(2024, 1, 1) + ) + db.upsert_release(release) + + # Test final_only=True - should get only 3 final releases + releases = db.get_all_releases(repo_id, final_only=True) + assert len(releases) == 3 + assert all(not r.is_prerelease for r in releases) + versions = [r.version for r in releases] + assert "1.0.0" in versions + assert "1.1.0" in versions + assert "2.0.0" in versions + assert "1.1.0-rc.1" not in versions + assert "2.0.0-beta.1" not in versions + + # Test final_only=False - should get all 7 releases + releases = db.get_all_releases(repo_id, final_only=False) + assert len(releases) == 7 + + +def test_get_all_releases_combined_filters(db): + """Test fetching releases with multiple filters combined.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create releases across dates with mix of final/prerelease + releases_data = [ + ("1.0.0", False, datetime(2024, 1, 1)), + ("1.1.0-rc.1", True, datetime(2024, 2, 1)), + ("1.1.0", False, datetime(2024, 2, 15)), + ("1.2.0-rc.1", True, datetime(2024, 3, 1)), + ("1.2.0", False, datetime(2024, 3, 15)), + ("2.0.0-beta.1", True, datetime(2024, 4, 1)), + ("2.0.0-rc.1", True, datetime(2024, 4, 15)), + ("2.0.0", False, datetime(2024, 5, 1)), + ] + + for version, is_prerelease, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=is_prerelease, + published_at=published_at + ) + db.upsert_release(release) + + # Test: final_only + since Feb 1 + limit 2 + # Should get: 2.0.0, 1.2.0 (most recent 2 finals since Feb 1) + releases = db.get_all_releases( + repo_id, + limit=2, + since=datetime(2024, 2, 1), + final_only=True + ) + assert len(releases) == 2 + assert releases[0].version == "2.0.0" + assert releases[1].version == "1.2.0" + assert all(not r.is_prerelease for r in releases) + + # Test: final_only + since April 1 + # Should get: 2.0.0 (only final after April 1) + releases = db.get_all_releases( + repo_id, + since=datetime(2024, 4, 1), + final_only=True + ) + assert len(releases) == 1 + assert releases[0].version == "2.0.0" + + +def test_get_all_releases_ordering(db): + """Test that releases are ordered by published_at DESC.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create releases in non-chronological order + releases_data = [ + ("1.0.0", datetime(2024, 1, 1)), + ("1.2.0", datetime(2024, 3, 1)), + ("1.1.0", datetime(2024, 2, 1)), + ] + + for version, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=False, + published_at=published_at + ) + db.upsert_release(release) + + releases = db.get_all_releases(repo_id) + + # Should be ordered newest first + assert len(releases) == 3 + assert releases[0].version == "1.2.0" # March (newest) + assert releases[1].version == "1.1.0" # February + assert releases[2].version == "1.0.0" # January (oldest) + + +def test_get_all_releases_with_version_prefix(db): + """Test filtering by version prefix (major or major.minor).""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create releases with different major versions + releases_data = [ + "8.5.0", + "9.0.0", + "9.1.0", + "9.1.1", + "9.2.0", + "9.3.0", + "9.3.1", + "9.3.2", + "10.0.0", + "10.1.0", + ] + + for version in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=False, + published_at=datetime(2024, 1, 1) + ) + db.upsert_release(release) + + # Test filtering by major version "9" + releases = db.get_all_releases(repo_id, version_prefix="9") + assert len(releases) == 7 # 9.0.0, 9.1.0, 9.1.1, 9.2.0, 9.3.0, 9.3.1, 9.3.2 + versions = [r.version for r in releases] + assert "9.0.0" in versions + assert "9.3.2" in versions + assert "8.5.0" not in versions + assert "10.0.0" not in versions + + # Test filtering by major.minor "9.3" + releases = db.get_all_releases(repo_id, version_prefix="9.3") + assert len(releases) == 3 + versions = [r.version for r in releases] + assert "9.3.0" in versions + assert "9.3.1" in versions + assert "9.3.2" in versions + assert "9.1.0" not in versions + assert "9.2.0" not in versions + + # Test filtering by major "10" + releases = db.get_all_releases(repo_id, version_prefix="10") + assert len(releases) == 2 + versions = [r.version for r in releases] + assert "10.0.0" in versions + assert "10.1.0" in versions + + +def test_get_all_releases_with_release_types(db): + """Test filtering by release types (final, rc, beta, alpha).""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create mix of different release types + releases_data = [ + ("9.0.0", False), # Final + ("9.1.0-alpha.1", True), # Alpha + ("9.1.0-beta.1", True), # Beta + ("9.1.0-rc.1", True), # RC + ("9.1.0-rc.2", True), # RC + ("9.1.0", False), # Final + ("9.2.0-alpha.1", True), # Alpha + ("9.2.0-beta.1", True), # Beta + ("9.2.0-rc.1", True), # RC + ("9.2.0", False), # Final + ] + + for version, is_prerelease in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=is_prerelease, + published_at=datetime(2024, 1, 1) + ) + db.upsert_release(release) + + # Test only final releases + releases = db.get_all_releases(repo_id, release_types=['final']) + assert len(releases) == 3 + versions = [r.version for r in releases] + assert "9.0.0" in versions + assert "9.1.0" in versions + assert "9.2.0" in versions + + # Test only RCs + releases = db.get_all_releases(repo_id, release_types=['rc']) + assert len(releases) == 3 + versions = [r.version for r in releases] + assert "9.1.0-rc.1" in versions + assert "9.1.0-rc.2" in versions + assert "9.2.0-rc.1" in versions + + # Test finals and RCs + releases = db.get_all_releases(repo_id, release_types=['final', 'rc']) + assert len(releases) == 6 + + # Test betas and alphas + releases = db.get_all_releases(repo_id, release_types=['beta', 'alpha']) + assert len(releases) == 4 + versions = [r.version for r in releases] + assert "9.1.0-alpha.1" in versions + assert "9.1.0-beta.1" in versions + assert "9.2.0-alpha.1" in versions + assert "9.2.0-beta.1" in versions + + +def test_get_all_releases_with_date_range(db): + """Test filtering by date range (after and before).""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create releases across different months + releases_data = [ + ("1.0.0", datetime(2024, 1, 15)), + ("1.1.0", datetime(2024, 2, 15)), + ("1.2.0", datetime(2024, 3, 15)), + ("1.3.0", datetime(2024, 4, 15)), + ("1.4.0", datetime(2024, 5, 15)), + ("1.5.0", datetime(2024, 6, 15)), + ] + + for version, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=False, + published_at=published_at + ) + db.upsert_release(release) + + # Test after March 1 + releases = db.get_all_releases(repo_id, after=datetime(2024, 3, 1)) + assert len(releases) == 4 # March, April, May, June + versions = [r.version for r in releases] + assert "1.2.0" in versions + assert "1.5.0" in versions + assert "1.0.0" not in versions + + # Test before April 1 + releases = db.get_all_releases(repo_id, before=datetime(2024, 4, 1)) + assert len(releases) == 3 # Jan, Feb, March + versions = [r.version for r in releases] + assert "1.0.0" in versions + assert "1.2.0" in versions + assert "1.3.0" not in versions + + # Test date range: March to May + releases = db.get_all_releases( + repo_id, + after=datetime(2024, 3, 1), + before=datetime(2024, 5, 31) + ) + assert len(releases) == 3 # March, April, May + versions = [r.version for r in releases] + assert "1.2.0" in versions + assert "1.3.0" in versions + assert "1.4.0" in versions + assert "1.5.0" not in versions + + +def test_get_all_releases_combined_advanced_filters(db): + """Test combining multiple new filter types.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create complex dataset + releases_data = [ + ("8.5.0", False, datetime(2024, 1, 1)), + ("9.0.0", False, datetime(2024, 2, 1)), + ("9.1.0-rc.1", True, datetime(2024, 3, 1)), + ("9.1.0", False, datetime(2024, 3, 15)), + ("9.2.0-beta.1", True, datetime(2024, 4, 1)), + ("9.2.0-rc.1", True, datetime(2024, 4, 15)), + ("9.2.0", False, datetime(2024, 5, 1)), + ("9.3.0-rc.1", True, datetime(2024, 5, 15)), + ("9.3.0", False, datetime(2024, 6, 1)), + ("10.0.0-rc.1", True, datetime(2024, 6, 15)), + ("10.0.0", False, datetime(2024, 7, 1)), + ] + + for version, is_prerelease, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=is_prerelease, + published_at=published_at + ) + db.upsert_release(release) + + # Test: version 9.x + final only + after March 1 + releases = db.get_all_releases( + repo_id, + version_prefix="9", + release_types=['final'], + after=datetime(2024, 3, 1) + ) + assert len(releases) == 3 # 9.1.0, 9.2.0, 9.3.0 + versions = [r.version for r in releases] + assert "9.1.0" in versions + assert "9.2.0" in versions + assert "9.3.0" in versions + assert "9.0.0" not in versions # Before March + assert "10.0.0" not in versions # Version 10 + + # Test: version 9.2.x + all types + limit 2 + releases = db.get_all_releases( + repo_id, + version_prefix="9.2", + limit=2 + ) + assert len(releases) == 2 + # Most recent first + assert releases[0].version == "9.2.0" + assert releases[1].version == "9.2.0-rc.1" + + # Test: RCs only + date range + releases = db.get_all_releases( + repo_id, + release_types=['rc'], + after=datetime(2024, 3, 1), + before=datetime(2024, 6, 1) + ) + assert len(releases) == 3 # 9.1.0-rc.1, 9.2.0-rc.1, 9.3.0-rc.1 + versions = [r.version for r in releases] + assert "9.1.0-rc.1" in versions + assert "9.2.0-rc.1" in versions + assert "9.3.0-rc.1" in versions + assert "10.0.0-rc.1" not in versions # After June 1 + + +def test_get_all_releases_backwards_compatibility(db): + """Test that deprecated parameters still work.""" + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Create test data + releases_data = [ + ("1.0.0", False, datetime(2024, 1, 1)), + ("1.1.0-rc.1", True, datetime(2024, 2, 1)), + ("1.1.0", False, datetime(2024, 3, 1)), + ] + + for version, is_prerelease, published_at in releases_data: + release = Release( + repo_id=repo_id, + version=version, + tag_name=f"v{version}", + is_prerelease=is_prerelease, + published_at=published_at + ) + db.upsert_release(release) + + # Test deprecated final_only parameter + releases = db.get_all_releases(repo_id, final_only=True) + assert len(releases) == 2 + versions = [r.version for r in releases] + assert "1.0.0" in versions + assert "1.1.0" in versions + + # Test deprecated since parameter + releases = db.get_all_releases(repo_id, since=datetime(2024, 2, 1)) + assert len(releases) == 2 + assert releases[0].version == "1.1.0" + assert releases[1].version == "1.1.0-rc.1" diff --git a/tests/test_default_template.py b/tests/test_default_template.py new file mode 100644 index 0000000..e3101fb --- /dev/null +++ b/tests/test_default_template.py @@ -0,0 +1,258 @@ +"""Tests for the default output_template structure.""" + +import pytest +from datetime import datetime +from release_tool.policies import ReleaseNoteGenerator +from release_tool.config import Config +from release_tool.models import ReleaseNote, Author + + +def test_default_template_structure(): + """Test that the default output_template has the expected structure.""" + # Use default config (no customization) + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + + # Create test notes with all features + author1 = Author(name="Alice", username="alice", company="Acme Corp") + author2 = Author(name="Bob", username="bob") + + notes = [ + # Breaking change with description + ReleaseNote( + title="Breaking: Remove old API", + category="💥 Breaking Changes", + labels=["breaking-change"], + authors=[author1], + pr_numbers=[100], + url="https://github.com/test/repo/pull/100", + description="The old API has been completely removed in favor of the new v2 API." + ), + # Feature with migration notes + ReleaseNote( + title="Add new authentication system", + category="🚀 Features", + labels=["feature"], + authors=[author1, author2], + pr_numbers=[101], + url="https://github.com/test/repo/pull/101", + description="Implemented OAuth2 authentication with JWT tokens.", + migration_notes="Run `python manage.py migrate_auth` to update your database schema." + ), + # Bug fix with description + ReleaseNote( + title="Fix login redirect", + category="🛠 Bug Fixes", + labels=["bug"], + authors=[author2], + pr_numbers=[102], + url="https://github.com/test/repo/pull/102", + description="Fixed an issue where users were redirected to the wrong page after login." + ), + # Feature without description + ReleaseNote( + title="Improve performance", + category="🚀 Features", + labels=["feature"], + authors=[author1], + pr_numbers=[103], + url="https://github.com/test/repo/pull/103" + ), + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + # Note: Title "# Release 1.0.0" is no longer in the release template + # It appears in the GitHub release UI title instead + + # Verify structure: Breaking Changes Descriptions section + assert "## 💥 Breaking Changes" in output + assert "### Breaking: Remove old API" in output + assert "The old API has been completely removed" in output + + # Verify structure: Migration Guide section + assert "## 🔄 Migrations" in output + assert "### Add new authentication system" in output + assert "Run `python manage.py migrate_auth`" in output + + # Verify structure: Detailed Descriptions section + assert "## 📝 Highlights" in output + # Should include feature and bug fix descriptions (but NOT breaking changes) + assert "### Add new authentication system" in output + assert "Implemented OAuth2 authentication" in output + assert "### Fix login redirect" in output + assert "Fixed an issue where users were redirected" in output + # Breaking change description should NOT appear in this section + # (it appears in Breaking Changes Descriptions section instead) + + # Verify structure: All Changes section + assert "## 📋 All Changes" in output + assert "### 💥 Breaking Changes" in output + assert "### 🚀 Features" in output + assert "### 🛠 Bug Fixes" in output + + # Verify all notes appear in All Changes + assert "Breaking: Remove old API" in output + assert "Add new authentication system" in output + assert "Fix login redirect" in output + assert "Improve performance" in output + + +def test_default_template_with_alias(): + """Test that category alias works in default template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + + # Check that categories have aliases + breaking_cat = next(c for c in config.release_notes.categories if c.name == "💥 Breaking Changes") + assert breaking_cat.alias == "breaking" + + features_cat = next(c for c in config.release_notes.categories if c.name == "🚀 Features") + assert features_cat.alias == "features" + + +def test_default_template_skips_empty_sections(): + """Test that empty sections are not shown.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + + # Create notes without breaking changes or migrations + author = Author(name="Alice", username="alice") + notes = [ + ReleaseNote( + title="Add feature", + category="🚀 Features", + labels=["feature"], + authors=[author], + pr_numbers=[100] + ), + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + # Should NOT have breaking changes section (no breaking changes) + assert "## 💥 Breaking Changes" not in output + + # Should NOT have migrations section (no migration notes) + assert "## 🔄 Migrations" not in output + + # Should NOT have detailed descriptions section (no descriptions) + assert "## 📝 Highlights" not in output + + # Should still have All Changes section + assert "## 📋 All Changes" in output + assert "Add feature" in output + + +def test_default_template_categories_ordering(): + """Test that categories appear in the correct order.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + + # Create notes in reverse order + author = Author(name="Alice", username="alice") + notes = [ + ReleaseNote( + title="Update docs", + category="📖 Documentation", + labels=["docs"], + authors=[author] + ), + ReleaseNote( + title="Fix bug", + category="🛠 Bug Fixes", + labels=["bug"], + authors=[author] + ), + ReleaseNote( + title="Add feature", + category="🚀 Features", + labels=["feature"], + authors=[author] + ), + ReleaseNote( + title="Breaking change", + category="💥 Breaking Changes", + labels=["breaking"], + authors=[author] + ), + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + # Find positions in output + breaking_pos = output.find("### 💥 Breaking Changes") + features_pos = output.find("### 🚀 Features") + bugfixes_pos = output.find("### 🛠 Bug Fixes") + docs_pos = output.find("### 📖 Documentation") + + # Verify order: Breaking < Features < Bug Fixes < Documentation + assert breaking_pos < features_pos + assert features_pos < bugfixes_pos + assert bugfixes_pos < docs_pos + + +def test_default_categories_match_backup_config(): + """Test that default categories match the backup config.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + } + } + config = Config.from_dict(config_dict) + + category_names = [c.name for c in config.release_notes.categories] + + # Should match backup config categories + assert "💥 Breaking Changes" in category_names + assert "🚀 Features" in category_names + assert "🛠 Bug Fixes" in category_names + assert "📖 Documentation" in category_names + assert "🛡 Security Updates" in category_names + assert "Other Changes" in category_names + + # Check labels + features_cat = next(c for c in config.release_notes.categories if c.name == "🚀 Features") + assert "feature" in features_cat.labels + assert "enhancement" in features_cat.labels + assert "feat" in features_cat.labels + + bugfixes_cat = next(c for c in config.release_notes.categories if c.name == "🛠 Bug Fixes") + assert "bug" in bugfixes_cat.labels + assert "fix" in bugfixes_cat.labels + assert "bugfix" in bugfixes_cat.labels + assert "hotfix" in bugfixes_cat.labels diff --git a/tests/test_docker.py b/tests/test_docker.py new file mode 100644 index 0000000..5137633 --- /dev/null +++ b/tests/test_docker.py @@ -0,0 +1,101 @@ +"""Tests for Docker image build and functionality.""" + +import subprocess +import pytest + + +@pytest.fixture(scope="module") +def docker_image_name(): + """Return the Docker image name to use for tests.""" + return "release-tool-test" + + +@pytest.fixture(scope="module") +def build_docker_image(docker_image_name): + """Build the Docker image before running tests.""" + # Build the Docker image + result = subprocess.run( + ["docker", "build", "-t", docker_image_name, "."], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + pytest.fail(f"Failed to build Docker image: {result.stderr}") + + yield docker_image_name + + # Cleanup: Remove the Docker image after tests + subprocess.run( + ["docker", "rmi", "-f", docker_image_name], + capture_output=True, + ) + + +def test_docker_image_builds_successfully(docker_image_name): + """Test that the Docker image can be built successfully from the Dockerfile.""" + # Build the Docker image + result = subprocess.run( + ["docker", "build", "-t", docker_image_name, "."], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Docker build failed: {result.stderr}" + # Modern Docker output may show success in stdout or stderr + output = result.stdout + result.stderr + assert ( + "Successfully built" in output + or "Successfully tagged" in output + or f"naming to docker.io/library/{docker_image_name}" in output + ) + + +def test_release_tool_executable_available(build_docker_image): + """Test that the release-tool executable is available in the Docker image.""" + # Check if release-tool command exists in the image + result = subprocess.run( + ["docker", "run", "--rm", build_docker_image, "which", "release-tool"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, "release-tool executable not found in PATH" + assert "release-tool" in result.stdout + + +def test_release_tool_help_returns_success(build_docker_image): + """Test that running release-tool -h inside the Docker container returns a successful exit code.""" + # Run release-tool -h in the container + result = subprocess.run( + ["docker", "run", "--rm", build_docker_image, "release-tool", "-h"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"release-tool -h failed with exit code {result.returncode}" + assert "Usage:" in result.stdout or "usage:" in result.stdout.lower() + + +def test_docker_image_default_command(build_docker_image): + """Test that the Docker image runs release-tool as its default command.""" + # Run the container without specifying a command + # This should execute the default CMD from the Dockerfile + result = subprocess.run( + ["docker", "run", "--rm", build_docker_image], + capture_output=True, + text=True, + timeout=5, + ) + + # The default command should run release-tool, which without arguments + # should either show help or an error message from release-tool + # We're checking that it's release-tool that runs, not bash or another shell + assert result.returncode in [0, 1, 2], f"Unexpected exit code: {result.returncode}" + + # Verify the output contains release-tool-specific content + output = result.stdout + result.stderr + assert any( + keyword in output.lower() + for keyword in ["release-tool", "usage", "command"] + ), "Default command does not appear to be release-tool" diff --git a/tests/test_git_ops.py b/tests/test_git_ops.py new file mode 100644 index 0000000..da98286 --- /dev/null +++ b/tests/test_git_ops.py @@ -0,0 +1,250 @@ +"""Tests for Git operations.""" + +import pytest +from unittest.mock import Mock, MagicMock +from release_tool.models import SemanticVersion +from release_tool.git_ops import find_comparison_version, determine_release_branch_strategy + + +class TestFindComparisonVersion: + """Tests for version comparison logic.""" + + def test_final_version_compares_to_previous_final(self): + target = SemanticVersion.parse("2.0.0") + available = [ + SemanticVersion.parse("1.0.0"), + SemanticVersion.parse("1.5.0"), + SemanticVersion.parse("1.9.0"), + SemanticVersion.parse("2.0.0-rc.1") + ] + + result = find_comparison_version(target, available) + assert result == SemanticVersion.parse("1.9.0") + + def test_rc_compares_to_previous_rc_of_same_version(self): + target = SemanticVersion.parse("2.0.0-rc.2") + available = [ + SemanticVersion.parse("1.9.0"), + SemanticVersion.parse("2.0.0-rc.1"), + SemanticVersion.parse("2.0.0-rc.0") + ] + + result = find_comparison_version(target, available) + assert result == SemanticVersion.parse("2.0.0-rc.1") + + def test_rc_compares_to_previous_final_if_no_rc(self): + target = SemanticVersion.parse("2.0.0-rc.1") + available = [ + SemanticVersion.parse("1.0.0"), + SemanticVersion.parse("1.5.0") + ] + + result = find_comparison_version(target, available) + assert result == SemanticVersion.parse("1.5.0") + + def test_first_version_has_no_comparison(self): + target = SemanticVersion.parse("1.0.0") + available = [SemanticVersion.parse("1.0.0")] + + result = find_comparison_version(target, available) + assert result is None + + def test_returns_none_if_no_earlier_versions(self): + target = SemanticVersion.parse("2.0.0") + available = [SemanticVersion.parse("3.0.0")] + + result = find_comparison_version(target, available) + assert result is None + + +class TestReleaseBranchStrategy: + """Tests for release branch strategy determination.""" + + def create_mock_git_ops(self, existing_branches=None, remote_branches=None, latest_release_branch=None): + """Create a mock GitOperations instance.""" + git_ops = Mock() + + # Mock branch_exists + def branch_exists_side_effect(name, remote=False): + branches = remote_branches if remote else existing_branches + return branches and name in branches + + git_ops.branch_exists = Mock(side_effect=branch_exists_side_effect) + git_ops.get_latest_release_branch = Mock(return_value=latest_release_branch) + + return git_ops + + def test_new_major_version_from_main(self): + """Test new major version branches from main.""" + version = SemanticVersion.parse("9.0.0") + available = [SemanticVersion.parse("8.5.0")] + git_ops = self.create_mock_git_ops(existing_branches=[]) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main" + ) + + assert branch == "release/9.0" + assert source == "main" + assert should_create is True + + def test_new_minor_from_previous_release(self): + """Test new minor version branches from previous release branch.""" + version = SemanticVersion.parse("9.1.0") + available = [SemanticVersion.parse("9.0.0")] + git_ops = self.create_mock_git_ops( + existing_branches=["release/9.0"], + latest_release_branch="release/9.0" + ) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main", + branch_from_previous=True + ) + + assert branch == "release/9.1" + assert source == "release/9.0" + assert should_create is True + + def test_new_minor_from_main_when_no_previous_branch(self): + """Test new minor without previous branch falls back to main.""" + version = SemanticVersion.parse("9.1.0") + available = [SemanticVersion.parse("9.0.0")] + git_ops = self.create_mock_git_ops( + existing_branches=[], + latest_release_branch=None + ) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main", + branch_from_previous=True + ) + + assert branch == "release/9.1" + assert source == "main" + assert should_create is True + + def test_existing_release_uses_same_branch(self): + """Test that existing release (RC) uses existing branch.""" + version = SemanticVersion.parse("9.1.0-rc.1") + available = [SemanticVersion.parse("9.1.0-rc.0")] # Same version exists + git_ops = self.create_mock_git_ops(existing_branches=["release/9.1"]) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main" + ) + + assert branch == "release/9.1" + assert source == "release/9.1" + assert should_create is False + + def test_branch_from_previous_disabled(self): + """Test that branch_from_previous=False uses main.""" + version = SemanticVersion.parse("9.1.0") + available = [SemanticVersion.parse("9.0.0")] + git_ops = self.create_mock_git_ops( + existing_branches=["release/9.0"], + latest_release_branch="release/9.0" + ) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main", + branch_from_previous=False + ) + + assert branch == "release/9.1" + assert source == "main" + assert should_create is True + + def test_custom_branch_template(self): + """Test custom branch naming template.""" + version = SemanticVersion.parse("9.1.0") + available = [] + git_ops = self.create_mock_git_ops(existing_branches=[]) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="rel-{{major}}.{{minor}}.x", + default_branch="develop" + ) + + assert branch == "rel-9.1.x" + assert source == "develop" + assert should_create is True + + def test_branch_exists_remotely(self): + """Test detecting branch that exists remotely.""" + version = SemanticVersion.parse("9.1.0") + available = [] + git_ops = self.create_mock_git_ops( + existing_branches=[], + remote_branches=["release/9.1"] + ) + + branch, source, should_create = determine_release_branch_strategy( + version, git_ops, available, + branch_template="release/{{major}}.{{minor}}", + default_branch="main" + ) + + assert branch == "release/9.1" + # Should not create if exists remotely + assert should_create is False + + +class TestGetLatestTag: + """Tests for get_latest_tag with final_only parameter.""" + + def test_get_latest_tag_includes_rc_by_default(self): + """Test that latest tag includes RCs by default.""" + from unittest.mock import Mock + from release_tool.git_ops import GitOperations + + git_ops = GitOperations(".") + git_ops.get_version_tags = Mock(return_value=[ + SemanticVersion.parse("9.2.0"), + SemanticVersion.parse("9.3.0-rc.1"), + SemanticVersion.parse("9.3.0-rc.6") + ]) + + latest = git_ops.get_latest_tag(final_only=False) + assert latest == "v9.3.0-rc.6" + + def test_get_latest_tag_final_only(self): + """Test that final_only=True excludes RCs.""" + from unittest.mock import Mock + from release_tool.git_ops import GitOperations + + git_ops = GitOperations(".") + git_ops.get_version_tags = Mock(return_value=[ + SemanticVersion.parse("9.2.0"), + SemanticVersion.parse("9.3.0-rc.1"), + SemanticVersion.parse("9.3.0-rc.6") + ]) + + latest = git_ops.get_latest_tag(final_only=True) + assert latest == "v9.2.0" + + def test_get_latest_tag_no_final_versions(self): + """Test that final_only=True returns None if no final versions.""" + from unittest.mock import Mock + from release_tool.git_ops import GitOperations + + git_ops = GitOperations(".") + git_ops.get_version_tags = Mock(return_value=[ + SemanticVersion.parse("9.3.0-rc.1"), + SemanticVersion.parse("9.3.0-rc.6") + ]) + + latest = git_ops.get_latest_tag(final_only=True) + assert latest is None diff --git a/tests/test_git_ops_range.py b/tests/test_git_ops_range.py new file mode 100644 index 0000000..b39659c --- /dev/null +++ b/tests/test_git_ops_range.py @@ -0,0 +1,64 @@ +"""Tests for release commit range calculation.""" + +import pytest +from unittest.mock import Mock, MagicMock +from release_tool.models import SemanticVersion +from release_tool.git_ops import get_release_commit_range + +class TestGetReleaseCommitRange: + """Tests for get_release_commit_range.""" + + def test_uses_explicit_head_ref(self): + """Test that get_release_commit_range uses the provided head_ref.""" + git_ops = Mock() + git_ops.get_version_tags = Mock(return_value=[]) + git_ops._find_tag_for_version = Mock(return_value=None) + + # Mock iter_commits to return a list + git_ops.repo.iter_commits = Mock(return_value=[]) + + target_version = SemanticVersion.parse("9.2.0") + + # Call with explicit head_ref + get_release_commit_range(git_ops, target_version, head_ref="release/9.2") + + # Verify iter_commits was called with the head_ref, not HEAD (implied or explicit) + # The current implementation (buggy) ignores head_ref and uses HEAD or None (which implies HEAD) + # The fix will make it use head_ref + + # We expect the call to be with "release/9.2" + # Note: The current implementation calls iter_commits() with no args if tag not found + # or iter_commits(tag) if tag found. + # We are simulating tag not found (new release). + + # If the code is fixed, it should call iter_commits("release/9.2") + # If the code is buggy, it calls iter_commits() (which means HEAD) + + # This assertion will fail on the buggy code if we were running it, + # but since we are mocking, we can just check what it was called with. + + # However, since I cannot run this test against the *actual* code without modifying it first + # (because the function signature doesn't accept head_ref yet), + # I will write this test to expect the *correct* behavior, and it will be valid once I update the code. + + git_ops.repo.iter_commits.assert_called_with("release/9.2") + + def test_uses_head_ref_with_comparison(self): + """Test using head_ref when there is a comparison version.""" + git_ops = Mock() + git_ops.get_version_tags = Mock(return_value=[SemanticVersion.parse("9.1.0")]) + git_ops._find_tag_for_version = Mock(return_value="v9.1.0") + + # Mock get_commits_for_version_range to raise ValueError (simulating target tag missing) + git_ops.get_commits_for_version_range.side_effect = ValueError("Tag not found") + + # Mock get_commits_between_refs + git_ops.get_commits_between_refs = Mock(return_value=[]) + + target_version = SemanticVersion.parse("9.2.0") + + # Call with explicit head_ref + get_release_commit_range(git_ops, target_version, head_ref="release/9.2") + + # Should call get_commits_between_refs with from_tag and head_ref + git_ops.get_commits_between_refs.assert_called_with("v9.1.0", "release/9.2") diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..c52e971 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,291 @@ +"""Tests for data models.""" + +import pytest +from datetime import datetime +from release_tool.models import ( + SemanticVersion, Repository, Label, PullRequest, Commit, + Ticket, Release, ReleaseNote, VersionType, Author +) + + +class TestSemanticVersion: + """Tests for SemanticVersion model.""" + + def test_parse_simple_version(self): + version = SemanticVersion.parse("1.2.3") + assert version.major == 1 + assert version.minor == 2 + assert version.patch == 3 + assert version.prerelease is None + + def test_parse_version_with_v_prefix(self): + version = SemanticVersion.parse("v2.0.0") + assert version.major == 2 + assert version.minor == 0 + assert version.patch == 0 + + def test_parse_prerelease_version(self): + version = SemanticVersion.parse("1.0.0-rc.1") + assert version.major == 1 + assert version.minor == 0 + assert version.patch == 0 + assert version.prerelease == "rc.1" + + def test_parse_invalid_version(self): + with pytest.raises(ValueError): + SemanticVersion.parse("invalid") + + def test_parse_partial_version_with_flag(self): + """Test parsing partial version (major.minor) with allow_partial=True.""" + version = SemanticVersion.parse("9.2", allow_partial=True) + assert version.major == 9 + assert version.minor == 2 + assert version.patch == 0 + assert version.prerelease is None + + def test_parse_partial_version_without_flag(self): + """Test parsing partial version fails without allow_partial flag.""" + with pytest.raises(ValueError): + SemanticVersion.parse("9.2", allow_partial=False) + + def test_parse_full_version_with_partial_flag(self): + """Test that full versions still work with allow_partial=True.""" + version = SemanticVersion.parse("9.2.1", allow_partial=True) + assert version.major == 9 + assert version.minor == 2 + assert version.patch == 1 + assert version.prerelease is None + + def test_to_string(self): + version = SemanticVersion(major=1, minor=2, patch=3) + assert version.to_string() == "1.2.3" + + def test_to_string_with_v(self): + version = SemanticVersion(major=1, minor=2, patch=3) + assert version.to_string(include_v=True) == "v1.2.3" + + def test_to_string_with_prerelease(self): + version = SemanticVersion(major=1, minor=0, patch=0, prerelease="rc.1") + assert version.to_string() == "1.0.0-rc.1" + + def test_is_final(self): + final_version = SemanticVersion(major=1, minor=0, patch=0) + rc_version = SemanticVersion(major=1, minor=0, patch=0, prerelease="rc.1") + + assert final_version.is_final() + assert not rc_version.is_final() + + def test_get_type(self): + final = SemanticVersion(major=1, minor=0, patch=0) + rc = SemanticVersion(major=1, minor=0, patch=0, prerelease="rc.1") + beta = SemanticVersion(major=1, minor=0, patch=0, prerelease="beta.1") + alpha = SemanticVersion(major=1, minor=0, patch=0, prerelease="alpha.1") + + assert final.get_type() == VersionType.FINAL + assert rc.get_type() == VersionType.RELEASE_CANDIDATE + assert beta.get_type() == VersionType.BETA + assert alpha.get_type() == VersionType.ALPHA + + def test_comparison_same_version(self): + v1 = SemanticVersion(major=1, minor=0, patch=0) + v2 = SemanticVersion(major=1, minor=0, patch=0) + assert v1 == v2 + assert not v1 < v2 + assert not v1 > v2 + + def test_comparison_different_versions(self): + v1 = SemanticVersion(major=1, minor=0, patch=0) + v2 = SemanticVersion(major=2, minor=0, patch=0) + assert v1 < v2 + assert v2 > v1 + + def test_comparison_with_prerelease(self): + final = SemanticVersion(major=1, minor=0, patch=0) + rc = SemanticVersion(major=1, minor=0, patch=0, prerelease="rc.1") + assert rc < final + assert final > rc + + def test_bump_major(self): + """Test bumping major version resets minor and patch to 0.""" + v1 = SemanticVersion(major=1, minor=2, patch=3) + v2 = v1.bump_major() + assert v2.major == 2 + assert v2.minor == 0 + assert v2.patch == 0 + assert v2.prerelease is None + + def test_bump_major_from_prerelease(self): + """Test bumping major from prerelease removes prerelease.""" + v1 = SemanticVersion(major=1, minor=2, patch=3, prerelease="rc.1") + v2 = v1.bump_major() + assert v2.major == 2 + assert v2.minor == 0 + assert v2.patch == 0 + assert v2.prerelease is None + + def test_bump_minor(self): + """Test bumping minor version resets patch to 0.""" + v1 = SemanticVersion(major=1, minor=2, patch=3) + v2 = v1.bump_minor() + assert v2.major == 1 + assert v2.minor == 3 + assert v2.patch == 0 + assert v2.prerelease is None + + def test_bump_minor_from_prerelease(self): + """Test bumping minor from prerelease removes prerelease.""" + v1 = SemanticVersion(major=1, minor=2, patch=3, prerelease="beta.2") + v2 = v1.bump_minor() + assert v2.major == 1 + assert v2.minor == 3 + assert v2.patch == 0 + assert v2.prerelease is None + + def test_bump_patch(self): + """Test bumping patch version.""" + v1 = SemanticVersion(major=1, minor=2, patch=3) + v2 = v1.bump_patch() + assert v2.major == 1 + assert v2.minor == 2 + assert v2.patch == 4 + assert v2.prerelease is None + + def test_bump_patch_from_prerelease(self): + """Test bumping patch from prerelease removes prerelease.""" + v1 = SemanticVersion(major=1, minor=2, patch=3, prerelease="alpha.1") + v2 = v1.bump_patch() + assert v2.major == 1 + assert v2.minor == 2 + assert v2.patch == 4 + assert v2.prerelease is None + + def test_bump_rc(self): + """Test creating RC version.""" + v1 = SemanticVersion(major=1, minor=2, patch=3) + v2 = v1.bump_rc(0) + assert v2.major == 1 + assert v2.minor == 2 + assert v2.patch == 3 + assert v2.prerelease == "rc.0" + + def test_bump_rc_with_number(self): + """Test creating RC version with specific number.""" + v1 = SemanticVersion(major=1, minor=2, patch=3) + v2 = v1.bump_rc(5) + assert v2.major == 1 + assert v2.minor == 2 + assert v2.patch == 3 + assert v2.prerelease == "rc.5" + + def test_bump_rc_from_existing_prerelease(self): + """Test creating RC from existing prerelease.""" + v1 = SemanticVersion(major=1, minor=2, patch=3, prerelease="beta.1") + v2 = v1.bump_rc(0) + assert v2.major == 1 + assert v2.minor == 2 + assert v2.patch == 3 + assert v2.prerelease == "rc.0" + + +class TestRepository: + """Tests for Repository model.""" + + def test_create_repository(self): + repo = Repository(owner="test", name="repo") + assert repo.owner == "test" + assert repo.name == "repo" + assert repo.full_name == "test/repo" + + def test_repository_with_full_name(self): + repo = Repository(owner="test", name="repo", full_name="test/repo") + assert repo.full_name == "test/repo" + + +class TestPullRequest: + """Tests for PullRequest model.""" + + def test_create_pull_request(self): + pr = PullRequest( + repo_id=1, + number=123, + title="Test PR", + state="closed", + merged_at=datetime.now() + ) + assert pr.number == 123 + assert pr.title == "Test PR" + assert pr.state == "closed" + + +class TestCommit: + """Tests for Commit model.""" + + def test_create_commit(self): + author = Author(name="John Doe", email="john@example.com") + commit = Commit( + sha="abc123", + repo_id=1, + message="Test commit", + author=author, + date=datetime.now() + ) + assert commit.sha == "abc123" + assert commit.message == "Test commit" + assert commit.author.name == "John Doe" + assert commit.author.email == "john@example.com" + + +class TestTicket: + """Tests for Ticket model.""" + + def test_create_ticket(self): + ticket = Ticket( + repo_id=1, + number=456, + key="456", + title="Fix bug", + state="closed", + labels=[Label(name="bug")] + ) + assert ticket.number == 456 + assert ticket.key == "456" # No "#" prefix since v1.3 migration + assert ticket.title == "Fix bug" + assert len(ticket.labels) == 1 + assert ticket.labels[0].name == "bug" + + +class TestRelease: + """Tests for Release model.""" + + def test_create_release(self): + release = Release( + repo_id=1, + version="1.0.0", + tag_name="v1.0.0", + is_prerelease=False + ) + assert release.version == "1.0.0" + assert release.tag_name == "v1.0.0" + assert not release.is_prerelease + + +class TestReleaseNote: + """Tests for ReleaseNote model.""" + + def test_create_release_note(self): + author1 = Author(name="dev1", username="dev1") + author2 = Author(name="dev2", username="dev2") + note = ReleaseNote( + ticket_key="123", + title="Add new feature", + category="Features", + authors=[author1, author2], + pr_numbers=[456] + ) + assert note.ticket_key == "123" # No "#" prefix since v1.3 migration + assert note.title == "Add new feature" + assert note.category == "Features" + assert len(note.authors) == 2 + assert note.authors[0].name == "dev1" + assert note.authors[1].name == "dev2" + assert 456 in note.pr_numbers diff --git a/tests/test_output_template.py b/tests/test_output_template.py new file mode 100644 index 0000000..44c38e8 --- /dev/null +++ b/tests/test_output_template.py @@ -0,0 +1,355 @@ +"""Tests for release_output_template master template functionality.""" + +import pytest +from datetime import datetime +from release_tool.policies import ReleaseNoteGenerator +from release_tool.config import Config +from release_tool.models import ReleaseNote, Author + + +@pytest.fixture +def test_config_with_release_output_template(): + """Create a test configuration with release_output_template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "categories": [ + {"name": "Features", "labels": ["feature"], "order": 1}, + {"name": "Bug Fixes", "labels": ["bug"], "order": 2}, + {"name": "Documentation", "labels": ["docs"], "order": 3}, + ], + "release_output_template": """# {{ title }} + +{% for category in categories %} +## {{ category.name }} +{% for note in category.notes %} +{{ render_entry(note) }} +{% endfor %} +{% endfor %}""" + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_config_flat_list(): + """Config with flat list output template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "categories": [ + {"name": "Features", "labels": ["feature"], "order": 1}, + {"name": "Bug Fixes", "labels": ["bug"], "order": 2}, + {"name": "Documentation", "labels": ["docs"], "order": 3}, + ], + "release_output_template": """# {{ title }} + +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}""" + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_config_with_migrations(): + """Config with migrations section.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "categories": [ + {"name": "Features", "labels": ["feature"], "order": 1}, + {"name": "Bug Fixes", "labels": ["bug"], "order": 2}, + ], + "release_output_template": """# {{ title }} + +## Changes +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %} + +## Migration Notes +{% for note in all_notes %} +{% if note.migration_notes %} +### {{ note.title }} +{{ note.migration_notes }} +{% endif %} +{% endfor %}""" + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def sample_notes(): + """Create sample release notes.""" + author1 = Author(name="Alice", username="alice") + author2 = Author(name="Bob", username="bob") + + return [ + ReleaseNote( + title="Add new feature", + category="Features", + labels=["feature"], + authors=[author1], + pr_numbers=[123], + url="https://github.com/test/repo/pull/123" + ), + ReleaseNote( + title="Fix critical bug", + category="Bug Fixes", + labels=["bug"], + authors=[author2], + pr_numbers=[124], + url="https://github.com/test/repo/pull/124", + migration_notes="Update config to use new format" + ), + ReleaseNote( + title="Update docs", + category="Documentation", + labels=["docs"], + authors=[author1, author2], + pr_numbers=[125] + ) + ] + + +def test_release_output_template_with_categories(test_config_with_release_output_template, sample_notes): + """Test release_output_template with category iteration.""" + generator = ReleaseNoteGenerator(test_config_with_release_output_template) + grouped = generator.group_by_category(sample_notes) + + output = generator.format_markdown(grouped, "1.0.0") + + assert "# Release 1.0.0" in output + assert "## Features" in output + assert "## Bug Fixes" in output + assert "## Documentation" in output + assert "Add new feature" in output + assert "Fix critical bug" in output + assert "Update docs" in output + + +def test_release_output_template_flat_list(test_config_flat_list, sample_notes): + """Test release_output_template with flat list (no categories).""" + generator = ReleaseNoteGenerator(test_config_flat_list) + grouped = generator.group_by_category(sample_notes) + + output = generator.format_markdown(grouped, "1.0.0") + + assert "# Release 1.0.0" in output + # Should NOT have category headers + assert "## Features" not in output + assert "## Bug Fixes" not in output + # But should have all notes + assert "Add new feature" in output + assert "Fix critical bug" in output + assert "Update docs" in output + + +def test_release_output_template_with_migrations(test_config_with_migrations, sample_notes): + """Test release_output_template with separate migrations section.""" + generator = ReleaseNoteGenerator(test_config_with_migrations) + grouped = generator.group_by_category(sample_notes) + + output = generator.format_markdown(grouped, "2.0.0") + + assert "# Release 2.0.0" in output + assert "## Changes" in output + assert "## Migration Notes" in output + # Migration note should appear in dedicated section + assert "### Fix critical bug" in output + assert "Update config to use new format" in output + # Note without migration should not appear in migration section + assert "### Add new feature" not in output + + +def test_legacy_format_without_release_output_template(): + """Test that legacy format still works when release_output_template is not set.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + + author = Author(name="Test", username="test") + notes = [ + ReleaseNote( + title="Test change", + category="🚀 Features", + labels=["feature"], + authors=[author] + ) + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + # Now uses default release_output_template + # Note: Title header removed - appears in GitHub release UI instead + assert "🚀 Features" in output + assert "Test change" in output + + +def test_render_entry_includes_all_fields(): + """Test that render_entry correctly passes all fields to entry template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "categories": [ + {"name": "Features", "labels": ["feature"], "order": 1}, + ], + "entry_template": """- {{ title }} by {{ authors[0].mention }} +{% if migration_notes %}Migration: {{ migration_notes }}{% endif %}""", + "release_output_template": """# {{ title }} +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}""" + } + } + config = Config.from_dict(config_dict) + + author = Author(name="Alice", username="alice") + notes = [ + ReleaseNote( + title="Add feature", + category="Features", + labels=["feature"], + authors=[author], + migration_notes="Run migration script" + ) + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + assert "Add feature by @alice" in output + assert "Migration: Run migration script" in output + + +def test_html_whitespace_processing_in_release_output_template(): + """Test that HTML-like whitespace processing works in release_output_template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "release_output_template": """# {{ title }} + +Test with multiple spaces +and
line break""" + } + } + config = Config.from_dict(config_dict) + + generator = ReleaseNoteGenerator(config) + output = generator.format_markdown({}, "1.0.0") + + # Multiple spaces should collapse to single space + assert "Test with multiple spaces" in output + #
should create an empty line + lines = output.split('\n') + assert "and" in output + assert "line break" in output + + +def test_nbsp_entity_preservation(): + """Test that   entities are preserved as spaces and not collapsed.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "release_output_template": """# {{ title }} + +Test  two spaces +Normal spaces collapse +Mixed:  preserved and collapsed""" + } + } + config = Config.from_dict(config_dict) + + generator = ReleaseNoteGenerator(config) + output = generator.format_markdown({}, "1.0.0") + + # Two consecutive   should preserve two spaces + assert "Test two spaces" in output + + # Normal multiple spaces should collapse + assert "Normal spaces collapse" in output + + # Mixed usage:   preserved, normal spaces collapsed + assert "Mixed: preserved and collapsed" in output + + +def test_nbsp_in_entry_template(): + """Test that   works correctly in entry_template.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "release_notes": { + "categories": [ + {"name": "Features", "labels": ["feature"], "order": 1}, + ], + "entry_template": """- {{ title }}
  by {{ authors[0].mention }}""", + "release_output_template": """# {{ title }} +{% for note in all_notes %} +{{ render_entry(note) }} +{% endfor %}""" + } + } + config = Config.from_dict(config_dict) + + author = Author(name="Alice", username="alice") + notes = [ + ReleaseNote( + title="Add feature", + category="Features", + labels=["feature"], + authors=[author] + ) + ] + + generator = ReleaseNoteGenerator(config) + grouped = generator.group_by_category(notes) + output = generator.format_markdown(grouped, "1.0.0") + + # Should have two spaces (from   ) before "by" + assert " by @alice" in output diff --git a/tests/test_partial_tickets.py b/tests/test_partial_tickets.py new file mode 100644 index 0000000..e1cd534 --- /dev/null +++ b/tests/test_partial_tickets.py @@ -0,0 +1,453 @@ +"""Tests for partial ticket match handling.""" + +import pytest +from io import StringIO +from unittest.mock import Mock, patch +from datetime import datetime + +from release_tool.policies import PartialTicketMatch, PartialTicketReason +from release_tool.config import Config, PolicyAction +from release_tool.models import Author, Commit, PullRequest, ConsolidatedChange +from release_tool.commands.generate import _handle_partial_tickets, _get_extraction_source + + +@pytest.fixture +def test_config_warn(): + """Create a test configuration with warn policy.""" + config_dict = { + "repository": { + "code_repo": "test/repo", + "ticket_repos": ["test/meta"] + }, + "github": { + "token": "test_token" + }, + "ticket_policy": { + "partial_ticket_action": "warn" + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_config_ignore(): + """Create a test configuration with ignore policy.""" + config_dict = { + "repository": { + "code_repo": "test/repo", + "ticket_repos": ["test/meta"] + }, + "github": { + "token": "test_token" + }, + "ticket_policy": { + "partial_ticket_action": "ignore" + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_config_error(): + """Create a test configuration with error policy.""" + config_dict = { + "repository": { + "code_repo": "test/repo", + "ticket_repos": ["test/meta"] + }, + "github": { + "token": "test_token" + }, + "ticket_policy": { + "partial_ticket_action": "error" + } + } + return Config.from_dict(config_dict) + + +class TestPartialTicketMatch: + """Tests for PartialTicketMatch dataclass.""" + + def test_create_not_found_partial(self): + """Test creating a partial match for a ticket not found.""" + partial = PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main, PR #123", + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF, + PartialTicketReason.TYPO, + PartialTicketReason.SYNC_NOT_RUN + } + ) + + assert partial.ticket_key == "8624" + assert partial.match_type == "not_found" + assert len(partial.potential_reasons) == 3 + assert PartialTicketReason.OLDER_THAN_CUTOFF in partial.potential_reasons + assert partial.found_in_repo is None + assert partial.ticket_url is None + + def test_create_different_repo_partial(self): + """Test creating a partial match for a ticket in different repo.""" + partial = PartialTicketMatch( + ticket_key="8853", + extracted_from="branch feat/step-8853/main, PR #456", + match_type="different_repo", + found_in_repo="test/different-repo", + ticket_url="https://github.com/test/different-repo/issues/8853", + potential_reasons={ + PartialTicketReason.REPO_CONFIG_MISMATCH, + PartialTicketReason.WRONG_TICKET_REPOS + } + ) + + assert partial.ticket_key == "8853" + assert partial.match_type == "different_repo" + assert partial.found_in_repo == "test/different-repo" + assert partial.ticket_url == "https://github.com/test/different-repo/issues/8853" + assert len(partial.potential_reasons) == 2 + assert PartialTicketReason.REPO_CONFIG_MISMATCH in partial.potential_reasons + + +class TestPartialTicketReason: + """Tests for PartialTicketReason enum.""" + + def test_reason_descriptions(self): + """Test that all reasons have descriptions.""" + assert PartialTicketReason.OLDER_THAN_CUTOFF.description == "Ticket may be older than sync cutoff date" + assert PartialTicketReason.TYPO.description == "Ticket may not exist (typo in branch/PR)" + assert PartialTicketReason.SYNC_NOT_RUN.description == "Sync may not have been run yet" + assert PartialTicketReason.REPO_CONFIG_MISMATCH.description == "Ticket found in different repo than configured" + assert PartialTicketReason.WRONG_TICKET_REPOS.description == "Check repository.ticket_repos in config" + + def test_reason_values(self): + """Test enum values are correct.""" + assert PartialTicketReason.OLDER_THAN_CUTOFF.value == "older_than_cutoff" + assert PartialTicketReason.TYPO.value == "typo" + assert PartialTicketReason.SYNC_NOT_RUN.value == "sync_not_run" + assert PartialTicketReason.REPO_CONFIG_MISMATCH.value == "repo_config_mismatch" + assert PartialTicketReason.WRONG_TICKET_REPOS.value == "wrong_ticket_repos" + + +class TestGetExtractionSource: + """Tests for _get_extraction_source helper.""" + + def test_extraction_from_pr_with_branch(self): + """Test extraction source from PR with branch.""" + pr = PullRequest( + repo_id=1, + number=123, + title="Fix bug", + state="closed", + head_branch="feat/meta-8624/main" + ) + change = ConsolidatedChange( + type="ticket", + ticket_key="8624", + prs=[pr], + commits=[] + ) + + source = _get_extraction_source(change) + + assert "feat/meta-8624/main" in source + assert "PR #123" in source + assert "branch" in source + + def test_extraction_from_pr_without_branch(self): + """Test extraction source from PR without branch.""" + pr = PullRequest( + repo_id=1, + number=456, + title="Fix bug", + state="closed" + ) + change = ConsolidatedChange( + type="pr", + ticket_key="8853", + prs=[pr], + commits=[] + ) + + source = _get_extraction_source(change) + + assert "PR #456" in source + assert "branch" not in source + + def test_extraction_from_commit(self): + """Test extraction source from commit.""" + commit = Commit( + sha="abc1234567890", + repo_id=1, + message="Fix bug", + author=Author(name="dev"), + date=datetime.now() + ) + change = ConsolidatedChange( + type="commit", + ticket_key="999", + prs=[], + commits=[commit] + ) + + source = _get_extraction_source(change) + + assert "commit" in source + assert "abc1234" in source + + def test_extraction_unknown_source(self): + """Test extraction source when unknown.""" + change = ConsolidatedChange( + type="ticket", + ticket_key="111", + prs=[], + commits=[] + ) + + source = _get_extraction_source(change) + + assert source == "unknown source" + + +class TestHandlePartialTickets: + """Tests for _handle_partial_tickets function.""" + + def test_ignore_policy_no_output(self, test_config_ignore, capsys): + """Test that ignore policy produces no output.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="not_found", + potential_reasons={PartialTicketReason.OLDER_THAN_CUTOFF} + ) + ] + + # Should not raise, should not print + _handle_partial_tickets(partials, set(), test_config_ignore, debug=False) + + # No exception raised means success + assert True + + def test_warn_policy_prints_message(self, test_config_warn, capsys): + """Test that warn policy prints warning message.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main, PR #123", + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF, + PartialTicketReason.TYPO + } + ) + ] + + _handle_partial_tickets(partials, set(), test_config_warn, debug=False) + + # Capture should include warning about partial matches + # Note: Using rich Console means we can't easily capture output in tests + # This test verifies no exception is raised + assert True + + def test_error_policy_raises_exception(self, test_config_error): + """Test that error policy raises RuntimeError.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="not_found", + potential_reasons={PartialTicketReason.SYNC_NOT_RUN} + ) + ] + + with pytest.raises(RuntimeError) as exc_info: + _handle_partial_tickets(partials, set(), test_config_error, debug=False) + + assert "Unresolved partial ticket matches found" in str(exc_info.value) or "Partial ticket matches found" in str(exc_info.value) + assert "1 total" in str(exc_info.value) + + def test_error_policy_with_multiple_partials(self, test_config_error): + """Test error policy with multiple partials.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="not_found", + potential_reasons={PartialTicketReason.OLDER_THAN_CUTOFF} + ), + PartialTicketMatch( + ticket_key="8853", + extracted_from="branch feat/step-8853/main", + match_type="different_repo", + found_in_repo="test/different", + ticket_url="https://github.com/test/different/issues/8853", + potential_reasons={PartialTicketReason.REPO_CONFIG_MISMATCH} + ) + ] + + with pytest.raises(RuntimeError) as exc_info: + _handle_partial_tickets(partials, set(), test_config_error, debug=False) + + assert "2 total" in str(exc_info.value) + + def test_no_partials_does_nothing(self, test_config_warn): + """Test that empty partial list does nothing.""" + partials = [] + + # Should not raise, should not print + _handle_partial_tickets(partials, set(), test_config_warn, debug=False) + + assert True + + @patch('release_tool.commands.generate.console') + def test_warn_consolidates_by_reason(self, mock_console, test_config_warn): + """Test that warnings consolidate tickets by reason.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF, + PartialTicketReason.SYNC_NOT_RUN + } + ), + PartialTicketMatch( + ticket_key="8853", + extracted_from="branch feat/meta-8853/main", + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF # Same reason as 8624 + } + ) + ] + + _handle_partial_tickets(partials, set(), test_config_warn, debug=False) + + # Verify console.print was called + assert mock_console.print.called + + # Get the printed message + call_args = mock_console.print.call_args[0][0] + + # Should mention both tickets + assert "8624" in call_args + assert "8853" in call_args + + # Should mention the reason + assert "older than sync cutoff" in call_args or "OLDER_THAN_CUTOFF" in call_args + + @patch('release_tool.commands.generate.console') + def test_warn_different_repo_includes_url(self, mock_console, test_config_warn): + """Test that different_repo warnings include URLs.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="different_repo", + found_in_repo="test/other-repo", + ticket_url="https://github.com/test/other-repo/issues/8624", + potential_reasons={PartialTicketReason.REPO_CONFIG_MISMATCH} + ) + ] + + _handle_partial_tickets(partials, set(), test_config_warn, debug=False) + + # Verify console.print was called + assert mock_console.print.called + + # Get the printed message + call_args = mock_console.print.call_args[0][0] + + # Should include the URL + assert "https://github.com/test/other-repo/issues/8624" in call_args + assert "test/other-repo" in call_args + + @patch('release_tool.commands.generate.console') + def test_warn_shows_resolution_steps(self, mock_console, test_config_warn): + """Test that warnings include resolution steps.""" + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch feat/meta-8624/main", + match_type="not_found", + potential_reasons={PartialTicketReason.SYNC_NOT_RUN} + ) + ] + + _handle_partial_tickets(partials, set(), test_config_warn, debug=False) + + # Get the printed message + call_args = mock_console.print.call_args[0][0] + + # Should include resolution steps + assert "To resolve:" in call_args + assert "release-tool sync" in call_args + + +class TestPartialTicketIntegration: + """Integration tests for partial ticket handling in generate command.""" + + def test_partial_match_enum_usage(self): + """Test that PartialTicketMatch uses enums correctly.""" + partial = PartialTicketMatch( + ticket_key="8624", + extracted_from="test", + match_type="not_found", + potential_reasons={ + PartialTicketReason.OLDER_THAN_CUTOFF, + PartialTicketReason.TYPO + } + ) + + # All reasons should be enum instances + for reason in partial.potential_reasons: + assert isinstance(reason, PartialTicketReason) + assert hasattr(reason, 'description') + assert isinstance(reason.description, str) + + def test_consolidation_groups_tickets_by_reason(self): + """Test that multiple tickets with same reason are grouped.""" + from collections import defaultdict + + partials = [ + PartialTicketMatch( + ticket_key="8624", + extracted_from="branch 1", + match_type="not_found", + potential_reasons={PartialTicketReason.OLDER_THAN_CUTOFF} + ), + PartialTicketMatch( + ticket_key="8853", + extracted_from="branch 2", + match_type="not_found", + potential_reasons={PartialTicketReason.OLDER_THAN_CUTOFF} + ), + PartialTicketMatch( + ticket_key="9000", + extracted_from="branch 3", + match_type="not_found", + potential_reasons={PartialTicketReason.TYPO} + ) + ] + + # Group tickets by reason (like _handle_partial_tickets does) + tickets_by_reason = defaultdict(list) + for p in partials: + for reason in p.potential_reasons: + tickets_by_reason[reason].append(p) + + # Should have 2 groups: OLDER_THAN_CUTOFF (2 tickets), TYPO (1 ticket) + assert len(tickets_by_reason) == 2 + assert len(tickets_by_reason[PartialTicketReason.OLDER_THAN_CUTOFF]) == 2 + assert len(tickets_by_reason[PartialTicketReason.TYPO]) == 1 + + # Verify ticket keys + cutoff_tickets = [p.ticket_key for p in tickets_by_reason[PartialTicketReason.OLDER_THAN_CUTOFF]] + assert "8624" in cutoff_tickets + assert "8853" in cutoff_tickets + + typo_tickets = [p.ticket_key for p in tickets_by_reason[PartialTicketReason.TYPO]] + assert "9000" in typo_tickets diff --git a/tests/test_policies.py b/tests/test_policies.py new file mode 100644 index 0000000..679b124 --- /dev/null +++ b/tests/test_policies.py @@ -0,0 +1,798 @@ +"""Tests for policy implementations.""" + +import pytest +from datetime import datetime +from release_tool.policies import ( + TicketExtractor, CommitConsolidator, ReleaseNoteGenerator, VersionGapChecker +) +from release_tool.config import Config, TicketPolicyConfig, ReleaseNoteConfig, CategoryConfig +from release_tool.models import Commit, PullRequest, Ticket, Label, ConsolidatedChange, Author + + +@pytest.fixture +def test_config(): + """Create a test configuration.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + } + } + return Config.from_dict(config_dict) + + +class TestTicketExtractor: + """Tests for ticket extraction.""" + + def test_extract_from_commit_with_jira_ticket(self, test_config): + """Test extraction from commit messages with JIRA-style tickets.""" + extractor = TicketExtractor(test_config) + commit = Commit( + sha="abc123", + repo_id=1, + message="Fix bug in TICKET-789", + author=Author(name="dev"), + date=datetime.now() + ) + + tickets = extractor.extract_from_commit(commit) + + assert len(tickets) > 0 + assert "789" in tickets + + def test_extract_from_commit_with_github_ref(self, test_config): + """Test extraction from commit messages with GitHub issue refs.""" + extractor = TicketExtractor(test_config) + commit = Commit( + sha="abc456", + repo_id=1, + message="This fixes #456", + author=Author(name="dev"), + date=datetime.now() + ) + + tickets = extractor.extract_from_commit(commit) + + assert "456" in tickets + + def test_extract_from_branch_name(self, test_config): + """Test extraction from branch names like feat/meta-123/main.""" + extractor = TicketExtractor(test_config) + + # Test various branch name patterns + assert "123" in extractor.extract_from_branch("feat/meta-123/main") + assert "456" in extractor.extract_from_branch("fix/meta-456.whatever/main") + assert "789" in extractor.extract_from_branch("hotfix/repo-789/develop") + + def test_extract_parent_issue_from_pr(self, test_config): + """Test extraction of parent issue URL from PR body.""" + extractor = TicketExtractor(test_config) + + pr = PullRequest( + repo_id=1, + number=1, + title="Fix bug", + body=""" + This PR fixes several issues. + + Parent issue: https://github.com/owner/repo/issues/999 + + ## Changes + - Fixed bug + """, + state="closed", + head_branch="feature/branch" + ) + + tickets = extractor.extract_from_pr(pr) + assert "999" in tickets + + def test_extract_from_pr_title(self, test_config): + """Test extraction from PR title.""" + extractor = TicketExtractor(test_config) + + pr = PullRequest( + repo_id=1, + number=2, + title="Fix issue #123", + state="closed", + head_branch="fix/branch" + ) + + tickets = extractor.extract_from_pr(pr) + assert "123" in tickets + + def test_debug_mode_output(self, test_config, capsys): + """Test that debug mode produces detailed pattern matching output.""" + extractor = TicketExtractor(test_config, debug=True) + + # Test with commit that has a ticket + commit = Commit( + sha="abc123def", + repo_id=1, + message="Fix authentication bug #456", + author=Author(name="developer"), + date=datetime.now() + ) + + tickets = extractor.extract_from_commit(commit) + + # Capture output + captured = capsys.readouterr() + + # Verify output contains key debug information + assert "Extracting from commit:" in captured.out + assert "abc123" in captured.out # Short SHA + assert "Fix authentication bug" in captured.out # Commit message + assert "Trying pattern" in captured.out # Pattern attempt + assert "Regex:" in captured.out # Pattern regex shown + assert "Extracted tickets:" in captured.out # Results shown + + # Verify correct extraction + assert "456" in tickets + + # Test with PR + pr = PullRequest( + repo_id=1, + number=789, + title="Fix bug in login", + body="This fixes the login issue.", + state="closed", + head_branch="feat/meta-123/main" + ) + + tickets_pr = extractor.extract_from_pr(pr) + + # Capture PR output + captured = capsys.readouterr() + + # Verify PR debug output + assert "Extracting from PR #789:" in captured.out + assert "Fix bug in login" in captured.out + assert "Pattern #" in captured.out + assert "strategy=" in captured.out + assert "MATCH!" in captured.out or "No match" in captured.out + + # Verify correct extraction (branch pattern should match) + assert "123" in tickets_pr + + +class TestCommitConsolidator: + """Tests for commit consolidation.""" + + def test_consolidate_by_ticket(self, test_config): + extractor = TicketExtractor(test_config) + consolidator = CommitConsolidator(test_config, extractor) + + commits = [ + Commit( + sha="1", + repo_id=1, + message="TICKET-1: Part 1", + author=Author(name="dev"), + date=datetime.now() + ), + Commit( + sha="2", + repo_id=1, + message="TICKET-1: Part 2", + author=Author(name="dev"), + date=datetime.now() + ) + ] + + consolidated = consolidator.consolidate(commits, {}) + + # Should consolidate commits with same ticket + assert len(consolidated) <= len(commits) + + def test_consolidation_disabled(self, test_config): + # Disable consolidation + test_config.ticket_policy.consolidation_enabled = False + + extractor = TicketExtractor(test_config) + consolidator = CommitConsolidator(test_config, extractor) + + commits = [ + Commit(sha="1", repo_id=1, message="Test 1", author=Author(name="dev"), date=datetime.now()), + Commit(sha="2", repo_id=1, message="Test 2", author=Author(name="dev"), date=datetime.now()) + ] + + consolidated = consolidator.consolidate(commits, {}) + + # Should return each commit separately + assert len(consolidated) == len(commits) + + +class TestReleaseNoteGenerator: + """Tests for release note generation.""" + + def test_create_release_note_from_change(self, test_config): + generator = ReleaseNoteGenerator(test_config) + + change = ConsolidatedChange( + type="commit", + commits=[ + Commit( + sha="abc", + repo_id=1, + message="Add new feature", + author=Author(name="dev1"), + date=datetime.now() + ) + ] + ) + + note = generator.create_release_note(change) + + assert note.title == "Add new feature" + assert note.authors[0].name == "dev1" + + def test_group_by_category(self, test_config): + from release_tool.models import ReleaseNote + + generator = ReleaseNoteGenerator(test_config) + + notes = [ + ReleaseNote( + title="Fix bug", + category="Bug Fixes", + labels=["bug"] + ), + ReleaseNote( + title="New feature", + category="Features", + labels=["feature"] + ) + ] + + grouped = generator.group_by_category(notes) + + assert "Features" in grouped + assert "Bug Fixes" in grouped + assert len(grouped["Features"]) >= 1 + assert len(grouped["Bug Fixes"]) >= 1 + + +class TestVersionGapChecker: + """Tests for version gap checking.""" + + def test_gap_detection_warn(self, test_config, capsys): + checker = VersionGapChecker(test_config) + + # This should trigger a warning (gap from 1.0.0 to 1.2.0) + checker.check_gap("1.0.0", "1.2.0") + + # Check if warning was printed + captured = capsys.readouterr() + assert "gap" in captured.out.lower() or len(captured.out) == 0 # May or may not warn depending on config + + def test_gap_detection_ignore(self, test_config, capsys): + from release_tool.config import PolicyAction + test_config.version_policy.gap_detection = PolicyAction.IGNORE + + checker = VersionGapChecker(test_config) + checker.check_gap("1.0.0", "1.2.0") + + # Should not print anything + captured = capsys.readouterr() + # Ignore policy means no output + + +class TestTOMLPatternEscaping: + """Tests for TOML pattern escaping and real-world examples.""" + + def test_toml_config_with_correct_escaping(self): + """Test that patterns loaded from TOML with correct escaping work.""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "branch_name", + # Double backslash in Python dict (simulates TOML parsing) + "pattern": r"/(?P\w+)-(?P\d+)", + "description": "Branch pattern" + }, + { + "order": 2, + "strategy": "pr_body", + "pattern": r"Parent issue:.*?/issues/(?P\d+)", + "description": "Parent issue pattern" + }, + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + # Test branch pattern + pr = PullRequest( + repo_id=1, + number=2169, + title="✨ Prepare Release 9.2.0", + body="Parent issue: https://github.com/sequentech/meta/issues/8853", + state="closed", + head_branch="docs/feat-8853/main" + ) + + tickets = extractor.extract_from_pr(pr) + + # Should match branch pattern (order 1) and extract "8853" + assert "8853" in tickets, f"Expected '8853' in tickets, but got: {tickets}" + + def test_branch_pattern_real_world_examples(self): + """Test branch pattern with real-world branch names.""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "branch_name", + "pattern": r"/(?P\w+)-(?P\d+)", + "description": "Branch pattern" + } + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + # Test various real-world branch names + test_cases = [ + ("docs/feat-8853/main", "8853"), + ("feat/meta-123/main", "123"), + ("fix/repo-456.whatever/main", "456"), + ("hotfix/bug-789/develop", "789"), + ("feature/issue-999/release", "999"), + ] + + for branch_name, expected_ticket in test_cases: + tickets = extractor.extract_from_branch(branch_name) + assert expected_ticket in tickets, \ + f"Branch '{branch_name}' should extract ticket '{expected_ticket}', but got: {tickets}" + + def test_parent_issue_pattern_real_world_examples(self): + """Test parent issue pattern with real-world PR bodies.""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "pr_body", + "pattern": r"Parent issue:.*?/issues/(?P\d+)", + "description": "Parent issue pattern" + } + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + # Test various real-world PR body formats + test_cases = [ + ("Parent issue: https://github.com/sequentech/meta/issues/8853", "8853"), + ("Parent issue: https://github.com/owner/repo/issues/123", "123"), + ("Parent issue:https://github.com/org/project/issues/456", "456"), + ("Parent issue: http://github.com/test/test/issues/789", "789"), + ] + + for pr_body, expected_ticket in test_cases: + pr = PullRequest( + repo_id=1, + number=1, + title="Test PR", + body=pr_body, + state="closed", + head_branch="test" + ) + tickets = extractor.extract_from_pr(pr) + assert expected_ticket in tickets, \ + f"PR body '{pr_body}' should extract ticket '{expected_ticket}', but got: {tickets}" + + def test_github_issue_reference_patterns(self): + """Test GitHub issue reference patterns (#123) in various contexts.""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "pr_title", + "pattern": r"#(?P\d+)", + "description": "GitHub issue in PR title" + }, + { + "order": 2, + "strategy": "commit_message", + "pattern": r"#(?P\d+)", + "description": "GitHub issue in commit" + } + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + # Test PR title + pr = PullRequest( + repo_id=1, + number=1, + title="Fix authentication bug #456", + state="closed", + head_branch="fix/bug" + ) + tickets = extractor.extract_from_pr(pr) + assert "456" in tickets + + # Test commit message + commit = Commit( + sha="abc123", + repo_id=1, + message="Resolve issue #789", + author=Author(name="dev"), + date=datetime.now() + ) + tickets = extractor.extract_from_commit(commit) + assert "789" in tickets + + def test_jira_style_pattern(self): + """Test JIRA-style ticket patterns (PROJ-123).""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "commit_message", + "pattern": r"(?P[A-Z]+)-(?P\d+)", + "description": "JIRA-style tickets" + } + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + test_cases = [ + ("Fix bug in TICKET-789", "789"), + ("PROJ-123: Add new feature", "123"), + ("Update ABC-456 implementation", "456"), + ] + + for message, expected_ticket in test_cases: + commit = Commit( + sha="abc123", + repo_id=1, + message=message, + author=Author(name="dev"), + date=datetime.now() + ) + tickets = extractor.extract_from_commit(commit) + assert expected_ticket in tickets, \ + f"Commit message '{message}' should extract ticket '{expected_ticket}', but got: {tickets}" + + def test_pattern_priority_order(self): + """Test that patterns are tried in order and first match wins for PRs.""" + config_dict = { + "repository": {"code_repo": "test/repo"}, + "github": {"token": "test_token"}, + "ticket_policy": { + "patterns": [ + { + "order": 1, + "strategy": "branch_name", + "pattern": r"/(?P\w+)-(?P\d+)", + "description": "Branch pattern (highest priority)" + }, + { + "order": 2, + "strategy": "pr_body", + "pattern": r"Parent issue:.*?/issues/(?P\d+)", + "description": "Parent issue pattern (lower priority)" + } + ] + } + } + config = Config.from_dict(config_dict) + extractor = TicketExtractor(config) + + # PR with both branch name and parent issue + # Should extract from branch (order 1) and stop + pr = PullRequest( + repo_id=1, + number=1, + title="Test PR", + body="Parent issue: https://github.com/owner/repo/issues/999", + state="closed", + head_branch="feat/meta-123/main" + ) + + tickets = extractor.extract_from_pr(pr) + + # Should only extract "123" from branch (first match wins) + # Should NOT extract "999" from body (stopped after first match) + assert "123" in tickets + assert len(tickets) == 1, f"Should only extract one ticket (first match wins), but got: {tickets}" + + +class TestURLHandlingAndShortLinks: + """Tests for URL priority and short link generation.""" + + def test_url_priority_ticket_over_pr(self, test_config): + """Test that ticket_url is prioritized over pr_url in the smart url field.""" + from release_tool.models import ConsolidatedChange, Ticket, PullRequest, Repository + + # Create a ticket + ticket = Ticket( + repo_id=1, + number=8853, + key="8853", + title="Implement feature X", + state="closed", + url="https://github.com/sequentech/meta/issues/8853" + ) + + # Create a PR + pr = PullRequest( + repo_id=2, + number=2169, + title="Implement feature X", + state="closed", + head_branch="feat/meta-8853/main", + url="https://github.com/sequentech/step/pull/2169" + ) + + # Create a consolidated change with both + change = ConsolidatedChange( + type="ticket", + ticket_key="8853", + prs=[pr], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, ticket) + + # Verify URL priority + assert note.ticket_url == "https://github.com/sequentech/meta/issues/8853" + assert note.pr_url == "https://github.com/sequentech/step/pull/2169" + assert note.url == note.ticket_url, "url should prioritize ticket_url" + assert note.url != note.pr_url + + def test_url_fallback_to_pr_when_no_ticket(self, test_config): + """Test that pr_url is used when ticket_url is not available.""" + from release_tool.models import ConsolidatedChange, PullRequest + + # Create a PR (no ticket) + pr = PullRequest( + repo_id=2, + number=2169, + title="Fix bug", + state="closed", + head_branch="fix/bug", + url="https://github.com/sequentech/step/pull/2169" + ) + + # Create a consolidated change without ticket + change = ConsolidatedChange( + type="pr", + pr_number=2169, + prs=[pr], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, None) + + # Verify URL fallback + assert note.ticket_url is None + assert note.pr_url == "https://github.com/sequentech/step/pull/2169" + assert note.url == note.pr_url, "url should fall back to pr_url" + + def test_short_link_from_ticket_url(self, test_config): + """Test short_link generation from ticket URL.""" + from release_tool.models import ConsolidatedChange, Ticket + + ticket = Ticket( + repo_id=1, + number=8853, + key="8853", + title="Test ticket", + state="closed", + url="https://github.com/sequentech/meta/issues/8853" + ) + + change = ConsolidatedChange( + type="ticket", + ticket_key="8853", + prs=[], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, ticket) + + assert note.short_link == "#8853" + assert note.short_repo_link == "sequentech/meta#8853" + + def test_short_link_from_pr_url(self, test_config): + """Test short_link generation from PR URL.""" + from release_tool.models import ConsolidatedChange, PullRequest + + pr = PullRequest( + repo_id=2, + number=2169, + title="Test PR", + state="closed", + head_branch="test", + url="https://github.com/sequentech/step/pull/2169" + ) + + change = ConsolidatedChange( + type="pr", + pr_number=2169, + prs=[pr], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, None) + + assert note.short_link == "#2169" + assert note.short_repo_link == "sequentech/step#2169" + + def test_short_link_prioritizes_ticket_url(self, test_config): + """Test that short links are computed from ticket_url when both exist.""" + from release_tool.models import ConsolidatedChange, Ticket, PullRequest + + ticket = Ticket( + repo_id=1, + number=8853, + key="8853", + title="Test ticket", + state="closed", + url="https://github.com/sequentech/meta/issues/8853" + ) + + pr = PullRequest( + repo_id=2, + number=2169, + title="Test PR", + state="closed", + head_branch="feat/meta-8853/main", + url="https://github.com/sequentech/step/pull/2169" + ) + + change = ConsolidatedChange( + type="ticket", + ticket_key="8853", + prs=[pr], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, ticket) + + # Should use ticket info for short links, not PR info + assert note.short_link == "#8853" + assert note.short_repo_link == "sequentech/meta#8853" + assert note.short_link != "#2169" + assert note.short_repo_link != "sequentech/step#2169" + + def test_short_link_none_when_no_url(self, test_config): + """Test that short_link is None when no URL is available.""" + from release_tool.models import ConsolidatedChange, Commit, Author + + commit = Commit( + sha="abc123", + repo_id=1, + message="Fix bug", + author=Author(name="dev"), + date=datetime.now() + ) + + change = ConsolidatedChange( + type="commit", + commits=[commit], + prs=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, None) + + assert note.url is None + assert note.short_link is None + assert note.short_repo_link is None + + def test_extract_github_url_info_various_formats(self, test_config): + """Test _extract_github_url_info with various URL formats.""" + generator = ReleaseNoteGenerator(test_config) + + # Test cases: (url, expected_owner_repo, expected_number) + test_cases = [ + ("https://github.com/sequentech/meta/issues/8853", "sequentech/meta", "8853"), + ("https://github.com/owner/repo/pull/123", "owner/repo", "123"), + ("http://github.com/test/project/issues/456", "test/project", "456"), + ("https://github.com/org/my-repo/pull/789", "org/my-repo", "789"), + # Invalid URLs + ("https://gitlab.com/owner/repo/issues/123", None, None), + ("not a url", None, None), + ("https://github.com/owner/repo", None, None), + ] + + for url, expected_owner_repo, expected_number in test_cases: + owner_repo, number = generator._extract_github_url_info(url) + assert owner_repo == expected_owner_repo, f"URL {url}: expected owner_repo={expected_owner_repo}, got {owner_repo}" + assert number == expected_number, f"URL {url}: expected number={expected_number}, got {number}" + + def test_short_links_in_template_context(self, test_config): + """Test that short_link and short_repo_link are passed to templates.""" + from release_tool.models import ConsolidatedChange, Ticket + + ticket = Ticket( + repo_id=1, + number=8853, + key="8853", + title="Test ticket", + state="closed", + url="https://github.com/sequentech/meta/issues/8853" + ) + + change = ConsolidatedChange( + type="ticket", + ticket_key="8853", + prs=[], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, ticket) + + # Prepare note for template + note_dict = generator._prepare_note_for_template(note, "1.0.0", None, None) + + assert "short_link" in note_dict + assert "short_repo_link" in note_dict + assert note_dict["short_link"] == "#8853" + assert note_dict["short_repo_link"] == "sequentech/meta#8853" + + def test_ticket_key_without_hash_prefix(self, test_config): + """Test that ticket keys without '#' prefix still work correctly.""" + from release_tool.models import ConsolidatedChange, Ticket + + # Ticket with bare number as key (as extracted from branch names) + ticket = Ticket( + repo_id=1, + number=8624, + key="8624", # Bare number, not "#8624" + title="Test ticket", + state="closed", + url="https://github.com/sequentech/meta/issues/8624" + ) + + change = ConsolidatedChange( + type="ticket", + ticket_key="8624", # Bare number + prs=[], + commits=[] + ) + + generator = ReleaseNoteGenerator(test_config) + note = generator.create_release_note(change, ticket) + + # Should use ticket URL and generate correct short links + assert note.ticket_url == "https://github.com/sequentech/meta/issues/8624" + assert note.url == note.ticket_url + assert note.short_link == "#8624" + assert note.short_repo_link == "sequentech/meta#8624" diff --git a/tests/test_publish.py b/tests/test_publish.py new file mode 100644 index 0000000..638603d --- /dev/null +++ b/tests/test_publish.py @@ -0,0 +1,418 @@ +"""Tests for publish command functionality.""" + +import pytest +from pathlib import Path +from unittest.mock import Mock, patch, MagicMock +from click.testing import CliRunner + +from release_tool.commands.publish import publish +from release_tool.config import Config + + +@pytest.fixture +def test_config(): + """Create a test configuration.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "github": { + "token": "test_token" + }, + "output": { + "create_github_release": False, + "create_pr": False, + "draft_release": False, + "prerelease": "auto", + "draft_output_path": ".release_tool_cache/draft-releases/{{repo}}/{{version}}.md", + "pr_templates": { + "branch_template": "docs/{{version}}/{{target_branch}}", + "title_template": "Release notes for {{version}}", + "body_template": "Automated release notes for version {{version}}." + } + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_notes_file(tmp_path): + """Create a temporary release notes file.""" + notes_file = tmp_path / "release_notes.md" + notes_file.write_text("# Release 1.0.0\n\nTest release notes content.") + return notes_file + + +def test_dry_run_shows_output_without_api_calls(test_config, test_notes_file): + """Test that dry-run shows expected output without making API calls.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client: + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--dry-run', '--release'], + obj={'config': test_config} + ) + + # Should not create GitHub client in dry-run + mock_client.assert_not_called() + + # Should show dry-run banner + assert 'DRY RUN' in result.output + assert 'Publish release 1.0.0' in result.output + + # Should show what would be created + assert 'Would create' in result.output + assert 'test/repo' in result.output + assert 'v1.0.0' in result.output + + # Should exit successfully + assert result.exit_code == 0 + + +def test_dry_run_with_pr_flag(test_config, test_notes_file): + """Test dry-run with PR creation flag.""" + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--dry-run', '--pr', '--no-release'], + obj={'config': test_config} + ) + + assert 'Would create pull request' in result.output + assert 'Would NOT create GitHub release' in result.output + assert result.exit_code == 0 + + +def test_dry_run_with_draft_and_prerelease(test_config, test_notes_file): + """Test dry-run with draft and prerelease flags.""" + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0-rc.1', '-f', str(test_notes_file), '--dry-run', '--release', '--release-mode', 'draft', '--prerelease', 'true'], + obj={'config': test_config} + ) + + assert 'DRY RUN' in result.output + assert 'Draft' in result.output or 'draft' in result.output + assert result.exit_code == 0 + + +def test_config_defaults_used_when_no_cli_flags(test_config, test_notes_file): + """Test that config defaults are used when CLI flags are not provided.""" + # Set config defaults + test_config.output.create_github_release = True + test_config.output.release_mode = "draft" + + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file)], + obj={'config': test_config} + ) + + # Should create release with draft=True (from config) + mock_client.create_release.assert_called_once() + call_args = mock_client.create_release.call_args + assert call_args.kwargs['draft'] == True + assert result.exit_code == 0 + + +def test_cli_flags_override_config(test_config, test_notes_file): + """Test that CLI flags override config values.""" + # Set config to not create release + test_config.output.create_github_release = False + + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + # Override with --release flag + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--release'], + obj={'config': test_config} + ) + + # Should create release despite config saying False + mock_client.create_release.assert_called_once() + assert result.exit_code == 0 + + +def test_debug_mode_shows_verbose_output(test_config, test_notes_file): + """Test that debug mode shows verbose information.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient'): + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--debug', '--dry-run'], + obj={'config': test_config} + ) + + # Should show debug output + assert 'Debug Mode:' in result.output + assert 'Repository:' in result.output or 'Configuration' in result.output + assert 'Version:' in result.output + assert result.exit_code == 0 + + +def test_debug_mode_shows_docusaurus_preview(test_config, test_notes_file, tmp_path): + """Test that debug mode shows Docusaurus file preview when configured.""" + # Create a docusaurus file + doc_file = tmp_path / "doc_release.md" + doc_file.write_text("---\nid: release-1.0.0\n---\n# Release 1.0.0\n\nDocusaurus notes") + + # Configure doc_output_path + test_config.output.doc_output_path = str(doc_file) + + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--debug', '--dry-run'], + obj={'config': test_config} + ) + + # Should show doc file info in debug mode + assert 'Docusaurus' in result.output + assert 'doc_release.md' in result.output # Just check for filename, not full path + assert 'Docusaurus notes preview' in result.output or 'Docusaurus file length' in result.output or 'File exists' in result.output + assert result.exit_code == 0 + + +def test_error_handling_with_debug(test_config, test_notes_file): + """Test that debug mode re-raises exceptions with stack trace.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.SemanticVersion.parse', side_effect=Exception("Test error")): + result = runner.invoke( + publish, + ['invalid', '-f', str(test_notes_file), '--debug'], + obj={'config': test_config} + ) + + # Should show the exception + assert result.exit_code != 0 + assert 'Test error' in result.output or result.exception + + +def test_error_handling_without_debug(test_config, test_notes_file): + """Test that non-debug mode shows error message without stack trace.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.SemanticVersion.parse', side_effect=Exception("Test error")): + result = runner.invoke( + publish, + ['invalid', '-f', str(test_notes_file)], + obj={'config': test_config} + ) + + # Should show error message + assert result.exit_code != 0 + assert 'Error:' in result.output + + +def test_auto_detect_prerelease_version(test_config, test_notes_file): + """Test that prerelease is auto-detected from version when set to 'auto'.""" + runner = CliRunner() + + # Ensure config is set to "auto" (default) + test_config.output.prerelease = "auto" + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + result = runner.invoke( + publish, + ['1.0.0-rc.1', '-f', str(test_notes_file), '--release'], + obj={'config': test_config} + ) + + # Should detect as prerelease + assert 'Auto-detected as prerelease' in result.output + + # Should call create_release with prerelease=True + call_args = mock_client.create_release.call_args + assert call_args.kwargs['prerelease'] == True + assert result.exit_code == 0 + + +def test_dry_run_shows_release_notes_preview(test_config, test_notes_file): + """Test that dry-run shows a preview of the release notes.""" + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--dry-run', '--release'], + obj={'config': test_config} + ) + + # Should show preview of release notes + assert 'Release notes preview' in result.output + assert 'Test release notes content' in result.output + assert result.exit_code == 0 + + +def test_dry_run_summary_at_end(test_config, test_notes_file): + """Test that dry-run shows summary at the end.""" + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--dry-run'], + obj={'config': test_config} + ) + + # Should show summary + assert 'DRY RUN complete' in result.output + assert 'No changes were made' in result.output + assert result.exit_code == 0 + + +def test_docusaurus_file_detection_in_dry_run(test_config, test_notes_file, tmp_path): + """Test that dry-run detects and reports Docusaurus file.""" + # Create a docusaurus file + doc_file = tmp_path / "doc_release.md" + doc_file.write_text("Docusaurus content") + + # Configure doc_output_path + test_config.output.doc_output_path = str(doc_file) + + runner = CliRunner() + + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--dry-run'], + obj={'config': test_config} + ) + + # Should mention docusaurus file + assert 'Docusaurus file' in result.output + assert 'doc_release.md' in result.output # Just check for filename, not full path + assert 'Existing Docusaurus file found' in result.output or 'File exists' in result.output + assert result.exit_code == 0 + + +def test_pr_without_notes_file_shows_warning(test_config): + """Test that creating PR without notes file shows warning and skips.""" + runner = CliRunner() + + with patch('release_tool.commands.publish._find_draft_releases', return_value=[]): + result = runner.invoke( + publish, + ['1.0.0', '--pr', '--dry-run'], + obj={'config': test_config} + ) + + # Should show warning about no notes available + assert 'No draft release notes found' in result.output or 'No release notes available' in result.output + + +def test_prerelease_explicit_true(test_config, test_notes_file): + """Test that --prerelease true always marks as prerelease.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + # Use a stable version but force prerelease + result = runner.invoke( + publish, + ['1.0.0', '-f', str(test_notes_file), '--release', '--prerelease', 'true'], + obj={'config': test_config} + ) + + # Should call create_release with prerelease=True + call_args = mock_client.create_release.call_args + assert call_args.kwargs['prerelease'] == True + assert result.exit_code == 0 + + +def test_prerelease_explicit_false(test_config, test_notes_file): + """Test that --prerelease false never marks as prerelease.""" + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + # Use an RC version but force stable + result = runner.invoke( + publish, + ['1.0.0-rc.1', '-f', str(test_notes_file), '--release', '--prerelease', 'false'], + obj={'config': test_config} + ) + + # Should call create_release with prerelease=False + call_args = mock_client.create_release.call_args + assert call_args.kwargs['prerelease'] == False + assert result.exit_code == 0 + + +def test_auto_find_draft_notes_success(test_config, tmp_path): + """Test successful auto-finding of draft notes.""" + # Create a draft notes file in current directory + draft_dir = Path(".release_tool_cache") / "draft-releases" / "test-repo" + draft_dir.mkdir(parents=True, exist_ok=True) + draft_file = draft_dir / "1.0.0.md" + draft_file.write_text("# Release 1.0.0\n\nAuto-found draft notes") + + # Use relative path with Jinja2 syntax + test_config.output.draft_output_path = ".release_tool_cache/draft-releases/{{code_repo}}/{{version}}.md" + + try: + runner = CliRunner() + + with patch('release_tool.commands.publish.GitHubClient') as mock_client_class: + mock_client = Mock() + mock_client_class.return_value = mock_client + + # Don't specify --notes-file, should auto-find + result = runner.invoke( + publish, + ['1.0.0', '--release'], + obj={'config': test_config}, + catch_exceptions=False + ) + + # Should find and use the draft notes + assert 'Auto-found release notes' in result.output or result.exit_code == 0 + # Should create release + if mock_client.create_release.called: + assert result.exit_code == 0 + finally: + # Cleanup + import shutil + if draft_file.exists(): + draft_file.unlink() + if draft_dir.exists(): + shutil.rmtree(draft_dir.parent.parent, ignore_errors=True) + + +def test_auto_find_draft_notes_not_found(test_config): + """Test error when no draft notes found.""" + runner = CliRunner() + + # Don't create any draft files + with patch('release_tool.commands.publish._find_draft_releases', return_value=[]): + result = runner.invoke( + publish, + ['1.0.0'], + obj={'config': test_config} + ) + + # Should error and list available drafts + assert 'No draft release notes found' in result.output + assert result.exit_code == 1 diff --git a/tests/test_query_tickets.py b/tests/test_query_tickets.py new file mode 100644 index 0000000..8637bd0 --- /dev/null +++ b/tests/test_query_tickets.py @@ -0,0 +1,969 @@ +"""Tests for tickets command and database querying.""" + +import pytest +import tempfile +from pathlib import Path +from datetime import datetime +from click.testing import CliRunner + +from release_tool.db import Database +from release_tool.models import Repository, Ticket, Label +from release_tool.main import cli + + +@pytest.fixture +def test_db(): + """Create a test database with sample data.""" + # Create temporary database + with tempfile.NamedTemporaryFile(mode='w', suffix='.db', delete=False) as f: + db_path = f.name + + db = Database(db_path) + db.connect() + + # Create test repositories + meta_repo = Repository(owner="sequentech", name="meta") + step_repo = Repository(owner="sequentech", name="step") + + meta_id = db.upsert_repository(meta_repo) + step_id = db.upsert_repository(step_repo) + + # Create test tickets + tickets_data = [ + # Meta repo tickets + Ticket( + repo_id=meta_id, + number=8624, + key="8624", + title="Add dark mode to UI", + body="Implement dark mode across the application", + state="open", + labels=[Label(name="feature"), Label(name="ui")], + url="https://github.com/sequentech/meta/issues/8624", + created_at=datetime(2024, 1, 15), + ), + Ticket( + repo_id=meta_id, + number=8625, + key="8625", + title="Fix login bug", + body="Users cannot log in", + state="closed", + labels=[Label(name="bug")], + url="https://github.com/sequentech/meta/issues/8625", + created_at=datetime(2024, 1, 16), + closed_at=datetime(2024, 1, 20), + ), + Ticket( + repo_id=meta_id, + number=8650, + key="8650", + title="Update documentation", + body="Refresh README", + state="open", + labels=[Label(name="docs")], + url="https://github.com/sequentech/meta/issues/8650", + created_at=datetime(2024, 2, 1), + ), + Ticket( + repo_id=meta_id, + number=8624, + key="#8624", # Duplicate with # prefix + title="Another ticket", + body="Test duplicate key handling", + state="open", + labels=[], + url="https://github.com/sequentech/meta/issues/8624", + created_at=datetime(2024, 1, 10), # Earlier than first 8624 + ), + # Step repo tickets + Ticket( + repo_id=step_id, + number=1024, + key="1024", + title="Performance optimization", + body="Improve query speed", + state="open", + labels=[Label(name="performance")], + url="https://github.com/sequentech/step/issues/1024", + created_at=datetime(2024, 3, 1), + ), + Ticket( + repo_id=step_id, + number=1124, + key="1124", + title="Security patch", + body="Fix vulnerability", + state="closed", + labels=[Label(name="security"), Label(name="high-priority")], + url="https://github.com/sequentech/step/issues/1124", + created_at=datetime(2024, 3, 10), + closed_at=datetime(2024, 3, 15), + ), + ] + + for ticket in tickets_data: + db.upsert_ticket(ticket) + + yield db, meta_id, step_id + + # Cleanup + db.close() + Path(db_path).unlink() + + +class TestParseTicketNumber: + """Tests for _parse_ticket_number helper.""" + + def test_parse_plain_number(self, test_db): + """Test parsing plain number.""" + db, _, _ = test_db + assert db._parse_ticket_number("8624") == 8624 + + def test_parse_hash_prefix(self, test_db): + """Test parsing with # prefix.""" + db, _, _ = test_db + assert db._parse_ticket_number("#8624") == 8624 + + def test_parse_jira_style(self, test_db): + """Test parsing JIRA-style key.""" + db, _, _ = test_db + assert db._parse_ticket_number("ISSUE-8624") == 8624 + assert db._parse_ticket_number("meta-8624") == 8624 + + def test_parse_no_number(self, test_db): + """Test parsing with no number.""" + db, _, _ = test_db + assert db._parse_ticket_number("no-numbers-here") is None + + def test_parse_empty(self, test_db): + """Test parsing empty string.""" + db, _, _ = test_db + assert db._parse_ticket_number("") is None + + +class TestQueryTicketsDatabase: + """Tests for database query_tickets method.""" + + def test_query_by_exact_ticket_key(self, test_db): + """Test finding ticket by exact key.""" + db, _, _ = test_db + tickets = db.query_tickets(ticket_key="8624") + + assert len(tickets) >= 1 + # Should find the most recent one + assert tickets[0].key in ["8624", "#8624"] + + def test_query_by_repo_id(self, test_db): + """Test finding all tickets in a repo.""" + db, meta_id, step_id = test_db + + meta_tickets = db.query_tickets(repo_id=meta_id, limit=100) + assert len(meta_tickets) == 4 # 4 tickets in meta repo + + step_tickets = db.query_tickets(repo_id=step_id, limit=100) + assert len(step_tickets) == 2 # 2 tickets in step repo + + def test_query_by_repo_full_name(self, test_db): + """Test finding tickets by repository name.""" + db, _, _ = test_db + + tickets = db.query_tickets(repo_full_name="sequentech/meta", limit=100) + assert len(tickets) == 4 + + tickets = db.query_tickets(repo_full_name="sequentech/step", limit=100) + assert len(tickets) == 2 + + def test_query_combined_ticket_and_repo(self, test_db): + """Test combining ticket key and repo filters.""" + db, meta_id, _ = test_db + + # Find specific ticket in specific repo + tickets = db.query_tickets(ticket_key="8624", repo_id=meta_id) + assert len(tickets) >= 1 + assert all(t.repo_id == meta_id for t in tickets) + + def test_query_starts_with(self, test_db): + """Test fuzzy matching with starts_with.""" + db, _, _ = test_db + + # Find all tickets starting with "86" + tickets = db.query_tickets(starts_with="86", limit=100) + assert len(tickets) >= 3 # 8624 (x2) and 8625, 8650 + assert all(t.key.startswith("86") or str(t.number).startswith("86") for t in tickets) + + def test_query_ends_with(self, test_db): + """Test fuzzy matching with ends_with.""" + db, _, _ = test_db + + # Find all tickets ending with "24" + tickets = db.query_tickets(ends_with="24", limit=100) + assert len(tickets) >= 3 # 8624 (x2), 1024, 1124 + + def test_query_close_to_default_range(self, test_db): + """Test proximity search with default range.""" + db, _, _ = test_db + + # Find tickets close to 8624 (±20 = 8604-8644) + tickets = db.query_tickets(close_to="8624", limit=100) + + # Should find 8624, 8625 + assert len(tickets) >= 2 + for ticket in tickets: + assert 8604 <= ticket.number <= 8644 + + def test_query_close_to_custom_range(self, test_db): + """Test proximity search with custom range.""" + db, _, _ = test_db + + # Find tickets close to 8624 with range of 50 (8574-8674) + tickets = db.query_tickets(close_to="8624", close_range=50, limit=100) + + # Should find 8624, 8625, 8650 + assert len(tickets) >= 3 + for ticket in tickets: + assert 8574 <= ticket.number <= 8674 + + def test_query_with_limit(self, test_db): + """Test pagination with limit.""" + db, _, _ = test_db + + # Query with limit of 2 + tickets = db.query_tickets(limit=2) + assert len(tickets) == 2 + + def test_query_with_offset(self, test_db): + """Test pagination with offset.""" + db, _, _ = test_db + + # Get all tickets + all_tickets = db.query_tickets(limit=100) + total = len(all_tickets) + + # Get tickets with offset + tickets_offset = db.query_tickets(offset=2, limit=100) + + # Should have 2 fewer tickets + assert len(tickets_offset) == total - 2 + + def test_query_limit_and_offset(self, test_db): + """Test combined pagination.""" + db, _, _ = test_db + + # Get first 2 + first_page = db.query_tickets(limit=2, offset=0) + assert len(first_page) == 2 + + # Get next 2 + second_page = db.query_tickets(limit=2, offset=2) + assert len(second_page) <= 2 + + # Should not overlap + first_ids = {t.id for t in first_page} + second_ids = {t.id for t in second_page} + assert first_ids.isdisjoint(second_ids) + + def test_query_no_results(self, test_db): + """Test query returning no results.""" + db, _, _ = test_db + + tickets = db.query_tickets(ticket_key="nonexistent-99999") + assert len(tickets) == 0 + + def test_query_repo_full_name_includes_repo_info(self, test_db): + """Test that tickets include repo_full_name when queried.""" + db, _, _ = test_db + + tickets = db.query_tickets(repo_full_name="sequentech/meta", limit=1) + assert len(tickets) >= 1 + + # Check that _repo_full_name is attached + ticket = tickets[0] + assert hasattr(ticket, '_repo_full_name') + assert ticket._repo_full_name == "sequentech/meta" + + +class TestQueryTicketsCLI: + """Tests for CLI tickets command.""" + + def test_cli_no_database(self, tmp_path, monkeypatch): + """Test error when database doesn't exist.""" + # Create config pointing to non-existent DB + config_file = tmp_path / "test_config.toml" + nonexistent_db = tmp_path / "nonexistent.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{nonexistent_db}" +""" + config_file.write_text(config_content) + + runner = CliRunner() + result = runner.invoke(cli, ['--config', str(config_file), 'tickets', '8624']) + + assert result.exit_code != 0 + assert "Database not found" in result.output + + def test_cli_exact_ticket(self, tmp_path, test_db): + """Test CLI with exact ticket number.""" + db, _, _ = test_db + + # Create config + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + # Copy database to expected location + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, ['--config', str(config_file), 'tickets', '8624']) + + assert result.exit_code == 0 + assert "8624" in result.output + + def test_cli_repo_filter(self, tmp_path, test_db): + """Test CLI with --repo filter.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--repo', 'sequentech/step', + '--limit', '10' + ]) + + assert result.exit_code == 0 + assert "step" in result.output.lower() + + def test_cli_fuzzy_starts_with(self, tmp_path, test_db): + """Test CLI with --starts-with option.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--starts-with', '86' + ]) + + assert result.exit_code == 0 + assert "86" in result.output + + def test_cli_csv_output(self, tmp_path, test_db): + """Test CLI with CSV format.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--repo', 'sequentech/meta', + '--format', 'csv', + '--limit', '2' + ]) + + assert result.exit_code == 0 + # CSV should have header row + assert "id,repo_id,number,key,title" in result.output + # Should have data rows + assert "8624" in result.output or "8625" in result.output + + def test_cli_pagination(self, tmp_path, test_db): + """Test CLI pagination options.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--limit', '2', + '--offset', '1' + ]) + + assert result.exit_code == 0 + # Should show pagination info + assert "Showing" in result.output or "tickets" in result.output.lower() + + def test_cli_invalid_range(self, tmp_path, test_db): + """Test CLI validation for invalid --range.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--close-to', '8624', + '--range', '-10' + ]) + + assert result.exit_code != 0 + assert "range must be >= 0" in result.output.lower() + + def test_cli_conflicting_filters(self, tmp_path, test_db): + """Test CLI validation for conflicting filters.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--close-to', '8624', + '--starts-with', '86' + ]) + + assert result.exit_code != 0 + assert "cannot combine" in result.output.lower() + + +class TestSmartTicketKeyParsing: + """Tests for smart TICKET_KEY argument parsing.""" + + def test_plain_number(self, tmp_path, test_db): + """Test parsing plain number: 8624.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '8624' + ]) + + assert result.exit_code == 0 + assert "8624" in result.output + + def test_hash_prefix(self, tmp_path, test_db): + """Test parsing with # prefix: #8624.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '#8624' + ]) + + assert result.exit_code == 0 + assert "8624" in result.output + + def test_repo_and_number(self, tmp_path, test_db): + """Test parsing repo + number: meta#8624.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + 'meta#8624' + ]) + + assert result.exit_code == 0 + assert "8624" in result.output + assert "meta" in result.output.lower() + + def test_repo_and_number_with_proximity(self, tmp_path, test_db): + """Test parsing repo + number + proximity: meta#8624~.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + 'meta#8624~' + ]) + + assert result.exit_code == 0 + # Should find multiple tickets in proximity (8624, 8625, 8650) + assert "8624" in result.output or "8625" in result.output + + def test_full_repo_path(self, tmp_path, test_db): + """Test parsing full repo path: sequentech/meta#8624.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + 'sequentech/meta#8624' + ]) + + assert result.exit_code == 0 + assert "8624" in result.output + assert "meta" in result.output.lower() + + def test_full_repo_path_with_proximity(self, tmp_path, test_db): + """Test parsing full repo path + proximity: sequentech/meta#8624~.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + 'sequentech/meta#8624~' + ]) + + assert result.exit_code == 0 + # Should find multiple tickets in proximity + assert "8624" in result.output or "8625" in result.output + + def test_repo_override_with_option(self, tmp_path, test_db): + """Test that --repo option overrides parsed repo.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--repo', 'sequentech/step', # Override with step + 'meta#1024' # This is actually in step repo + ]) + + assert result.exit_code == 0 + assert "1024" in result.output + assert "step" in result.output.lower() + + +class TestOutputFormats: + """Tests for output formatting.""" + + def test_table_format_with_results(self, tmp_path, test_db): + """Test table format displays correctly.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--limit', '3', + '--format', 'table' + ]) + + assert result.exit_code == 0 + # Should have table headers (Repository may be truncated) + assert "Key" in result.output + assert "Reposi" in result.output # May be truncated to "Reposi…" + assert "Title" in result.output + assert "State" in result.output + + def test_table_format_no_results(self, tmp_path, test_db): + """Test table format with no results.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + 'nonexistent-999999' # Positional argument, not --ticket-key + ]) + + assert result.exit_code == 0 + assert "No tickets found" in result.output + + def test_csv_all_fields(self, tmp_path, test_db): + """Test CSV includes all expected fields.""" + db, _, _ = test_db + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db.db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--limit', '1', + '--format', 'csv' + ]) + + assert result.exit_code == 0 + + # Check all expected fields in header + expected_fields = [ + 'id', 'repo_id', 'number', 'key', 'title', 'body', 'state', + 'labels', 'url', 'created_at', 'closed_at', 'category', 'tags', + 'repo_full_name' + ] + for field in expected_fields: + assert field in result.output + + def test_csv_escaping(self, tmp_path): + """Test CSV properly escapes special characters.""" + # Create database with special characters + with tempfile.NamedTemporaryFile(mode='w', suffix='.db', delete=False) as f: + db_path = f.name + + db = Database(db_path) + db.connect() + + repo = Repository(owner="test", name="repo") + repo_id = db.upsert_repository(repo) + + # Ticket with commas and quotes in title/body + ticket = Ticket( + repo_id=repo_id, + number=1, + key="1", + title='Title with "quotes" and, commas', + body='Body with "quotes" and\nnewlines', + state="open", + labels=[], + url="https://github.com/test/repo/issues/1", + created_at=datetime.now(), + ) + db.upsert_ticket(ticket) + + config_file = tmp_path / "test_config.toml" + db_copy_path = tmp_path / "release_tool.db" + config_content = f""" +config_version = "1.4" + +[repository] +code_repo = "test/repo" + +[github] +token = "fake-token" + +[database] +path = "{db_copy_path}" +""" + config_file.write_text(config_content) + + import shutil + shutil.copy(db_path, db_copy_path) + + runner = CliRunner() + result = runner.invoke(cli, [ + '--config', str(config_file), + 'tickets', + '--format', 'csv' + ]) + + assert result.exit_code == 0 + # CSV should properly quote fields with special chars + assert '"Title with ""quotes"" and, commas"' in result.output or 'Title with "quotes" and, commas' in result.output + + # Cleanup + db.close() + Path(db_path).unlink() diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..16b9be2 --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,342 @@ +"""Tests for sync functionality.""" + +import pytest +from datetime import datetime, timedelta +from unittest.mock import Mock, MagicMock, patch +from pathlib import Path + +from release_tool.config import Config +from release_tool.db import Database +from release_tool.sync import SyncManager +from release_tool.github_utils import GitHubClient +from release_tool.models import Ticket, PullRequest, Author, Label + + +@pytest.fixture +def test_config(): + """Create test configuration.""" + config_dict = { + "repository": { + "code_repo": "sequentech/step", + "ticket_repos": ["sequentech/meta"], + "default_branch": "main" + }, + "github": { + "token": "test_token" + }, + "sync": { + "parallel_workers": 2, + "show_progress": False, + "clone_code_repo": False + } + } + return Config.from_dict(config_dict) + + +@pytest.fixture +def test_db(tmp_path): + """Create test database.""" + db_path = tmp_path / "test_sync.db" + db = Database(str(db_path)) + db.connect() + yield db + db.close() + + +@pytest.fixture +def mock_github(): + """Create mock GitHub client.""" + mock = Mock(spec=GitHubClient) + return mock + + +def test_sync_metadata_tracking(test_db): + """Test sync metadata CRUD operations.""" + # No sync initially + last_sync = test_db.get_last_sync("sequentech/meta", "tickets") + assert last_sync is None + + # Update sync metadata + test_db.update_sync_metadata( + "sequentech/meta", + "tickets", + cutoff_date="2024-01-01", + total_fetched=50 + ) + + # Should have sync timestamp now + last_sync = test_db.get_last_sync("sequentech/meta", "tickets") + assert last_sync is not None + assert isinstance(last_sync, datetime) + + # Get all sync status + status = test_db.get_all_sync_status() + assert len(status) == 1 + assert status[0]['repo_full_name'] == "sequentech/meta" + assert status[0]['entity_type'] == "tickets" + assert status[0]['total_fetched'] == 50 + + +def test_get_existing_ticket_numbers(test_db): + """Test retrieval of existing ticket numbers.""" + from release_tool.models import Repository, Ticket, Label + + # Create repository + repo = Repository( + owner="sequentech", + name="meta", + full_name="sequentech/meta", + url="https://github.com/sequentech/meta" + ) + repo_id = test_db.upsert_repository(repo) + + # Add some tickets + for num in [1, 5, 10, 25]: + ticket = Ticket( + repo_id=repo_id, + number=num, + key=f"#{num}", + title=f"Test ticket {num}", + body="", + state="open", + labels=[], + url=f"https://github.com/sequentech/meta/issues/{num}", + created_at=datetime.now(), + closed_at=None + ) + test_db.upsert_ticket(ticket) + + # Get existing numbers + existing = test_db.get_existing_ticket_numbers("sequentech/meta") + assert existing == {1, 5, 10, 25} + + +def test_get_existing_pr_numbers(test_db): + """Test retrieval of existing PR numbers.""" + from release_tool.models import Repository, PullRequest, Author + + # Create repository + repo = Repository( + owner="sequentech", + name="step", + full_name="sequentech/step", + url="https://github.com/sequentech/step" + ) + repo_id = test_db.upsert_repository(repo) + + # Add some PRs + author = Author(name="Test User", username="testuser") + for num in [10, 20, 30]: + pr = PullRequest( + repo_id=repo_id, + number=num, + title=f"Test PR {num}", + body="", + state="merged", + merged_at=datetime.now(), + author=author, + base_branch="main", + head_branch="feature", + head_sha="abc123", + labels=[], + url=f"https://github.com/sequentech/step/pull/{num}" + ) + test_db.upsert_pull_request(pr) + + # Get existing numbers + existing = test_db.get_existing_pr_numbers("sequentech/step") + assert existing == {10, 20, 30} + + +def test_config_get_ticket_repos(test_config): + """Test getting ticket repos from config.""" + ticket_repos = test_config.get_ticket_repos() + assert ticket_repos == ["sequentech/meta"] + + +def test_config_get_ticket_repos_defaults_to_code_repo(): + """Test that ticket_repos defaults to code_repo if not specified.""" + config_dict = { + "repository": { + "code_repo": "sequentech/step" + }, + "github": { + "token": "test_token" + } + } + config = Config.from_dict(config_dict) + ticket_repos = config.get_ticket_repos() + assert ticket_repos == ["sequentech/step"] + + +def test_config_get_code_repo_path_default(test_config): + """Test default code repo path generation.""" + path = test_config.get_code_repo_path() + assert "step" in path + assert ".release_tool_cache" in path + + +def test_config_get_code_repo_path_custom(): + """Test custom code repo path.""" + config_dict = { + "repository": { + "code_repo": "sequentech/step" + }, + "github": { + "token": "test_token" + }, + "sync": { + "code_repo_path": "/custom/path/to/repo" + } + } + config = Config.from_dict(config_dict) + path = config.get_code_repo_path() + assert path == "/custom/path/to/repo" + + +def test_sync_config_defaults(): + """Test sync configuration defaults.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + } + } + config = Config.from_dict(config_dict) + + assert config.sync.parallel_workers == 20 + assert config.sync.show_progress is True + assert config.sync.clone_code_repo is True + assert config.sync.cutoff_date is None + + +def test_sync_config_cutoff_date(): + """Test sync configuration with cutoff date.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "sync": { + "cutoff_date": "2024-01-01" + } + } + config = Config.from_dict(config_dict) + assert config.sync.cutoff_date == "2024-01-01" + + +@patch('release_tool.sync.subprocess.run') +def test_sync_git_repository_clone(mock_run, test_config, tmp_path): + """Test cloning a new git repository.""" + # Update config to use temp path + test_config.sync.code_repo_path = str(tmp_path / "test_repo") + + mock_db = Mock(spec=Database) + mock_github = Mock(spec=GitHubClient) + + sync_manager = SyncManager(test_config, mock_db, mock_github) + + # Mock successful clone + mock_run.return_value = Mock(returncode=0, stdout="", stderr="") + + repo_path = sync_manager._sync_git_repository("sequentech/step") + + # Should have called git clone + assert mock_run.called + call_args = mock_run.call_args[0][0] + assert 'git' in call_args + assert 'clone' in call_args + assert "sequentech/step" in ' '.join(call_args) + + +@patch('release_tool.sync.subprocess.run') +def test_sync_git_repository_update(mock_run, test_config, tmp_path): + """Test updating an existing git repository.""" + # Create fake repo directory with .git + repo_path = tmp_path / "test_repo" + repo_path.mkdir() + (repo_path / ".git").mkdir() + + test_config.sync.code_repo_path = str(repo_path) + + mock_db = Mock(spec=Database) + mock_github = Mock(spec=GitHubClient) + + sync_manager = SyncManager(test_config, mock_db, mock_github) + + # Mock successful fetch and reset + mock_run.return_value = Mock(returncode=0, stdout="", stderr="") + + result_path = sync_manager._sync_git_repository("sequentech/step") + + # Should have called git fetch and git reset + assert mock_run.call_count >= 2 + calls = [call[0][0] for call in mock_run.call_args_list] + + # Check for fetch + fetch_call = [c for c in calls if 'fetch' in c] + assert len(fetch_call) > 0 + + # Check for reset + reset_call = [c for c in calls if 'reset' in c] + assert len(reset_call) > 0 + + +def test_incremental_sync_filters_existing(test_config, test_db, mock_github): + """Test that incremental sync only fetches new items.""" + from release_tool.models import Repository + + # Create repository in DB + repo = Repository( + owner="sequentech", + name="meta", + full_name="sequentech/meta", + url="https://github.com/sequentech/meta" + ) + repo_id = test_db.upsert_repository(repo) + + # Add existing tickets + for num in [1, 2, 3]: + ticket = Ticket( + repo_id=repo_id, + number=num, + key=f"#{num}", + title=f"Test {num}", + body="", + state="open", + labels=[], + url=f"https://github.com/sequentech/meta/issues/{num}", + created_at=datetime.now(), + closed_at=None + ) + test_db.upsert_ticket(ticket) + + # Mock GitHub to return all tickets (including existing) + mock_github.search_ticket_numbers.return_value = [1, 2, 3, 4, 5, 6] + + sync_manager = SyncManager(test_config, test_db, mock_github) + + # Get ticket numbers to fetch + to_fetch = sync_manager._get_ticket_numbers_to_fetch("sequentech/meta", None) + + # Should only fetch new tickets (4, 5, 6) + assert set(to_fetch) == {4, 5, 6} + + +def test_parallel_workers_config(): + """Test that parallel_workers configuration is respected.""" + config_dict = { + "repository": { + "code_repo": "test/repo" + }, + "sync": { + "parallel_workers": 20 + } + } + config = Config.from_dict(config_dict) + + assert config.sync.parallel_workers == 20 + + mock_db = Mock(spec=Database) + mock_github = Mock(spec=GitHubClient) + + sync_manager = SyncManager(config, mock_db, mock_github) + assert sync_manager.parallel_workers == 20 diff --git a/tests/test_template_utils.py b/tests/test_template_utils.py new file mode 100644 index 0000000..85762d2 --- /dev/null +++ b/tests/test_template_utils.py @@ -0,0 +1,157 @@ +"""Tests for template utilities.""" + +import pytest +from release_tool.template_utils import ( + render_template, + validate_template_vars, + get_template_variables, + TemplateError +) + + +def test_render_template_simple(): + """Test basic template rendering.""" + template = "Version {{version}}" + context = {'version': '1.2.3'} + result = render_template(template, context) + assert result == "Version 1.2.3" + + +def test_render_template_multiple_variables(): + """Test template with multiple variables.""" + template = "Release {{version}} ({{major}}.{{minor}}.{{patch}})" + context = {'version': '1.2.3', 'major': '1', 'minor': '2', 'patch': '3'} + result = render_template(template, context) + assert result == "Release 1.2.3 (1.2.3)" + + +def test_render_template_multiline(): + """Test multiline template rendering.""" + template = """Parent issue: {{issue_link}} + +Automated release notes for version {{version}}. + +## Summary +This PR adds release notes for {{version}} with {{num_changes}} changes.""" + + context = { + 'issue_link': 'https://github.com/owner/repo/issues/123', + 'version': '1.0.0', + 'num_changes': 5 + } + result = render_template(template, context) + assert "Parent issue: https://github.com/owner/repo/issues/123" in result + assert "version 1.0.0" in result + assert "with 5 changes" in result + + +def test_render_template_undefined_variable(): + """Test that undefined variables raise TemplateError.""" + template = "Version {{version}} by {{author}}" + context = {'version': '1.2.3'} # Missing 'author' + + with pytest.raises(TemplateError) as exc_info: + render_template(template, context) + assert "undefined" in str(exc_info.value).lower() + + +def test_render_template_invalid_syntax(): + """Test that invalid template syntax raises TemplateError.""" + template = "Version {{version" # Missing closing braces + context = {'version': '1.2.3'} + + with pytest.raises(TemplateError) as exc_info: + render_template(template, context) + assert "syntax" in str(exc_info.value).lower() + + +def test_get_template_variables(): + """Test extracting variables from template.""" + template = "Release {{version}} on {{date}} by {{author}}" + variables = get_template_variables(template) + assert variables == {'version', 'date', 'author'} + + +def test_get_template_variables_empty(): + """Test template with no variables.""" + template = "This is a static template" + variables = get_template_variables(template) + assert variables == set() + + +def test_get_template_variables_invalid_syntax(): + """Test that invalid syntax raises TemplateError.""" + template = "Release {{version" + + with pytest.raises(TemplateError): + get_template_variables(template) + + +def test_validate_template_vars_success(): + """Test validation succeeds when all variables are available.""" + template = "Release {{version}} on {{date}}" + available_vars = {'version', 'date', 'author'} + + # Should not raise + validate_template_vars(template, available_vars, "test_template") + + +def test_validate_template_vars_failure(): + """Test validation fails when variables are not available.""" + template = "Release {{version}} by {{author}}" + available_vars = {'version', 'date'} # Missing 'author' + + with pytest.raises(TemplateError) as exc_info: + validate_template_vars(template, available_vars, "test_template") + assert "author" in str(exc_info.value) + assert "undefined" in str(exc_info.value).lower() + + +def test_validate_template_vars_empty_template(): + """Test validation with empty template.""" + template = "Static content" + available_vars = {'version'} + + # Should not raise + validate_template_vars(template, available_vars, "test_template") + + +def test_path_template_rendering(): + """Test rendering path templates.""" + template = ".release_tool_cache/draft-releases/{{repo}}/{{version}}.md" + context = {'repo': 'owner-repo', 'version': '1.2.3'} + result = render_template(template, context) + assert result == ".release_tool_cache/draft-releases/owner-repo/1.2.3.md" + + +def test_branch_template_rendering(): + """Test rendering branch templates.""" + template = "docs/{{repo}}-{{issue_number}}/{{target_branch}}" + context = { + 'repo': 'sequentech/meta', + 'issue_number': '8853', + 'target_branch': 'main' + } + result = render_template(template, context) + assert result == "docs/sequentech/meta-8853/main" + + +def test_pr_body_template(): + """Test rendering PR body template.""" + template = """Parent issue: {{issue_link}} + +Automated release notes for version {{version}}. + +## Summary +This PR adds release notes for {{version}} with {{num_changes}} changes across {{num_categories}} categories.""" + + context = { + 'issue_link': 'https://github.com/sequentech/meta/issues/8853', + 'version': '9.2.0', + 'num_changes': 10, + 'num_categories': 3 + } + result = render_template(template, context) + assert "Parent issue: https://github.com/sequentech/meta/issues/8853" in result + assert "version 9.2.0" in result + assert "with 10 changes across 3 categories" in result diff --git a/tests_comprehensive_release_notes.py b/tests_comprehensive_release_notes.py deleted file mode 100644 index 32e85ca..0000000 --- a/tests_comprehensive_release_notes.py +++ /dev/null @@ -1,157 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -from comprehensive_release_notes import ( - parse_arguments, - get_comprehensive_release_notes, - main -) - -class TestComprehensiveReleaseNotes(unittest.TestCase): - def test_get_comprehensive_release_notes(self): - """ - Test that the get_comprehensive_release_notes function returns deduplicated release notes. - """ - args = MagicMock() - args.silent = True - token = "dummy_token" - repos = ["org/repo1", "org/repo2"] - prev_release = "1.1.0" - new_release = "1.2.0" - config = {} - - # Mock GitHub API calls - gh = MagicMock() - repo1 = MagicMock() - repo2 = MagicMock() - gh.get_repo.side_effect = [repo1, repo2] - - repo1_notes = { - "Feature": ["Feature 1 in repo1 (https://link1)", "Feature 2 in repo1 (https://link2)"], - "Bugfix": ["Bugfix 1 in repo1 (https://link3)"] - } - - repo2_notes = { - "Feature": ["Feature 1 in repo2 (https://link4)", "Feature 1 in repo2 (https://link1)"], - "Bugfix": ["Bugfix 2 in repo2 (https://link5)", "Bugfix 1 in repo2 (https://link3)"] - } - - get_release_notes = MagicMock() - get_release_notes.side_effect = [repo1_notes, repo2_notes] - - # Test get_comprehensive_release_notes - with unittest.mock.patch("comprehensive_release_notes.Github", return_value=gh): - with unittest.mock.patch("comprehensive_release_notes.get_release_notes", side_effect=[repo1_notes, repo2_notes]): - release_notes = get_comprehensive_release_notes(args, token, repos, prev_release, new_release, config) - - expected_release_notes = { - "Feature": [ - "Feature 1 in repo1 (https://link1)", - "Feature 2 in repo1 (https://link2)", - "Feature 1 in repo2 (https://link4)" - ], - "Bugfix": [ - "Bugfix 1 in repo1 (https://link3)", - "Bugfix 2 in repo2 (https://link5)" - ] - } - - self.assertEqual(release_notes, expected_release_notes) - - -class TestMainComprehensiveReleaseNotes(unittest.TestCase): - """ - Test cases for comprehensive_release_notes.py - """ - - def test_parse_arguments(self): - """ - Test that parse_arguments() correctly parses the command line arguments. - """ - with patch('argparse.ArgumentParser.parse_args') as mock_args: - mock_args.return_value = MagicMock( - previous_release='1.1', - new_release='1.2.0', - dry_run=True, - silent=True - ) - - args = parse_arguments() - - self.assertEqual(args.previous_release, '1.1') - self.assertEqual(args.new_release, '1.2.0') - self.assertTrue(args.dry_run) - self.assertTrue(args.silent) - - @patch('comprehensive_release_notes.get_release_notes') - @patch('comprehensive_release_notes.Github') - def test_get_comprehensive_release_notes(self, mock_github, mock_get_release_notes): - """ - Test that get_comprehensive_release_notes() correctly generates and deduplicates release notes. - """ - mock_args = MagicMock(silent=True) - token = "test_token" - repos = ["org/repo1", "org/repo2"] - prev_release = "1.1.0" - new_release = "1.2.0" - config = {} - - mock_get_release_notes.side_effect = [ - { - "Feature": ["Feature 1 in repo1 (https://link1)", "Feature 2 in repo1 (https://link2)"], - "Bugfix": ["Bugfix 1 in repo1 (https://link3)"] - }, - { - "Feature": ["Feature 1 in repo2 (https://link4)", "Feature 1 in repo2 (https://link1)"], - "Bugfix": ["Bugfix 2 in repo2 (https://link5)", "Bugfix 1 in repo2 (https://link3)"] - } - ] - - release_notes = get_comprehensive_release_notes(mock_args, token, repos, prev_release, new_release, config) - - self.assertEqual(release_notes, { - "Feature": [ - "Feature 1 in repo1 (https://link1)", - "Feature 2 in repo1 (https://link2)", - "Feature 1 in repo2 (https://link4)" - ], - "Bugfix": [ - "Bugfix 1 in repo1 (https://link3)", - "Bugfix 2 in repo2 (https://link5)" - ] - }) - - @patch('comprehensive_release_notes.parse_arguments') - @patch('comprehensive_release_notes.get_comprehensive_release_notes') - @patch('comprehensive_release_notes.create_release_notes_md') - @patch('comprehensive_release_notes.Github') - def test_main(self, mock_github, mock_create_release_notes_md, mock_get_comprehensive_release_notes, mock_parse_arguments): - """ - Test the main() function with proper execution and handling of dry_run flag. - """ - # Setup mock arguments - mock_parse_arguments.return_value = MagicMock( - previous_release='1.1', - new_release='1.2.0', - dry_run=True, - silent=True - ) - - # Setup other mocks - mock_create_release_notes_md.return_value = "Release Notes" - mock_get_comprehensive_release_notes.return_value = {} - - with patch('comprehensive_release_notes.os.getenv') as mock_getenv: - mock_getenv.return_value = 'test_token' - - main() - - # Check if Github object is instantiated - mock_github.assert_called_with('test_token') - - # Check if create_git_tag_and_release is not called due to dry_run flag - meta_repo = mock_github.return_value.get_repo.return_value - meta_repo.create_git_tag_and_release.assert_not_called() - - -if __name__ == '__main__': - unittest.main() diff --git a/tests_release_notes.py b/tests_release_notes.py deleted file mode 100644 index 717e3ba..0000000 --- a/tests_release_notes.py +++ /dev/null @@ -1,249 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -import unittest -from unittest.mock import MagicMock, Mock -from github import Repository, Github -from release_notes import ( - get_label_category, get_release_notes, create_release_notes_md -) - -class TestGetReleaseNotes(unittest.TestCase): - def generate_labels(self, labels): - ret = [] - for label in labels: - mock = Mock(name=label) - mock.name = label - ret.append(mock) - return ret - - def setUp(self): - self.args = commit = Mock(silent=True) - self.github = MagicMock(spec=Github) - self.repo = MagicMock(spec=Repository.Repository) - self.github.get_repo.return_value = self.repo - self.repo.compare.return_value.commits = [] - self.config = { - "changelog": { - "exclude": {"labels": ["skip-changelog"]}, - "categories": [ - {"title": "Bug Fixes", "labels": ["bug"]}, - {"title": "New Features", "labels": ["enhancement"]} - ] - } - } - - def test_get_release_notes_empty(self): - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config) - self.assertEqual(release_notes, {}) - - def test_get_release_notes_with_data(self): - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.number = 1 - pr.labels = self.generate_labels(["bug"]) - pr.title = "Fix a bug" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit] - - expected_release_notes = { - "Bug Fixes": [ - "* Fix a bug by @user in https://github.com/username/repo/pull/1" - ] - } - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config, self.args) - self.assertEqual(release_notes, expected_release_notes) - - def test_get_release_notes_with_excluded_label(self): - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.labels = self.generate_labels(["skip-changelog"]) - pr.title = "Fix a bug" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit] - - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "master", self.config, self.args) - self.assertEqual(release_notes, {}) - - def test_get_release_notes_with_parent_issue(self): - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.labels = self.generate_labels(["bug"]) - pr.title = "Fix a bug PR" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "Parent issue: https://github.com/username/repo/issues/1" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit] - - issue = Mock() - issue.title = "Fix a bug" - issue.html_url = "https://github.com/username/repo/issues/1" - self.repo.get_issue = Mock(return_value=issue) - - expected_release_notes = { - "Bug Fixes": [ - "* Fix a bug by @user in https://github.com/username/repo/issues/1" - ] - } - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config, self.args) - self.assertEqual(release_notes, expected_release_notes) - - def test_get_release_notes_with_parent_issue_already_included(self): - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.labels = self.generate_labels(["bug"]) - pr.title = "Fix a bug" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "Parent issue: https://github.com/username/repo/issues/1" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit, commit] - - issue = Mock() - issue.title = "Fix a bug" - issue.html_url = "https://github.com/username/repo/issues/1" - self.repo.get_issue = Mock(return_value=issue) - - expected_release_notes = { - "Bug Fixes": [ - "* Fix a bug by @user in https://github.com/username/repo/issues/1" - ] - } - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config, self.args) - self.assertEqual(release_notes, expected_release_notes) - - def test_get_release_notes_with_no_matching_category(self): - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.labels = self.generate_labels(["other"]) - pr.title = "Other change" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit] - - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config, self.args) - self.assertEqual(release_notes, {}) - - def test_get_release_notes_with_wildcard_category(self): - self.config["changelog"]["categories"].append( - {"title": "Other Changes", "labels": ["*"]} - ) - commit = Mock() - commit.commit.message = "A commit message" - pr = Mock() - pr.labels = self.generate_labels(["other"]) - pr.title = "Other change" - pr.user.login = "user" - pr.html_url = "https://github.com/username/repo/pull/1" - pr.body = "" - commit.get_pulls.return_value = [pr] - self.repo.compare.return_value.commits = [commit] - - expected_release_notes = { - "Other Changes": [ - "* Other change by @user in https://github.com/username/repo/pull/1" - ] - } - release_notes = get_release_notes(self.github, self.repo, "1.0.0", "1.1.0", self.config, self.args) - self.assertEqual(release_notes, expected_release_notes) - - -class TestGetLabelCategory(unittest.TestCase): - def generate_labels(self, labels): - ret = [] - for label in labels: - mock = Mock(name=label) - mock.name = label - ret.append(mock) - return ret - - - def test_no_matching_labels(self): - labels = self.generate_labels(["bugfix", "documentation"]) - categories = [ - {"labels": ["enhancement"], "title": "Enhancements"}, - {"labels": ["security"], "title": "Security"} - ] - result = get_label_category(labels, categories) - self.assertIsNone(result) - - def test_single_matching_label(self): - labels = self.generate_labels(["bugfix", "documentation"]) - categories = [ - {"labels": ["bugfix"], "title": "Bug Fixes"}, - {"labels": ["security"], "title": "Security"} - ] - expected = {"labels": ["bugfix"], "title": "Bug Fixes"} - result = get_label_category(labels, categories) - self.assertEqual(result, expected) - - def test_multiple_matching_labels(self): - labels = self.generate_labels(["bugfix", "documentation"]) - categories = [ - {"labels": ["bugfix"], "title": "Bug Fixes"}, - {"labels": ["documentation"], "title": "Documentation"} - ] - expected = {"labels": ["bugfix"], "title": "Bug Fixes"} - result = get_label_category(labels, categories) - self.assertEqual(result, expected) - - def test_wildcard_matching_label(self): - labels = self.generate_labels(["bugfix", "documentation"]) - categories = [ - {"labels": ["*"], "title": "All"}, - {"labels": ["security"], "title": "Security"} - ] - expected = {"labels": ["*"], "title": "All"} - result = get_label_category(labels, categories) - self.assertEqual(result, expected) - - def test_wildcard_matching_label_with_other_labels(self): - labels = self.generate_labels(["bugfix", "documentation"]) - categories = [ - {"labels": ["*", "security"], "title": "All and Security"}, - {"labels": ["enhancement"], "title": "Enhancements"} - ] - expected = {"labels": ["*", "security"], "title": "All and Security"} - result = get_label_category(labels, categories) - self.assertEqual(result, expected) - - -class TestCreateReleaseNotesMd(unittest.TestCase): - def test_create_release_notes_md(self): - release_notes = { - "Bug Fixes": [ - "* Fix a bug by @user in https://github.com/username/repo/pull/1" - ], - "New Features": [ - "* Add a new feature by @user2 in https://github.com/username/repo/pull/2" - ] - } - new_release = "1.1.0" - expected_md = """ - -## What's Changed -### Bug Fixes -* Fix a bug by @user in https://github.com/username/repo/pull/1 -### New Features -* Add a new feature by @user2 in https://github.com/username/repo/pull/2 -""" - md = create_release_notes_md(release_notes, new_release) - self.assertEqual(md, expected_md) - -if __name__ == '__main__': - unittest.main() diff --git a/tests_update_parent_issue.py b/tests_update_parent_issue.py deleted file mode 100644 index d116f33..0000000 --- a/tests_update_parent_issue.py +++ /dev/null @@ -1,196 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only -import unittest -from unittest.mock import MagicMock, patch -from github import Github, Project, Issue -from datetime import datetime, timedelta -from update_parent_issue import (get_closed_issues_in_last_n_days, get_project_id) - - -class TestGetClosedIssuesInLastNDays(unittest.TestCase): - def setUp(self): - self.project = MagicMock(spec=Project.Project) - self.access_token = "fake_access_token" - self.silent = True - - def create_mock_issue(self, state, closed_at): - issue = MagicMock(spec=Issue.Issue) - issue.state = state - issue.closed_at = closed_at - return issue - - def test_no_closed_issues(self): - """ - Test get_closed_issues_in_last_n_days when there are no closed issues. - """ - self.project.get_columns.return_value = [] - closed_issues = get_closed_issues_in_last_n_days(self.project, 7, self.access_token, self.silent) - self.assertEqual(closed_issues, []) - - def test_closed_issues_within_days(self): - """ - Test get_closed_issues_in_last_n_days when there are closed issues - within the specified days. - """ - now = datetime.now() - n_days_ago = now - timedelta(days=7) - closed_at = (n_days_ago + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ") - - mock_response = { - "data": { - "node": { - "items": { - "nodes": [ - { - "content": { - "__typename": "Issue", - "title": "Mock issue", - "closedAt": closed_at, - "state": "CLOSED", - "body": "Mock issue body", - "url": "https://github.com/mock_owner/mock_repo/issues/1", - "repository": {"name": "mock_repo"}, - "assignees": {"nodes": [{"login": "mock_user"}]}, - } - } - ], - "totalCount": 1, - "pageInfo": {"hasNextPage": False, "endCursor": None}, - } - } - } - } - - with patch("update_parent_issue.send_graphql_query") as mock_send_graphql_query: - mock_send_graphql_query.return_value = mock_response - - closed_issues = get_closed_issues_in_last_n_days(self.project, 7, self.access_token, self.silent) - self.assertEqual(len(closed_issues), 1) - self.assertEqual(closed_issues[0]["content"]["title"], "Mock issue") - - def test_closed_issues_outside_days(self): - """ - Test get_closed_issues_in_last_n_days when there are closed issues but they are outside the specified days. - """ - now = datetime.now() - n_days_ago = now - timedelta(days=7) - mock_closed_issue = self.create_mock_issue("closed", n_days_ago - timedelta(hours=1)) - - self.project.get_columns.return_value = [ - MagicMock(get_cards=MagicMock(return_value=[ - MagicMock( - get_content=MagicMock(return_value=mock_closed_issue), - content_url="/issues/367" - ) - ])) - ] - - closed_issues = get_closed_issues_in_last_n_days(self.project, 7, self.access_token, self.silent) - self.assertEqual(closed_issues, []) - - def test_mixed_closed_issues(self): - """ - Test get_closed_issues_in_last_n_days when there are a mix of open and - closed issues within the specified days. - """ - now = datetime.now() - n_days_ago = now - timedelta(days=7) - closed_at = (n_days_ago + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ") - - mock_response = { - "data": { - "node": { - "items": { - "nodes": [ - { - "content": { - "__typename": "Issue", - "title": "Closed issue", - "closedAt": closed_at, - "state": "CLOSED", - "body": "Closed issue body", - "url": "https://github.com/mock_owner/mock_repo/issues/1", - "repository": {"name": "mock_repo"}, - "assignees": {"nodes": [{"login": "mock_user"}]}, - } - }, - { - "content": { - "__typename": "Issue", - "title": "Open issue", - "closedAt": None, - "state": "OPEN", - "body": "Open issue body", - "url": "https://github.com/mock_owner/mock_repo/issues/2", - "repository": {"name": "mock_repo"}, - "assignees": {"nodes": [{"login": "mock_user"}]}, - } - } - ], - "totalCount": 2, - "pageInfo": {"hasNextPage": False, "endCursor": None}, - } - } - } - } - - with patch("update_parent_issue.send_graphql_query") as mock_send_graphql_query: - mock_send_graphql_query.return_value = mock_response - - closed_issues = get_closed_issues_in_last_n_days(self.project, 7, self.access_token, self.silent) - self.assertEqual(len(closed_issues), 1) - self.assertEqual(closed_issues[0]["content"]["title"], "Closed issue") - - -class TestGetProjectByUrl(unittest.TestCase): - - def setUp(self): - self.access_token = "fake_access_token" - self.project_board_url = "https://github.com/orgs/test-org/projects/123" - self.silent = True - - @patch("update_parent_issue.re") - @patch("update_parent_issue.send_graphql_query") - def test_get_project_by_url_success(self, mock_send_graphql_query, mock_re): - mock_re.search.side_effect = [ - MagicMock(group=MagicMock(return_value="test-org")), - MagicMock(group=MagicMock(return_value="123")) - ] - - mock_send_graphql_query.return_value = { - "data": { - "organization": { - "projectV2": { - "id": 123 - } - } - } - } - - project_id = get_project_id(self.access_token, self.project_board_url, self.silent) - self.assertEqual(project_id, 123) - - @patch("update_parent_issue.re") - @patch("update_parent_issue.send_graphql_query") - def test_get_project_by_url_project_not_found(self, mock_send_graphql_query, mock_re): - mock_re.search.side_effect = [ - MagicMock(group=MagicMock(return_value="test-org")), - MagicMock(group=MagicMock(return_value="999")) - ] - - mock_send_graphql_query.return_value = None - - project_id = get_project_id(self.access_token, self.project_board_url, self.silent) - self.assertIsNone(project_id) - - @patch("update_parent_issue.re") - def test_get_project_by_url_invalid_url(self, mock_re): - mock_re.search.return_value = None - - with self.assertRaises(AttributeError): - get_project_id(self.access_token, "https://invalid_url.com", self.silent) - - -if __name__ == "__main__": - unittest.main() diff --git a/update_parent_issue.py b/update_parent_issue.py deleted file mode 100644 index 5b23d63..0000000 --- a/update_parent_issue.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2023 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -import os -import re -import sys -import requests -import argparse -from github import Github -from datetime import datetime, timedelta - -def verbose_print(message, silent): - """ - Print a timestamped message if silent mode is not enabled. - - :param message: str, message to be printed - :param silent: bool, flag for silent mode - """ - if not silent: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"[{timestamp}] {message}") - - -def send_graphql_query(query, access_token, silent): - """ - Send a GraphQL query to the GitHub API. - - :param query: str, the GraphQL query - :param access_token: str, the access token for GitHub API - :param silent: bool, flag for silent mode - :return: dict, the JSON response from the API - """ - headers = { - "Authorization": f"Bearer {access_token}" - } - - request_data = {"query": query} - response = requests.post( - "https://api.github.com/graphql", - json=request_data, - headers=headers - ) - - if response.status_code != 200: - verbose_print(f"ERROR GraphQL request failed with status {response.status_code}: {response.text}", silent) - return None - - json_data = response.json() - if 'errors' in json_data: - verbose_print(f"ERROR GraphQL request failed with status {response.status_code}: {response.text}", silent) - return None - - return json_data - -def get_project_id(access_token, project_board_url, silent): - """ - Find and return a Github project given its URL. - - :param access_token: str, the access token for GitHub API - :param project_board_url: str, URL of the project board - :param silent: bool, flag for silent mode - :return: github.Project.Project instance or None - """ - - # Extract project number and organization name from the URL - org_name = re.search(r"https://github.com/orgs/([\w-]+)/", project_board_url).group(1) - project_num = int(re.search(r"/projects/(\d+)", project_board_url).group(1)) - - verbose_print(f"- Github org_name = `{org_name}`", silent) - verbose_print(f"- Github project_num = int({project_num})`", silent) - verbose_print(f"Obtaining project_id ...", silent) - - query = f""" - query {{ - organization(login: "{org_name}") {{ - projectV2(number: {project_num}) {{ - id - }} - }} - }} - """ - response = send_graphql_query(query, access_token, silent) - if response is None: - return None - - project_id = response["data"]["organization"]["projectV2"]["id"] - verbose_print(f"... Done! Github project_id = `{project_id}`", silent) - return project_id - -def get_closed_issues_in_last_n_days(project_id, days, access_token, silent): - """ - Get all closed issues in the last N days for a given project. - - :param project_id: str, the ID of the project - :param days: int, number of days to look back for closed issues - :param access_token: str, the access token for GitHub API - :param silent: bool, flag for silent mode - :return: list of github.Issue.Issue instances - """ - now = datetime.now() - n_days_ago = now - timedelta(days=days) - closed_issues = [] - - has_next_page = True - end_cursor = None - total_count = 0 - - verbose_print(f"Obtaining project_id=`{project_id}` issues ..", silent) - while has_next_page: - verbose_print("...", silent) - query = f""" - query {{ - node(id: "{project_id}") {{ - ... on ProjectV2 {{ - items(first: 100{f', after: "{end_cursor}"' if end_cursor else ''}) {{ - nodes {{ - id - updatedAt - content {{ - __typename - ... on Issue {{ - title - closedAt - state - body - url - repository {{ name }} - assignees(first: 1) {{ - nodes {{ - login - }} - }} - }} - }} - }} - totalCount - pageInfo {{ - hasNextPage - endCursor - }} - }} - }} - }} - }} - """ - - response = send_graphql_query(query, access_token, silent) - if response is None: - break - - nodes = response["data"]["node"]["items"]["nodes"] - page_info = response["data"]["node"]["items"]["pageInfo"] - total_count = response["data"]["node"]["items"]['totalCount'] - has_next_page = page_info["hasNextPage"] - end_cursor = page_info["endCursor"] - - for node in nodes: - if ( - node["content"]["__typename"] == "Issue" and - node["content"]["state"] == 'CLOSED' - ): - issue_closed_at = datetime.strptime( - node["content"]["closedAt"], "%Y-%m-%dT%H:%M:%SZ" - ) - if issue_closed_at > n_days_ago: - issue_title = node["content"]["title"] - issue_url = node["content"]["url"] - verbose_print(f"- Adding issue: {issue_url} ({issue_title})", silent) - closed_issues.append(node) - - verbose_print(f"...done! read {total_count} issues, filtered to {len(closed_issues)} issues", silent) - - return closed_issues - -def quote_text(text): - return text.replace("\"", "\\\"") - -def update_pull_request_body(pr_url, access_token, start_text, silent, dry_run): - verbose_print(f"\t- PR {pr_url}:", silent) - # Extract the repository name, owner and pull request number from the URL - split_url = pr_url.split("/") - owner, repo, pr_number = split_url[3], split_url[4], split_url[-1] - - # Prepare the GraphQL query to get the pull request's ID and body - query = f""" - query {{ - repository(owner: "{owner}", name: "{repo}") {{ - pullRequest(number: {pr_number}) {{ - id - body - }} - }} - }} - """ - - # Execute the query - response_data = send_graphql_query(query, access_token, silent) - if response_data is None: - sys.exit(1) - - # Parse the response - pr_data = response_data["data"]["repository"]["pullRequest"] - pr_id, current_body = pr_data["id"], pr_data["body"] - - # Check if the desired text is at the start of the body - if current_body.startswith(start_text): - verbose_print(f"\t\t - Not updating, it's ok", silent) - return - - # If not, update the body to include the desired text at the beginning - new_body = start_text + "\n" + current_body - - # Prepare the GraphQL mutation to update the pull request body - mutation_query = f""" - mutation {{ - updatePullRequest(input: {{ - pullRequestId: "{pr_id}", - body: "{quote_text(new_body)}" - }}) {{ - pullRequest {{ - number - body - }} - }} - }} - """ - - if dry_run: - verbose_print(f"\t\t - [dry-run] Would be updating PR to be ```{new_body[:300]}```", silent) - return - else: - verbose_print(f"\t\t - Updating PR..", silent) - - # Execute the mutation - response_data = send_graphql_query(mutation_query, access_token, silent) - if response_data is None: - sys.exit(1) - - verbose_print(f"\t\t ... done!", silent) - -def update_prs_with_parent_issue(issue, access_token, dry_run, silent): - """ - Update PRs associated with a closed issue by adding a "Parent issue" link to their body. - - :param issue: github.Issue.Issue instance - :param dry_run: bool, indicating whether the code should perform changes or not - :param silent: bool, flag for silent mode - """ - issue_title = issue["content"]["title"] - issue_url = issue["content"]["url"] - issue_body = issue["content"]["body"] - pr_links = re.findall(r"https://github.com/\S+/pull/\d+", issue_body) - verbose_print(f"- Issue: {issue_url} ({issue_title})", silent) - - for pr_link in pr_links: - start_text = f"Parent issue: {issue_url}\n" - update_pull_request_body( - pr_link, access_token, start_text, silent, dry_run - ) - -def main(): - """ - Main function to update PRs with a "Parent issue" link for closed issues in the last N days. - """ - parser = argparse.ArgumentParser(description="Add Parent Issue links to related PRs for closed issues in the last N days.") - parser.add_argument("project_board_url", help="The URL of the project board.") - parser.add_argument("--dry-run", action="store_true", help="Don't modify PRs, just simulate changes.") - parser.add_argument("--days", type=int, default=180, help="The number of days to look back for closed issues. Default is 180.") - parser.add_argument("--silent", action="store_true", help="Don't print verbose messages.") - args = parser.parse_args() - - access_token = os.environ.get("GITHUB_TOKEN") - - verbose_print(f"Input Parameters: {args}", args.silent) - - if access_token is None: - verbose_print("ERROR Environment variable 'GITHUB_TOKEN' not set.", args.silent) - exit(1) - - project = get_project_id(access_token, args.project_board_url, args.silent) - - if project is None: - verbose_print("ERROR Project not found", args.silent) - exit(1) - - - closed_issues = get_closed_issues_in_last_n_days( - project, args.days, access_token, args.silent - ) - - for issue in closed_issues: - update_prs_with_parent_issue( - issue, access_token, args.dry_run, args.silent - ) - -if __name__ == "__main__": - main() diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 0000000..0f97189 --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,78 @@ +// @ts-check +// Note: type annotations allow type checking and IDEs autocompletion + +const { themes } = require('prism-react-renderer'); +const lightCodeTheme = themes.github; +const darkCodeTheme = themes.dracula; + +/** @type {import('@docusaurus/types').Config} */ +const config = { + title: 'Release Tool', + tagline: 'Semantic Versioning & Release Management', + url: 'https://sequentech.github.io', + baseUrl: '/release-tool/', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + favicon: 'img/favicon.ico', + organizationName: 'sequentech', + projectName: 'release-tool', + + presets: [ + [ + 'classic', + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + path: '../docs', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: 'https://github.com/sequentech/release-tool/tree/main/website/', + }, + theme: { + customCss: require.resolve('./src/css/custom.css'), + }, + }), + ], + ], + + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + navbar: { + title: 'Release Tool', + items: [ + { + type: 'doc', + docId: 'intro', + position: 'left', + label: 'Docs', + }, + { + href: 'https://github.com/sequentech/release-tool', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ + { + label: 'Introduction', + to: '/docs/intro', + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} Sequent Tech Inc. Built with Docusaurus.`, + }, + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme, + }, + }), +}; + +module.exports = config; diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..eee59c6 --- /dev/null +++ b/website/package.json @@ -0,0 +1,37 @@ +{ + "name": "release-tool-website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "latest", + "@docusaurus/preset-classic": "latest", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file